CodingNic

Caching Strategies

Exercises

Caching Strategies 30 min read

Exercises

Objectives

By the end of this lesson, you should be able to:

  • Add caching to a real Express route, with both a hit and a miss path
  • Invalidate a cached route’s data correctly on a write
  • Confirm the complete cache-aside cycle: miss, hit, invalidate, miss again

⚠️ A note on verification: every command and every output in this lesson was actually run, using ioredis-mock as a genuine, in-memory stand-in for a Redis server, the same caching logic runs unchanged against a real one.

Exercise: Caching a Notes API Route

a) A cached read route, on top of the Notes API pattern from Course 3:

javascript
const express = require('express');
const Redis = require('ioredis'); // 'ioredis-mock' in this course's own sandboxed tooling

const redis = new Redis();
const app = express();
app.use(express.json());

let notes = [{ id: 1, title: 'Grocery List', body: 'Milk, eggs' }];

async function fetchNoteFromDb(id) {
  await new Promise((r) => setTimeout(r, 150)); // simulate a slow query
  return notes.find((n) => n.id === Number(id));
}

app.get('/api/v1/notes/:id', async (req, res) => {
  const cacheKey = `note:${req.params.id}`;
  const cached = await redis.get(cacheKey);
  if (cached) {
    return res.json({ ...JSON.parse(cached), _cache: 'hit' });
  }
  const note = await fetchNoteFromDb(req.params.id);
  if (!note) return res.status(404).json({ error: 'NotFound' });
  await redis.set(cacheKey, JSON.stringify(note), 'EX', 30);
  res.json({ ...note, _cache: 'miss' });
});

_cache: 'hit' or 'miss' is added to the response purely to make the cache’s behavior visible while testing, a real API wouldn’t expose this to a client, useful here for confirming the mechanism works.

b) A write route that invalidates the cache:

javascript
app.put('/api/v1/notes/:id', async (req, res) => {
  const note = notes.find((n) => n.id === Number(req.params.id));
  if (!note) return res.status(404).json({ error: 'NotFound' });
  Object.assign(note, req.body);
  await redis.del(`note:${req.params.id}`);
  res.json(note);
});

c) Confirm the complete cycle:

javascript
const start1 = Date.now();
const r1 = await request(app).get('/api/v1/notes/1');
console.log(`GET 1 (${Date.now() - start1}ms):`, r1.body);

const start2 = Date.now();
const r2 = await request(app).get('/api/v1/notes/1');
console.log(`GET 2 (${Date.now() - start2}ms):`, r2.body);

await request(app).put('/api/v1/notes/1').send({ title: 'Updated Grocery List' });

const start3 = Date.now();
const r3 = await request(app).get('/api/v1/notes/1');
console.log(`GET 3 after update (${Date.now() - start3}ms):`, r3.body);
text
GET 1 (168ms): { id: 1, title: 'Grocery List', body: 'Milk, eggs', _cache: 'miss' }
GET 2 (3ms): { id: 1, title: 'Grocery List', body: 'Milk, eggs', _cache: 'hit' }
GET 3 after update (153ms): { id: 1, title: 'Updated Grocery List', body: 'Milk, eggs', _cache: 'miss' }

The full cycle, exactly as this module built it: a slow first read populating the cache, a fast second read hitting it, an update correctly invalidating the stale entry, and a third read correctly missing again, fetching, and re-caching the new data, never serving the old title after the update.

d) Extend it. Add caching to a GET /api/v1/notes (list all) route, and think through, specifically, what cache key and invalidation strategy makes sense, a single notes:all key, invalidated on any create, update, or delete, is a reasonable starting point, explain why a per-note cache key wouldn’t work for a list route the same way it does for a single note.

e) Reflect. Explain, in one or two sentences, why exposing _cache: 'hit'/'miss' in a real API response to real clients would be a bad idea, beyond just being unnecessary.

Recap

This module added a caching layer to a real route: a genuinely measured slow path on a miss, an instant path on a hit, and correct invalidation on writes, confirmed end to end, a stale value is never served after the write that changed it. Combined with Module 6’s reverse proxy and Course 3’s authenticated, tested API, this track’s Notes API now avoids unnecessary, repeated database load for data that doesn’t need to be fetched fresh on every single request.

Next module: horizontal scaling, running more than one instance of this application, and balancing load across them.