REST API Design Best Practices: The Complete Guide
A well-designed API is a joy to work with. A poorly designed one creates bugs, confusion, and support tickets for years. This guide covers the patterns that the best APIs in production follow.
1. URL Structure
Use nouns, not verbs
URLs represent resources (things), not actions. The HTTP method communicates the action.
# Good - nouns
GET /api/users
POST /api/users
GET /api/users/123
PUT /api/users/123
DELETE /api/users/123
# Bad - verbs in URLs
GET /api/getUsers
POST /api/createUser
POST /api/deleteUser/123Use plural nouns
Stay consistent. /users for the collection, /users/123 for a single resource.
Nest for relationships (but keep it shallow)
# Good - one level of nesting
GET /api/users/123/orders
GET /api/users/123/orders/456
# Bad - too deep
GET /api/users/123/orders/456/items/789/reviews
# Better: flatten with query params or separate endpoint
GET /api/order-items/789/reviewsUse kebab-case
# Good
/api/user-profiles
/api/order-items
# Bad
/api/userProfiles
/api/order_items2. HTTP Methods
| Method | Purpose | Idempotent? | Request Body? |
|---|---|---|---|
| GET | Read a resource | Yes | No |
| POST | Create a resource | No | Yes |
| PUT | Replace a resource entirely | Yes | Yes |
| PATCH | Partially update a resource | Yes* | Yes |
| DELETE | Remove a resource | Yes | No |
PUT vs PATCH:PUT replaces the entire resource (send all fields). PATCH updates only the fields you send. In practice, most "update" endpoints use PATCH because clients rarely want to resend unchanged fields.
// PUT - full replacement (must send everything)
PUT /api/users/123
{
"name": "Alice",
"email": "[email protected]",
"role": "admin",
"avatar": "https://..."
}
// PATCH - partial update (only changed fields)
PATCH /api/users/123
{
"name": "Alice Updated"
}3. Status Codes
Use the right status code. Clients depend on them for control flow.
Success (2xx)
200 OK — General success (GET, PUT, PATCH)
201 Created — Resource created (POST). Include Location header.
204 No Content — Success with no body (DELETE)Client Errors (4xx)
400 Bad Request — Malformed request, validation error
401 Unauthorized — No auth credentials / invalid token
403 Forbidden — Authenticated but not authorized
404 Not Found — Resource doesn't exist
405 Method Not Allowed — Wrong HTTP method for this endpoint
409 Conflict — Duplicate resource, version conflict
422 Unprocessable — Valid JSON but semantically invalid
429 Too Many Requests — Rate limit exceededServer Errors (5xx)
500 Internal Server Error — Unexpected server failure
502 Bad Gateway — Upstream service error
503 Service Unavailable — Temporarily down (maintenance)
504 Gateway Timeout — Upstream service timeoutRule of thumb: If the client can fix the problem by changing the request, use 4xx. If the server has a bug or is overloaded, use 5xx.
4. Error Handling
Consistent error responses are critical. Every error should use the same shape.
// Standard error response structure
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Must be a valid email address"
},
{
"field": "name",
"message": "Must be between 2 and 100 characters"
}
]
}
}
// 404 error
{
"error": {
"code": "NOT_FOUND",
"message": "User with id 999 not found"
}
}
// 401 error
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or expired access token"
}
}Never expose stack traces, SQL queries, or internal implementation details in production error responses.
5. Pagination
Every list endpoint must be paginated. Returning unbounded results is a ticking time bomb.
Offset-based (simple, most common)
GET /api/users?page=2&limit=20
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 156,
"totalPages": 8
}
}Cursor-based (better for real-time data)
GET /api/feed?cursor=eyJpZCI6MTIzfQ&limit=20
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTQzfQ",
"hasMore": true
}
}When to use which: Offset-based is simpler and allows jumping to arbitrary pages. Cursor-based is more performant on large datasets and handles real-time insertions/deletions without skipping or duplicating items.
6. Filtering, Sorting & Field Selection
Filtering
# Simple equality
GET /api/users?role=admin&status=active
# Operators for ranges
GET /api/orders?created_after=2026-01-01&total_min=100
# Search
GET /api/users?q=aliceSorting
# Sort ascending
GET /api/users?sort=name
# Sort descending (prefix with -)
GET /api/users?sort=-created_at
# Multiple sort fields
GET /api/users?sort=-created_at,nameField selection (sparse fieldsets)
# Only return specific fields
GET /api/users?fields=id,name,email
# Reduces payload size and database load7. Versioning
APIs evolve. You need a strategy for breaking changes that does not break existing clients.
URL path versioning (most common, most explicit)
GET /api/v1/users
GET /api/v2/usersHeader versioning
GET /api/users
Accept: application/vnd.myapp.v2+jsonQuery parameter versioning
GET /api/users?version=2Recommendation: Use URL path versioning. It is the easiest to discover, test in a browser, and route at the infrastructure level. Only bump the version for breaking changes.
8. Authentication Patterns
Bearer tokens (JWT or opaque)
GET /api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...API keys (for server-to-server)
GET /api/data
X-API-Key: sk_live_abc123...OAuth 2.0 flow summary
1. Client redirects user to:
GET /oauth/authorize?client_id=...&redirect_uri=...&scope=read
2. User approves, redirect back with code:
GET /callback?code=abc123
3. Client exchanges code for token:
POST /oauth/token
{ "grant_type": "authorization_code", "code": "abc123", ... }
4. Client uses token:
GET /api/users
Authorization: Bearer <access_token>Best practices: Use short-lived access tokens (15 minutes) with refresh tokens. Store tokens securely (httpOnly cookies for web apps). Always use HTTPS.
9. Rate Limiting
Protect your API from abuse and ensure fair usage. Communicate limits via headers.
# Response headers
X-RateLimit-Limit: 1000 # Max requests per window
X-RateLimit-Remaining: 847 # Remaining in current window
X-RateLimit-Reset: 1714003200 # Unix timestamp when window resets
# When rate limited:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Retry after 60 seconds."
}
}10. Response Format
Single resource
GET /api/users/123
{
"data": {
"id": 123,
"name": "Alice",
"email": "[email protected]",
"createdAt": "2026-01-15T10:30:00Z"
}
}Collection
GET /api/users?page=1&limit=2
{
"data": [
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
],
"pagination": {
"page": 1,
"limit": 2,
"total": 156
}
}Consistent date format
Always use ISO 8601 with timezone: 2026-04-02T14:30:00Z. Never use Unix timestamps in responses (they are unreadable for humans debugging).
11. Discoverability & HATEOAS
Include links to related resources and actions. This makes your API self-documenting.
{
"data": {
"id": 123,
"name": "Alice",
"links": {
"self": "/api/users/123",
"orders": "/api/users/123/orders",
"avatar": "/api/users/123/avatar"
}
}
}12. Security Checklist
- Always use HTTPS in production
- Validate and sanitize all input (never trust the client)
- Use parameterized queries to prevent SQL injection
- Implement rate limiting on all endpoints
- Set CORS headers appropriately (not
*in production) - Never expose internal IDs or implementation details in errors
- Log all authentication failures for monitoring
- Use request size limits to prevent payload bombs
- Add
Content-Typevalidation (reject unexpected types) - Implement request signing for webhook endpoints
13. Documentation
An undocumented API is an unusable API. At minimum, document every endpoint with:
- URL, method, and description
- Request parameters (path, query, body) with types and validation rules
- Response shape with example JSON
- Error codes this endpoint can return
- Authentication requirements
- Rate limits
Use OpenAPI/Swagger for machine-readable specs. Tools like Swagger UI or Redoc generate interactive docs from the spec.
Quick Reference
| Principle | Rule |
|---|---|
| URLs | Plural nouns, kebab-case, max 2 levels deep |
| Methods | GET read, POST create, PUT replace, PATCH update, DELETE remove |
| Status codes | 2xx success, 4xx client error, 5xx server error |
| Errors | Consistent JSON shape with code, message, details |
| Pagination | Always paginate lists. Offset or cursor-based. |
| Auth | Bearer tokens or API keys. Always HTTPS. |
| Versioning | URL path (/v1/) for breaking changes only |
| Dates | ISO 8601 with timezone |
Tools for API Development
Format JSON responses, decode JWTs from auth headers, convert timestamps, or generate hashes for API keys — all free in your browser.