Building RESTful APIs

1. Implementing Resource Routes

HTTPURLActionStatus
GET/usersList200
GET/users/:idRead200 / 404
POST/usersCreate201 + Location
PUT/users/:idReplace200 / 204
PATCH/users/:idPartial update200 / 204
DELETE/users/:idRemove204

Example: Resource router

const r = Router();
r.get("/",       list);
r.post("/",      create);
r.get("/:id",    read);
r.put("/:id",    replace);
r.patch("/:id",  partialUpdate);
r.delete("/:id", remove);
app.use("/api/v1/users", r);

2. Using Proper HTTP Methods

VerbUse For
GETRead-only, safe, idempotent — cacheable
POSTCreate resource OR non-idempotent action
PUTReplace entire resource (idempotent)
PATCHPartial update — JSON Merge Patch / JSON Patch
DELETERemove resource (idempotent)

3. Setting HTTP Status Codes

CodeWhen
200 OKSuccessful read or update with body
201 CreatedPOST created a resource (include Location)
202 AcceptedAsync work started
204 No ContentSuccess without body (DELETE, PUT)
301/308Permanent redirect
400 Bad RequestMalformed body / params
401 UnauthorizedMissing/invalid credentials
403 ForbiddenAuthenticated but not allowed
404 Not FoundResource doesn't exist
409 ConflictVersion mismatch / duplicate
422 Unprocessable EntitySemantic validation failure
429 Too Many RequestsRate-limited
500/502/503Server errors

4. Structuring JSON Responses

Example: Envelope vs flat

// Flat
res.json(user);

// Envelope (preferred for collections)
res.json({
  data: users,
  meta: { page: 2, limit: 20, total: 137 },
  links: { self: "/users?page=2", next: "/users?page=3", prev: "/users?page=1" }
});

// Errors
res.status(422).json({ error: { code: "VALIDATION", message: "...", details: [...] } });

5. Implementing Resource Naming

RuleExample
Plural nouns/users not /user
Lowercase/order-items
Hyphens, not underscores/api/v1/order-items
No verbsPOST /users not /createUser
Sub-resources/users/:id/posts
Filtering via query/users?role=admin

6. Using Nested Resources

Example: Nested router

const posts = Router({ mergeParams: true });
posts.get("/", (req, res) => res.json(getPosts(req.params.userId)));
posts.post("/", (req, res) => res.status(201).json(createPost(req.params.userId, req.body)));

const users = Router();
users.use("/:userId/posts", posts);
app.use("/api/v1/users", users);
Note: Limit nesting to 1–2 levels. Deeper hierarchies become unwieldy — flatten with query filters instead.
res.json({
  id: 42,
  name: "Alice",
  _links: {
    self:    { href: `/users/42` },
    posts:   { href: `/users/42/posts` },
    avatar:  { href: `/users/42/avatar` },
    edit:    { href: `/users/42`, method: "PATCH" },
    "delete": { href: `/users/42`, method: "DELETE" }
  }
});

8. Handling Bulk Operations

Example: Bulk create

app.post("/users/bulk", async (req, res) => {
  const items = z.array(userSchema).max(100).parse(req.body);
  const results = await Promise.allSettled(items.map(db.users.create));
  const created = results.filter(r => r.status === "fulfilled").map(r => r.value);
  const errors  = results
    .map((r, i) => r.status === "rejected" ? { index: i, error: r.reason.message } : null)
    .filter(Boolean);
  res.status(207).json({ created, errors });  // 207 Multi-Status
});

9. Using Proper Error Responses

Example: Consistent error shape

res.status(422).json({
  error: {
    code: "VALIDATION_ERROR",
    message: "Invalid input",
    details: [
      { field: "email", message: "Invalid format" },
      { field: "age",   message: "Must be ≥ 18" }
    ],
    requestId: req.id
  }
});

10. Implementing Idempotent Operations

VerbIdempotent?Implementation tip
PUTYesReplace entire state
DELETEYesReturn 204 even if already gone
POSTNo (default)Use Idempotency-Key header to dedupe

Example: Idempotency-Key

app.post("/payments", async (req, res) => {
  const key = req.get("Idempotency-Key");
  if (!key) return res.status(400).json({ error: "Idempotency-Key required" });
  const existing = await cache.get(`idem:${key}`);
  if (existing) return res.status(200).json(JSON.parse(existing));
  const result = await processPayment(req.body);
  await cache.set(`idem:${key}`, JSON.stringify(result), { EX: 86400 });
  res.status(201).json(result);
});