Build a small task API without hiding Node.js fundamentals behind a framework. The goal is not feature count; it is a releaseable service whose boundaries, failure behavior, tests, and operational contract can be explained.
Implement POST /tasks, GET /tasks, GET /tasks/:id, PATCH /tasks/:id, DELETE /tasks/:id, /live, and /ready. A task has a server-generated ID, bounded title, status enum, createdAt, and updatedAt. Publish exact JSON shapes and status outcomes.
Accept JSON only within a byte limit, reject unknown fields, normalize deliberately, and return a stable error envelope with requestId, code, and safe message. Use 201 for create, 200 for reads and returned updates, 204 for successful delete, 400 for malformed syntax, 422 for domain validation, 404 for absent tasks, and 409 for version conflict.
Use package.json with type module and scripts for start, test, and check. Separate server.mjs, app.mjs, routes, task-service, repository, validation, errors, config, logger, and tests. app constructs request handling; server owns configuration, listen, signals, and resource shutdown.
A module exports behavior and receives dependencies instead of importing mutable global singletons throughout the tree. The in-memory repository is acceptable for the first milestone; swap it for the MongoDB or MySQL repository only after repository contract tests pass.
task-api/
package.json
src/
server.mjs
app.mjs
config.mjs
errors.mjs
task-service.mjs
repositories/memory-tasks.mjs
test/
task-service.test.mjs
http.test.mjs
The entry point owns the process; application and domain modules remain importable without opening a port.
Parse each request URL with new URL(req.url, base). Route from method and pathname, decode one path segment safely, and reject unsupported methods with Allow where appropriate. Set content-type, nosniff, cache, and correlation headers deliberately.
Read the body as a bounded stream. Stop and destroy or drain according to server policy when the limit is exceeded. Decode UTF-8, parse JSON once, verify an object shape, and do not let parser or validation stacks reach the response.
The service enforces title length, allowed status transitions, version or updatedAt conflict checks, and not-found outcomes. The repository owns storage operations but not HTTP objects. Return copies from the memory repository so callers cannot mutate stored state accidentally.
For a database repository, parameterize values, select only response fields, enforce constraints, use one pool or client per process, and run multi-write invariants in one transaction. Preserve the same domain outcomes so HTTP and service tests do not depend on a driver.
Give the handler a request ID, logger, service, clock, and ID generator. This makes deterministic tests possible. Await every operation and route all known errors through one mapper. If a response has started, record and close the partial response rather than attempting a second JSON document.
Propagate an AbortSignal from client disconnect and server shutdown to cooperative work. Do not claim cancellation rolled back a database commit; use conditional updates and idempotency where retries are allowed.
{
"error": {
"code": "TASK_NOT_FOUND",
"message": "Task was not found",
"requestId": "018f..."
}
}
Clients receive a stable classification and correlation ID, while stacks and causes stay in protected logs.
Unit-test task validation and state transitions. Run repository contract tests against every implementation. Start the HTTP server on port 0 for integration tests covering status, headers, body, malformed JSON, oversized input, unknown fields, missing records, conflicts, and dependency failure.
Test concurrent updates to one task, client abort, SIGTERM during a request, unavailable storage at startup, and storage failure after readiness. Assert final state and released resources, not only response text. Run the suite from a clean npm ci installation.
Add body, field, request-time, concurrency, and pagination limits. If identity is added, authorize every task operation by owner; never trust an owner ID from the request body. Run with narrow Node permissions and least-privilege storage credentials.
Emit structured request.start and request.complete events with request ID, route template, method, status, duration, and safe error code. Measure request latency, errors, active requests, repository time, event-loop delay, and memory. Keep task titles and authorization headers out of telemetry.
Provide a production start command, container or service definition, configuration schema, migration plan for database storage, readiness and liveness checks, SIGTERM behavior, and rollback instructions. Build one immutable artifact with Node 24 LTS and the committed lockfile.
The capstone is complete when a reviewer can clone it, install reproducibly, run tests, start it, exercise every endpoint, observe a safe failure, terminate it gracefully, and connect each design decision to a lesson in this course.
It exposes the Node.js HTTP, stream, URL, error, and process contracts taught in this course. The separate Express tutorial can then show how a framework organizes the same boundaries.
No. Start with an in-memory repository and contract tests, then replace it with the MongoDB or MySQL implementation without changing domain behavior.
Explore 500+ free tutorials across 20+ languages and frameworks.