1 Resources Are Nouns, Methods Are Verbs
The URL identifies the resource. The HTTP method describes the action. Never encode actions in the URL:
❌ POST /api/createUser
❌ GET /api/getOrderById?id=42
❌ POST /api/deleteProduct/99
✅ POST /api/users
✅ GET /api/orders/42
✅ DELETE /api/products/992 Use Plural Nouns Consistently
Collections are plural; individual resources use their identifier:
GET /users → list all users
POST /users → create a user
GET /users/42 → get user with id 42
PUT /users/42 → replace user 42
PATCH /users/42 → partial update user 42
DELETE /users/42 → delete user 42Stick to plural: /users not /user, /orders not /order. Mixing breaks developer muscle memory.
3 HTTP Methods: What Each One Actually Means
| Method | Idempotent? | Safe? | Use for |
|---|---|---|---|
| GET | Yes | Yes | Read resource(s) — never side-effects |
| POST | No | No | Create new resource; non-idempotent actions |
| PUT | Yes | No | Replace entire resource (full update) |
| PATCH | Usually | No | Partial update (only provided fields) |
| DELETE | Yes | No | Remove a resource |
| HEAD | Yes | Yes | GET but response body omitted (metadata only) |
| OPTIONS | Yes | Yes | Describe allowed methods (CORS preflight) |
Idempotent means calling the same request multiple times produces the same result. Safe means no server-side state changes. These properties matter for retry logic and caching.
4 Status Codes: Be Precise, Not Generic
Returning 200 OK with { "success": false } is the single most developer-hostile pattern in REST APIs. Use the right code:
201 Created → POST succeeded, resource created
204 No Content → DELETE or PATCH succeeded, no body
400 Bad Request → client sent invalid data (validation)
401 Unauthorized → no/invalid authentication token
403 Forbidden → authenticated but not authorised
404 Not Found → resource does not exist
409 Conflict → duplicate create, version conflict
422 Unprocessable→ semantically invalid (e.g., future birthdate)
429 Too Many Req → rate limit exceeded
500 Server Error → unhandled server-side exceptionNever return 200 OK for errors. Clients use the HTTP status code as the primary routing signal. If your errors return 200, every HTTP client, proxy, CDN, and monitoring tool will classify them as successes. This breaks logging, alerting, and retry logic silently.
5 Versioning: Put It in the URL Path
There are three versioning strategies (header-based, Accept header, URL path). URL path is the most visible and easiest for developers to work with:
/api/v1/users
/api/v2/usersVersion only when you make breaking changes. Non-breaking additions (new fields, new endpoints) don't need a version bump — that's the additive compatibility rule. A "version" is a compatibility boundary, not a release number.
6 Filtering, Sorting, and Searching via Query Parameters
GET /orders?status=pending&page=2&limit=25
GET /users?sort=createdAt&order=desc
GET /products?q=laptop&minPrice=500&maxPrice=2000Never encode filter criteria in the URL path. /orders/pending is not RESTful — it conflates resource identity with query. Keep the path for identity (/orders/42), query string for filtering.
7 Nested Resources: One Level Deep Maximum
GET /users/42/orders → orders for user 42 ✅
GET /users/42/orders/7/items/3/reviews → too deep ❌
// Deep nesting alternative: use query params
GET /reviews?orderId=7&userId=42Deep nesting couples your URL structure to your database schema. When the schema changes, all deep URLs break. Two levels of nesting is the practical limit.
8 Always Return Consistent Response Envelopes
// Single resource
{ "data": { "id": "42", "name": "Alice" } }
// Collection
{
"data": [ ... ],
"pagination": { "total": 100, "nextCursor": "abc" }
}
// Error
{
"error": {
"code": "NOT_FOUND",
"message": "User 42 does not exist",
"requestId": "req_abc123"
}
}9 Rate Limiting Headers Are Developer UX
Always include rate limiting information in response headers so clients can self-throttle:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1749254400
Retry-After: 60 # on 429 responses10 Design for Additive Change from Day One
The contract you sign with API consumers: never remove fields, never change types, never rename keys. Only add. Consumers must be built to ignore unknown fields. Your server must treat missing optional fields as absent, not error. Follow this and you can ship improvements weekly without breaking a single integration.
- URLs identify resources (nouns), methods are actions (verbs)
- Use 201 Created for POST, 204 No Content for DELETE
- 401 = not authenticated; 403 = not authorised (different things)
- Version in URL path:
/api/v1/ - Filters in query params, identifiers in path
- Always wrap lists in
{ "data": [] } - Never return HTTP 200 for errors