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

@periodic/osmium

v1.0.6

Published

Production-grade Redis caching middleware for Express with tag-based invalidation, auto-caching, and cluster support

Downloads

277

Readme

🧪 Periodic Osmium

npm version License: MIT TypeScript

Production-grade Redis caching middleware for Express.js

Part of the Periodic series of Node.js middleware packages by Uday Thakur.


✨ Features

  • Auto-caching - Automatic caching for GET requests with zero code changes
  • 🏷️ Tag-based invalidation - Intelligently invalidate related cache entries
  • 🔄 Pattern matching - Bulk cache clearing with wildcard patterns
  • 🎯 User-specific caching - Built-in support for authenticated user caching
  • 🚀 Non-blocking operations - Invalidation happens after response for optimal latency
  • 🔒 Redis Cluster support - Horizontal scaling with Redis Cluster
  • 🛡️ TypeScript - Full type safety and IntelliSense support
  • 🎨 Flexible strategies - Auto, manual, or disabled caching per route
  • ⚙️ Configurable TTL - Per-route or global time-to-live settings

📦 Installation

npm install @periodic/osmium ioredis

Or with yarn:

yarn add @periodic/osmium ioredis

🚀 Quick Start

import express from 'express';
import { createRedisClient, cacheMiddleware } from '@periodic/osmium';

const app = express();

// Create Redis client
const redis = createRedisClient({
  host: 'localhost',
  port: 6379,
});

// Auto-cache GET requests
app.get(
  '/api/users',
  cacheMiddleware(redis, {
    ttl: 300, // Cache for 5 minutes
    autoCache: {
      tags: ['users'], // Tag for invalidation
    },
  }),
  async (req, res) => {
    const users = await getUsersFromDatabase();
    res.json(users);
  }
);

// Invalidate cache on mutation
app.post(
  '/api/users',
  cacheMiddleware(redis, {
    invalidate: {
      tags: ['users'], // Invalidate all 'users' tagged cache
      afterResponse: true, // Non-blocking invalidation
    },
  }),
  async (req, res) => {
    const newUser = await createUser(req.body);
    res.json(newUser);
  }
);

app.listen(3000);

📖 Core Concepts

1. Auto-Caching (GET Requests)

Auto-caching automatically stores responses from GET requests and serves them from cache on subsequent requests.

app.get(
  '/api/products',
  cacheMiddleware(redis, {
    strategy: 'auto',
    ttl: 600, // 10 minutes
    autoCache: {
      enabled: true,
      tags: ['products', 'catalog'],
    },
  }),
  getProductsController
);

How it works:

  1. First request → Cache MISS → Executes controller → Caches response
  2. Subsequent requests → Cache HIT → Returns cached data immediately
  3. Response headers indicate cache status: X-Cache: HIT or X-Cache: MISS

2. Tag-Based Invalidation

Tags allow you to group related cache entries and invalidate them together.

// Cache with tags
app.get('/api/products/:id', 
  cacheMiddleware(redis, {
    autoCache: {
      tags: (req) => ['products', `product:${req.params.id}`],
    },
  }),
  getProductController
);

// Invalidate specific product
app.put('/api/products/:id',
  cacheMiddleware(redis, {
    invalidate: {
      tags: (req) => [`product:${req.params.id}`],
      afterResponse: true,
    },
  }),
  updateProductController
);

// Invalidate all products
app.post('/api/products',
  cacheMiddleware(redis, {
    invalidate: {
      tags: ['products'],
      patterns: ['list:*'], // Also clear list caches
    },
  }),
  createProductController
);

3. Cache Strategies

Auto Strategy (Default)

cacheMiddleware(redis, { strategy: 'auto' })
// GET → Auto-cached
// POST/PUT/DELETE → Auto-invalidated

Manual Strategy

app.get('/api/custom',
  cacheMiddleware(redis, { strategy: 'manual' }),
  async (req, res) => {
    // Use req.cache manually
    const cached = await req.cache.get('my-key');
    if (cached) return res.json(cached);
    
    const data = await fetchData();
    await req.cache.set('my-key', data, 300);
    res.json(data);
  }
);

None Strategy

cacheMiddleware(redis, { strategy: 'none' })
// Cache service attached but no automatic behavior

🎯 Advanced Usage

User-Specific Caching

Cache different responses for different users:

app.get(
  '/api/dashboard',
  authMiddleware,
  cacheMiddleware(redis, {
    autoCache: {
      includeAuth: true,
      tags: (req) => [`user:${req.user.userId}:dashboard`],
    },
  }),
  getDashboardController
);

Custom Cache Keys

Generate custom cache keys based on query parameters:

app.get(
  '/api/search',
  cacheMiddleware(redis, {
    autoCache: {
      keyGenerator: (req) => {
        const { query, page, limit } = req.query;
        return `search:${query}:${page}:${limit}`;
      },
      tags: ['search-results'],
    },
  }),
  searchController
);

Conditional Caching

Only cache under certain conditions:

app.get(
  '/api/data',
  cacheMiddleware(redis, {
    autoCache: {
      condition: (req) => {
        // Only cache if user is not admin
        return req.user?.role !== 'admin';
      },
    },
  }),
  getDataController
);

Pattern-Based Invalidation

Clear multiple related cache entries:

app.post(
  '/api/bulk-update',
  cacheMiddleware(redis, {
    invalidate: {
      patterns: ['list:*', 'search:*', 'detail:*'],
      afterResponse: true,
    },
  }),
  bulkUpdateController
);

🔧 API Reference

createRedisClient(config: RedisConfig)

Creates a Redis client instance.

Parameters:

  • config.host - Redis host (default: 'localhost')
  • config.port - Redis port (default: 6379)
  • config.password - Redis password
  • config.clusterNodes - Array of cluster nodes (e.g., ['node1:6379', 'node2:6379'])
  • config.isProduction - Enable cluster mode (default: false)

