Designing REST APIs That Developers Actually Love Using
I've consumed hundreds of APIs. The good ones are a joy. The bad ones make you want to quit coding.
What Makes an API "Good"?
❌ Bad API:
GET /getUsers?userId=123
→ { "data": { "user_info": { "name_str": "John" } }, "status": "success", "error": null }
✅ Good API:
GET /users/123
→ { "id": 123, "name": "John Doe", "email": "john@example.com" }
1. Resource Naming (The Foundation)
// ✅ Use nouns, not verbs
GET /users // List users
POST /users // Create user
GET /users/123 // Get specific user
PUT /users/123 // Update user (full)
PATCH /users/123 // Partial update
DELETE /users/123 // Delete user
// ❌ Don't use verbs in URLs
GET /getAllUsers // Wrong!
POST /createUser // Wrong!
DELETE /deleteUser/123 // Wrong!
// ❌ Don't use URL query params for actions
POST /users?action=create // Wrong!
PUT /users?id=123&action=update // Wrong!
// Plural vs singular: Use PLURAL for collections
GET /users // Collection → plural
GET /users/123/items // Sub-collection of user 123's items → plural
2. Proper HTTP Methods & Status Codes
// Method semantics matter!
app.get('/users', listUsers); // Read — safe, idempotent
app.post('/users', createUser); // Create — not idempotent
app.put('/users/:id', replaceUser); // Full replace — idempotent
app.patch('/users/:id', updateUser); // Partial update — not idempotent
app.delete('/users/:id', deleteUser); // Remove — idempotent
// Status codes — use them correctly!
const StatusCodes = {
// Success (2xx)
OK: 200, // Standard success response
Created: 201, // POST created a resource
NoContent: 204, // DELETE succeeded (no body needed)
// Redirection (3xx)
NotModified: 304, // Conditional GET, resource unchanged
// Client Errors (4xx)
BadRequest: 400, // Malformed syntax
Unauthorized: 401, // Not authenticated
Forbidden: 403, // Authenticated but no permission
NotFound: 404, // Resource doesn't exist
MethodNotAllowed: 405, // Wrong HTTP method for this endpoint
Conflict: 409, // State conflict (e.g., duplicate email)
UnprocessableEntity: 422, // Valid request but semantic errors
TooManyRequests: 429, // Rate limited
// Server Errors (5xx)
InternalServerError: 500, // Something went wrong
NotImplemented: 501, // Feature not implemented yet
};
3. Consistent Response Format
// Option A: Simple (for public/read-heavy APIs)
// GET /users/123
{
"id": 123,
"name": "Alice",
"email": "alice@example.com",
"role": "admin",
"created_at": "2026-01-15T10:30:00Z",
"_links": {
"self": "/users/123",
"posts": "/users/123/posts"
}
}
// Option B: Enveloped (for APIs that need metadata)
// GET /users?page=1&per_page=20
{
"data": [
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
],
"meta": {
"page": 1,
"per_page": 20,
"total": 150,
"total_pages": 8
}
}
// Error responses (ALWAYS consistent!)
// ❌ Inconsistent errors
{ "error": "not found" } // Sometimes string
{ "message": "User doesn't exist" } // Sometimes message
{ "errors": ["not found"] } // Sometimes array
// ✅ Consistent error format
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User with ID 999 does not exist",
"status": 404,
"details": {
"field": "id",
"value": 999,
"expected": "existing user ID"
}
},
"request_id": "req_abc123"
}
4. Pagination Done Right
// ❌ Bad pagination (no way to navigate!)
GET /users?offset=0&limit=20
→ { data: [...] }
// ✅ Cursor-based (for infinite feeds, real-time data)
GET /users?cursor=eyJpZCI6MTAwfQ&limit=20
{
"data": [...],
"pagination": {
"cursor": "eyJpZCI6MTIwfQ", // Next page cursor
"has_next": true,
"has_prev": false
}
}
// ✅ Offset-based (for admin panels, sortable tables)
GET /users?page=2&per_page=20&sort=name&order=asc
{
"data": [...],
"meta": {
"page": 2,
"per_page": 20,
"total": 150,
"total_pages": 8,
"has_prev": true,
"has_next": true
},
"links": {
"first": "/users?page=1&per_page=20",
"prev": "/users?page=1&per_page=20",
"next": "/users?page=3&per_page=20",
"last": "/users?page=8&per_page=20"
}
}
5. Filtering, Sorting & Field Selection
// Filtering
GET /posts?status=published&author_id=5&tag=javascript
GET /products?price_min=10&price_max=100&category=electronics
GET /users?role=admin&active=true
// Sorting
GET /users?sort=created_at&order=desc
GET /posts?sort=-published_at,title // - prefix = descending
GET /products?sort=price&order=asc
// Field selection (reduce payload size!)
GET /users/123?fields=id,name,email
// Returns only requested fields instead of full object
// Search
GET /posts?q=react+hooks&search_fields=title,body
GET /users?search=john&search_fields=name,email
6. Versioning
// Option A: URL path versioning (most common)
/api/v1/users
/api/v2/users // Breaking changes go here
// Option B: Header versioning (cleaner URLs)
Accept: application/vnd.api.v1+json
GET /api/users
// My recommendation: Start with URL versioning.
// It's explicit and works everywhere.
// When to bump versions?
// v1 → v2: Breaking changes (removed fields, changed types, new required fields)
// Within same version: Additive changes are OK (new optional fields, new endpoints)
7. Authentication & Security
// API Key (for server-to-server or simple cases)
GET /api/users
X-API-Key: ak_live_abc123
// Bearer Token (JWT/OAuth) — most common for user-facing APIs
GET /api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
// Security headers (always include!)
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
res.setHeader('Access-Control-Allow-Origin', 'https://your-app.com');
// Rate limiting headers
res.setHeader('X-RateLimit-Limit', '1000');
res.setHeader('X-RateLimit-Remaining', '999');
res.setHeader('X-RateLimit-Reset', '1715841200');
// Request ID for tracing
res.setHeader('X-Request-ID', generateRequestId());
8. Documentation (Non-Negotiable!)
# OpenAPI/Swagger spec — the standard
openapi: 3.0.0
info:
title: My API
version: 1.0.0
description: A well-designed REST API
paths:
/users:
get:
summary: List all users
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: per_page
in: query
schema:
type: integer
maximum: 100
default: 20
responses:
'200':
description: List of users
content:
application/json:
example:
data:
- id: 1
name: Alice
meta:
total: 150
page: 1
'401':
description: Unauthorized
'429':
description: Rate limited
Quick Checklist
□ Nouns for resources, verbs for HTTP methods
□ Plural collection names
□ Correct status codes (2xx/4xx/5xx)
□ Consistent error format across ALL endpoints
□ Pagination on list endpoints
□ Filtering, sorting, field selection support
□ API versioning strategy
□ Auth headers documented
□ Rate limiting with informative headers
□ Request ID for debugging
□ OpenAPI/Swagger documentation
□ Example requests/responses for each endpoint
What's the best/worst API you've used?
Follow @armorbreak for more backend content.
Top comments (0)