RESTful API Design: Best Practices for Building Scalable APIs
Source: Dev.to
Resource Naming
✅ 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
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
Rule of thumb: Use plural nouns for collections (
/users) and singular identifiers for specific resources (/users/123).
Naming conventions
-
Kebab‑case for multi‑word resources
GET /user-profiles GET /order-items GET /shipping-addresses -
snake_case for query parameters
GET /products?category_id=5&sort_by=price&sort_order=desc
HTTP Methods
| Method | Purpose | Idempotent? | Cacheable? |
|---|---|---|---|
GET | Retrieve resources | ✅ | ✅ |
POST | Create new resources | ❌ | ❌ |
PUT | Replace an entire resource | ✅ | ❌ |
PATCH | Partial update | ❌ | ❌ |
DELETE | Remove a resource | ✅ | ❌ |
OPTIONS | Discover allowed methods | ✅ | ✅ |
HEAD | Retrieve headers only | ✅ | ✅ |
Status Codes
Success
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 Entity# Validation errors
429 Too Many Requests # Rate limit exceeded
Server errors
500 Internal Server Error # Unexpected server error
502 Bad Gateway # Upstream service error
503 Service Unavailable # Temporary overload
Response Payloads
Single‑resource success
{
"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"
}
}
Tip: Always include a
request_id(or similar) in responses for debugging and support.
Query‑Based Features
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 -
Cursor‑based (better for large datasets)
GET /products?cursor=eyJpZCI6MTAwfQ&limit=20
Sparse fieldsets (field selection)
GET /users/123?fields=id,name,email
Including related resources
GET /orders/123?include=user,items,shipping_address
Versioning
GET /v1/users
GET /v2/users
Header‑based versioning (recommended)
GET /users
Accept: application/vnd.myapi.v2+json
Deprecation headers (for older versions)
{
"headers": {
"Deprecation": "Sun, 01 Jan 2025 00:00:00 GMT",
"Sunset": "Sun, 01 Jul 2025 00:00:00 GMT",
"Link": "; rel=\"successor-version\""
}
}
Authentication & Authorization
Bearer token (Authorization header)
GET /api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT payload example
{
"header": { "alg": "RS256", "typ": "JWT" },
"payload": {
"sub": "user_123",
"email": "user@example.com",
"roles": ["user", "admin"],
"iat": 1704067200,
"exp": 1704153600
}
}
API‑Key (header – preferred)
GET /api/data
X-API-Key: sk_live_abc123xyz
API‑Key (query – less secure)
GET /api/data?api_key=sk_live_abc123xyz
Granular permission model
read:users– Read user datawrite:users– Create / update usersdelete:users– Delete usersread:orders– Read order dataadmin– Full access
Token with scopes
{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:users read:orders"
}
Rate Limiting
Successful response 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
Sliding‑window implementation (example)
const rateLimiter = {
// Per‑user limits
authenticated: {
requests: 1000,
window: '1 hour'
},
// Per‑IP limits for unauthenticated callers
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' }
}
};
Structured Error Catalog
{
"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"
}
}
Centralised error‑code map (example)
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: 'Insufficient permissions' },
// Validation errors (2xxx)
VALIDATION_ERROR: { status: 422, message: 'Validation failed' },
// Resource errors (3xxx)
RESOURCE_NOT_FOUND: { status: 404, message: 'Resource not found' },
CONFLICT: { status: 409, message: 'Resource conflict' },
// Rate‑limit errors (4xxx)
TOO_MANY_REQUESTS: { status: 429, message: 'Rate limit exceeded' },
// Server errors (5xxx)
INTERNAL_ERROR: { status: 500, message: 'Internal server error' }
};
Quick Checklist
- Resources: nouns, plural for collections, singular IDs for items.
- Naming: kebab‑case for URLs, snake_case for query params.
- Methods: use the correct HTTP verb; keep them idempotent where appropriate.
- Status codes: return the most specific code for each situation.
- Responses: include
data,meta(withrequest_id), andlinkswhen paginating. - Filtering / Sorting / Pagination: expose via query parameters.
- Versioning: URL or media‑type versioning; deprecate with proper headers.
- Auth: prefer header‑based tokens or API keys; embed scopes/roles.
- Rate limiting: communicate limits via response headers; implement sliding windows.
- Errors: consistent JSON structure, error codes, documentation links, and request IDs.
Follow these guidelines to build APIs that are clear, consistent, and easy to consume.
Error Definitions
RMISSIONS: { 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' },
};
Sample HAL‑style Response
{
"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"
}
}
}
OpenAPI Specification (YAML)
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
API Design Best Practices
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.