API Design Best Practices: RESTful Patterns That Stand the Test of Time
A well-designed API is a pleasure to consume. A poorly designed one creates support tickets forever.
Resource Naming
# Good: nouns, plural, hierarchical
GET /users List users
GET /users/:id Get user
POST /users Create user
PUT /users/:id Replace user
PATCH /users/:id Update user fields
DELETE /users/:id Delete user
GET /users/:id/orders List user's orders
POST /users/:id/orders Create order for user
# Bad: verbs in URLs
POST /createUser
GET /getUserById
POST /user/delete
HTTP Status Codes
// Use the right codes
200 OK // Successful GET, PATCH, DELETE
201 Created // Successful POST (return new resource)
204 No Content // Successful DELETE (no body)
400 Bad Request // Validation error (return field errors)
401 Unauthorized // Not authenticated
403 Forbidden // Authenticated but not authorized
404 Not Found // Resource doesn't exist
409 Conflict // Duplicate resource (email already taken)
422 Unprocessable// Validation failed (business rule, not format)
429 Too Many // Rate limited
500 Server Error // Something broke
Consistent Error Format
// Every error response uses the same shape
interface ApiError {
error: {
code: string; // Machine-readable: 'USER_NOT_FOUND'
message: string; // Human-readable description
details?: Record<string, string[]>; // Field-level errors
};
}
// 400 validation error
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Request validation failed",
"details": {
"email": ["Invalid email format"],
"password": ["Must be at least 8 characters"]
}
}
}
Pagination
// Cursor-based (preferred for large datasets)
GET /orders?cursor=eyJpZCI6MTIzfQ&limit=20
// Response includes next cursor
{
"data": [...],
"pagination": {
"hasMore": true,
"nextCursor": "eyJpZCI6MTQzfQ"
}
}
Versioning and Filtering
// Filtering via query params
GET /orders?status=pending&createdAfter=2024-01-01&limit=50
// Sparse fieldsets (only return what the client needs)
GET /users?fields=id,name,email
// Embedding related resources
GET /orders/:id?include=user,items
Idempotency Keys
// Client sends idempotency key with POST requests
app.post('/api/orders', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
if (idempotencyKey) {
// Check if we've already processed this request
const existing = await redis.get(`idem:${idempotencyKey}`);
if (existing) return res.json(JSON.parse(existing));
}
const order = await createOrder(req.body);
if (idempotencyKey) {
await redis.setex(`idem:${idempotencyKey}`, 86400, JSON.stringify(order));
}
res.status(201).json(order);
});
Well-designed REST APIs with consistent error handling, pagination, and idempotency are the foundation of the backend in the AI SaaS Starter Kit.
Top comments (0)