The caching pattern you will use most — check the cache first, fall back to the database on a miss, then store the result with a TTL. Build it once as a reusable helper.
Cache-aside (also called lazy loading) is the core pattern: on a read, look in Redis first. A hit returns instantly; a miss falls through to the database, and you write that result back into Redis with a TTL so the next read is a hit. Values in Redis are strings, so you JSON.stringify on the way in and JSON.parse on the way out.
async function getProduct(id: string) {
const key = 'product:' + id
// 1. Try the cache first.
const cached = await redis.get(key)
if (cached) return JSON.parse(cached) // HIT — done
// 2. MISS — go to the source of truth.
const product = await db.product.findUnique({ where: { id } })
// 3. Store it for next time, expiring in 5 minutes.
if (product) {
await redis.set(key, JSON.stringify(product), 'EX', 300)
}
return product
}You do not want that check-miss-fill dance copy-pasted everywhere. Wrap it in one generic helper that takes a key, a TTL, and a function that loads the data on a miss. Now any expensive call — a query, a third-party API — gets caching by wrapping it once.
async function getOrSet<T>(
key: string,
ttlSeconds: number,
load: () => Promise<T>
): Promise<T> {
const cached = await redis.get(key)
if (cached) return JSON.parse(cached) as T
const fresh = await load() // only runs on a miss
await redis.set(key, JSON.stringify(fresh), 'EX', ttlSeconds)
return fresh
}
// Use it anywhere — the caching is now a one-liner:
const product = await getOrSet('product:' + id, 300, () =>
db.product.findUnique({ where: { id } })
)