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.

Create a free account to save this page, take notes and track your progress. Sign in

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 in after.
  • Test behaviour through HTTP: status, headers and body, including error cases.
  • Run with node --test; it finds *.test.js files and reports results (TAP or spec format).

The code

End-to-end tests with node:test
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.

Was this page helpful?