Designing REST APIs That Developers Love
I've consumed hundreds of APIs. Here's what separates the good from the painful.
1. Consistent URL Structure
✅ Good: ❌ Bad:
/api/users /getUsers
/api/users/123 /user/get?id=123
/api/users/123/posts /getUserPosts
/api/users/123/posts/456 /api/getPost?userId=123&postId=456
Rules:
- Nouns only (no verbs in URLs)
- Plural resource names
- Hierarchical (nested = relationship)
- kebab-case, not camelCase or snake_case
2. Use HTTP Methods Correctly
// CRUD mapping:
GET /api/users → List all users
GET /api/users/123 → Get specific user
POST /api/users → Create new user
PUT /api/users/123 → Full update (replace)
PATCH /api/users/123 → Partial update
DELETE /api/users/123 → Delete user
// Actions (when you need verbs):
POST /api/users/123/activate // Not GET /activate-user!
POST /api/orders/123/cancel // Not GET /cancel-order
POST /api/posts/456/publish // Not GET /publish-post
3. Proper Status Codes (The Developer's Perspective)
// When I consume your API, here's what I expect:
// Success
200 OK // Standard response
201 Created // POST success — include Location header
204 No Content // DELETE success — no body needed
// Client errors (MY fault)
400 Bad Request // My request is malformed — tell me what's wrong!
401 Unauthorized // I need to authenticate
403 Forbidden // I'm authenticated but not allowed
404 Not Found // Resource doesn't exist
405 Method Not Allowed // Wrong HTTP method for this endpoint
409 Conflict // Duplicate resource, version conflict
422 Unprocessable // Valid syntax but validation failed
429 Too Many Requests // Slow down — include Retry-After header
// Server errors (YOUR fault)
500 Internal Error // Something broke on your end
502 Bad Gateway // Your upstream service is down
503 Unavailable // You're in maintenance mode
4. Pagination That Doesn't Suck
// ✅ Good pagination response:
GET /api/users?page=1&per_page=20
{
"data": [...],
"pagination": {
"page": 1,
"per_page": 20,
"total": 142,
"total_pages": 8,
"has_next": true,
"has_prev": false
},
"links": {
"first": "/api/users?page=1&per_page=20",
"prev": null,
"next": "/api/users?page=2&per_page=20",
"last": "/api/users?page=8&per_page=20"
}
}
// Cursor-based (for infinite scroll):
GET /api/posts?cursor=abc123&limit=20
{
"data": [...],
"cursor": {
"next": "def456",
"has_more": true
}
}
5. Filtering, Sorting, and Searching
// Filtering:
GET /api/users?role=admin&active=true&created_after=2026-01-01
// Sorting (allow multiple fields):
GET /api/users?sort=created_at&order=desc
GET /api/users?sort=name,age&order=asc,desc
// Field selection (reduce payload):
GET /api/users?fields=id,name,email
// Only returns id, name, email fields
// Search:
GET /api/users?q=alex&search_by=name,email
// Search across name and email fields
// Include related resources:
GET /api/users/123?include=posts,comments
// Embeds related data to avoid extra requests
6. Error Responses That Help Me Debug
// ❌ Bad error response:
{
"error": "Something went wrong"
}
// I have NO idea what went wrong!
// ✅ Good error response:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Invalid email format",
"value": "not-an-email"
},
{
"field": "password",
"message": "Password must be at least 8 characters",
"value": "short"
}
],
"request_id": "req_abc123", // For support lookup
"docs": "https://api.example.com/docs/errors#validation"
}
}
// Now I know EXACTLY what to fix!
7. Versioning
// URL versioning (most common):
/api/v1/users
/api/v2/users
// Header versioning:
Accept: application/vnd.api.v2+json
// My recommendation: Start with v1 from day one.
// Even if it's your first release.
// You WILL break things later and wish you had versions.
8. Authentication & Rate Limiting
// Always return rate limit info in headers:
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1703980800
// Standard auth headers:
Authorization: Bearer eyJhbGciOi...
X-API-Key: api_key_abc123
// For OAuth2:
// POST /oauth/token → { access_token, token_type, expires_in }
// Then use access_token in Authorization header
9. Async Operations
// Long-running tasks should return immediately:
POST /api/reports/generate
// 202 Accepted
{
"job_id": "job_abc123",
"status": "pending",
"estimated_time": 30,
"check_url": "/api/jobs/job_abc123"
}
// Check status:
GET /api/jobs/job_abc123
{
"job_id": "job_abc123",
"status": "completed", // pending, running, completed, failed
"result_url": "/api/reports/report_xyz",
"progress": 100,
"started_at": "2026-01-15T10:00:00Z",
"completed_at": "2026-01-15T10:00:28Z"
}
10. Documentation (Non-Negotiable)
// What every API needs:
// 1. OpenAPI/Swagger spec (machine-readable)
// https://api.example.com/swagger.json
// or https://api.example.com/docs (Swagger UI)
// 2. Quick start guide (5 minutes to first request)
// Show a real cURL example with real data
// 3. Realistic examples for EVERY endpoint
// Include request AND response bodies
// 4. Error code reference
// List all possible errors with solutions
// 5. Interactive playground
// Swagger UI, Postman collection, or Insomnia
What API design rule do you swear by? What's the worst API you've used?
Follow @armorbreak for more backend content.
Top comments (0)