A well-designed API is the foundation of any successful application. This guide covers best practices for designing RESTful APIs that are intuitive, scalable, and maintainable.
Resource Naming Conventions
Use Nouns, Not Verbs
# Good - Resources are nouns
GET /users # List users
POST /users # Create user
GET /users/123 # Get user
PUT /users/123 # Update user
DELETE /users/123 # Delete user
# Bad - Verbs in URLs
GET /getUsers
POST /createUser
GET /getUserById/123
Hierarchical Resources
# Nested resources for relationships
GET /users/123/orders # User's orders
GET /users/123/orders/456 # Specific order
POST /users/123/orders # Create order for user
# Alternative: Query parameters for filtering
GET /orders?user_id=123 # Filter orders by user
Use plural nouns for collections (/users) and singular identifiers for specific resources (/users/123).
Consistent Naming
# Use kebab-case for multi-word resources
GET /user-profiles
GET /order-items
GET /shipping-addresses
# Use snake_case for query parameters
GET /products?category_id=5&sort_by=price&sort_order=desc
HTTP Methods and Status Codes
Proper Method Usage
GET - Retrieve resources (idempotent, cacheable)
POST - Create new resources
PUT - Replace entire resource (idempotent)
PATCH - Partial update (not idempotent)
DELETE - Remove resource (idempotent)
OPTIONS - Get allowed methods
HEAD - Get headers only
Status Code Reference
// Success responses
200 OK // Successful GET, PUT, PATCH
201 Created // Successful POST (include Location header)
204 No Content // Successful DELETE
// Client errors
400 Bad Request // Invalid request body/parameters
401 Unauthorized // Missing or invalid authentication
403 Forbidden // Authenticated but not authorized
404 Not Found // Resource doesn't exist
409 Conflict // Resource conflict (duplicate, etc.)
422 Unprocessable // Validation errors
429 Too Many Requests // Rate limit exceeded
// Server errors
500 Internal Error // Unexpected server error
502 Bad Gateway // Upstream service error
503 Service Unavailable // Temporary overload
Request and Response Design
Consistent Response Structure
// Success response
{
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"created_at": "2024-01-15T10:30:00Z"
},
"meta": {
"request_id": "req_abc123"
}
}
// Collection response with pagination
{
"data": [
{ "id": 1, "name": "Product A" },
{ "id": 2, "name": "Product B" }
],
"meta": {
"total": 150,
"page": 1,
"per_page": 20,
"total_pages": 8
},
"links": {
"self": "/products?page=1",
"next": "/products?page=2",
"last": "/products?page=8"
}
}
// Error response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request contains invalid data",
"details": [
{
"field": "email",
"message": "Invalid email format"
},
{
"field": "age",
"message": "Must be at least 18"
}
]
},
"meta": {
"request_id": "req_xyz789"
}
}
Always include a request ID in responses for debugging and support purposes.
Filtering, Sorting, and Pagination
# Filtering
GET /products?category=electronics&brand=apple&min_price=100&max_price=500
# Sorting
GET /products?sort=price:asc,created_at:desc
# Pagination (offset-based)
GET /products?page=2&per_page=20
# Pagination (cursor-based - better for large datasets)
GET /products?cursor=eyJpZCI6MTAwfQ&limit=20
# Field selection (sparse fieldsets)
GET /users/123?fields=id,name,email
# Including related resources
GET /orders/123?include=user,items,shipping_address
API Versioning
URL Path Versioning (Recommended)
GET /v1/users
GET /v2/users
# Clear and explicit
# Easy to route and cache
# Visible in logs and documentation
Header Versioning
GET /users
Accept: application/vnd.myapi.v2+json
# Cleaner URLs
# More complex to implement
# Harder to test in browser
Version Deprecation Strategy
// Response headers for deprecated versions
{
"headers": {
"Deprecation": "Sun, 01 Jan 2025 00:00:00 GMT",
"Sunset": "Sun, 01 Jul 2025 00:00:00 GMT",
"Link": "</v2/users>; rel=\"successor-version\""
}
}
Authentication and Authorization
JWT Authentication
// Request with Bearer token
GET /api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
// Token structure
{
"header": {
"alg": "RS256",
"typ": "JWT"
},
"payload": {
"sub": "user_123",
"email": "user@example.com",
"roles": ["user", "admin"],
"iat": 1704067200,
"exp": 1704153600
}
}
API Key Authentication
// Header-based (recommended)
GET /api/data
X-API-Key: sk_live_abc123xyz
// Query parameter (less secure - visible in logs)
GET /api/data?api_key=sk_live_abc123xyz
OAuth 2.0 Scopes
# Define granular permissions
read:users - Read user data
write:users - Create/update users
delete:users - Delete users
read:orders - Read order data
admin - Full access
# Token with scopes
{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:users read:orders"
}
Rate Limiting
Implementation Headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1704067200
# When rate limited
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1704067200
Rate Limit Strategies
// Sliding window implementation
const rateLimiter = {
// Per-user limits
authenticated: {
requests: 1000,
window: '1 hour'
},
// Per-IP limits for unauthenticated
anonymous: {
requests: 100,
window: '1 hour'
},
// Per-endpoint limits
endpoints: {
'POST /auth/login': { requests: 5, window: '1 minute' },
'POST /users': { requests: 10, window: '1 hour' }
}
};
Error Handling
Comprehensive Error Response
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "The requested user was not found",
"details": {
"resource_type": "User",
"resource_id": "123"
},
"documentation_url": "https://api.example.com/docs/errors#RESOURCE_NOT_FOUND",
"request_id": "req_abc123"
}
}
Error Code Catalog
const errorCodes = {
// Authentication errors (1xxx)
INVALID_CREDENTIALS: { status: 401, message: 'Invalid email or password' },
TOKEN_EXPIRED: { status: 401, message: 'Authentication token has expired' },
INSUFFICIENT_PERMISSIONS: { status: 403, message: 'You do not have permission' },
// Validation errors (2xxx)
VALIDATION_ERROR: { status: 422, message: 'Request validation failed' },
INVALID_FORMAT: { status: 400, message: 'Invalid request format' },
// Resource errors (3xxx)
RESOURCE_NOT_FOUND: { status: 404, message: 'Resource not found' },
RESOURCE_CONFLICT: { status: 409, message: 'Resource already exists' },
// Rate limiting (4xxx)
RATE_LIMIT_EXCEEDED: { status: 429, message: 'Too many requests' },
// Server errors (5xxx)
INTERNAL_ERROR: { status: 500, message: 'An unexpected error occurred' },
SERVICE_UNAVAILABLE: { status: 503, message: 'Service temporarily unavailable' },
};
HATEOAS and Hypermedia
Self-Describing Responses
{
"data": {
"id": 123,
"status": "pending",
"total": 99.99
},
"links": {
"self": { "href": "/orders/123" },
"customer": { "href": "/users/456" },
"items": { "href": "/orders/123/items" }
},
"actions": {
"cancel": {
"href": "/orders/123/cancel",
"method": "POST",
"title": "Cancel this order"
},
"pay": {
"href": "/orders/123/pay",
"method": "POST",
"title": "Process payment"
}
}
}
API Documentation
OpenAPI Specification
openapi: 3.0.3
info:
title: My API
version: 1.0.0
description: A sample API
paths:
/users:
get:
summary: List all users
parameters:
- name: page
in: query
schema:
type: integer
default: 1
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email
Conclusion
Well-designed APIs are intuitive, consistent, and scalable. By following these best practices—proper resource naming, appropriate status codes, comprehensive error handling, and clear documentation—you create APIs that developers love to use.
Key takeaways:
- Use nouns for resources, HTTP methods for actions
- Return appropriate status codes
- Implement proper versioning from the start
- Design comprehensive error responses
- Document everything with OpenAPI
Top comments (0)