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/99

2 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 42

Stick to plural: /users not /user, /orders not /order. Mixing breaks developer muscle memory.

3 HTTP Methods: What Each One Actually Means

MethodIdempotent?Safe?Use for
GETYesYesRead resource(s) — never side-effects
POSTNoNoCreate new resource; non-idempotent actions
PUTYesNoReplace entire resource (full update)
PATCHUsuallyNoPartial update (only provided fields)
DELETEYesNoRemove a resource
HEADYesYesGET but response body omitted (metadata only)
OPTIONSYesYesDescribe 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 exception
🚫 The 200-for-everything anti-pattern

Never 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/users

Version 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=2000

Never 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=42

Deep 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 responses

10 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.

✅ Quick Reference
  • 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