Unit Testing Route Logic

Two design choices make an Express 24,430 app testable, and both cost nothing: export a factory that builds the app instead of a module that calls listen, and take collaborators as arguments rather than importing them. So src/app.js exports createApp({ store = makeStore() } = {}), which mounts express.json(), binds the routes to makeBooksController(store) methods, adds a catch-all 404 and returns the app; server.js alone calls listen. Pull query parsing out of the handler and it becomes a table of inputs and outputs; for a handler itself, a stub of the two res methods you use is enough.

test/books-controller.test.js — a fake response and a stub storeJavaScript
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { parseListQuery, makeBooksController } from '../src/books-controller.js';
const fakeRes = () => {
  const res = { statusCode: 200, body: undefined };
  res.status = (c) => { res.statusCode = c; return res; };          // must return res
  res.json = (p) => { res.body = p; return res; };  return res;
};
test('caps limit at 50 and floors offset at 0', () => {
  assert.deepEqual(parseListQuery({ limit: '999', offset: '-5' }), { limit: 50, offset: 0 });
});
test('show returns 404 for an unknown id', async () => {
  const store = { get: async (id) => (id === 1 ? { id: 1, title: 'Dune' } : null) };
  const res = fakeRes();
  await makeBooksController(store).show({ params: { id: '404' } }, res);
  assert.equal(res.statusCode, 404);
  assert.deepEqual(res.body, { error: { code: 'not_found' } }); });

Write the test before the clamp exists and node --test shows what is missing, + marking the value it got and - the value you expected:

Output of 57
✖ caps limit at 50 and floors offset at 0 (2.0917ms)
✔ show returns 404 for an unknown id (0.3728ms)
ℹ tests 2   ℹ pass 1   ℹ fail 1
  AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
  + actual - expected
    {
  +   limit: 999,
  +   offset: -5
  -   limit: 50,
  -   offset: 0
    }

The fix is Math.min(Number(query.limit ?? 10) || 10, 50) with a matching Math.max for the offset, after which both lines are ticks. Chaining is the detail most fake responses get wrong: res.status(404).json(...) only works because status returns res.