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.
| Concept | What it means | Decision rule |
|---|---|---|
| Unit test | Checks one function with controlled dependencies | Use for validation, service rules, and error mapping |
| Integration test | Checks layers together through a real boundary | Use HTTP requests against an ephemeral local server |
| Test double | A controlled substitute for a dependency | Inject repositories and clocks instead of mocking arbitrary internals |
| API specification | A machine-readable contract for paths, operations, and schemas | Keep it versioned and checked against representative responses |
Professional workflow
Build and verify Node.js programs from the terminal in small, observable steps.
- List contract behaviors before implementation details
- Unit-test pure validation and services
- Construct the app with injected fake dependencies
- Listen on port 0 and discover the assigned port
- Use fetch to assert statuses, headers, and bodies
- Close the server after tests even on failure
- Document the same contract in OpenAPI
Keep the feedback loop short
Guided code lab
Unit-test a validator
Node's built-in runner treats thrown assertions as failures and supports async tests without another framework.
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.
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: 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
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
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