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).
| Code | Meaning | Typical use |
|---|---|---|
200 OK | Success, with a response body | A successful GET, or a PUT/PATCH returning the updated resource |
201 Created | A new resource now exists | A successful POST that created something — response typically includes the new resource, often with a Location header |
204 No Content | Success, deliberately no body | A successful DELETE, or an update where there's nothing meaningful to return |
400 Bad Request | The request itself is malformed or fails validation | Missing required field, wrong data type, invalid JSON |
401 Unauthorized | No valid identity presented at all | Missing or invalid auth token — despite the name, this is about authentication, not authorization |
403 Forbidden | Identity is known, but not allowed to do this | A logged-in user trying to access another user's private resource |
404 Not Found | No resource exists at this URL | Requesting /orders/999 when order 999 doesn't exist |
409 Conflict | The request conflicts with the resource's current state | Trying to create a resource that already exists (duplicate email on signup), or a version/optimistic-lock mismatch |
500 Internal Server Error | The server failed in a way that isn't the client's fault | An 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).
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.