Node.js Backend Fundamentals with Express · Data, Auth, Tests and Production
Testing an Express API with node:test
Test routes end to end with Node's built-in test runner: start the app on a free port, send requests, assert responses.
What you'll build
Tests for a small API using node:test and node:assert, no extra libraries. The app is created by a factory function so tests get a fresh instance with test data, and each test checks status codes and bodies.
The idea
- Export a
createApp(deps)function instead of a started server, so tests can build the app with fakes or test databases. - Start it on port 0 in
before, close it inafter. - Test behaviour through HTTP: status, headers and body, including error cases.
- Run with
node --test; it finds*.test.jsfiles and reports results (TAP or spec format).
The code
import express from "express";
import { test, before, after } from "node:test";
import assert from "node:assert/strict";
export function createApp({ inventory }) {
const app = express();
app.get("/stock/:sku", (req, res) => {
const qty = inventory.get(req.params.sku);
return qty === undefined ? res.status(404).json({ error: "unknown sku" }) : res.json({ sku: req.params.sku, qty });
});
return app;
}
let server, base;
before(() => {
server = createApp({ inventory: new Map([["MUG-1", 12], ["TEA-2", 0]]) }).listen(0);
base = `http://127.0.0.1:${server.address().port}`;
});
after(() => server.close());
test("returns stock for a known SKU", async () => {
const r = await fetch(`${base}/stock/MUG-1`);
assert.equal(r.status, 200);
assert.deepEqual(await r.json(), { sku: "MUG-1", qty: 12 });
});
test("zero stock is still found", async () => {
const r = await fetch(`${base}/stock/TEA-2`);
assert.equal((await r.json()).qty, 0);
});
test("unknown SKU is a 404", async () => {
const r = await fetch(`${base}/stock/NOPE`);
assert.equal(r.status, 404);
});
Output
✓ returns stock for a known SKU ✓ zero stock is still found ✓ unknown SKU is a 404 ℹ tests 3 ℹ suites 0 ℹ pass 3 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0
Run this on your computer — see “Get Started” for setup.
Common mistakes
- Starting the server at import time, so tests can't control the port or dependencies.
- Testing only happy paths; error responses are part of the API contract.
- Sharing mutable state between tests so order changes results.
Key points
- Build the app with a factory and inject dependencies.
- Start on port 0 per test file and close afterwards.
- Assert status, headers and bodies, including errors.
Further reading: node:test · node:assert
Practice questions
Why export createApp() instead of a started server?
Answer
Tests can create fresh instances with fake or test dependencies and start them on a free port, without side effects at import time.
Why bind a test server to port 0?
Answer
The operating system picks a free port, so tests don't collide with each other or with running services.
Should API tests cover error responses?
Answer
Yes. Status codes and error bodies are part of the contract clients depend on, so 400, 401, 404 and 409 cases need tests too.
Open in the app to mark lessons complete, take notes and track your progress.