Express

Lesson 07

Response shape consistency

Return predictable success and error envelopes across routes.

Good Code

src/http/respond.ts
import type { Response } from "express";

export function ok<T>(res: Response, data: T) {
  return res.json({ data });
}

export function created<T>(res: Response, data: T) {
  return res.status(201).json({ data });
}

export function fail(res: Response, status: number, message: string) {
  return res.status(status).json({ error: { message } });
}

Bad Code

src/reviews/router.ts
router.get("/", async (_req, res) => {
  res.json(await service.listOpenReviews());
});

router.post("/", async (req, res) => {
  res.status(201).json({ review: await service.createReview(req.body) });
});

router.get("/:id", async (req, res) => {
  const review = await service.getReview(req.params.id);

  if (!review) {
    res.status(404).send("not found");
    return;
  }

  res.json({ data: { review } });
});

Review Notes

What to review

Good Code

The good version gives routes small helpers for success and error responses, making the API contract consistent.

Bad Code

The bad version returns arrays, objects, strings, and nested data shapes depending on the route.

Takeaways

  • Clients should not need route-specific parsing rules for common API responses.