After this lesson, you will be able to: Design REST APIs: routing, middleware, error handling, request validation, consistent response shape.
Same patterns whether Express or Fastify. Knowing them = production-grade APIs.
Composition is the model.
import express from "express";const app = express();// Middleware: runs for every request that matchesapp.use(express.json());app.use((req, _res, next) => {console.log(req.method, req.url);next();});// Mount a router (modular routes)import userRoutes from "./routes/users.js";app.use("/api/users", userRoutes);// routes/users.jsimport { Router } from "express";const router = Router();router.get("/", listUsers);router.get("/:id", getUser);router.post("/", requireAuth, createUser);export default router;
One central handler for consistent responses.
// Errors thrown in handlers bubble to this middleware (Express 5 auto-awaits)app.use((err, _req, res, _next) => {if (err instanceof z.ZodError) {return res.status(400).json({error: { code: "validation", details: err.flatten() },});}if (err instanceof NotFoundError) {return res.status(404).json({ error: { code: "not_found" } });}console.error(err);res.status(500).json({ error: { code: "internal" } });});
Pick a shape and stick to it: `{ data, error }` or `{ ok, data, error }`. Wrong: random `{ users: [...] }` here, `{ result: ... }` there, raw arrays elsewhere. Clients write less defensive code when the shape is predictable.
Use Zod for request bodies, query strings, path params.
import { z } from "zod";const ListQuery = z.object({limit: z.coerce.number().int().positive().max(100).default(20),cursor: z.string().optional(),});app.get("/users", async (req, res) => {const q = ListQuery.parse(req.query); // throws on bad input; caught by central error handlerconst users = await db.user.findMany({ take: q.limit, cursor: q.cursor ? { id: q.cursor } : undefined });res.json({ data: users });});
Trusting req.body without validation. Always parse. Inconsistent response shapes. Clients suffer. Returning stack traces. Information leak. No request ID. Logs become un-traceable. Add `req.id = crypto.randomUUID()` early.
Pick the cleanest answer.
Sign in and purchase access to unlock this lesson.