Conventions That Make Your API Read Like Documentation
REST Intro
REST isn't a spec — it's a set of conventions. Following them makes your API predictable for every dev who hits it.
What you'll learn
- Apply REST conventions to URLs and methods
- Pick the right status codes
- Shape JSON responses consistently
REST is a set of conventions for HTTP APIs. Follow them and any dev can read your API at a glance.
Resources, Not Actions
URLs name nouns, methods name verbs:
| Good | Bad |
|---|---|
GET /users | GET /listUsers |
POST /users | POST /createUser |
DELETE /users/42 | POST /deleteUser?id=42 |
PATCH /users/42 | POST /updateUser?id=42 |
The verb is in the HTTP method, not the URL.
Pluralize Collections
/users, /posts, /orders — not /user, /post. Even when
operating on one item: /users/42. The URL pattern reads as
“the user resource with ID 42.”
Methods Map to Operations
| Method | What it does | Idempotent? |
|---|---|---|
GET /users | List | yes |
GET /users/42 | Read one | yes |
POST /users | Create | no |
PUT /users/42 | Full replace | yes |
PATCH /users/42 | Partial update | yes-ish |
DELETE /users/42 | Delete | yes |
Idempotent = repeating the same call gives the same end state.
Status Codes
Use them with intent:
res.status(200).json(user); // OK
res.status(201).json(newUser); // Created
res.status(204).end(); // No Content (after DELETE)
res.status(400).json({ ... }); // Bad Request (invalid input)
res.status(401).json({ ... }); // Unauthorized (not logged in)
res.status(403).json({ ... }); // Forbidden (no permission)
res.status(404).json({ ... }); // Not Found
res.status(409).json({ ... }); // Conflict (duplicate, version mismatch)
res.status(422).json({ ... }); // Unprocessable (valid format, invalid semantics)
res.status(429).json({ ... }); // Too Many Requests
res.status(500).json({ ... }); // Internal Server Error The 401 vs 403 distinction trips lots of devs:
- 401 — “I don’t know who you are” (no/invalid auth)
- 403 — “I know you, but you can’t”
Consistent Response Shape
Pick one shape for success and one for errors:
// single resource
{ "data": { "id": 1, "name": "Ada" } }
// collection
{ "data": [...], "pagination": { "nextCursor": "...", "total": 100 } }
// error
{ "error": { "code": "user_not_found", "message": "..." } } Mix shapes ({ user: ... }, { users: [...] }, { data: ... })
and your client code becomes a special-case grab-bag.
Versioning
Bake a version into the URL when you might break clients:
/api/v1/users
/api/v2/users Bump when you make a breaking change. The other approach (header
versioning, Accept: application/vnd.myapi.v2+json) is cleaner
in theory and a pain in practice.
Pragmatic Exits
Some operations don’t fit REST:
- Login?
POST /auth/login(creating a session). - Trigger a recompute?
POST /reports/:id/recompute. - Bulk operation?
POST /users/bulk-updatewith a request body.
REST is conventions, not religion. Break them where they don’t help.
Controllers →