Returns: Redis or Cluster instance

cacheMiddleware(redisClient, config: CacheConfig)

Express middleware for caching.

Parameters:

  • config.strategy - 'auto' | 'manual' | 'none' (default: 'auto')
  • config.ttl - Time to live in seconds (default: 3600)
  • config.namespace - Cache namespace (default: 'app')
  • config.autoCache - Auto-cache configuration
  • config.invalidate - Invalidation configuration

Returns: Express middleware function

Cache Service Methods (via req.cache)

// Get cached value
await req.cache.get(key: string): Promise<any>

// Set cache value
await req.cache.set(key: string, value: any, ttl?: number, tags?: string[]): Promise<boolean>

// Delete specific key
await req.cache.del(key: string): Promise<boolean>

// Invalidate by tags
await req.cache.invalidateByTags(tags: string[]): Promise<number>

// Delete by pattern
await req.cache.delPattern(pattern: string): Promise<number>

// Get or set (cache-aside pattern)
await req.cache.getOrSet(key: string, fetcher: () => Promise<any>, ttl?: number, tags?: string[]): Promise<any>

// Check if key exists
await req.cache.exists(key: string): Promise<boolean>

// Get TTL
await req.cache.ttl(key: string): Promise<number>

// Increment counter
await req.cache.incr(key: string): Promise<number>

// Health check
await req.cache.healthCheck(): Promise<boolean>

📊 Real-World Example

import express from 'express';
import { createRedisClient, cacheMiddleware } from '@periodic/osmium';

const app = express();
const redis = createRedisClient({ host: 'localhost', port: 6379 });

// List all products (cached for 5 minutes)
app.get(
  '/api/products',
  cacheMiddleware(redis, {
    ttl: 300,
    autoCache: {
      tags: ['products-list'],
      keyGenerator: (req) => {
        const { page = 1, limit = 10, category } = req.query;
        return `products:page:${page}:limit:${limit}:category:${category}`;
      },
    },
  }),
  async (req, res) => {
    const products = await db.products.findMany(req.query);
    res.json(products);
  }
);

// Get single product (cached for 10 minutes)
app.get(
  '/api/products/:id',
  cacheMiddleware(redis, {
    ttl: 600,
    autoCache: {
      tags: (req) => ['products', `product:${req.params.id}`],
      keyGenerator: (req) => `product:${req.params.id}`,
    },
  }),
  async (req, res) => {
    const product = await db.products.findById(req.params.id);
    res.json(product);
  }
);

// Create product - invalidate lists and searches
app.post(
  '/api/products',
  cacheMiddleware(redis, {
    invalidate: {
      tags: ['products-list', 'products-search'],
      patterns: ['products:page:*'],
      afterResponse: true,
    },
  }),
  async (req, res) => {
    const product = await db.products.create(req.body);
    res.json(product);
  }
);

// Update product - invalidate specific product and lists
app.put(
  '/api/products/:id',
  cacheMiddleware(redis, {
    invalidate: {
      tags: (req) => [
        `product:${req.params.id}`,
        'products-list',
        'products-search',
      ],
      keys: (req) => [`product:${req.params.id}`],
      afterResponse: true,
    },
  }),
  async (req, res) => {
    const product = await db.products.update(req.params.id, req.body);
    res.json(product);
  }
);

app.listen(3000);

🎨 Cache Headers

The middleware adds helpful headers to responses:

  • X-Cache: HIT - Response served from cache
  • X-Cache: MISS - Response generated and cached
  • X-Cache: ERROR - Cache error occurred
  • X-Cache-Key: <key> - The cache key used

Monitor these in your logs or browser dev tools!


🔍 Best Practices

1. Choose Appropriate TTLs

// Frequently changing data - short TTL
ttl: 60  // 1 minute

// Relatively stable data - medium TTL
ttl: 300  // 5 minutes

// Rarely changing data - long TTL
ttl: 3600  // 1 hour

2. Use Namespaces

// Separate caches by app or environment
cacheMiddleware(redis, {
  namespace: 'prod:api',
  // vs
  namespace: 'dev:api',
})

3. Tag Strategically

// Hierarchical tags for granular invalidation
tags: [
  'products',              // All products
  `category:${catId}`,     // Category-specific
  `product:${productId}`,  // Single product
]

4. Non-Blocking Invalidation

// Always use afterResponse for mutations
invalidate: {
  afterResponse: true, // User gets response immediately
}

5. Monitor Cache Performance

app.get('/health/cache', async (req, res) => {
  const healthy = await req.cache.healthCheck();
  res.json({ redis: healthy ? 'ok' : 'error' });
});

🧪 Testing

# Run tests
npm test

# Run tests with coverage
npm run test:coverage

# Run tests in watch mode
npm run test:watch

Note: Tests require a running Redis instance on localhost:6379 (or configure via environment variables).


🔒 Environment Variables

Recommended .env setup:

# Development
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=

# Production
NODE_ENV=production
REDIS_CLUSTER_NODES=node1:6379,node2:6379,node3:6379
REDIS_PASSWORD=your-secure-password

📈 Performance

Periodic Osmium is designed for high performance:

  • Non-blocking SCAN for pattern deletion (doesn't block Redis)
  • Pipeline operations for bulk invalidation
  • Efficient tag-based lookups using Redis sets
  • Lazy connections to reduce startup time

🤝 Related Packages

Part of the Periodic series:


📝 License

MIT © Uday Thakur


🙏 Contributing

Contributions are welcome! Please read CONTRIBUTING.md for details.


📞 Support


🌟 Show Your Support

Give a ⭐️ if this project helped you!


Made with ❤️ by Uday Thakur