What you'll learn
The Phase 2 project turns the task tracker into a layered HTTP service. You will combine Express routing, validation, JSON persistence, error contracts, request observability, integration tests, and documentation into one review-ready API.
By the end of this lesson, you'll be able to:
- Design and implement a complete task REST contract
- Separate HTTP, application, domain, and persistence boundaries
- Protect the API with validation and centralized errors
- Deliver tests, documentation, health signals, and safe lifecycle behavior
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 |
|---|---|---|
| Composition root | One place constructs concrete dependencies and the application | Wire repository, service, logger, config, and routes at startup |
| Service layer | Coordinates task rules independent of HTTP | Accept ordinary values and throw domain/application errors, not Express responses |
| Repository contract | Abstracts task persistence operations | Use the same contract for JSON storage and in-memory tests |
| Delivery contract | Routes, statuses, schemas, errors, and headers promised to clients | Protect it with integration tests and OpenAPI |
Professional workflow
Build and verify Node.js programs from the terminal in small, observable steps.
- Freeze the route/status/schema contract
- Define repository and service boundaries
- Implement and test task rules
- Add a JSON repository with bounded validated data
- Create Express routers and validation middleware
- Install request context, not-found, and error handling
- Add health, shutdown, integration tests, and OpenAPI
- Run the full verification workflow from a clean install
Keep the feedback loop short
Guided code lab
Keep the service independent of Express
The service coordinates identity, timestamps, and repository persistence using plain JavaScript values.
import { randomUUID } from "node:crypto";
import { HttpError } from "./http-errors.js";
export function createTasksService({ repository, clock = () => new Date() }) {
return {
async create(input) {
const task = {
id: randomUUID(),
title: input.title,
completed: false,
createdAt: clock().toISOString(),
};
await repository.insert(task);
return task;
},
async find(id) {
const task = await repository.findById(id);
if (!task) throw new HttpError(404, "TASK_NOT_FOUND", "Task not found");
return task;
},
};
}Translate HTTP in a thin router
Validation and service behavior are dependencies; the handler maps the result into the published HTTP contract.
export function createTasksRouter({ service, validateBody }) {
const router = Router();
router.post("/", validateBody(validateNewTask), async (request, response) => {
const task = await service.create(request.validatedBody);
response
.status(201)
.location("/api/tasks/" + task.id)
.json({ data: task });
});
router.get("/:taskId", async (request, response) => {
const task = await service.find(request.params.taskId);
response.json({ data: task });
});
return router;
}Define the clean delivery workflow
A reviewer should be able to reproduce setup and verification with a short stable command sequence.
npm ci
npm run check
npm test
npm start
# In another terminal:
curl -i http://127.0.0.1:3000/health/ready
curl -i -X POST http://127.0.0.1:3000/api/tasks -H "content-type: application/json" -d '{"title":"Ship Phase 2"}'Production practice
Protect boundaries
HTTP knows Express, the service knows task operations, and the repository knows persistence. Dependencies point inward through small contracts.
Ship reproducibly
A clean install, one verification command, deterministic tests, and documented environment variables are part of the deliverable.
Design for replacement
The JSON repository is temporary; a narrow contract lets Phase 3 introduce a database without rewriting routes and rules.
Common failure mode
Independent workshop
Complete the Task REST API as a Phase 2 portfolio project ready for review and future database integration.
Your finished workshop must include:
- GET/POST/PATCH/DELETE task endpoints with filters and pagination
- Layered app, router, service, and repository modules
- Bounded JSON parsing and validated inputs
- Consistent error codes and request IDs
- Liveness, readiness, and graceful shutdown
- Unit and integration test coverage
- OpenAPI 3.1 documentation and curl examples
- Clean-install and verification instructions
Definition of done
Recap & quick check
Key takeaways
- The composition root owns concrete dependencies
- Services keep business behavior independent of Express
- Repositories isolate persistence
- Routers translate the HTTP contract
- Tests and OpenAPI protect delivery behavior
- Operational readiness is part of API design
- Phase 3 can replace JSON with a database behind the same boundary
Quick check
1. Where should concrete dependencies be wired together?
2. Should a task service return Express response objects?
3. What enables replacing JSON persistence later?
4. What protects the public API contract?
Next: Phase 3 — Databases & Persistent Data