Framework Integrations

Layercache provides first-class integrations with popular Node.js frameworks and observability tools. Each integration is designed to be lightweight, type-safe, and minimal in configuration.

Express

The Express middleware caches JSON responses from your route handlers.

Installation

npm install layercache

Basic Usage

import express from 'express'
import { CacheStack, MemoryLayer, RedisLayer, createExpressCacheMiddleware } from 'layercache'
import Redis from 'ioredis'

const cache = new CacheStack([
  new MemoryLayer({ ttl: 60_000 }),
  new RedisLayer({ client: new Redis(), ttl: 300_000 })
])

const app = express()

// Cache all GET requests to /api/users
app.get('/api/users',
  createExpressCacheMiddleware(cache, {
    ttl: 30_000,
    tags: ['users']
  }),
  async (req, res) => {
    const users = await fetchUsersFromDB()
    res.json(users)
  }
)

app.listen(3000)

Custom Cache Keys

app.get('/api/users/:id',
  createExpressCacheMiddleware(cache, {
    keyResolver: (req) => `user:${req.params.id}`,
    ttl: 300_000
  }),
  async (req, res) => {
    const user = await fetchUser(req.params.id)
    res.json(user)
  }
)

Options

  • keyResolver - Function to generate cache keys from requests
  • methods - HTTP methods to cache (default: ['GET'])
  • ttl - Time-to-live in milliseconds
  • tags - Tags for invalidation
  • allowPrivateCaching - Allow implicit URL-based keys (default: false). Implicit URL keys omit common sensitive query parameters such as access_token, api_key, apikey, auth, authorization, code, credentials, id_token, jwt, password, private_key, refresh_token, secret, session, sessionid, session_id, and token; upgrading can therefore cause cache misses for older entries that included those parameters.

Only 2xx JSON responses are written to the cache. 3xx, 4xx, and 5xx responses still return to the client but are not cached.

Response Headers

The middleware adds x-cache: HIT or x-cache: MISS headers to responses.

Fastify

The Fastify plugin decorates your app with a cache instance and provides an optional stats endpoint.

Installation

npm install layercache

Basic Usage

import Fastify from 'fastify'
import { CacheStack, MemoryLayer, RedisLayer, createFastifyLayercachePlugin } from 'layercache'
import Redis from 'ioredis'

const cache = new CacheStack([
  new MemoryLayer({ ttl: 60_000 }),
  new RedisLayer({ client: new Redis(), ttl: 300_000 })
])

const fastify = Fastify()

await fastify.register(createFastifyLayercachePlugin(cache, {
  exposeStatsRoute: true,
  statsPath: '/cache/stats',
  allowPublicStatsRoute: false
}))

// Use the cache directly in routes
fastify.get('/api/users', async (request, reply) => {
  const users = await cache.get('users', () => fetchUsersFromDB())
  return users
})

Options

  • exposeStatsRoute - Enable stats endpoint (default: false)
  • statsPath - Path for stats endpoint (default: '/cache/stats')
  • allowPublicStatsRoute - Allow public access (default: false)
  • authorizeStatsRoute - Async authorization function
  • unauthorizedStatusCode - Status code for unauthorized (default: 403)

Hono

The Hono middleware caches JSON responses with minimal overhead.

Installation

npm install layercache

Basic Usage

import { Hono } from 'hono'
import { CacheStack, MemoryLayer, createHonoCacheMiddleware } from 'layercache'

const cache = new CacheStack([
  new MemoryLayer({ ttl: 60_000 })
])

const app = new Hono()

app.use('/api/*', createHonoCacheMiddleware(cache, {
  ttl: 60_000,
  tags: ['api']
}))

app.get('/api/users', async (c) => {
  const users = await fetchUsersFromDB()
  return c.json(users)
})

Custom Cache Keys

app.get('/api/users/:id',
  createHonoCacheMiddleware(cache, {
    keyResolver: (req) => `user:${req.path.split('/').pop()}`,
    ttl: 300_000
  }),
  async (c) => {
    const id = c.req.param('id')
    const user = await fetchUser(id)
    return c.json(user)
  }
)

Options

  • keyResolver - Function to generate cache keys from Hono requests
  • methods - HTTP methods to cache (default: ['GET'])
  • ttl - Time-to-live in milliseconds
  • tags - Tags for invalidation
  • allowPrivateCaching - Allow implicit URL-based keys (default: false). Implicit URL keys use the same sensitive query parameter filtering as the Express middleware.

Only 2xx JSON responses are written to the cache. The middleware also respects status set via context.status(500) before context.json(body).

NestJS

Use CacheStack directly in your NestJS providers. Import from layercache — no separate package needed.

Module Setup

import { Module } from '@nestjs/common'
import { CacheStack, MemoryLayer, RedisLayer } from 'layercache'
import Redis from 'ioredis'

@Module({
  providers: [
    {
      provide: 'CACHE_STACK',
      useFactory: () => new CacheStack([
        new MemoryLayer({ ttl: 60_000 }),
        new RedisLayer({ client: new Redis(), ttl: 300_000 })
      ])
    }
  ],
  exports: ['CACHE_STACK']
})
export class CacheModule {}

Async Configuration

import { Module } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { CacheStack, MemoryLayer, RedisLayer } from 'layercache'
import Redis from 'ioredis'

