Designing REST APIs That Developers Actually Love Using
A good API is invisible. A bad API ruins your day. Here's how to design the former.
The REST Basics
REST = REpresentational State Transfer
Core principles:
- Resources are nouns (users, posts, orders)
- HTTP methods are verbs (GET, POST, PUT, DELETE)
- Stateless — each request contains all info needed
- Uniform interface — consistent patterns everywhere
URL Design
# ✅ Good: Resource-based, nested for relationships
GET /api/users # List users
GET /api/users/123 # Get user 123
POST /api/users # Create user
PUT /api/users/123 # Update user 123 (full)
PATCH /api/users/123 # Update user 123 (partial)
DELETE /api/users/123 # Delete user 123
GET /api/users/123/posts # Posts by user 123
POST /api/users/123/posts # Create post for user 123
GET /api/posts/456/comments # Comments on post 456
# ❌ Bad: Verb-based URLs, inconsistent patterns
GET /api/getUsers
POST /api/createUser
GET /api/user/list
POST /api/User/create
DELETE /api/deleteUser?id=123
Query Parameters
# Pagination
GET /api/users?page=1&limit=20
# Filtering
GET /api/users?role=admin&active=true
# Sorting
GET /api/users?sort=created_at&order=desc
# Field selection (sparse fieldsets)
GET /api/users?fields=id,name,email
# Search
GET /api/users?q=john&search=name,email
# Combined
GET /api/users?role=admin&page=1&limit=20&sort=created_at&fields=id,name,email
Response Format
// ✅ Consistent envelope
{
"data": [
{ "id": 1, "name": "Alex", "email": "alex@example.com" },
{ "id": 2, "name": "Sarah", "email": "sarah@example.com" }
],
"meta": {
"page": 1,
"limit": 20,
"total": 156,
"totalPages": 8
}
}
// ✅ Single resource
{
"data": { "id": 1, "name": "Alex", "email": "alex@example.com" }
}
// ✅ Error response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request data",
"details": [
{ "field": "email", "message": "Must be a valid email" },
{ "field": "name", "message": "Must be at least 2 characters" }
]
}
}
// ❌ Inconsistent responses
{ "users": [...] } // Sometimes "users"
{ "data": [...] } // Sometimes "data"
{ "items": [...] } // Sometimes "items"
{ "error": "bad request" } // No details
Status Codes (Use Them Correctly!)
// Success
200 OK // Standard response
201 Created // Resource created (POST)
204 No Content // Success, no body (DELETE)
// Client Errors
400 Bad Request // Invalid request body/params
401 Unauthorized // Not authenticated
403 Forbidden // Authenticated but not authorized
404 Not Found // Resource doesn't exist
409 Conflict // Duplicate resource
422 Unprocessable // Validation errors
429 Too Many Requests // Rate limited
// Server Errors
500 Internal Error // Something broke
502 Bad Gateway // Upstream server issue
503 Unavailable // Server overloaded/maintenance
// ❌ Don't do this:
// Always returning 200 with error in body
{ "status": "error", "message": "Not found" } // HTTP 200! Wrong!
Versioning
# Option 1: URL path (most common)
/api/v1/users
/api/v2/users
# Option 2: Header
GET /api/users
Accept: application/vnd.myapi.v2+json
# Option 3: Query parameter (least recommended)
GET /api/users?version=2
# My recommendation: URL path — clearest, easiest to implement
Rate Limiting
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97
X-RateLimit-Reset: 1715841200
Retry-After: 42
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Quick Reference
| Method | Path | Purpose | Returns |
|---|---|---|---|
| GET | /resource | List | Array |
| GET | /resource/:id | Read | Object |
| POST | /resource | Create | 201 + Object |
| PUT | /resource/:id | Replace | Object |
| PATCH | /resource/:id | Update | Object |
| DELETE | /resource/:id | Delete | 204 |
What API design patterns do you follow? Any pet peeves?
Follow @armorbreak for more backend content.
Top comments (0)