REST API design
Resources, endpoints, CRUD mapping, versioning.
REST isn't a spec — it's a discipline: model the world as resources addressed by URLs, manipulate them with a uniform set of HTTP verbs, keep the server stateless. Do this consistently and your API will feel obvious to people who have never seen it.
The big idea
A good REST API answers two questions the same way every time: "what is the thing?" and
"what am I doing to it?". The first becomes a noun in the URL (/orders/42), the second
becomes an HTTP method (GET, POST, PUT, PATCH, DELETE). If a developer can guess
the URL for a resource they have never seen and be right, you have done it well.
Designing resources, not actions
Resources are nouns, never verbs. POST /createOrder is a smell. The interesting design
work is choosing the right granularity and naming.
GET /users/42/orders
POST /users/42/orders
DELETE /users/42/orders/9001The resource is "orders belonging to user 42". Method picks the action.
GET /getUserOrders?id=42
POST /createOrderForUser
POST /deleteOrder?id=9001Verbs in the URL, IDs in query strings, POST doing everything.
CRUD ↔ HTTP mapping
The default mapping is unambiguous; deviate only when the protocol forces you.
| Attribute | Operation | HTTP |
|---|---|---|
| Create one | POST /orders | 201 Created + Location header |
| Read collection | GET /orders?page=1 | 200 OK + array |
| Read one | GET /orders/42 | 200 OK + object · 404 if absent |
| Replace one | PUT /orders/42 | 200 / 204 — full body required |
| Patch one | PATCH /orders/42 | 200 / 204 — partial body |
| Delete one | DELETE /orders/42 | 204 No Content |
Pagination, filtering, sorting
Lists grow unboundedly. Pick one convention up front and use it everywhere:
GET /orders?status=paid&sort=-created_at&page=2&per_page=20For high-volume APIs, prefer cursor pagination over offset (?cursor=eyJpZCI6OTAwMX0):
offset gets slower as you go deeper and breaks on inserts.
Versioning
You will get the model wrong. Plan for it: pick one versioning scheme and never mix.
- URL path
/v1/orders— cache-friendly, easy to debug. Default for most public APIs.- Header
Accept: application/vnd.api+json;version=2— keeps URLs stable but hides the version.- Date
2025-11-01— Stripe's approach; rarely worth its complexity outside that scale.
Error responses
Use the right status family, and put the structured error in the body. Be consistent.
{
"error": {
"code": "order.invalid_state",
"message": "Cannot cancel an order that has already shipped.",
"details": { "order_id": 9001, "current_state": "shipped" }
}
}In practice
Pick a stack with strong opinions (FastAPI, NestJS, .NET Minimal APIs) and let it enforce consistency — content negotiation, request validation, status codes, OpenAPI generation. The endpoints you hand-design will be a thin layer of routing on top.
Key takeaways
- Resources are nouns; HTTP methods supply the verbs. No `getX` in URLs.
- Map CRUD onto HTTP with the default conventions; deviate only with reason.
- Standardise pagination, filtering, and sorting across every list endpoint.
- Choose one versioning scheme up front — URL path is the safe default.
- Errors use the right status code; the body adds machine-readable detail.
Checkpoint questions
Use these to test whether the lesson is clear enough to explain without rereading.
- 1Can you turn an action-shaped endpoint into a resource-shaped REST endpoint?
- 2Which HTTP method and status code would you use for create, read, update, and delete?
- 3When would cursor pagination be safer than offset pagination?
- 4What should a consistent API error response include?
References
External resources for going deeper after the lesson above.