@Module({
  providers: [
    {
      provide: 'CACHE_STACK',
      inject: [ConfigService],
      useFactory: (config: ConfigService) => new CacheStack([
        new MemoryLayer({ ttl: 60_000 }),
        new RedisLayer({
          client: new Redis(config.get('REDIS_URL')),
          ttl: 300_000
        })
      ])
    }
  ],
  exports: ['CACHE_STACK']
})
export class CacheModule {}

Using in Services

import { Injectable, Inject } from '@nestjs/common'
import { CacheStack } from 'layercache'

@Injectable()
export class UsersService {
  constructor(
    @Inject('CACHE_STACK') private readonly cache: CacheStack
  ) {}

  async getUser(id: string): Promise<User> {
    return this.cache.get(`user:${id}`, () =>
      this.usersRepository.findOne(id)
    )
  }
}

Note: The separate @cachestack/nestjs package was removed in v1.3.2. Use CacheStack directly from layercache.

tRPC

The tRPC middleware caches procedure results based on input arguments.

Installation

npm install layercache

Basic Usage

import { initTRPC } from '@trpc/server'
import { CacheStack, MemoryLayer, createTrpcCacheMiddleware } from 'layercache'

const cache = new CacheStack([
  new MemoryLayer({ ttl: 60_000 })
])

const t = initTRPC.create()

const cacheMiddleware = createTrpcCacheMiddleware(cache, 'trpc', {
  keyResolver: (input) => JSON.stringify(input),
  ttl: 300_000
})

export const cachedProcedure = t.procedure.use(cacheMiddleware)

export const appRouter = t.router({
  user: cachedProcedure
    .input((val: unknown) => val as { id: string })
    .query(async ({ input }) => {
      return fetchUser(input.id)
    })
})

Context-Aware Caching

const cacheMiddleware = createTrpcCacheMiddleware(cache, 'user', {
  keyResolver: (input, path, type) => {
    return `${type}:${path}:${JSON.stringify(input)}`
  },
  ttl: 300_000
})

GraphQL

Cache resolver results with the GraphQL wrapper.

Installation

npm install layercache

Basic Usage

import { CacheStack, MemoryLayer, cacheGraphqlResolver } from 'layercache'

const cache = new CacheStack([
  new MemoryLayer({ ttl: 60_000 })
])

const resolvers = {
  Query: {
    user: cacheGraphqlResolver(
      cache,
      'user',
      async (_root, { id }) => {
        return fetchUser(id)
      },
      {
        keyResolver: (_root, { id }) => id,
        ttl: 300_000
      }
    )
  }
}

With Tags

const resolvers = {
  Query: {
    user: cacheGraphqlResolver(
      cache,
      'user',
      async (_root, { id }) => {
        return fetchUser(id)
      },
      {
        keyResolver: (_root, { id }) => id,
        ttl: 300_000,
        tags: ({ id }) => ['user', `user:${id}`]
      }
    )
  }
}

OpenTelemetry

Add distributed tracing to cache operations with OpenTelemetry integration.

Installation

npm install layercache @opentelemetry/api

Basic Setup

import { trace } from '@opentelemetry/api'
import { CacheStack, MemoryLayer, createOpenTelemetryPlugin } from 'layercache'

const cache = new CacheStack([
  new MemoryLayer({ ttl: 60_000 })
])

const tracer = trace.getTracer('layercache')

const plugin = createOpenTelemetryPlugin(cache, tracer)

// Cache operations are now traced
await cache.get('user:123', fetchUser)

// Clean up on shutdown
plugin.uninstall()

Span Attributes

Each cache operation creates a span with the following attributes:

  • layercache.success - Whether the operation succeeded
  • layercache.result - The result type (hit, miss, etc.)
  • Error details if the operation failed

Custom Tracer

import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
import { Resource } from '@opentelemetry/resources'

const provider = new NodeTracerProvider({
  resource: new Resource({ service: 'my-app' })
})
provider.register()

const tracer = trace.getTracer('my-app', '1.0.0')
const plugin = createOpenTelemetryPlugin(cache, tracer)

Stats HTTP Handler

Expose cache statistics via HTTP for monitoring dashboards.

Basic Usage

import { createCacheStatsHandler } from 'layercache'
import http from 'node:http'

const cache = new CacheStack([
  new MemoryLayer({ ttl: 60_000 })
])

const handler = createCacheStatsHandler(cache)

const server = http.createServer(handler)
server.listen(9090)

With Authorization

const handler = createCacheStatsHandler(cache, {
  authorize: async (req) => {
    const authHeader = req.headers['authorization']
    return authHeader === `Bearer ${process.env.API_KEY}`
  },
  unauthorizedStatusCode: 401
})

Public Access

const handler = createCacheStatsHandler(cache, {
  allowPublicAccess: true
})

Response Format

{
  "metrics": {
    "hits": 1234,
    "misses": 56,
    "fetches": 56,
    "sets": 890,
    "deletes": 12,
    "backfills": 234,
    "invalidations": 45,
    "staleHits": 5,
    "refreshes": 3,
    "refreshErrors": 0,
    "writeFailures": 0,
    "singleFlightWaits": 7,
    "negativeCacheHits": 12,
    "circuitBreakerTrips": 0,
    "degradedOperations": 0
  },
  "layers": [
    {
      "name": "memory",
      "isLocal": true,
      "degradedUntil": null
    },
    {
      "name": "redis",
      "isLocal": false,
      "degradedUntil": null
    }
  ],
  "backgroundRefreshes": 3
}