hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Caching with @hectoday/http

Why Caching

  • The Same Query, A Thousand Times
  • Project Setup

HTTP Caching

  • Cache-Control Headers
  • ETags and Conditional Requests
  • Stale-While-Revalidate

Server-Side Caching

  • In-Memory Caching with Map
  • TTL and Expiration
  • Cache-Aside Pattern
  • LRU Eviction

What to Cache

  • Caching Database Queries
  • Caching Computed Results
  • Caching External API Responses

Invalidation

  • The Hardest Problem
  • Time-Based Invalidation
  • Event-Based Invalidation
  • Tag-Based Invalidation

Putting It All Together

  • Caching Checklist
  • Capstone: Caching the Book Catalog

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?

← Stale-While-Revalidate TTL and Expiration →

© 2026 hectoday. All rights reserved.