CodeOath
← All posts
Node.js36 min total · 14 parts

Node.js Fundamentals: The Runtime, the Event Loop, and Building Real APIs

Part 11 of 14 · ~3 min

REST API Design in Express

Resource-oriented URLs

REST URLs name resources (nouns), not actions (verbs) — the HTTP method is what already carries the verb:

GET    /orders          → list orders
POST   /orders          → create an order
GET    /orders/:id      → get one order
PATCH  /orders/:id      → partially update one order
DELETE /orders/:id      → delete one order
GET    /orders/:id/items → items belonging to a specific order (nested resource)

GET /orders/delete/5 mixes a verb into the URL that the method (GET) already contradicts — it's not just a style nit, it actively fights the semantics HTTP already gives you for free (caching, idempotency guarantees, and so on all key off the method).

Status codes that actually get tested

CodeMeaningTypical use
200 OKSuccess, with a response bodyA successful GET, or a PUT/PATCH returning the updated resource
201 CreatedA new resource now existsA successful POST that created something — response typically includes the new resource, often with a Location header
204 No ContentSuccess, deliberately no bodyA successful DELETE, or an update where there's nothing meaningful to return
400 Bad RequestThe request itself is malformed or fails validationMissing required field, wrong data type, invalid JSON
401 UnauthorizedNo valid identity presented at allMissing or invalid auth token — despite the name, this is about authentication, not authorization
403 ForbiddenIdentity is known, but not allowed to do thisA logged-in user trying to access another user's private resource
404 Not FoundNo resource exists at this URLRequesting /orders/999 when order 999 doesn't exist
409 ConflictThe request conflicts with the resource's current stateTrying to create a resource that already exists (duplicate email on signup), or a version/optimistic-lock mismatch
500 Internal Server ErrorThe server failed in a way that isn't the client's faultAn unhandled exception, a database connection failure

401 vs. 403 is the pair people mix up most: 401 means "I don't know who you are, or you haven't proven it" (no token, expired token); 403 means "I know exactly who you are, and the answer is no" (a valid, authenticated user who simply lacks permission for this specific resource).

Request validation

Validate input before it reaches business logic — checking shape and types at the boundary means everything downstream can trust the data it receives, instead of re-checking it defensively at every layer:

app.post("/orders", (req, res, next) => {
  const { customerId, items } = req.body;

  if (typeof customerId !== "string" || customerId.length === 0) {
    return res.status(400).json({ error: "customerId is required and must be a string" });
  }
  if (!Array.isArray(items) || items.length === 0) {
    return res.status(400).json({ error: "items must be a non-empty array" });
  }

  createOrder({ customerId, items })
    .then((order) => res.status(201).json(order))
    .catch(next);
});

Hand-writing checks like this works for small APIs, but real projects generally reach for a schema-validation library (Zod, Joi, and similar) once there's more than a couple of fields — the value isn't just less boilerplate, it's a single declarative source of truth for what a valid request even looks like, instead of validation logic scattered across if statements that drift out of sync with what the code actually expects.