Skip to content

anasmohammed361/hono-universal-cache

Repository files navigation

hono-universal-cache

Universal cache middleware for Hono powered by unstorage.

Cache API responses across any runtime - Cloudflare Workers, Vercel Edge, Node.js, Bun, Deno, and more.

Note: Optimized for API responses (JSON, text, HTML). For static assets (images, videos, files), use CDN/edge caching instead.

Features

Universal Runtime Support - Works everywhere Hono works
🗄️ Multiple Storage Drivers - Memory, Redis, Cloudflare KV, Vercel KV, filesystem, and more
TTL Support - Automatic expiration with configurable time-to-live
🎯 Selective Caching - Control what gets cached by status code
🔑 Custom Key Generation - Flexible cache key strategies
🪶 Lightweight - Minimal overhead, focused on storage operations
🎨 Simple & Predictable - No magic, just storage-based caching
📦 Efficient Storage - Optimized for text-based API responses

Installation

npm install hono-universal-cache
# or
pnpm add hono-universal-cache
# or
yarn add hono-universal-cache

Note: unstorage is included as a dependency - no need to install it separately!

Quick Start

Basic Usage (In-Memory)

import { Hono } from "hono";
import { universalCache } from "hono-universal-cache";

const app = new Hono();

app.use(
  "*",
  universalCache({
    cacheName: "my-app-cache",
    ttl: 3600, // 1 hour
  }),
);

app.get("/api/data", (c) => {
  return c.json({ timestamp: Date.now() });
});

export default app;

With Custom Storage Driver

import { createStorage } from "unstorage";
import redisDriver from "unstorage/drivers/redis";

const storage = createStorage({
  driver: redisDriver({
    host: "localhost",
    port: 6379,
  }),
});

app.use(
  "*",
  universalCache({
    cacheName: "api-cache",
    storage,
    ttl: 3600,
  }),
);

Starter Examples

Ready-to-use examples for different runtimes and storage backends:

Complete Node.js server with Redis cache storage, Docker Compose setup, and environment configuration.

cd examples/nodejs-redis
pnpm install && docker-compose up -d
cp .env.example .env && pnpm dev

Bun runtime with a custom Redis storage driver using Bun's native RedisClient - no ioredis dependency.

cd examples/bun-redis-custom-driver
bun install && docker-compose up -d
cp .env.example .env && bun dev

Next.js App Router with Hono API routes and Vercel KV storage. Includes local development setup and production deployment guide.

cd examples/nextjs-vercel-kv
pnpm install && docker-compose up -d
cp .env.example .env.local && pnpm dev

Each example includes detailed setup instructions, Docker Compose configuration, and testing guides. Perfect for getting started quickly!

Runtime-Specific Examples

Cloudflare Workers

import { Hono } from "hono";
import { universalCache } from "hono-universal-cache";
import { createStorage } from "unstorage";
import cloudflareKVBindingDriver from "unstorage/drivers/cloudflare-kv-binding";

type Env = {
  MY_KV: KVNamespace;
};

const app = new Hono<{ Bindings: Env }>();

app.use("*", async (c, next) => {
  const storage = createStorage({
    driver: cloudflareKVBindingDriver({
      binding: c.env.MY_KV,
    }),
  });

  return universalCache({
    cacheName: "worker-cache",
    storage,
    ttl: 3600,
  })(c, next);
});

export default app;

Vercel Edge

import { createStorage } from "unstorage";
import vercelKVDriver from "unstorage/drivers/vercel-kv";

const storage = createStorage({
  driver: vercelKVDriver({
    // Auto-detects from environment:
    // KV_REST_API_URL and KV_REST_API_TOKEN
  }),
});

app.use(
  "*",
  universalCache({
    cacheName: "edge-cache",
    storage,
    ttl: 3600,
  }),
);

Node.js / Bun (Filesystem)

import { createStorage } from "unstorage";
import fsDriver from "unstorage/drivers/fs";

const storage = createStorage({
  driver: fsDriver({
    base: "./cache",
  }),
});

app.use(
  "*",
  universalCache({
    cacheName: "fs-cache",
    storage,
    ttl: 3600,
  }),
);

Redis

import { createStorage } from "unstorage";
import redisDriver from "unstorage/drivers/redis";

const storage = createStorage({
  driver: redisDriver({
    host: "localhost",
    port: 6379,
    // password: 'your-password'
  }),
});

app.use(
  "*",
  universalCache({
    cacheName: "redis-cache",
    storage,
    ttl: 3600,
  }),
);

API Reference

universalCache(options)

Creates a Hono middleware for response caching.

Options

type CacheOptions = {
  // Required: Cache namespace
  cacheName: string | ((c: Context) => Promise<string> | string);

  // Optional: Unstorage instance (defaults to in-memory)
  storage?: Storage;

  // Optional: Time-to-live in seconds
  ttl?: number;

  // Optional: Status codes to cache (default: [200])
  cacheableStatusCodes?: number[];

  // Optional: Custom cache key generator
  keyGenerator?: (c: Context) => Promise<string> | string;
};

Common Use Cases

Dynamic Cache Names

Cache different tenants or users separately:

app.use(
  "*",
  universalCache({
    cacheName: (c) => {
      const tenant = c.req.header("X-Tenant-ID") || "default";
      return `cache:${tenant}`;
    },
    storage,
    ttl: 3600,
  }),
);

