Building a REST API — status codes, validation, and resource structure

A REST API's URLs and status codes aren't arbitrary — they're a real, established vocabulary that lets a client understand what happened without reading custom documentation for every single endpoint, and using the wrong one defeats that entire shared vocabulary.

Intermediate

4 min read

Resources as nouns, HTTP methods as the verbs

app.get("/orders", listOrders);        // GET a collection
app.get("/orders/:id", getOrder);       // GET one specific resource
app.post("/orders", createOrder);        // POST creates a NEW resource
app.put("/orders/:id", replaceOrder);      // PUT replaces the ENTIRE resource
app.patch("/orders/:id", updateOrder);      // PATCH updates PART of a resource
app.delete("/orders/:id", deleteOrder);      // DELETE removes it

The REST convention treats a URL as identifying a resource (a noun — /orders, a specific order) and the HTTP method as the action taken on it (a verb) — rather than encoding the action into the URL itself (/getOrders, /createOrder, an older, non-RESTful style). This separation is what makes a well-designed REST API's shape predictable across completely different applications: once the convention is known, DELETE /orders/42 reads as "delete order 42" without needing endpoint-specific documentation to guess at.

PUT vs PATCH: a real, meaningful difference, not interchangeable synonyms

// PUT — client sends the ENTIRE resource; anything omitted is typically treated as CLEARED
app.put("/orders/42", (req, res) => {
  // req.body should contain every field — a partial body means missing fields get wiped
});
 
// PATCH — client sends ONLY the fields that should change; everything else stays untouched
app.patch("/orders/42", (req, res) => {
  // req.body = { status: "shipped" } — only status changes, every other field is left alone
});

PUT is defined as a full replacement of the resource — a client sending a partial body is, by the convention's own definition, saying "replace the entire thing with this," which typically means unspecified fields get reset or cleared. PATCH is a partial update — only the fields actually present in the request body are meant to change. This distinction is real and meaningful, not a style preference: using PUT for what's actually a partial update is a common, real API design mistake that leads to fields silently getting wiped when a client only intended to update one field.

Status codes: a real, shared vocabulary — not just "200 good, other bad"

res.status(200).json(order);              // OK — a successful GET, PUT, or PATCH
res.status(201).json(newOrder);             // Created — a successful POST that created something new
res.status(204).end();                       // No Content — a successful DELETE, nothing to return
res.status(400).json({ error: "Invalid" });    // Bad Request — the CLIENT sent something malformed
res.status(401).json({ error: "Unauthorized" }); // the client isn't authenticated at all
res.status(403).json({ error: "Forbidden" });      // authenticated, but not ALLOWED to do this
res.status(404).json({ error: "Not found" });        // the resource genuinely doesn't exist
res.status(500).json({ error: "Server error" });       // the SERVER'S fault, not the client's

Status codes are a genuine, shared vocabulary a well-behaved HTTP client (a browser, a library, another service) can react to generically, without knowing anything about a specific API's own documentation — a 401 universally signals "authenticate and try again," a 404 universally signals "this doesn't exist," regardless of which API returned it. Returning 200 for every response and putting the real result in the body (a real, common anti-pattern) throws away this entire shared vocabulary, forcing every client to parse the body just to know whether something even succeeded.

Input validation: never trust req.body to already be correct

app.post("/orders", (req, res) => {
  const { customerId, items } = req.body;
  if (!customerId || typeof customerId !== "string") {
    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" });
  }
  // ... only proceed once the input is actually verified to have the expected shape
});

req.body is exactly whatever the client sent — there's no guarantee it matches the shape an endpoint expects, whether from a genuine client bug, a malicious request, or simply a missing field — so validating it explicitly before acting on it is a real, necessary step, not defensive over-caution. Libraries like zod or joi handle this validation more declaratively at scale (defining the expected shape once, validating against it), but the underlying principle is the same regardless of tooling: never assume the request body already matches what the handler expects.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What's the real, meaningful difference between PUT and PATCH?

2. Why does returning 200 for every response, with the real result encoded only in the body, defeat REST's purpose?

3. Why is validating req.body explicitly, rather than trusting it, a necessary step rather than defensive over-caution?