Universal cache middleware for Hono powered by unstorage - works across Cloudflare Workers, Vercel Edge, Node.js, Bun, Deno, and more.
npm install hono-universal-cacheUniversal 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.
⨠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
``bash`
npm install hono-universal-cacheor
pnpm add hono-universal-cacheor
yarn add hono-universal-cache
> Note: unstorage is included as a dependency - no need to install it separately!
`typescript
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;
`
`typescript
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,
}),
);
`
`typescript
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;
`
`typescript
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,
}),
);
`
`typescript
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,
}),
);
`
`typescript
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,
}),
);
`
Creates a Hono middleware for response caching.
#### Options
`typescript
type CacheOptions = {
// Required: Cache namespace
cacheName: string | ((c: Context) => Promise
// 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
};
`
Cache different tenants or users separately:
`typescriptcache:${tenant}
app.use(
"*",
universalCache({
cacheName: (c) => {
const tenant = c.req.header("X-Tenant-ID") || "default";
return ;`
},
storage,
ttl: 3600,
}),
);
Cache based on custom logic (e.g., ignore specific query params):
`typescript`
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,
}),
);
Cache successful and redirect responses:
`typescript`
app.use(
"*",
universalCache({
cacheName: "selective-cache",
cacheableStatusCodes: [200, 201, 301, 302],
storage,
}),
);
Use any unstorage driver:
- Memory - unstorage/drivers/memory (default, ephemeral)unstorage/drivers/fs
- Filesystem - (Node.js/Bun)unstorage/drivers/redis
- Redis - (persistent, distributed)unstorage/drivers/cloudflare-kv-binding
- Cloudflare KV - unstorage/drivers/vercel-kv
- Vercel KV - unstorage/drivers/mongodb
- MongoDB - unstorage/drivers/upstash
- Upstash Redis - unstorage/drivers/lru-cache
- LRU Cache - (in-memory with eviction)
- AWS S3 - unstorage/drivers/s3unstorage/drivers/azure-storage-blob
- Azure Blob - unstorage/drivers/cloudflare-r2-binding
- Cloudflare R2 - unstorage/drivers/vercel-blob
- Vercel Blob -
See all drivers in the unstorage documentation.
1. Request arrives ā Middleware generates cache key
2. Check cache ā Retrieve from storage if exists and not expired
3. Cache hit ā Return cached response immediately
4. Cache miss ā Execute route handler
5. Check cacheability ā Verify status code is cacheable
6. Store response ā Save text body to storage with TTL metadata (non-blocking)
7. Return response ā Send to client
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
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
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
Access the low-level cache manager for manual operations:
`typescript
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();
`
Apply caching to specific routes only:
`typescript
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
}),
);
``
MIT
Contributions welcome! Please open an issue or PR.
- Hono - Ultrafast web framework
- unstorage - Universal storage layer