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

express-memorize

v2.3.0

Published

In-memory cache middleware for Express.js. Caches GET responses with optional TTL — zero dependencies, fully typed.

Readme

express-memorize


Features

  • Caches GET responses automatically when status code is 2xx
  • Works with Express, Fastify, Koa, NestJS, Hono, Fetch API / serverless, and direct service-level usage
  • Per-route TTL override and noCache bypass
  • maxEntries cap with LRU eviction to bound memory usage
  • Size metrics: size(), byteSize(), getStats()
  • Service-level cache: remember(), set(), getValue()
  • Pluggable serializer: 'auto' (node:v8 when available, else JSON), 'json', 'v8', or custom
  • Event hooks: set, delete, expire, evict
  • Cache inspection and invalidation API (get, getAll, delete, deleteMatching, clear)
  • Hit counter per cache entry
  • X-Cache: HIT | MISS | BYPASS response header
  • Zero runtime dependencies, fully typed

Installation

npm install express-memorize

Adapters for non-Express runtimes are optional — install only what you need:

npm install fastify   # only if using the Fastify adapter
npm install koa @koa/router   # only if using the Koa adapter
npm install hono   # only if using the Hono adapter
npm install @nestjs/common @nestjs/core rxjs   # only if using the NestJS adapter

Quick Start

Express

import express from 'express';
import { memorize } from 'express-memorize';

const app = express();
const cache = memorize({ ttl: 30_000 });

app.get('/users', cache(), async (req, res) => {
  const users = await db.getUsers();
  res.json({ data: users });
});

app.listen(3000);

Fastify

import Fastify from 'fastify';
import { memorize } from 'express-memorize';
import { createFastifyPlugin } from 'express-memorize/fastify';

const app = Fastify();
const cache = memorize({ ttl: 30_000 });

await app.register(createFastifyPlugin(cache));

app.get('/users', async () => {
  return usersService.findAll();
});

Koa

import Koa from 'koa';
import Router from '@koa/router';
import { memorize } from 'express-memorize';
import { createKoaMiddleware } from 'express-memorize/koa';

const app = new Koa();
const router = new Router();
const cache = memorize({ ttl: 30_000 });

router.get('/users', createKoaMiddleware(cache), async (ctx) => {
  ctx.body = await usersService.findAll();
});

app.use(router.routes());
app.use(router.allowedMethods());

Hono

import { Hono } from 'hono';
import { memorize } from 'express-memorize';
import { createHonoMiddleware } from 'express-memorize/hono';

const app = new Hono();
const cache = memorize({ ttl: 30_000 });

app.get('/users', createHonoMiddleware(cache), async (c) => {
  return c.json(await usersService.findAll());
});

NestJS

import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import {
  MemorizeCacheKey,
  MemorizeInterceptor,
  MemorizeModule,
  MemorizeTtl,
} from 'express-memorize/nestjs';

@Module({
  imports: [MemorizeModule.forRoot({ ttl: 30_000 })],
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useExisting: MemorizeInterceptor,
    },
  ],
})
export class AppModule {}

export class UsersController {
  @MemorizeCacheKey('users:list')
  @MemorizeTtl(10_000)
  findAll() {
    return usersService.findAll();
  }
}

Fetch API / Serverless

import { memorize } from 'express-memorize';
import { cacheFetchHandler } from 'express-memorize/fetch';

const cache = memorize({ ttl: 30_000 });

export default cacheFetchHandler(cache, async (request) => {
  const users = await usersService.findAll();
  return Response.json(users);
});

Service-level caching

Cache arbitrary values directly — no HTTP layer required.

const cache = memorize({ ttl: 60_000 });

// Compute-and-cache pattern
const users = await cache.remember('users:list', () => usersService.findAll());

// Explicit set/get
cache.set('config', appConfig);
const config = cache.getValue<AppConfig>('config');

Serializer

The serializer option controls how values passed to set() / getValue() / remember() are stored internally. It does not affect HTTP middleware caching — adapters store response bodies as-is.

| Value | Serializes to | Handles Date, Map, Set, Buffer | Runtime | |-------|--------------|----------------------------------------|---------| | 'auto' (default) | Buffer (v8) or string (JSON) | Yes — when node:v8 is available | Any | | 'json' | string | No | Any (edge runtimes, human-readable) | | 'v8' | Buffer | Yes | Node.js / Bun — throws at construction otherwise | | Custom object | user-defined | user-defined | Any |

// auto (default): uses node:v8 when available, falls back to JSON silently
const cache = memorize();

// Always JSON — useful for edge runtimes or when you need human-readable bodies
const cache = memorize({ serializer: 'json' });

// Always v8 — throws at construction if node:v8 is not available
const cache = memorize({ serializer: 'v8' });

// Custom serializer — bring your own (MessagePack, CBOR, etc.)
import { pack, unpack } from 'msgpackr';
const cache = memorize({
  serializer: {
    serialize:   (v) => Buffer.from(pack(v)),
    deserialize: (d) => unpack(d as Buffer),
  },
});

With 'v8' or 'auto', set() correctly round-trips types that JSON cannot represent:

const cache = memorize({ serializer: 'v8', ttl: Infinity });

cache.set('created', new Date());
cache.getValue<Date>('created');   // Date instance preserved

cache.set('roles', new Set(['admin', 'editor']));
cache.getValue<Set<string>>('roles');  // Set instance preserved

Usage

Global middleware (Express)

const cache = memorize({ ttl: 60_000 });

app.use(cache()); // applies to all GET routes

Per-route TTL override

const cache = memorize({ ttl: 60_000 }); // global: 60s

app.get('/users',    cache(),                  handler); // 60s
app.get('/products', cache({ ttl: 10_000 }),    handler); // 10s
app.get('/config',   cache({ ttl: Infinity }),  handler); // no expiry

noCache bypass

app.get('/live-feed', cache({ noCache: true }), handler);
// Sets X-Cache: BYPASS, never reads or writes the cache

Fastify route-level usage

import { createFastifyPreHandler } from 'express-memorize/fastify';

app.get(
  '/users',
  {
    preHandler: createFastifyPreHandler(cache, { ttl: 10_000 }),
  },
  async () => usersService.findAll(),
);

GraphQL caching

GraphQL is not a simple route-level caching problem. A single POST /graphql endpoint can execute different operations, use variables, depend on the current viewer, and return partial data with errors. For now, the recommended strategy is service-level caching with remember() inside resolvers or the services they call.

const cache = memorize({ ttl: 30_000 });

const resolvers = {
  Query: {
    user: (_parent, args, context) => {
      const viewerScope = context.user ? `user:${context.user.id}` : 'anonymous';
      return cache.remember(
        `graphql:${viewerScope}:user:${args.id}`,
        () => usersService.findVisibleById(args.id, context.user),
      );
    },
  },
  Mutation: {
    updateUser: async (_parent, args) => {
      const user = await usersService.update(args.id, args.input);
      cache.deleteMatching(`graphql:*:user:${args.id}`);
      return user;
    },
  },
};

GraphQL cache key rules:

  • Include every input that can change the result: operation name, normalized query or field name, variables, locale, feature flags, and any relevant authorization scope.
  • Do not share cached data across users unless the resolver result is genuinely public.
  • Keep mutation invalidation explicit with delete() or deleteMatching(); automatic invalidation is too schema-specific for a generic adapter.
  • Avoid caching responses that contain GraphQL errors unless your application has a deliberate policy for partial data.
  • Prefer resolver or service-level caching for expensive data fetches. Operation-level caching may be considered later for public, query-only workloads with strict keying rules.

There is currently no dedicated GraphQL adapter. If one is added later, the first practical target should be Apollo Server, because its plugin lifecycle can cache complete operation responses without coupling the core package entry point to GraphQL. NestJS GraphQL, Mercurius, and Yoga integrations should stay separate implementation issues unless a shared GraphQL abstraction emerges.

NestJS decorators

Use MemorizeInterceptor on a controller or globally, then configure caching at the controller or method level.

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import {
  MemorizeCacheKey,
  MemorizeInterceptor,
  MemorizeNoCache,
  MemorizeTtl,
} from 'express-memorize/nestjs';

@Controller('users')
@UseInterceptors(MemorizeInterceptor)
@MemorizeTtl(30_000)
export class UsersController {
  @Get()
  @MemorizeCacheKey('users:list')
  findAll() {
    return usersService.findAll();
  }

  @Get('live')
  @MemorizeNoCache()
  live() {
    return usersService.live();
  }
}

For global usage, import MemorizeModule.forRoot() and register APP_INTERCEPTOR with useExisting: MemorizeInterceptor so the interceptor receives the module's shared cache instance.

Cache invalidation

app.post('/users', (req, res) => {
  users.push(req.body);
  cache.delete('/users');
  res.status(201).json(req.body);
});

Pattern-based invalidation

Use cache.deleteMatching(pattern) to remove entries by glob pattern.

app.put('/users/:id', (req, res) => {
  users.update(req.params.id, req.body);
  const deleted = cache.deleteMatching(`**/users/${req.params.id}*`);
  console.log(`${deleted} entries removed`);
  res.json({ ok: true });
});

Glob rules:

| Pattern | Behaviour | |---------|-----------| | * | Matches any sequence within a single path segment (does not cross /) | | ** | Matches any sequence across path segments (crosses /) | | ? | Matches any single character except / |

Bounding memory with maxEntries

Prevent unbounded growth by setting a maximum number of entries. When the limit is reached, the least-recently-used (LRU) entry is evicted before the new one is stored.

const cache = memorize({ ttl: 30_000, maxEntries: 1_000 });

Size metrics

cache.size();      // number of active entries
cache.byteSize();  // approximate total body size in bytes
cache.getStats();  // { entries, maxEntries, byteSize }

byteSize() is an estimate based on UTF-8 encoding for strings and byteLength for buffers. It may not reflect actual VM memory usage.

Inspect the cache

cache.get('/users');   // CacheInfo | null
cache.getAll();        // Record<string, CacheInfo>

CacheInfo shape:

{
  key: string;
  body: unknown;
  statusCode: number;
  contentType: string;
  expiresAt: number | null;
  remainingTtl: number | null; // ms until expiry, null when ttl is Infinity
  hits: number;                // times this key was served from cache
  size: number;                // approximate body size in bytes
}

hits starts at 1 on the initial cache miss and increments on every hit. It resets to 1 if the entry is evicted and re-cached.

Event hooks

import { MemorizeEventType } from 'express-memorize';

cache.on(MemorizeEventType.Set,    (e) => console.log('stored',  e.key));
cache.on(MemorizeEventType.Delete, (e) => console.log('deleted', e.key));
cache.on(MemorizeEventType.Expire, (e) => console.log('expired', e.key));
cache.on(MemorizeEventType.Evict,  (e) => console.log('evicted', e.key)); // maxEntries LRU
cache.on(MemorizeEventType.Empty,  ()  => console.log('cache is empty'));

API Reference

memorize(options?)

Creates a cache instance. Returns a Memorize object.

| Option | Type | Default | Description | |--------|------|---------|-------------| | ttl | number | 60_000 | Time-to-live in milliseconds. Pass Infinity for no expiry. | | maxEntries | number | undefined | Maximum number of entries. LRU eviction when reached. | | serializer | 'auto' \| 'json' \| 'v8' \| Serializer | 'auto' | Serializer for set() / getValue(). 'auto' uses node:v8 when available, falls back to JSON. Does not affect HTTP middleware caching. |

cache(options?) / cache.express(options?)

Returns an Express RequestHandler. cache() is a backwards-compatible alias for cache.express().

| Option | Type | Default | Description | |--------|------|---------|-------------| | ttl | number | global ttl | TTL override for this route. Pass Infinity for no expiry. | | noCache | boolean | false | Skip cache entirely. Sets X-Cache: BYPASS. |

Service-level cache methods

| Method | Signature | Description | |--------|-----------|-------------| | remember | (key, factory, ttl?) => Promise<T> | Return cached value or call factory and cache the result. | | set | (key, value, ttl?) => void | Store an arbitrary value. | | getValue | (key) => T \| undefined | Retrieve a value stored via set or remember. |

Cache management

| Method | Signature | Description | |--------|-----------|-------------| | get | (key) => CacheInfo \| null | Returns info for a cached key. | | getAll | () => Record<string, CacheInfo> | Returns all active entries. | | delete | (key) => boolean | Removes a single entry. | | deleteMatching | (pattern) => number | Removes entries matching a glob pattern. | | clear | () => void | Removes all entries. | | size | () => number | Number of active entries. | | byteSize | () => number | Approximate total body size in bytes. | | getStats | () => MemorizeStats | Aggregate stats: { entries, maxEntries, byteSize }. |

Adapters

| Import path | Export | Framework | |-------------|--------|-----------| | express-memorize | memorize | Core factory | | express-memorize/express | createExpressAdapter(cache, options?) | Express | | express-memorize/fastify | createFastifyPlugin(cache, options?), createFastifyPreHandler(cache, options?) | Fastify | | express-memorize/koa | createKoaMiddleware(cache, options?) | Koa | | express-memorize/nestjs | MemorizeModule, MemorizeInterceptor, decorators | NestJS | | express-memorize/hono | createHonoMiddleware(cache, options?) | Hono | | express-memorize/fetch | cacheFetchHandler(cache, handler, options?) | Fetch API / Serverless |

Events

| Event | Payload | When | |-------|---------|------| | set | { type, key, body, statusCode, contentType, expiresAt, size } | A response is stored | | delete | { type, key } | Manual removal via delete, deleteMatching, or clear | | expire | { type, key } | TTL timer fires or lazy expiry is detected | | evict | { type, key } | LRU eviction due to maxEntries limit | | empty | { type } | Last entry removed, cache is now empty |

Response Headers

| Header | Value | Description | |--------|-------|-------------| | X-Cache | HIT | Response served from cache | | X-Cache | MISS | Response computed and stored | | X-Cache | BYPASS | Cache skipped — noCache: true |

Behavior

  • Only GET requests are cached. All other methods bypass the cache entirely.
  • Only responses with a 2xx status code are stored.
  • All middleware and adapter instances created from the same memorize() call share the same store.
  • Two separate memorize() calls produce independent stores.
  • Byte size is an approximation — strings use UTF-8 encoding, objects use JSON.stringify length.

License

MIT