REST API Design: Building APIs Developers Love (2026)
A good API is like a good UI — intuitive, consistent, and a pleasure to use.
Core Principles
1. Consistency over cleverness
→ Same patterns everywhere
→ Predictable response shapes
→ No surprises
2. Resources, not actions
→ GET /users (not /getUsers)
→ POST /users (not /createUser)
→ Nouns, not verbs
3. Statelessness
→ Each request contains everything needed
→ Server doesn't store session state between requests
→ Scales horizontally
4. Practical defaults
→ JSON for request/response bodies
→ camelCase for JSON keys (JavaScript convention)
→ snake_case for URL parameters/headers (HTTP convention)
URL Design
# Good: Resource-oriented, hierarchical
GET /api/users # List all users
GET /api/users?role=admin&sort=-createdAt # Filtered/sorted list
POST /api/users # Create user
GET /api/users/:id # Get specific user
PUT /api/users/:id # Full update
PATCH /api/users/:id # Partial update
DELETE /api/users/:id # Delete user
# Nested resources (relationships)
GET /api/users/:id/orders # User's orders
POST /api/users/:id/orders # Create order for user
GET /api/users/:id/orders/:orderId # Specific order
# Actions on resources (when CRUD doesn't fit)
POST /api/users/:id/activate # Activate account
POST /api/users/:id/deactivate # Deactivate
POST /api/orders/:id/cancel # Cancel order
POST /api/orders/:id/refund # Request refund
# Search (complex queries)
GET /api/search?q=keyword&type=users
POST /api/search # For complex filters (body > URL length)
# Bad URLs to avoid:
❌ /api/getAllUsers # Verb in path
❌ /api/user-data # Inconsistent naming
❌ /api/users/1 # Use :id pattern consistently
❌ /api/getUserById?id=1 # Query params for required IDs
❌ /api/v2/new-users # Version in weird places
HTTP Methods: The Standard Mapping
| Method | Safe | Idempotent | Meaning | Example |
|---|---|---|---|---|
| GET | ✅ | ✅ | Read resource | Get user profile |
| HEAD | ✅ | ✅ | Like GET, no body | Check if exists |
| POST | ❌ | ❌ | Create resource | New user |
| PUT | ❌ | ✅ | Replace entirely | Update full profile |
| PATCH | ❌ | ❌ | Partial update | Change email only |
| DELETE | ❌ | ❌ | Remove resource | Delete account |
| OPTIONS | ✅ | ✅ | CORS preflight | Browser auto-sends |
Key concepts:
- Safe: Doesn't modify server state (GET is safe)
- Idempotent: Same request = same result (PUT same data twice = identical outcome; POST creates TWO resources)
Response Format
Success Responses
// Single resource
{
"success": true,
"data": {
"id": "usr_abc123",
"email": "user@example.com",
"name": "Alice",
"role": "user",
"createdAt": "2026-05-27T10:33:00Z",
"updatedAt": "2026-05-27T10:33:00Z"
}
}
// Collection with pagination
{
"success": true,
"data": [
{ "id": "usr_001", "name": "Alice" },
{ "id": "usr_002", "name": "Bob" }
],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"totalPages": 8,
"hasNext": true,
"hasPrev": false
}
}
// Created (201)
{
"success": true,
"data": { "id": "usr_new123", ... },
"message": "User created successfully"
}
// No content (204)
// Response body is empty (for successful DELETE)
Error Responses
// Client error (4xx)
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{ "field": "email", "issue": "Invalid email format" },
{ "field": "password", "issue": "Must be at least 8 characters" }
],
"requestId": "req_abc123"
}
}
// Not found (404)
{
"error": {
"code": "NOT_FOUND",
"message": "User not found (id: usr_xyz)",
"requestId": "req_abc123"
}
}
// Server error (500)
{
"error": {
"code": "INTERNAL_ERROR",
"message": "An internal error occurred",
"requestId": "req_abc123"
// Never expose stack traces or internals in production!
}
}
Error Code Standards
// Use consistent, machine-readable error codes:
const ERROR_CODES = {
// Client errors (4xx)
BAD_REQUEST: 'BAD_REQUEST',
UNAUTHORIZED: 'UNAUTHORIZED',
FORBIDDEN: 'FORBIDDEN',
NOT_FOUND: 'NOT_FOUND',
NOT_ACCEPTABLE: 'NOT_ACCEPTABLE', // 406
CONFLICT: 'CONFLICT', // 409
VALIDATION_ERROR: 'VALIDATION_ERROR', // 422
RATE_LIMITED: 'RATE_LIMITED', // 429
// Server errors (5xx)
INTERNAL_ERROR: 'INTERNAL_ERROR',
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE', // 503
TIMEOUT: 'TIMEOUT', // 504 (gateway timeout)
};
Query Parameters & Filtering
// Standard pagination
GET /api/users?page=2&limit=20
// page starts at 1, default limit = 20
// Sorting
GET /api/users?sort=createdAt // Ascending
GET /api/users?sort=-createdAt // Descending (- prefix)
GET /api/users?sort=name,-createdAt // Multi-sort
// Filtering
GET /api/users?role=admin // Exact match
GET /api/users?status=active,pending // Multiple values (OR)
GET /api/users?minAge=18&maxAge=65 // Range filter
GET /api/users?search=alice // Full-text search
GET /api/users?tags=javascript,node // Has ALL tags (AND)
// Field selection (reduce payload)
GET /api/users?fields=id,name,email // Only return these fields
GET /api/users?exclude=passwordHash,lastLogin // Exclude sensitive fields
// Include related resources
GET /api/users/:id?include=orders,profile // Eager load relations
// Server-side implementation:
function parseQuery(req) {
return {
page: Math.max(1, parseInt(req.query.page) || 1),
limit: Math.min(100, parseInt(req.query.limit) || 20), // Cap at 100
sort: req.query.sort || '-createdAt',
search: req.query.search,
fields: req.query.fields?.split(','),
include: req.query.include?.split(','),
// ... build filters from remaining query params
};
}
Versioning Your API
Three approaches:
1. URL Path Versioning (most common)
/api/v1/users
/api/v2/users
Pros: Clear, works with CDNs, easy to deprecate
Cons: URL clutter
2. Header Versioning
Accept: application/vnd.api.v2+json
Pros: Clean URLs
Cons: Harder to debug in browser, not obvious
3. No Versioning (just evolve)
/api/users
Pros: Simplest
Cons: Breaking changes break clients
My recommendation: Start with URL versioning.
Only add v1 when you have real users and need stability guarantees.
Authentication Patterns
// Pattern 1: Bearer Token (JWT) — Most common for APIs
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
// Pattern 2: API Key — For server-to-server or public APIs
X-API-Key: sk_live_abc123
// Or query param: ?api_key=sk_live_abc123 (less secure but simpler)
// Pattern 3: OAuth 2.0 — For third-party access
Authorization: Bearer oauth_access_token_here
// Response should include auth info when relevant:
{
"data": { /* ... */ },
"meta": {
"scopes": ["read:profile", "write:profile"],
"rateLimit": { "remaining": 95, "reset": "2026-05-27T11:00:00Z" }
}
}
Rate Limiting Done Right
// Headers on EVERY response (even errors!):
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1716825600 // Unix timestamp
Retry-After: 60 // On 429 responses
// Rate limit tiers (common pattern):
// Unauthenticated: 30/hour
// Authenticated (free): 100/hour
// Paid tier: 1000/hour
// Admin: unlimited
// Always include rate limit info in response body on 429:
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Please try again later.",
"retryAfter": 45,
"rateLimit": {
"limit": 100,
"remaining": 0,
"resetAt": "2026-05-27T11:00:00Z"
}
}
}
Documentation
Your API is only as good as its documentation.
Minimum requirements:
→ Every endpoint documented
→ Request/response examples for each
→ Authentication explanation
→ Error codes reference
→ Rate limits stated
→ SDK/client library examples
Tools I recommend:
→ OpenAPI/Swagger (auto-generate interactive docs)
→ Postman Collections (shareable, importable)
→ README.md with quick-start guide (always!)
The best API docs let developers succeed WITHOUT contacting you.
Quick Checklist
Before shipping any endpoint:
□ Uses correct HTTP method (GET/POST/PATCH/PUT/DELETE)
□ URL follows resource-naming convention
□ Returns consistent response shape ({ success, data/error })
□ Handles pagination for collections
□ Validates input and returns 422 with details
□ Returns 404 (not 500) for missing resources
□ Includes rate limit headers
□ Has authentication where needed
□ Returns appropriate status codes
□ Documented with examples
□ Error messages are helpful (not exposing internals)
□ Request ID included for support debugging
□ Fields use consistent naming (camelCase in JSON body)
What's the best/worst API you've worked with?
Follow @armorbreak for more practical developer guides.
Top comments (0)