javascript
59 lines · 10 steps
Building an in-memory cache middleware in Express
A configurable middleware factory that caches GET responses in memory and serves ETag-based 304s.
Explained by
highlit
1const crypto = require('crypto');
2
3function inMemoryCache({ maxAge = 60_000, maxEntries = 500 } = {}) {
4 const store = new Map();
5
6 const evictExpired = () => {
7 const now = Date.now();
8 for (const [key, entry] of store) {
9 if (entry.expiresAt <= now) store.delete(key);
10 }
11 };
12
13 return function cache(req, res, next) {
14 if (req.method !== 'GET') return next();
15
16 const key = req.originalUrl;
17 const cached = store.get(key);
18
19 if (cached && cached.expiresAt > Date.now()) {
20 res.set('ETag', cached.etag);
21 res.set('X-Cache', 'HIT');
22 if (req.headers['if-none-match'] === cached.etag) {
23 return res.status(304).end();
24 }
25 res.set(cached.headers);
26 return res.status(cached.status).send(cached.body);
27 }
28
29 const originalSend = res.send.bind(res);
30 res.send = (body) => {
31 if (res.statusCode >= 200 && res.statusCode < 300) {
32 const payload = Buffer.isBuffer(body) ? body : Buffer.from(String(body));
33 const etag = '"' + crypto.createHash('sha1').update(payload).digest('hex') + '"';
34 res.set('ETag', etag);
35 res.set('X-Cache', 'MISS');
36
37 if (store.size >= maxEntries) evictExpired();
38 if (store.size < maxEntries) {
39 store.set(key, {
40 etag,
41 status: res.statusCode,
42 headers: { 'Content-Type': res.get('Content-Type') },
43 body: payload,
44 expiresAt: Date.now() + maxAge,
45 });
46 }
47
48 if (req.headers['if-none-match'] === etag) {
49 return res.status(304).end();
50 }
51 }
52 return originalSend(body);
53 };
54
55 next();
56 };
57}
58
59module.exports = inMemoryCache;
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A middleware factory closes over shared state like a Map so every request reuses one cache instance.
- 2Wrapping res.send lets you intercept and store a response body without changing route handlers.
- 3ETags plus if-none-match let the server return an empty 304 instead of re-sending unchanged bodies.
Related explainers
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
9 steps
python
import secrets from django.contrib.auth import authenticate, login from django.core.cache import cache
Two-factor login with OTP in Django
two-factor-auth
one-time-passwords
caching
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-an-in-memory-cache-middleware-in-express-explained-javascript-3b86/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.