REST API Best Practices 2025: Complete Guide
REST API best practices in 2025: resource design, versioning, OAuth 2.1 authentication.
Introduction to REST API Design
REST (Representational State Transfer) is the dominant architectural style for web APIs. Well-designed REST APIs are intuitive, consistent, and scalable. This guide covers best practices for designing and implementing REST APIs that developers love to use.
Whether you're building a new API or improving an existing one, these principles will help you create interfaces that are easy to understand, maintain, and evolve.
REST Fundamentals
Core Principles
- Stateless: Each request contains all information needed to process it
- Client-Server: Separation between UI and data storage
- Cacheable: Responses indicate cacheability
- Uniform Interface: Consistent resource naming and methods
- Layered System: Client doesn't need to know if connected directly to server
Resources and URLs
Resources are the fundamental concept in REST. URLs should represent resources (nouns), not actions (verbs):
# Good: Resources as nouns
GET /users
GET /users/123
GET /users/123/orders
POST /orders
# Bad: Actions as verbs
GET /getUsers
POST /createUser
GET /getUserOrders?userId=123
HTTP Methods
Standard Methods
| Method | Purpose | Idempotent | Safe |
|---|---|---|---|
| GET | Retrieve resource | Yes | Yes |
| POST | Create resource | No | No |
| PUT | Replace resource | Yes | No |
| PATCH | Partial update | No | No |
| DELETE | Remove resource | Yes | No |
Method Usage Examples
# Retrieve all users
GET /users
# Retrieve specific user
GET /users/123
# Create new user
POST /users
Content-Type: application/json
{"name": "John", "email": "john@example.com"}
# Replace entire user
PUT /users/123
Content-Type: application/json
{"name": "John Smith", "email": "john.smith@example.com"}
# Partial update
PATCH /users/123
Content-Type: application/json
{"email": "newemail@example.com"}
# Delete user
DELETE /users/123
URL Design Best Practices
Naming Conventions
# Use plural nouns for collections
/users (not /user)
/orders (not /order)
# Use lowercase with hyphens
/user-profiles (not /userProfiles or /user_profiles)
# Represent relationships in URLs
/users/123/orders
/orders/456/items
# Avoid deep nesting (max 2-3 levels)
# Bad: /users/123/orders/456/items/789/details
# Better: /order-items/789
Query Parameters
# Filtering
GET /users?status=active&role=admin
# Sorting
GET /users?sort=created_at&order=desc
GET /users?sort=-created_at # Alternative: prefix with minus for desc
# Pagination
GET /users?page=2&limit=20
GET /users?offset=20&limit=20 # Alternative: offset-based
# Field selection
GET /users?fields=id,name,email
# Searching
GET /users?search=john
GET /users?q=john # Common shorthand
HTTP Status Codes
Success Codes (2xx)
200 OK - Successful GET, PUT, PATCH, or DELETE
201 Created - Successful POST that created a resource
204 No Content - Successful DELETE with no response body
Client Error Codes (4xx)
400 Bad Request - Invalid request syntax or parameters
401 Unauthorized - Missing or invalid authentication
403 Forbidden - Authenticated but not authorized
404 Not Found - Resource doesn't exist
405 Method Not Allowed - HTTP method not supported for this resource
409 Conflict - Request conflicts with current state
422 Unprocessable Entity - Valid syntax but semantic errors
429 Too Many Requests - Rate limit exceeded
Server Error Codes (5xx)
500 Internal Server Error - Unexpected server error
502 Bad Gateway - Invalid response from upstream server
503 Service Unavailable - Server temporarily unavailable
504 Gateway Timeout - Upstream server didn't respond in time
Request and Response Design
Request Body
# POST /users
{
"name": "John Smith",
"email": "john@example.com",
"role": "user"
}
# Don't include read-only fields in requests
# Bad: { "id": 123, "created_at": "..." }
# Server generates these automatically
Response Structure
# Single resource
{
"id": 123,
"name": "John Smith",
"email": "john@example.com",
"created_at": "2025-01-15T10:30:00Z"
}
# Collection with metadata
{
"data": [
{"id": 1, "name": "John"},
{"id": 2, "name": "Jane"}
],
"pagination": {
"total": 100,
"page": 1,
"limit": 20,
"pages": 5
}
}
# Error response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request data",
"details": [
{"field": "email", "message": "Invalid email format"},
{"field": "name", "message": "Name is required"}
]
}
}
Pagination Patterns
Offset Pagination
GET /users?offset=0&limit=20 # Page 1
GET /users?offset=20&limit=20 # Page 2
# Response
{
"data": [...],
"pagination": {
"offset": 20,
"limit": 20,
"total": 150
}
}
Cursor Pagination
Better for large datasets and real-time data:
GET /users?limit=20
GET /users?cursor=eyJpZCI6MjB9&limit=20
# Response
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6NDB9",
"has_more": true
}
}
Link Headers (HATEOAS)
Link: </users?page=1>; rel="first",
</users?page=2>; rel="prev",
</users?page=4>; rel="next",
</users?page=10>; rel="last"
Versioning
URL Versioning (Recommended)
GET /v1/users
GET /v2/users
Header Versioning
GET /users
Accept: application/vnd.api+json; version=2
Query Parameter
GET /users?version=2
Versioning Strategy
- Don't version too oftenāchanges should be backward compatible when possible
- Support old versions for a deprecation period
- Communicate deprecation clearly with sunset headers
Authentication and Authorization
Bearer Token (JWT)
GET /users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
API Keys
# In header (preferred)
X-API-Key: your-api-key
# In query parameter (less secure)
GET /users?api_key=your-api-key
OAuth 2.0 Scopes
# Request specific permissions
Authorization: Bearer token-with-scope:read:users,write:users
Error Handling
Consistent Error Format
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "User with ID 123 not found",
"request_id": "req_abc123",
"documentation_url": "https://api.example.com/docs/errors#RESOURCE_NOT_FOUND"
}
}
Validation Errors
# 422 Unprocessable Entity
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Email must be a valid email address"
},
{
"field": "age",
"code": "OUT_OF_RANGE",
"message": "Age must be between 18 and 120"
}
]
}
}
Rate Limiting
Response Headers
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 998
X-RateLimit-Reset: 1640995200
Retry-After: 3600 # When limit exceeded (429 response)
Rate Limit Response
# 429 Too Many Requests
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Try again in 3600 seconds.",
"retry_after": 3600
}
}
Caching
Cache Headers
# Response headers
Cache-Control: max-age=3600, public
ETag: "abc123"
Last-Modified: Wed, 15 Jan 2025 10:30:00 GMT
# Conditional requests
If-None-Match: "abc123" # Returns 304 if unchanged
If-Modified-Since: Wed, 15 Jan 2025 10:30:00 GMT
Cacheable Resources
# Highly cacheable
GET /products/123 # Static product data
GET /categories # Rarely changes
# Don't cache
GET /users/me # User-specific
GET /notifications # Real-time data
POST, PUT, DELETE # State-changing
Documentation
OpenAPI (Swagger)
openapi: 3.0.0
info:
title: Users API
version: 1.0.0
paths:
/users:
get:
summary: List users
parameters:
- name: page
in: query
schema:
type: integer
responses:
'200':
description: List of users
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email
Testing APIs
Validate your API responses with proper tools:
- JSON Formatter to validate and format responses
- JSON Schema Converter to generate validation schemas
- Base64 Encoder for handling encoded payloads
- URL Encoder for query parameter encoding
Security Best Practices
- Always use HTTPS
- Validate all input on the server
- Use parameterized queries to prevent SQL injection
- Implement rate limiting
- Don't expose sensitive data in URLs (use headers/body)
- Return minimal error information in production
- Use CORS properly to restrict origins
Performance Tips
- Use pagination for large collections
- Support field selection to reduce payload size
- Implement ETag/conditional requests
- Use compression (gzip, brotli)
- Consider GraphQL for complex data requirements
Conclusion
Well-designed REST APIs follow consistent patterns that make them predictable and easy to use. By applying these best practicesāproper URL design, meaningful status codes, consistent error handling, and thorough documentationāyou'll create APIs that developers enjoy integrating with.
Key takeaways:
- Use nouns for resources, HTTP methods for actions
- Be consistent with naming and response formats
- Provide meaningful error messages
- Document everything with OpenAPI
- Implement proper authentication and rate limiting
For more developer resources, explore our free online tools. For comprehensive API design guidance, see RESTful API design principles and the OpenAPI specification.