Phase 2 · Web Servers & HTTP APIsModule 15~64 min read

API Testing & Documentation

Protect API contracts with Node's test runner, unit and integration tests, isolated server lifecycles, and concise OpenAPI documentation.

What you'll learn

An API contract is trustworthy only when automated checks protect it and documentation makes it discoverable. You will use Node's stable test runner, start isolated servers on ephemeral ports, test HTTP outcomes, and record a concise OpenAPI contract.

By the end of this lesson, you'll be able to:

  • Write unit tests with node:test and node:assert
  • Start and stop an API per integration test suite
  • Test success, validation, not-found, and error responses
  • Describe endpoints and schemas with OpenAPI basics

Core mental model

Node.js becomes easier when you separate the JavaScript language from the runtime and the operating-system capabilities it exposes. Use this table as a decision guide.

ConceptWhat it meansDecision rule
Unit testChecks one function with controlled dependenciesUse for validation, service rules, and error mapping
Integration testChecks layers together through a real boundaryUse HTTP requests against an ephemeral local server
Test doubleA controlled substitute for a dependencyInject repositories and clocks instead of mocking arbitrary internals
API specificationA machine-readable contract for paths, operations, and schemasKeep it versioned and checked against representative responses

Professional workflow

Build and verify Node.js programs from the terminal in small, observable steps.

  1. List contract behaviors before implementation details
  2. Unit-test pure validation and services
  3. Construct the app with injected fake dependencies
  4. Listen on port 0 and discover the assigned port
  5. Use fetch to assert statuses, headers, and bodies
  6. Close the server after tests even on failure
  7. Document the same contract in OpenAPI

Keep the feedback loop short

Run the smallest useful command after every meaningful change. Read the complete error message before editing again, and keep inputs and outputs visible while you learn.

Guided code lab

Unit-test a validator

Node's built-in runner treats thrown assertions as failures and supports async tests without another framework.

test/task-validator.test.js
import test from "node:test";
import assert from "node:assert/strict";
import { validateNewTask } from "../src/task-validator.js";

test("normalizes a valid title", () => {
  assert.deepEqual(validateNewTask({ title: "  Learn testing  " }), {
    ok: true,
    value: { title: "Learn testing" },
  });
});

test("rejects a blank title", () => {
  const result = validateNewTask({ title: "   " });
  assert.equal(result.ok, false);
  assert.match(result.issues[0].message, /required/i);
});

Test through a real HTTP socket

Port 0 asks the operating system for a free port. The test owns startup and cleanup and uses built-in fetch as the client.

test/tasks-api.test.js
import test from "node:test";
import assert from "node:assert/strict";
import { createApp } from "../src/app.js";

test("POST /api/tasks creates a task", async (t) => {
  const app = createApp({ repository: createMemoryRepository() });
  const server = app.listen(0, "127.0.0.1");
  await new Promise((resolve) => server.once("listening", resolve));
  t.after(() => new Promise((resolve) => server.close(resolve)));

  const { port } = server.address();
  const response = await fetch("http://127.0.0.1:" + port + "/api/tasks", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ title: "Protect the contract" }),
  });

  assert.equal(response.status, 201);
  assert.match(response.headers.get("location"), /^/api/tasks//);
  assert.equal((await response.json()).data.title, "Protect the contract");
});

Document the creation contract

This small OpenAPI fragment makes request and response shapes explicit and can grow with the API.

openapi.yaml
openapi: 3.1.0
info:
  title: Task API
  version: 1.0.0
paths:
  /api/tasks:
    post:
      summary: Create a task
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title]
              properties:
                title: { type: string, minLength: 1 }
      responses:
        "201": { description: Task created }
        "422": { description: Validation failed }

Production practice

Own cleanup

Every test-created server, file, timer, and fake must be released through test hooks, including when assertions fail.

Test behavior

Assert public inputs and outputs rather than private function call order unless interaction itself is the contract.

Keep docs executable

Version specifications beside code and add checks that representative examples match real responses.

Common failure mode

Integration tests that always use port 3000 race with development servers and parallel test runs. Let the operating system assign an ephemeral port and close it after the suite.

Independent workshop

Create a complete verification and documentation layer for the project API.

Your finished workshop must include:

  • Unit tests for validation and service rules
  • Integration tests for every CRUD success path
  • 400/404/409/422 and unexpected-error coverage
  • Fresh fake repository per test
  • An OpenAPI document for paths and core schemas
  • npm scripts for test and test watch mode

Definition of done

Run the happy path and at least two edge cases, keep responsibilities separated, and add a short README explaining how to run the program.

Recap & quick check

Key takeaways

  • node:test is a stable built-in runner
  • Unit and integration tests protect different boundaries
  • Port 0 isolates HTTP tests
  • Injected dependencies make behavior deterministic
  • Tests must own cleanup
  • OpenAPI records a machine-readable contract

Quick check

1. What does listening on port 0 do?

2. Which test best protects status and headers?

3. Why inject a repository into createApp?

4. What does OpenAPI describe?

Next: Phase Project: Build a Task REST API