REST API Design Best Practices
A well-designed REST API is intuitive to use, consistent across endpoints, and resilient to change. Good API design reduces integration time for clients, minimizes breaking changes, and makes your service easier to maintain. This article covers the core principles — resource naming, HTTP methods, status codes, pagination, and error handling — that every API designer should follow.
Resource Naming Conventions
Use nouns (not verbs) to represent resources, and use plural forms for collections. The URL should describe the resource, not the action. Actions on resources are expressed through HTTP methods, not URL paths. For example, POST /api/users creates a user, DELETE /api/users/42 deletes user 42 — the verb is in the HTTP method, not the URL. Nest resources hierarchically when there is a clear ownership relationship: /api/users/42/orders lists orders belonging to user 42. Avoid nesting deeper than three levels because deeply nested URLs are hard to navigate and maintain. Use query parameters for filtering, sorting, and searching rather than encoding these in the path.
# Good resource naming
GET /api/v2/users # list users
POST /api/v2/users # create user
GET /api/v2/users/42 # get user 42
PATCH /api/v2/users/42 # partially update user 42
DELETE /api/v2/users/42 # delete user 42
GET /api/v2/users/42/orders # list orders for user 42
# Filtering and sorting via query parameters
GET /api/users?role=admin&status=active
GET /api/users?sort=created_at&order=desc
GET /api/users?search=alice
# Bad naming — verbs in URLs
GET /api/getUser # should be GET /api/users/42
POST /api/createUser # should be POST /api/users
POST /api/deleteUser # should be DELETE /api/users/42
HTTP Methods and Status Codes
Use HTTP methods according to their defined semantics — GET for reading, POST for creating, PUT for full replacement, PATCH for partial updates, DELETE for removal. Each method should return the appropriate status code: 200 for successful GET and PATCH responses (with the resource in the body), 201 for successful creation (with the new resource and a Location header), 204 for successful deletion (no body), and 422 for validation errors. Use 400 for malformed requests, 401 for missing or invalid authentication, 403 for authenticated but unauthorized access, 404 for resources that do not exist, and 409 for conflicts (e.g., duplicate resource creation).
# Response status codes
POST /api/users -> 201 Created
Location: /api/users/42
Body: { "id": 42, "name": "Alice", ... }
GET /api/users/42 -> 200 OK
Body: { "id": 42, "name": "Alice", ... }
DELETE /api/users/42 -> 204 No Content
(no body)
POST /api/users -> 422 Unprocessable Entity
Body: { "error": "validation_failed", "fields": { "email": "must be a valid email" } }
GET /api/users/999 -> 404 Not Found
Body: { "error": "not_found", "message": "User 999 does not exist" }
Pagination
Any endpoint that returns a list of resources MUST support pagination. Without it, a single request could return millions of records, overwhelming both the server and the client. The two most common pagination strategies are offset-based (page and per_page) and cursor-based (using a cursor or token from the last item). Offset-based pagination is simpler but becomes inefficient on large datasets because the database must scan and skip rows. Cursor-based pagination is more performant but requires clients to handle opaque tokens. Whichever strategy you choose, always include metadata in the response so clients know the total count and how to paginate further.
# Offset-based pagination request
GET /api/users?page=2&per_page=25
# Response with pagination metadata
{
"data": [
{ "id": 26, "name": "Alice" },
{ "id": 27, "name": "Bob" }
],
"meta": {
"page": 2,
"per_page": 25,
"total": 142,
"total_pages": 6
},
"links": {
"first": "/api/users?page=1",
"prev": "/api/users?page=1",
"next": "/api/users?page=3",
"last": "/api/users?page=6"
}
}
# Cursor-based pagination
GET /api/users?cursor=eyJpZCI6IDI1fQ==&limit=25
{
"data": [ ... ],
"meta": {
"next_cursor": "eyJpZCI6IDUwfQ==",
"has_more": true
}
}
Consistent Error Responses
All errors should return a consistent JSON structure that includes an error code (machine-readable), a message (human-readable), and optionally a list of field-level errors for validation failures. Never return raw HTML, stack traces, or server error pages from your API — these expose implementation details and make client-side error handling impossible. Use standard error codes that map to HTTP status codes but provide more granularity: validation_error, not_found, authentication_required, insufficient_permissions, rate_limit_exceeded, and internal_error.
# Standard error response format
{
"error": {
"code": "validation_error",
"message": "The request body contains invalid fields",
"details": [
{
"field": "email",
"code": "invalid_format",
"message": "Must be a valid email address"
},
{
"field": "age",
"code": "out_of_range",
"message": "Must be between 0 and 150"
}
],
"request_id": "req_abc123"
}
}
REST API design is primarily about consistency. Once you establish conventions for naming, pagination, error formats, and status codes, apply them uniformly across every endpoint. Developers who consume your API should be able to predict how a new endpoint works based on how the existing ones work — that is the hallmark of a well-designed API.
