Cookbook
Caching
Cache-aside pattern, invalidation, per-user caches, and rate counters
Patterns for caching expensive computations and database queries.
Cache-Aside Pattern
Compute on miss, serve from cache on hit:
// api/dashboard/stats.js
export const GET = {
handler: async (ctx) => {
const { cache, db } = ctx;
const stats = await cache.wrap(
`dashboard:stats:${ctx.req.session.userId}`,
async () => {
const [users, posts, revenue] = await Promise.all([
db('users').count('* as count').first(),
db('posts').where('published', true).count('* as count').first(),
db('orders').sum('total as sum').first(),
]);
return {
totalUsers: Number(users.count),
publishedPosts: Number(posts.count),
totalRevenue: Number(revenue.sum) || 0,
};
},
'5m',
);
return { data: stats };
},
};Cache Invalidation on Mutation
Delete the cache key when the underlying data changes:
// api/posts/[id].js
export const PUT = {
handler: async (ctx) => {
const { db, cache } = ctx;
const { id } = ctx.req.query;
await db('posts').where('id', id).update(ctx.req.body);
const updated = await db('posts').where('id', id).first();
// Invalidate the cached version
await cache.delete(`post:${id}`);
return { data: updated };
},
};
export const DELETE = {
handler: async (ctx) => {
const { db, cache } = ctx;
const { id } = ctx.req.query;
await db('posts').where('id', id).delete();
await cache.delete(`post:${id}`);
return { data: { ok: true } };
},
};Or invalidate from an event listener to keep cache logic out of route handlers:
// listeners/posts/updated.js
export default async (ctx) => {
const { cache } = ctx;
await cache.delete(`post:${ctx.event.payload.id}`);
};Per-User Cache Keys
Scope cache keys to individual users to avoid data leaks:
// api/feed.js
export const GET = {
handler: async (ctx) => {
const { cache, db } = ctx;
const userId = ctx.req.session.userId;
const feed = await cache.wrap(
`feed:${userId}`,
async () => {
return db('posts')
.join('follows', 'posts.author_id', 'follows.following_id')
.where('follows.follower_id', userId)
.orderBy('posts.created_at', 'desc')
.limit(50)
.select('posts.*');
},
'1m',
);
return { data: feed };
},
};Short-Lived Rate Counters
Use the cache as a simple rate-limiting store with a TTL:
// api/sms/send.js
export const POST = {
handler: async (ctx) => {
const { cache } = ctx;
const userId = ctx.req.session.userId;
const key = `sms_sent:${userId}`;
const count = (await cache.get(key)) || 0;
if (count >= 5) {
return { status: 429, error: { code: 'RATE_LIMITED', message: 'Too many SMS requests. Try again later.' } };
}
// Increment counter; set a 1-hour TTL on first hit
await cache.set(key, count + 1, count === 0 ? '1h' : undefined);
// ... send the SMS ...
return { data: { ok: true } };
},
};