Custom Key Generation

Cache based on custom logic (e.g., ignore specific query params):

app.use(
  "*",
  universalCache({
    cacheName: "api-cache",
    keyGenerator: (c) => {
      const url = new URL(c.req.url);
      // Ignore tracking parameters
      url.searchParams.delete("utm_source");
      url.searchParams.delete("utm_campaign");
      return url.toString();
    },
    storage,
  }),
);

Selective Caching by Status Code

Cache successful and redirect responses:

app.use(
  "*",
  universalCache({
    cacheName: "selective-cache",
    cacheableStatusCodes: [200, 201, 301, 302],
    storage,
  }),
);

Storage Drivers

Use any unstorage driver:

Popular Drivers

  • Memory - unstorage/drivers/memory (default, ephemeral)
  • Filesystem - unstorage/drivers/fs (Node.js/Bun)
  • Redis - unstorage/drivers/redis (persistent, distributed)
  • Cloudflare KV - unstorage/drivers/cloudflare-kv-binding
  • Vercel KV - unstorage/drivers/vercel-kv
  • MongoDB - unstorage/drivers/mongodb
  • Upstash Redis - unstorage/drivers/upstash
  • LRU Cache - unstorage/drivers/lru-cache (in-memory with eviction)

Cloud Storage

  • AWS S3 - unstorage/drivers/s3
  • Azure Blob - unstorage/drivers/azure-storage-blob
  • Cloudflare R2 - unstorage/drivers/cloudflare-r2-binding
  • Vercel Blob - unstorage/drivers/vercel-blob

See all drivers in the unstorage documentation.

How It Works

  1. Request arrives → Check if request method is GET (or bypass is enabled)
  2. Method check → Skip caching for non-GET requests (POST, PUT, DELETE, etc.)
  3. Generate cache key → Create unique key from request
  4. Check cache → Retrieve from storage if exists and not expired
  5. Cache hit → Return cached response immediately
  6. Cache miss → Execute route handler
  7. Check cacheability → Verify status code is cacheable
  8. Store response → Save text body to storage with TTL metadata (non-blocking)
  9. Return response → Send to client

Important Notes

GET Requests Only

By default, only GET requests are cached. This follows HTTP semantics and prevents issues with caching state-changing operations:

// ✅ GET requests are cached
GET /api/users  cached

// ❌ POST/PUT/DELETE requests skip cache entirely
POST /api/users  never cached
PUT /api/users/123  never cached
DELETE /api/users/123  never cached

Why? Caching POST/PUT/DELETE responses can lead to:

  • Stale data being served
  • Lost updates and data corruption
  • Security vulnerabilities
  • Incorrect application behavior

Bypass Method Check (Advanced)

If you have a specific use case requiring caching of non-GET requests, use the bypassMethodCheck flag:

app.use(
  "*",
  universalCache({
    cacheName: "api-cache",
    bypassMethodCheck: true, // ⚠️ Use with extreme caution!
    ttl: 60,
  }),
);

⚠️ Warning: Only enable this if you fully understand the implications and have a valid use case (e.g., caching idempotent POST requests in a controlled environment).

Optimized for API Responses

This middleware is designed for text-based API responses:

  • JSON APIs - Perfect use case
  • Text responses - Works great
  • HTML pages - Fully supported
  • XML/RSS feeds - No problem
  • Binary assets (images, PDFs, videos) - Use CDN/edge caching instead

Why not binary? The middleware uses response.text() for optimal storage efficiency. For static assets, use:

  • CDN caching (Cloudflare, CloudFront)
  • Object storage (S3, R2, Blob Storage)
  • Hono's built-in static file serving with CDN

Storage-Only Caching

This middleware handles server-side storage caching only:

  • ✅ Stores responses in Redis, KV, filesystem, etc.
  • ✅ Caches based on status codes and TTL configuration
  • 💡 Independent of HTTP caching headers - works purely at the storage layer

Non-blocking Cache Writes

Cache writes happen asynchronously and don't block responses:

  • Cloudflare Workers/Vercel Edge: Uses waitUntil() for background writes
  • Other runtimes: Uses promises with error handling

Advanced Usage

CacheManager API

Access the low-level cache manager for manual operations:

import { CacheManager } from "hono-universal-cache";
import { createStorage } from "unstorage";

const storage = createStorage();
const cache = new CacheManager(storage, 3600); // 1 hour TTL

// Manual cache operations
await cache.set("key", response);
const cached = await cache.get("key");
const exists = await cache.has("key");
await cache.delete("key");
await cache.clear();
const keys = await cache.keys();

Per-Route Caching

Apply caching to specific routes only:

const app = new Hono();

// Global middleware without cache
app.use("*", logger());

// Cache only API routes
app.use(
  "/api/*",
  universalCache({
    cacheName: "api-cache",
    storage,
    ttl: 300, // 5 minutes
  }),
);

// Cache product pages longer
app.use(
  "/products/*",
  universalCache({
    cacheName: "products-cache",
    storage,
    ttl: 3600, // 1 hour
  }),
);

License

MIT

Contributing

Contributions welcome! Please open an issue or PR.

Credits

  • Hono - Ultrafast web framework
  • unstorage - Universal storage layer

About

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages