npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cachyer

v1.4.2

Published

A flexible, type-safe caching layer with support for multiple database adapters (Redis, MongoDB, PostgreSQL, CouchDB, and more)

Downloads

1,065

Readme

Cachyer

A flexible, type-safe caching layer with support for multiple database adapters (Redis, MongoDB, PostgreSQL, and more).

Features

  • 🔌 Pluggable Adapters - Redis, In-Memory (built-in), with easy extension for MongoDB, PostgreSQL, CouchDB
  • 📝 Type-Safe - Full TypeScript support with generics and type inference
  • 🏗️ Schema Builder - Define your cache schemas with a fluent API
  • High Performance - Pipeline and transaction support, Lua script caching
  • 🔒 Rate Limiting - Built-in rate limiting service with multiple algorithms
  • 📊 Metrics - Built-in metrics collection for monitoring
  • 🧪 Testing-Friendly - In-memory adapter for unit tests

Table of Contents

Installation

npm install cachyer

# With Redis adapter
npm install cachyer ioredis

# With MongoDB adapter (coming soon)
npm install cachyer mongodb

Tree-Shakeable Imports

For smaller bundle sizes, import only the adapter you need:

// Full bundle (~11KB gzipped)
import { Cachyer, createRedisAdapter } from "cachyer";

// Or import adapters separately for smaller bundles:
// Redis only (~2.8KB gzipped)
import { createRedisAdapter, RedisAdapter } from "cachyer/redis";

// Memory only (~3.2KB gzipped)
import { createMemoryAdapter, MemoryAdapter } from "cachyer/memory";

Quick Start

Basic Usage with Redis

import Redis from "ioredis";
import { createRedisCachyer } from "cachyer";

// Create Redis client
const redis = new Redis("redis://localhost:6379");

// Create Cachyer instance
const cache = createRedisCachyer(redis, {
  keyPrefix: "myapp",
  defaultTtl: 3600, // 1 hour
});

// Basic operations
await cache.set("user:1", JSON.stringify({ name: "John" }));
const user = await cache.get("user:1");

// Hash operations
await cache.hset("profile:1", "name", "John");
await cache.hset("profile:1", "email", "[email protected]");

Architecture

Important: Cachyer uses a two-layer architecture:

// Core operations: Use Cachyer (automatic key prefixing)
await cache.get('user:123')
await cache.zadd('leaderboard', [...])

// Advanced features: Use adapter directly (NO key prefixing)
await cache.adapter.xadd('logs', '*', { msg: 'hello' })
await cache.adapter.bfAdd('bloom:users', 'user123')

Why two layers?

  • Cachyer: Core operations everyone needs (get/set/hashes/sorted sets) with key prefixing, metrics, and logging
  • Adapter: Advanced features (streams, bloom filters, hyperloglog, geo) with full control

See ARCHITECTURE.md for detailed explanation, including:

  • When to use Cachyer vs Adapter
  • Key prefixing behavior
  • Examples for different use cases
  • Implementation guidelines

Examples

Check out the examples/ directory for complete, working examples:

  • Social Media Cache - Complete example showing feeds, engagement tracking, activity streams, and social graphs
  • Domain-Specific Types - How to extend Cachyer's generic types for your specific domain

The examples demonstrate:

  • Type-safe key patterns with createKeyPatterns
  • Building domain-specific parameter types
  • Cache service layers for complex applications
  • Using sorted sets, hashes, sets, lists, and streams
  • Best practices for organizing cache logic
  • Using adapter for advanced features (streams, bloom filters)

Quick Example: Core vs Advanced Operations

const cache = new Cachyer({ adapter: redisAdapter, keyPrefix: "myapp" });

// Core operations via Cachyer
await cache.set("user:1", JSON.stringify({ name: "John" }));
await cache.hset("profile:1", "email", "[email protected]");
const profile = await cache.hgetall("profile:1");

// Sorted sets (for feeds, leaderboards)
await cache.zadd("leaderboard", [
  { score: 100, member: "player1" },
  { score: 200, member: "player2" },
]);
const top10 = await cache.zrevrange("leaderboard", 0, 9, { withScores: true });

// Advanced: Redis Streams via adapter
await cache.adapter.xadd("myapp:logs", "*", {
  level: "info",
  message: "User logged in",
  userId: "123",
});

// Advanced: Bloom Filters via adapter
await cache.adapter.bfReserve("myapp:users:bloom", 0.01, 10000);
await cache.adapter.bfAdd("myapp:users:bloom", "user123");
const exists = await cache.adapter.bfExists("myapp:users:bloom", "user123");

### Using the In-Memory Adapter (for testing)

```typescript
import { createMemoryCachyer } from "cachyer";

const cache = createMemoryCachyer({
  keyPrefix: "test",
  maxEntries: 1000,
});

// Works exactly like Redis!
await cache.set("key", "value");

Defining Custom Schemas

Cachyer allows you to define type-safe cache schemas:

import { createSchema, TTL } from "cachyer";

// Define your parameter types
interface UserProfileParams {
  userId: string;
}

// Create a schema
const userProfileSchema = createSchema<UserProfileParams>()
  .name("userProfile")
  .keyPattern("user:profile:{userId}")
  .structure("HASH")
  .ttl(TTL.ONE_HOUR)
  .description("User profile cache")
  .operations((ops) => {
    ops
      .addHashGetAll<UserProfile>()
      .addHashSet()
      .addHashSetMultiple()
      .addDelete()
      .addExists()
      .addExpire()
      .addTtl();
  })
  .build();

// Use with executor
const result = await cache.execute(userProfileSchema.operations.getAll, {
  userId: "123",
});

Pre-built Schema Templates

import {
  createKeyValueSchema,
  createHashSchema,
  createSortedSetSchema,
  createSetSchema,
  createCounterSchema,
} from "cachyer";

// Simple key-value cache
const sessionSchema = createKeyValueSchema<{ sessionId: string }>(
  "session",
  "session:{sessionId}",
  3600 // TTL in seconds
);

// Hash for complex objects
const userSchema = createHashSchema<{ userId: string }>(
  "user",
  "user:{userId}",
  7200
);

// Sorted set for feeds
const feedSchema = createSortedSetSchema<{ userId: string }>(
  "feed",
  "user:feed:{userId}",
  3600,
  500 // maxSize
);

// Set for relationships
const followersSchema = createSetSchema<{ userId: string }>(
  "followers",
  "user:followers:{userId}",
  86400
);

// Counter for rate limiting
const apiCounterSchema = createCounterSchema<{
  userId: string;
  endpoint: string;
}>("apiCounter", "ratelimit:{endpoint}:{userId}", 60);

Custom Operations

For advanced use cases where the built-in operations don't cover your needs, you can define custom operations using addCustomOperation. This gives you full control over the Redis command, arguments, and result parsing.

import type { CacheOperation } from "cachyer";
import { createSchema, createKeyBuilder } from "cachyer";

// Define your key params and operation params
interface GlobalTrendingParams {
  // No key params for static keys
}

interface GlobalTrendingUpdateParams {
  postId: string;
  score: number;
}

// Create a static key builder for global trending
const GlobalKeys = {
  trendingPosts: createKeyBuilder<GlobalTrendingParams>(
    "global:trending:posts"
  ),
};

// Define a custom operation with advanced Redis options
const globalTrendingUpdateIfHigher: CacheOperation<
  GlobalTrendingUpdateParams,
  number
> = {
  command: "ZADD",
  buildArgs: (params) => [
    GlobalKeys.trendingPosts({}),
    "GT", // Only update if new score is greater than current
    params.score,
    params.postId,
  ],
  parseResult: (result) => result as number,
  description: "Update post score only if new score is higher",
};

// Use it in a schema
const trendingSchema = createSchema<GlobalTrendingParams>()
  .name("globalTrending")
  .keyPattern("global:trending:posts")
  .structure("SORTED_SET")
  .ttl(3600)
  .operations((ops) => {
    ops
      .addSortedSetGetRange("getTop", false)
      .addSortedSetGetRange("getTopWithScores", true)
      // Add custom operation for conditional updates
      .addCustomOperation("updateIfHigher", globalTrendingUpdateIfHigher);
  })
  .build();

// Execute the custom operation
const result = await cache.execute(trendingSchema.operations.updateIfHigher, {
  postId: "post:12345",
  score: 1500,
});

console.log(`Updated ${result} entries`);

Custom Operation Structure

A CacheOperation has the following structure:

interface CacheOperation<TParams, TResult> {
  /** The Redis command to execute (e.g., 'ZADD', 'EVAL', 'HSET') */
  command: string;

  /** Function to build command arguments from params */
  buildArgs: (params: TParams) => (string | number)[];

  /** Function to parse the raw Redis result into your desired type */
  parseResult?: (result: unknown) => TResult;

  /** Optional description for documentation */
  description?: string;
}

More Custom Operation Examples

// Custom ZADD with NX (only add new members, don't update existing)
const addNewMemberOnly: CacheOperation<
  { userId: string; member: string; score: number },
  number
> = {
  command: "ZADD",
  buildArgs: (params) => [
    `user:feed:${params.userId}`,
    "NX", // Only add if not exists
    params.score,
    params.member,
  ],
  parseResult: (result) => result as number,
  description: "Add member only if it doesn't exist",
};

// Custom GETEX with expiration refresh
const getAndRefreshTtl: CacheOperation<
  { key: string; ttl: number },
  string | null
> = {
  command: "GETEX",
  buildArgs: (params) => [params.key, "EX", params.ttl],
  parseResult: (result) => result as string | null,
  description: "Get value and refresh TTL atomically",
};

// Custom SETNX for distributed locking
const acquireLock: CacheOperation<
  { lockKey: string; ownerId: string; ttl: number },
  boolean
> = {
  command: "SET",
  buildArgs: (params) => [
    params.lockKey,
    params.ownerId,
    "NX", // Only set if not exists
    "EX", // Set expiration
    params.ttl,
  ],
  parseResult: (result) => result === "OK",
  description: "Acquire a distributed lock",
};

Rate Limiting

Built-in rate limiting service with multiple algorithms:

import { createRedisAdapter, createRateLimitService } from "cachyer";

const adapter = createRedisAdapter({ client: redis });
const rateLimiter = createRateLimitService(adapter, {
  defaultConfig: { maxRequests: 100, windowSeconds: 60 },
  endpoints: {
    "api:create": { maxRequests: 10, windowSeconds: 60 },
    "api:search": { maxRequests: 30, windowSeconds: 60 },
  },
});

// Check rate limit
const result = await rateLimiter.check("user123", "api:create");

if (!result.allowed) {
  console.log(`Rate limited. Retry after ${result.retryAfter}s`);
  // Use result.headers for HTTP response
}

// Sliding window (more accurate)
const slidingResult = await rateLimiter.checkSlidingWindow(
  "user123",
  "api:create"
);

// IP-based rate limiting
const ipResult = await rateLimiter.checkIP("192.168.1.1", {
  maxRequests: 100,
  windowSeconds: 60,
});

Pipeline & Transactions

Execute multiple operations efficiently:

import { pipelineEntry } from "cachyer";

// Pipeline (batched operations)
const result = await cache.pipeline([
  pipelineEntry(userSchema.operations.getAll, { userId: "1" }),
  pipelineEntry(userSchema.operations.getAll, { userId: "2" }),
  pipelineEntry(userSchema.operations.getAll, { userId: "3" }),
]);

// Transaction (atomic)
const txResult = await cache.transaction([
  pipelineEntry(counterSchema.operations.increment, { userId: "1" }),
  pipelineEntry(feedSchema.operations.add, {
    userId: "1",
    member: "post:123",
    score: Date.now(),
  }),
]);

Creating Custom Adapters

Implement the CacheAdapter interface to support any database:

import { CacheAdapter, ConnectionStatus, ExecutorMetrics } from "cachyer";

class MongoCacheAdapter implements CacheAdapter {
  readonly name = "mongodb";
  private _status: ConnectionStatus = "disconnected";

  get status() {
    return this._status;
  }

  async connect(): Promise<void> {
    // Connect to MongoDB
    this._status = "ready";
  }

  async disconnect(): Promise<void> {
    this._status = "disconnected";
  }

  isConnected(): boolean {
    return this._status === "ready";
  }

  async ping(): Promise<boolean> {
    return true;
  }

  // Implement all required methods...
  async set(key: string, value: string, options?: CacheSetOptions) {
    // Store in MongoDB
    return "OK";
  }

  async get(key: string): Promise<string | null> {
    // Retrieve from MongoDB
    return null;
  }

  // ... implement remaining methods
}

Key Patterns

Build type-safe cache keys:

import { createKeyBuilder, createKeyPattern } from "cachyer";

// Create a key builder
const userKey = createKeyBuilder<{ userId: string }>("user:profile:{userId}");
console.log(userKey({ userId: "123" })); // 'user:profile:123'

// With prefix
const userKey = createKeyBuilder<{ userId: string }>("profile:{userId}", {
  prefix: "myapp",
});
console.log(userKey({ userId: "123" })); // 'myapp:profile:123'

// Create patterns for scanning
const pattern = createKeyPattern("user", "profile");
console.log(pattern); // 'user:profile:*'

Advanced: Organized Key Pattern Factory

For large applications with many key patterns, use createKeyPatterns to organize them:

import { createKeyPatterns } from "cachyer";

// Define all your key patterns in one place
const keys = createKeyPatterns(
  {
    user: {
      profile: { pattern: "user:profile:{userId}" },
      feed: { pattern: "user:feed:{userId}" },
      followers: { pattern: "user:followers:{userId}" },
      settings: { pattern: "user:settings:{userId}:{setting}" },
      // Static keys (no parameters)
      allUsers: "user:all",
    },
    post: {
      data: { pattern: "post:{postId}" },
      likes: { pattern: "post:likes:{postId}" },
      comments: { pattern: "post:comments:{postId}" },
    },
    session: {
      token: { pattern: "session:token:{token}" },
      user: { pattern: "session:user:{userId}" },
    },
  },
  { prefix: "myapp" }
);

// Type-safe usage with autocomplete
const profileKey = keys.user.profile({ userId: "123" });
// 'myapp:user:profile:123'

const settingsKey = keys.user.settings({ userId: "123", setting: "theme" });
// 'myapp:user:settings:123:theme'

const allUsersKey = keys.user.allUsers();
// 'myapp:user:all'

const postKey = keys.post.data({ postId: "456" });
// 'myapp:post:456'

Benefits of createKeyPatterns:

  • ✅ Centralized key management
  • ✅ Full TypeScript type safety and autocomplete
  • ✅ Consistent key structure across your app
  • ✅ Easy refactoring - change patterns in one place
  • ✅ Supports both parameterized and static keys

TTL Presets

import { TTL } from "cachyer";

TTL.ONE_MINUTE; // 60
TTL.FIVE_MINUTES; // 300
TTL.FIFTEEN_MINUTES; // 900
TTL.THIRTY_MINUTES; // 1800
TTL.ONE_HOUR; // 3600
TTL.TWO_HOURS; // 7200
TTL.SIX_HOURS; // 21600
TTL.TWELVE_HOURS; // 43200
TTL.ONE_DAY; // 86400
TTL.ONE_WEEK; // 604800
TTL.ONE_MONTH; // 2592000

Metrics

// Get metrics
const metrics = cache.getMetrics();
console.log(metrics);
// {
//   totalOperations: 1000,
//   successfulOperations: 995,
//   failedOperations: 5,
//   totalExecutionTimeMs: 5000,
//   avgExecutionTimeMs: 5,
//   operationCounts: { GET: 500, SET: 300, ... }
// }

// Reset metrics
cache.resetMetrics();

Error Handling

import { CacheError, CacheErrorCode } from "cachyer";

try {
  await cache.execute(operation, params);
} catch (error) {
  if (error instanceof CacheError) {
    switch (error.code) {
      case CacheErrorCode.CONNECTION_ERROR:
        // Handle connection issues
        break;
      case CacheErrorCode.TIMEOUT_ERROR:
        // Handle timeouts
        break;
      case CacheErrorCode.COMMAND_ERROR:
        // Handle command failures
        break;
    }
  }
}

Configuration Options

const cache = new Cachyer({
  adapter: createRedisAdapter({ client: redis }),

  // Global key prefix
  keyPrefix: "myapp",

  // Default TTL for all operations
  defaultTtl: 3600,

  // Custom serializer
  serializer: {
    serialize: (value) => JSON.stringify(value),
    deserialize: (value) => JSON.parse(value.toString()),
  },

  // Custom logger
  logger: {
    debug: (msg, meta) => console.debug(msg, meta),
    info: (msg, meta) => console.info(msg, meta),
    warn: (msg, meta) => console.warn(msg, meta),
    error: (msg, meta) => console.error(msg, meta),
  },

  // Default execution options
  defaultOptions: {
    timeout: 5000,
    retries: 2,
    retryDelay: 100,
    throwOnError: true,
  },

  // Enable metrics collection
  enableMetrics: true,

  // Auto-connect on creation
  autoConnect: true,
});

API Reference

Cachyer Class

| Method | Description | | --------------------------------------- | --------------------------------- | | connect() | Connect to the cache backend | | disconnect() | Disconnect from the cache backend | | isConnected() | Check if connected | | ping() | Ping the backend | | get(key) | Get a string value | | set(key, value, options?) | Set a string value | | del(...keys) | Delete keys | | exists(...keys) | Check if keys exist | | expire(key, seconds) | Set expiration | | ttl(key) | Get TTL | | hget(key, field) | Get hash field | | hset(key, field, value) | Set hash field | | hgetall(key) | Get all hash fields | | zadd(key, scoreMembers) | Add to sorted set | | zrange(key, start, stop, options?) | Get sorted set range | | zrevrange(key, start, stop, options?) | Get reverse sorted set range | | incr(key) | Increment value | | sadd(key, ...members) | Add to set | | smembers(key) | Get set members | | lpush(key, ...values) | Push to list | | lrange(key, start, stop) | Get list range | | execute(operation, params) | Execute a cache operation | | executeScript(script, keys, args) | Execute a Lua script | | pipeline(entries) | Execute operations in pipeline | | transaction(entries) | Execute operations in transaction | | getMetrics() | Get execution metrics | | resetMetrics() | Reset metrics |

License

MIT