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 threading from functools import wraps
A thread-safe debounce decorator in Python
decorators
debounce
threading
Advanced
6 steps
php
<?php namespace App\Services;
Tagged cache invalidation in Laravel
caching
cache-tags
invalidation
Intermediate
5 steps
javascript
import { NextResponse } from 'next/server'; import { createWriteStream } from 'node:fs'; import { mkdir } from 'node:fs/promises'; import { pipeline } from 'node:stream/promises';
Streaming file uploads in a Next.js route
file-upload
streams
validation
Intermediate
9 steps
go
package middleware import ( "log"
Per-IP rate limiting middleware in Gin
rate-limiting
middleware
concurrency
Intermediate
8 steps
rust
use axum::{ body::{Body, Bytes}, extract::Request, http::StatusCode,
Logging request bodies in Axum middleware
middleware
streaming-body
logging
Intermediate
8 steps
javascript
import { createContext, useContext, useState, useId } from "react"; const AccordionContext = createContext(null); const ItemContext = createContext(null);
Building a compound Accordion in React
context
compound-components
state-management
Intermediate
8 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.