REST API Design: Principles and Patterns
I've designed and consumed hundreds of APIs over my career. Some were a joy to work with—intuitive, consistent, well-documented. Others made me question my career choices. Let's talk about what separates the two.
REST is a Contract, Not Just an Architecture
When you design an API, you're creating a contract with every developer who will ever use it. Break that contract—with inconsistent naming, unexpected behaviors, or poor error handling—and you'll spend more time answering support questions than building features.
The best APIs are boring. They're predictable. They do exactly what you expect them to do.
1. Resource Naming: The Foundation
URLs should represent resources (nouns), not actions (verbs). The HTTP method tells you the action.
// Bad - verbs in URLs
POST /createUser
GET /getUserById/123
POST /deleteUser/123
// Good - nouns with proper HTTP methods
POST /users → Create a user
GET /users/123 → Get user 123
PUT /users/123 → Update user 123
DELETE /users/123 → Delete user 123
Plural vs Singular
Pick one and stick with it. I prefer plural because it's more consistent:
GET /users → List all users
GET /users/123 → Get specific user
POST /users → Create a user
GET /users/123/posts → Get posts for user 123
Nested Resources
Keep nesting shallow. More than two levels deep becomes confusing:
// Good - shallow nesting
GET /users/123/posts
GET /posts/456/comments
// Avoid - too deep
GET /users/123/posts/456/comments/789/replies
2. HTTP Methods: Use Them Correctly
| Method | Purpose | Idempotent? | Safe? |
|---|---|---|---|
GET |
Retrieve resource(s) | Yes | Yes |
POST |
Create a new resource | No | No |
PUT |
Replace entire resource | Yes | No |
PATCH |
Partial update | No* | No |
DELETE |
Remove resource | Yes | No |
Idempotent operations can be safely retried. If a network error occurs during a PUT request, the client can retry without fear of creating duplicate data. This is crucial for building reliable systems.
3. Status Codes: Tell the Client What Happened
Don't return 200 OK with an error message in the body. Use status codes properly:
// Success codes
200 OK → Request succeeded
201 Created → Resource created (return the new resource)
204 No Content → Success, nothing to return (good for DELETE)
// Client errors
400 Bad Request → Malformed request syntax
401 Unauthorized → Authentication required
403 Forbidden → Authenticated but not authorized
404 Not Found → Resource doesn't exist
409 Conflict → Resource conflict (e.g., duplicate email)
422 Unprocessable→ Validation errors
// Server errors
500 Internal → Something broke on our end
503 Unavailable → Service temporarily down
4. Error Handling: Be Helpful
When things go wrong, give developers enough information to fix the problem:
// Bad - unhelpful error
{
"error": "Invalid request"
}
// Good - actionable error response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request contains invalid data",
"details": [
{
"field": "email",
"message": "Must be a valid email address",
"received": "not-an-email"
},
{
"field": "age",
"message": "Must be at least 18",
"received": 16
}
],
"documentation": "https://api.example.com/docs/errors#VALIDATION_ERROR"
}
}
5. Pagination: Don't Return Everything
Never return unbounded lists. A table with a million rows will crash your server and the client.
// Cursor-based pagination (preferred)
GET /posts?cursor=abc123&limit=20
{
"data": [...],
"pagination": {
"next_cursor": "def456",
"has_more": true
}
}
// Offset-based pagination (simpler but less efficient)
GET /posts?page=3&per_page=20
{
"data": [...],
"pagination": {
"current_page": 3,
"per_page": 20,
"total_pages": 50,
"total_items": 1000
}
}
Offset pagination breaks when data changes between requests. If new items are added, users might see duplicates or miss items. Cursor pagination is stable regardless of data changes.
6. Filtering, Sorting, and Searching
Use query parameters for filtering and sorting:
// Filtering
GET /posts?status=published&author_id=123
// Sorting
GET /posts?sort=-created_at,title // "-" prefix for descending
// Searching
GET /posts?search=javascript
// Combined
GET /posts?status=published&sort=-created_at&search=api&limit=20
7. Versioning: Plan for Change
Your API will change. Plan for it from day one:
// URL versioning (most common)
GET /v1/users
GET /v2/users
// Header versioning (cleaner URLs)
GET /users
Accept: application/vnd.myapi.v2+json
My preference is URL versioning—it's explicit, easy to understand, and works in browser address bars.
8. Security Essentials
- Always use HTTPS. No exceptions. Ever.
- Validate all input. Never trust client data.
- Rate limiting. Protect yourself from abuse.
- Authentication. Use proven standards like OAuth 2.0 or JWT.
- Never expose internal IDs. Use UUIDs or public-facing identifiers.
// Rate limit headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640000000
// When rate limited
HTTP/1.1 429 Too Many Requests
Retry-After: 60
9. Documentation: Your API's First Impression
If your API isn't documented, it doesn't exist. Use OpenAPI (Swagger) specification:
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/users:
post:
summary: Create a new user
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email, password]
properties:
email:
type: string
format: email
password:
type: string
minLength: 8
responses:
'201':
description: User created successfully
Real-World Design Checklist
- ✅ Resource names are plural nouns
- ✅ HTTP methods match their semantic meaning
- ✅ Status codes accurately reflect outcomes
- ✅ Errors include actionable information
- ✅ Pagination is implemented for list endpoints
- ✅ Filtering and sorting use query parameters
- ✅ API is versioned from day one
- ✅ Authentication is implemented
- ✅ Rate limiting is in place
- ✅ OpenAPI documentation is up to date
Conclusion
Great API design is about empathy. Think about the developer who will consume your API at 2 AM trying to ship a feature. Make their life easy with consistent patterns, clear errors, and comprehensive documentation.
Your API is a product. Treat it like one.