RESTful API Design: Best Practices for Building Web APIs
Learn REST principles, resource naming, HTTP methods, status codes, and best practices. Design clean, maintainable, and scalable RESTful APIs.
RESTful API Design: Best Practices for Building Web APIs
REST (Representational State Transfer) is the dominant approach for designing web APIs. It provides a structured way to expose resources over HTTP. Understanding REST principles helps you build APIs that are intuitive, scalable, and maintainable.
The HTTP/HTTPS protocol post covers HTTP methods and status codes. This post focuses on applying those concepts to API design.
What is REST?
REST is an architectural style, not a specification. It was defined by Roy Fielding in 2000. The key idea: treat everything as a resource that clients can interact with through standard HTTP operations.
REST APIs expose resources through URLs. The HTTP method indicates the action:
graph LR
A[Client] -->|GET /users| B[Read users]
A -->|POST /users| C[Create user]
A -->|PUT /users/123| D[Update user]
A -->|DELETE /users/123| E[Delete user]
Resource Naming
Resource names are the foundation of REST API design. Good resource names are:
- Nouns, not verbs -
/usersnot/getUsers - Plural -
/usersnot/user - Hierarchical -
/users/123/ordersfor users orders - Consistent - Same pattern throughout
# Good resource naming
GET /users # List users
GET /users/123 # Get user 123
GET /users/123/orders # Get orders for user 123
POST /users # Create user
PUT /users/123 # Update user 123
DELETE /users/123 # Delete user 123
# Bad naming (mixing verbs and nouns)
GET /getUsers
GET /getUserById/123
POST /createUser
Nested Resources
Nested resources express relationships:
# A user posts
GET /users/123/posts
# A post comments
GET /posts/456/comments
# Limit nesting depth - typically 2 levels is practical
GET /users/123/posts/789/comments # Too deep
HTTP Methods in REST
REST uses HTTP methods to indicate actions.
GET
Retrieves resources. GET requests should be safe (no side effects) and idempotent.
GET /users
GET /users/123
POST
Creates new resources. Each POST request typically creates one resource.
POST /users
Content-Type: application/json
{"name": "Alice", "email": "alice@example.com"}
Response includes the created resource and a 201 status code:
HTTP/1.1 201 Created
Location: /users/124
PUT
Replaces a resource entirely. If you only need to update specific fields, use PATCH.
PUT /users/123
Content-Type: application/json
{"name": "Alice Smith", "email": "alice.smith@example.com"}
PATCH
Partially updates a resource. Only the specified fields change.
PATCH /users/123
Content-Type: application/json
{"email": "alice.new@example.com"}
DELETE
Removes a resource.
DELETE /users/123
Status Codes
REST APIs should use appropriate HTTP status codes:
Success Codes
| Code | Meaning | When to use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST creating resource |
| 204 | No Content | Successful DELETE |
Error Codes
| Code | Meaning | When to use |
|---|---|---|
| 400 | Bad Request | Malformed request body |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Resource conflict (duplicate, state conflict) |
| 422 | Unprocessable Entity | Validation errors |
| 500 | Internal Server Error | Unexpected server error |
// Example: Successful response
{
"id": 123,
"name": "Alice",
"email": "alice@example.com"
}
// Example: Error response
{
"error": "validation_error",
"message": "Email is required",
"details": [
{ "field": "email", "message": "This field is required" }
]
}
Request and Response
Content Type
Always specify content type for requests with bodies:
POST /users
Content-Type: application/json
Pagination
For endpoints returning lists, use pagination:
GET /users?page=2&limit=20
Response includes pagination metadata:
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 100,
"totalPages": 5
}
}
Filtering and Sorting
Support filtering and sorting through query parameters:
# Filter by status
GET /orders?status=pending
# Filter by multiple fields
GET /orders?status=shipped&customer_id=456
# Sort by field
GET /users?sort=created_at&order=desc
Field Selection
Let clients request specific fields:
GET /users?fields=id,name,email
API Versioning
APIs need to evolve without breaking existing clients. Versioning provides a way to introduce changes.
URL Path Versioning
# Most common approach
GET /v1/users
GET /v2/users
Header Versioning
GET /users
Accept: application/vnd.example.v2+json
Query Parameter Versioning
GET /users?version=2
URL path versioning is the most common because it is explicit and easy to test.
The API Versioning Strategies post covers this topic in depth.
Error Handling
Consistent error responses help clients handle failures:
// Standard error format
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "The requested user does not exist",
"details": {
"resource": "user",
"id": 999
}
}
}
Include enough information for debugging but do not leak sensitive details.
Handling Validation Errors
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{ "field": "email", "message": "Invalid email format" },
{ "field": "name", "message": "Name must be at least 2 characters" }
]
}
}
Authentication
API Keys
Simple but limited. Suitable for server-to-server communication.
GET /users
X-API-Key: your-api-key-here
JWT (JSON Web Tokens)
Stateless tokens with embedded user information. Common for modern applications.
GET /users
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
OAuth 2.0
For APIs accessed by third-party applications. More complex but more powerful.
GET /user
Authorization: Bearer access_token_from_oauth
Rate Limiting
Protect your API from abuse by limiting request rates:
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1640995200
When clients exceed the limit:
HTTP/1.1 429 Too Many Requests
Retry-After: 3600
Best Practices Summary
RESTful API design comes down to a few core principles. Use nouns for resource names, not verbs. Use plural names consistently. Apply HTTP methods correctly and use the right status codes. Paginate list endpoints. Format errors consistently. Version your API from the start. Secure endpoints with appropriate authentication. Implement rate limiting. Document everything with OpenAPI. Make incremental changes carefully.
When to Use REST
REST is the right choice when:
- You need a simple, well-understood API pattern
- HTTP caching benefits your use case
- You have multiple clients (web, mobile, third-party) consuming the same API
- Stateless request-response fits your data access patterns
- You want easy documentation and discoverability
- Standard HTTP infrastructure (CDNs, proxies, caches) helps
- You are building CRUD-oriented services
When Not to Use REST
REST may not be the best fit when:
- Clients need different data shapes (overfetching/underfetching issues)
- You need real-time updates (WebSockets, Server-Sent Events make more sense)
- Your queries are highly complex and deeply nested (GraphQL may fit better)
- You need batched operations that span multiple resources
- Mobile apps with limited bandwidth need precise data fetching
- Your API is tightly coupled to a single client needs
Production Failure Scenarios
| Failure | Impact | Mitigation |
|---|---|---|
| Missing pagination | Large datasets cause timeout or memory issues | Implement cursor or offset pagination; set reasonable limits |
| No rate limiting | API can be overwhelmed; DoS vulnerability | Implement rate limiting per client; return 429 with Retry-After |
| Inconsistent error responses | Clients cannot handle errors gracefully | Standardize error format; document all error codes |
| Version not implemented | Breaking changes affect existing clients | Version from day one; support multiple versions during transition |
| N+1 query problem | Database overwhelmed with queries | Use eager loading; batch related queries; optimize data fetching |
| Unbounded queries | Complex queries slow or crash database | Set query complexity limits; implement query timeouts |
| Missing authentication | Unauthorized access to sensitive data | Implement authentication on all endpoints; verify permissions |
| Insufficient validation | Invalid data corrupts database | Validate all input; use schema validation; return 400 for bad data |
Observability Checklist
Metrics
- Request rate by endpoint, method, and client
- Response time distribution (p50, p95, p99) per endpoint
- Error rate by status code and endpoint
- Active connections or concurrent requests
- Authentication failures (attempted unauthorized access)
- Rate limit hits (429 responses)
- Payload size (request and response)
- Cache hit ratio (if using caching)
Logs
- All requests with request ID, method, path, status, duration
- Authentication and authorization failures with attempted credentials
- Validation errors with field-level details
- Database query times for slow queries (> 100ms)
- Rate limiting events with client identifier
- Error stack traces with correlation IDs
Alerts
- Error rate exceeds 1% for 5 minutes
- p99 latency exceeds 2 seconds
- Authentication failures spike (potential attack)
- Rate limit hits exceed normal threshold
- Unusual request size or pattern
- Database query times increasing
- Memory or CPU usage on API servers
Security Checklist
- Implement authentication on every protected endpoint
- Use authorization to verify permissions (not just authentication)
- Validate and sanitize all input data
- Use HTTPS for all API endpoints
- Set appropriate timeouts for all requests
- Implement rate limiting to prevent abuse
- Log security events (auth failures, suspicious patterns)
- Return minimal information in errors (do not leak internal details)
- Use secure, HttpOnly, SameSite cookies for sessions
- Implement CORS properly for cross-origin requests
- Protect against injection attacks (SQL, NoSQL)
- Validate Content-Type matches expected format
- Implement HEAD and OPTIONS method security
- Use API keys or JWT tokens, not passwords in URLs
Common Pitfalls / Anti-Patterns
Using Verbs in URLs
REST uses nouns for resources, not verbs for actions.
# WRONG - verbs in URLs
GET /api/getUsers
POST /api/createUser
POST /api/deleteUser
# CORRECT - nouns and HTTP methods
GET /api/users
POST /api/users
DELETE /api/users/123
Returning 200 for Errors
Always use appropriate status codes for errors.
# WRONG - 200 for error
HTTP/1.1 200 OK
{"error": "Not found"}
# CORRECT - proper status code
HTTP/1.1 404 Not Found
{"error": "User not found"}
Ignoring Pagination
Never return unbounded lists.
# WRONG - unbounded response
GET /api/users
# Could return millions of users
# CORRECT - paginated response
GET /api/users?page=1&limit=20
# Returns: {"data": [...], "pagination": {"page": 1, "limit": 20, "total": 1000}}
Not Implementing Versioning
APIs need to evolve without breaking clients.
# Version in URL path (most explicit)
GET /v1/users
GET /v2/users
# Version in header (cleaner URLs, harder to test)
GET /users
Accept: application/vnd.example.v2+json
Exposing Internal Details in Errors
Keep error responses informative but not leaky.
// WRONG - exposing internal details
{
"error": "Database connection failed: mysql://user:pass@host:3306/db"
}
// CORRECT - safe error response
{
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred"
}
}
Quick Recap
Key Bullets
- REST uses nouns for resources (urls), not verbs (actions are HTTP methods)
- Use plural names consistently:
/usersnot/user - Use appropriate HTTP methods: GET (read), POST (create), PUT (replace), PATCH (modify), DELETE (remove)
- Return proper status codes: 2xx for success, 4xx for client errors, 5xx for server errors
- Implement pagination for list endpoints
- Standardize error response format across the API
- Version your API from the start to allow evolution
- Secure endpoints with authentication and authorization
- Implement rate limiting to protect against abuse
- Validate all input and return 400 for invalid data
Copy/Paste Checklist
# Test REST endpoint with curl
curl -X GET "https://api.example.com/users?page=1&limit=10" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Accept: application/json"
# Create resource with POST
curl -X POST "https://api.example.com/users" \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com"}'
# Update resource with PATCH
curl -X PATCH "https://api.example.com/users/123" \
-H "Content-Type: application/json" \
-d '{"email":"alice.new@example.com"}'
# Delete resource
curl -X DELETE "https://api.example.com/users/123" \
-H "Authorization: Bearer ..."
# Check API version (if using header versioning)
curl -X GET "https://api.example.com/users" \
-H "Accept: application/vnd.example.v2+json"
Conclusion
RESTful API design comes down to applying HTTP conventions consistently. Resources are nouns, actions are HTTP methods, and responses use standard status codes. Good naming, proper error handling, and thoughtful versioning create APIs that developers actually want to use.
For comparing REST with GraphQL, see the GraphQL vs REST post. For API versioning details, see the API Versioning Strategies post.
Category
Related Posts
GraphQL vs REST: Choosing the Right API Paradigm
Compare GraphQL and REST APIs, understand when to use each approach, schema design, queries, mutations, and trade-offs between the two paradigms.
API Versioning Strategies: Managing API Evolution Without Breaking Clients
Learn different API versioning strategies including URL versioning, header versioning, and query parameters. Understand backward compatibility and deprecation practices.
Rate Limiting: Token Bucket, Sliding Window, and Distributed
Rate limiting protects APIs from abuse. Learn token bucket, sliding window, fixed window algorithms and distributed rate limiting at scale.