REST API Design Best Practices: A Practical Guide
A well-designed REST API is intuitive, consistent, and doesn't surprise its consumers. This guide covers the conventions that separate APIs developers love from APIs that require a support ticket to understand.
Resource Naming and URL Structure
Resources are nouns, never verbs. The HTTP method expresses the action:
# Good
GET /users → list users
GET /users/42 → get user 42
POST /users → create user
PUT /users/42 → replace user 42
PATCH /users/42 → update user 42 partially
DELETE /users/42 → delete user 42
# Bad — verbs in URLs
GET /getUser?id=42
POST /createUser
POST /deleteUser/42Use lowercase, hyphen-separated words for multi-word resources: /order-items, not /orderItems or /order_items. URL paths are case-sensitive on most servers; lowercase avoids confusion.
Nested Resources
Model relationships with nesting, but limit to two levels deep:
GET /users/42/orders → orders belonging to user 42
GET /users/42/orders/7 → order 7 belonging to user 42
# Avoid deeper nesting — it becomes unwieldy:
GET /users/42/orders/7/items/3/reviews # too deepFor deeply nested resources, consider flattening: GET /order-items/3 with the parent IDs as query parameters or in the response body.
HTTP Methods
| Method | Action | Idempotent | Body |
|---|---|---|---|
| GET | Read | Yes | No |
| POST | Create | No | Yes |
| PUT | Replace | Yes | Yes |
| PATCH | Partial update | No* | Yes |
| DELETE | Delete | Yes | No |
*PATCH can be made idempotent with careful design (JSON Patch operations), but isn't by default.
HTTP Status Codes
Use the right status code. Here are the most important ones for REST APIs:
2xx Success
200 OK— general success (GET, PATCH, PUT)201 Created— resource created (POST). Include aLocationheader pointing to the new resource.204 No Content— success with no response body (DELETE, PATCH with no return value)
4xx Client Errors
400 Bad Request— malformed request, validation error401 Unauthorized— not authenticated (missing or invalid token)403 Forbidden— authenticated but not authorized404 Not Found— resource doesn't exist409 Conflict— state conflict (duplicate, version mismatch)422 Unprocessable Entity— valid JSON but failed business validation429 Too Many Requests— rate limit exceeded
5xx Server Errors
500 Internal Server Error— unexpected server failure503 Service Unavailable— temporarily down, use withRetry-After
See the full reference at HTTP Status Codes.
Error Response Format
Consistent error bodies make client error handling predictable. A widely adopted format:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "must be a valid email address"
},
{
"field": "age",
"message": "must be a positive integer"
}
]
}
}Always include a machine-readable code (for programmatic handling) and a human-readable message (for debugging). The details array helps clients surface field-level validation errors to users.
Versioning
Version your API from day one. Breaking changes are inevitable. The three main approaches:
# URL versioning (most common, most explicit)
GET /v1/users
GET /v2/users
# Header versioning
GET /users
Accept: application/vnd.myapi+json;version=2
# Query parameter (least recommended)
GET /users?version=2URL versioning is the most widely used because it's visible in logs, bookmarkable, and easy to test in a browser. Maintain old versions for at least 6–12 months after announcing deprecation.
Pagination
Never return unbounded lists. Two common pagination patterns:
Offset pagination:
GET /users?page=2&per_page=25
{
"data": [...],
"pagination": {
"page": 2,
"per_page": 25,
"total": 143,
"total_pages": 6
}
}Cursor pagination (better for large datasets and real-time data):
GET /users?after=cursor_abc123&limit=25
{
"data": [...],
"pagination": {
"next_cursor": "cursor_def456",
"has_more": true
}
}Cursor pagination is more stable — offset pagination breaks when records are inserted or deleted between pages.
Filtering, Sorting, and Field Selection
# Filtering
GET /users?status=active&role=admin
# Sorting
GET /users?sort=created_at&order=desc
# Field selection (reduces payload size)
GET /users?fields=id,name,email
# Search
GET /users?q=aliceResponse Envelope
Use a consistent response envelope so clients can reliably parse responses:
// Collection
{
"data": [ { "id": 1, ... }, { "id": 2, ... } ],
"pagination": { ... },
"meta": { "total": 42 }
}
// Single resource
{
"data": { "id": 1, "name": "Alice", ... }
}Building or debugging a REST API? Format your JSON responses · HTTP Status Codes reference · Decode JWT tokens
Frequently Asked Questions
Should I use plural or singular nouns for resource names?▾
Use plural nouns. /users is the collection of all users; /users/42 is one user. Consistent plurals avoid the ambiguity of mixing /user (singular) and /users/42 (plural). Almost every major API (GitHub, Stripe, Twilio) uses plural resource names.
When should I use PUT vs PATCH?▾
PUT replaces the entire resource with the request body. If you PUT a user with only a name field, all other fields should be cleared or reset to defaults. PATCH applies a partial update — only the fields in the request body are changed. For most update operations, PATCH is more practical and less error-prone. PUT is appropriate when the client is responsible for the complete state of a resource (idempotent creation or replacement).
Is it OK to return 200 for errors?▾
No. Always use the appropriate HTTP status code. Some legacy APIs return 200 OK with an 'error' field in the body — this breaks HTTP clients, caches, monitoring tools, and any middleware that checks status codes. The HTTP status code is the primary error signal; use it correctly. Your API clients will thank you.
How should I handle API versioning?▾
URL versioning (/v1/users) is the most explicit and widely understood approach — it's immediately visible in logs, browser address bars, and documentation. Header versioning (Accept: application/vnd.api+json;version=1) is cleaner in theory but harder to test and cache. Query parameter versioning (?version=1) is the least recommended. Whatever you choose, be consistent and document your deprecation policy clearly.