In-Memory Caching with Map
HTTP caching has a gap
The previous section covered HTTP caching — Cache-Control headers and ETags that let browsers and CDNs store responses. But HTTP caching does not help with the first request (no cached copy exists), or when the cache expires and the browser revalidates.
For those cases, the server still runs the database query. Server-side caching stores query results in memory so even the server skips the database.
The simplest cache
A JavaScript Map is the simplest key-value store:
// src/cache.ts
const cache = new Map<string, any>();
export function cacheGet(key: string): any | undefined {
return cache.get(key);
}
export function cacheSet(key: string, value: any): void {
cache.set(key, value);
}
export function cacheDelete(key: string): void {
cache.delete(key);
}
export function cacheClear(): void {
cache.clear();
} route.get("/books/top", {
resolve: () => {
const cached = cacheGet("top-books");
if (cached) return Response.json(cached);
const books = db.prepare("SELECT ...").all();
cacheSet("top-books", books);
return Response.json(books);
},
}); First request: cache miss → query database → store result → return. Second request: cache hit → return immediately. The database is never queried again until the cache is cleared.
Why Map works
A Map stores data in the Node.js process memory. Access is O(1) — looking up a key is essentially instant. No network latency (unlike Redis), no serialization (the data stays as JavaScript objects), no extra infrastructure.
For a single-server application with moderate traffic (the kind built in this course series), an in-memory Map is fast enough and simple enough. You do not need Redis until you have multiple servers or need to cache gigabytes of data.
The problem: no expiration
The simple Map cache has a critical flaw: data never expires. Once cached, the top books list stays in memory forever — even after new reviews change the rankings.
// This data is cached FOREVER
cacheSet("top-books", books);
// A new review is posted... the cache still has the old data
// Users see stale top books until the server restarts The next lesson solves this with TTL (Time-To-Live).
Cache keys
A cache key uniquely identifies the cached data. Choose keys that match the data’s identity:
// Simple: one key per endpoint
cacheGet("top-books");
cacheGet("all-books");
cacheGet("book:book-1");
// With query parameters: include them in the key
cacheGet("books:genre=fiction:page=1");
cacheGet("books:genre=fantasy:page=2");
// BAD: same key for different data
cacheGet("books"); // Is this all books? Fiction books? Top books? Use a consistent naming convention. I recommend resource:identifier:params:
function cacheKey(resource: string, id?: string, params?: Record<string, string>): string {
let key = resource;
if (id) key += `:${id}`;
if (params) {
const sorted = Object.entries(params).sort(([a], [b]) => a.localeCompare(b));
key += ":" + sorted.map(([k, v]) => `${k}=${v}`).join(":");
}
return key;
}
cacheKey("books"); // "books"
cacheKey("books", "book-1"); // "books:book-1"
cacheKey("books", undefined, { genre: "fiction", page: "1" }); // "books:genre=fiction:page=1" Sorting the parameters ensures genre=fiction:page=1 and page=1:genre=fiction produce the same key.
Exercises
Exercise 1: Add the Map cache to your project. Cache the /books/top response. Verify the second request is faster (no database query).
Exercise 2: Add a new review. Call /books/top. Verify it still returns the old data (stale cache).
Exercise 3: Call cacheClear(). Call /books/top again. Verify it returns the updated data.
Why is a Map sufficient for server-side caching in a single-server application?