Capstone: Building a Production-Ready REST API

Advanced
16 min

Capstone: Building a Production-Ready REST API

Every previous lesson introduced one capability in isolation. This chapter assembles them into TaskFlow, a multi-user task management API with projects, tasks, attachments and real-time updates. After this lesson you will have a reference architecture to clone for your own products and a checklist for judging whether an Express API is ready to ship.

Requirements

| Area | Requirement | Lessons applied | | --- | --- | --- | | Accounts | Register, log in, refresh JWT, Google sign-in | Sessions and JWT, Passport | | Projects and tasks | CRUD, members, ownership rules, admin role | MVC, Mongoose, Authorization | | Lists | Pagination, filters by status and assignee, sorting, search | Pagination | | Attachments | Image upload per task, size and type limits | Multer | | Real time | Task changes pushed to project members | Socket.IO | | Quality | Validation, consistent errors, structured logs, tests, docs | Zod, Errors, Pino, Jest, Swagger | | Operations | Config, security headers, rate limits, Redis cache, Docker, Nginx | Config, Security, Redis, Docker |

Project structure

bash
taskflow-api/ src/ app.js # composition root (shown above) server.js # DB connect, Socket.IO, listen, graceful shutdown config/index.js # validated environment lib/{logger,redis}.js routes/index.js # mounts auth, projects, tasks, uploads modules/ auth/ {auth.routes,auth.controller,auth.service,auth.schema}.js projects/ {project.routes,project.controller,project.service,project.model}.js tasks/ {task.routes,task.controller,task.service,task.model,task.schema}.js middleware/ {auth,authorize,validate,upload,cache,error-handler}.js docs/openapi.yaml tests/ # supertest suites per module + tests/setup.js Dockerfile compose.yaml nginx/default.conf .env.example

Grouping by feature keeps a task's routes, controller, service and schema together; the layer discipline from the MVC lesson still applies inside each folder.

One feature slice, end to end

The task routes compose authentication, authorization, validation, caching and upload handling in one readable line per endpoint:

javascript
// src/modules/tasks/task.routes.js import { Router } from "express"; import { requireAuth } from "../../middleware/auth.js"; import { loadProjectMember } from "../../middleware/authorize.js"; import { validate } from "../../middleware/validate.js"; import { cacheResponse } from "../../middleware/cache.js"; import { upload } from "../../middleware/upload.js"; import { createTaskSchema, updateTaskSchema, listTasksSchema } from "./task.schema.js"; import * as c from "./task.controller.js"; const router = Router({ mergeParams: true }); // mounted at /projects/:projectId/tasks router.use(requireAuth, loadProjectMember); // sets req.user and req.project router.get("/", validate(listTasksSchema), cacheResponse(30), c.list); router.post("/", validate(createTaskSchema), c.create); router.patch("/:id", validate(updateTaskSchema), c.update); router.post("/:id/attachment", upload.single("file"), c.attach); router.delete("/:id", c.remove); export default router;
javascript
// src/modules/tasks/task.controller.js import * as tasks from "./task.service.js"; export async function create(req, res) { const task = await tasks.create(req.validated.body, req.project.id, req.user.id); req.app.get("io").to(`project:${req.project.id}`).emit("task:created", task); res.status(201).json({ data: task }); }

The controller is four lines because every other concern lives elsewhere: the schema validated the body, loadProjectMember proved access, the service enforces rules and clears the Redis cache, and the error middleware handles anything thrown.

The production checklist

  • Config: the config module throws on missing variables; .env.example is committed, .env is not.
  • Security: helmet, CORS allowlist, general and login rate limits, body size limit, trust proxy.
  • Auth: hashed passwords, short-lived access tokens with refresh, httpOnly cookies for sessions.
  • Data: validation on every write, indexes on filtered and sorted fields, lean() reads, capped limit.
  • Errors: AppError for operational failures, one error handler, no stack traces in production, a 404 route.
  • Observability: pino-http with request IDs, a /health endpoint, errors logged under the err key.
  • Tests and docs: Supertest suites covering 401, 403, 404 and 422 paths in CI; OpenAPI served at /docs.
  • Delivery: multi-stage Dockerfile, Compose, Nginx with forwarded headers, graceful SIGTERM handling.
Quick Quiz
Question 1 of 3

Why is the task controller only a few lines long?

Key Takeaways

  • A production Express API is a composition of small, single-purpose middleware and layered modules, wired together in app.js.
  • Feature folders keep routes, controller, service, model and schema together while preserving layer boundaries.
  • A route's middleware chain should read like its access policy: authenticate, authorize, validate, then handle.
  • The production checklist (config, security, auth, data, errors, observability, tests, docs, delivery) is the definition of done.

Next lesson: What to learn next — you have completed the Express.js course; continue with the MongoDB course for aggregation and indexing, the REST APIs course for API design depth, or the Docker course to run this project in production with confidence.

Capstone: Building a Production-Ready REST API - Express.js | CodeYourCraft | CodeYourCraft