• 6 min read • By Michael Rodriguez
API Design: Building RESTful APIs That Last
Learn the principles of designing robust, scalable RESTful APIs that are easy to use and maintain for years to come.
API REST Backend Web Development
What Makes a Good API?
A well-designed API is consistent, intuitive, and built to last. It should be easy for developers to understand and use without extensive documentation.
RESTful Principles
Resource-Based URLs
# Good - Resource-oriented
GET /api/users
GET /api/users/123
POST /api/users
PUT /api/users/123
DELETE /api/users/123
# Bad - Action-oriented
GET /api/getUsers
GET /api/getUserById/123
POST /api/createUser
POST /api/updateUser/123
POST /api/deleteUser/123
Use HTTP Methods Correctly
GET # Retrieve resource(s) - Safe & Idempotent
POST # Create new resource - Not idempotent
PUT # Update/replace resource - Idempotent
PATCH # Partial update - Not idempotent
DELETE # Remove resource - Idempotent
HTTP Status Codes
// Success
200 OK // Successful GET, PUT, PATCH, DELETE
201 Created // Successful POST
204 No Content // Successful DELETE with no body
// Client Errors
400 Bad Request // Invalid request data
401 Unauthorized // Missing or invalid auth
403 Forbidden // Authenticated but not allowed
404 Not Found // Resource doesn't exist
409 Conflict // Duplicate resource
422 Unprocessable // Validation error
429 Too Many // Rate limit exceeded
// Server Errors
500 Internal Error // Server error
502 Bad Gateway // Upstream error
503 Service Unavail // Server down
URL Structure Best Practices
Naming Conventions
# Use plural nouns
GET /api/products
GET /api/products/123
# Nested resources
GET /api/users/123/posts
GET /api/posts/456/comments
# Actions on resources (when needed)
POST /api/users/123/activate
POST /api/orders/456/cancel
# Filtering, sorting, pagination
GET /api/products?category=electronics&sort=-price&page=2&limit=20
Versioning
# URL versioning (recommended)
GET /api/v1/users
GET /api/v2/users
# Header versioning
GET /api/users
Accept: application/vnd.myapi.v1+json
# Query parameter versioning
GET /api/users?version=1
Request/Response Design
Request Body
// POST /api/users
{
"email": "user@example.com",
"name": "John Doe",
"role": "user"
}
// PUT /api/users/123
{
"email": "newemail@example.com",
"name": "John Smith",
"role": "admin"
}
// PATCH /api/users/123
{
"name": "John Smith"
}
Response Body
// GET /api/users/123
{
"id": "123",
"email": "user@example.com",
"name": "John Doe",
"role": "user",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-02-20T14:20:00Z"
}
// GET /api/users (Collection)
{
"data": [
{ "id": "123", "name": "John" },
{ "id": "124", "name": "Jane" }
],
"meta": {
"total": 150,
"page": 1,
"perPage": 20,
"totalPages": 8
},
"links": {
"self": "/api/users?page=1",
"next": "/api/users?page=2",
"last": "/api/users?page=8"
}
}
Error Responses
// Consistent error structure
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{
"field": "email",
"message": "Invalid email format"
},
{
"field": "password",
"message": "Password must be at least 8 characters"
}
],
"requestId": "abc-123-def",
"timestamp": "2024-02-20T14:30:00Z"
}
}
Authentication & Authorization
JWT Authentication
// Request
POST /api/auth/login
{
"email": "user@example.com",
"password": "secret123"
}
// Response
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "abc123def456",
"expiresIn": 3600
}
// Authenticated request
GET /api/users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
API Keys
// Header
GET /api/data
X-API-Key: sk_live_abc123def456
// Query parameter (less secure)
GET /api/data?api_key=sk_live_abc123def456
Pagination
Offset-Based
GET /api/posts?page=2&limit=20
# Response
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 150,
"totalPages": 8
}
}
Cursor-Based
GET /api/posts?cursor=eyJpZCI6MTIzfQ&limit=20
# Response
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTQzfQ",
"hasMore": true
}
}
Filtering & Sorting
# Filtering
GET /api/products?category=electronics&price_min=100&price_max=500
# Sorting (prefix - for descending)
GET /api/posts?sort=-createdAt,title
# Search
GET /api/users?search=john
# Multiple filters
GET /api/orders?status=pending&customer_id=123&date_from=2024-01-01
Rate Limiting
Response Headers
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1677234567
Retry-After: 3600
Implementation
// Express middleware
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: {
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests, please try again later.'
}
},
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', limiter);
Caching
Cache Headers
# Response headers
Cache-Control: public, max-age=3600
ETag: "abc123"
Last-Modified: Wed, 21 Feb 2024 07:28:00 GMT
# Conditional request
If-None-Match: "abc123"
If-Modified-Since: Wed, 21 Feb 2024 07:28:00 GMT
# Response if unchanged
HTTP/1.1 304 Not Modified
Cache Strategies
// Express example
app.get('/api/public/config', (req, res) => {
res.set('Cache-Control', 'public, max-age=86400'); // 24 hours
res.json(config);
});
app.get('/api/users/:id', (req, res) => {
res.set('Cache-Control', 'private, max-age=300'); // 5 minutes
res.json(user);
});
app.get('/api/sensitive', (req, res) => {
res.set('Cache-Control', 'no-store'); // Never cache
res.json(sensitiveData);
});
Documentation
OpenAPI/Swagger Example
openapi: 3.0.0
info:
title: My API
version: 1.0.0
description: API for managing users and posts
paths:
/users:
get:
summary: List users
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
post:
summary: Create user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserCreate'
responses:
'201':
description: Created
components:
schemas:
User:
type: object
properties:
id:
type: string
email:
type: string
name:
type: string
Security Best Practices
Input Validation
const { body, validationResult } = require('express-validator');
app.post('/api/users',
[
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }),
body('name').trim().escape()
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process request
}
);
CORS Configuration
const cors = require('cors');
app.use(cors({
origin: ['https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
}));
Helmet for Security Headers
const helmet = require('helmet');
app.use(helmet());
// Sets various HTTP headers for security
Testing
// Jest example
describe('POST /api/users', () => {
it('should create a new user', async () => {
const response = await request(app)
.post('/api/users')
.send({
email: 'test@example.com',
name: 'Test User',
password: 'password123'
})
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.email).toBe('test@example.com');
});
it('should return 400 for invalid email', async () => {
await request(app)
.post('/api/users')
.send({
email: 'invalid-email',
name: 'Test User',
password: 'password123'
})
.expect(400);
});
});
Monitoring & Logging
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// Middleware
app.use((req, res, next) => {
logger.info({
method: req.method,
url: req.url,
ip: req.ip,
userAgent: req.get('user-agent')
});
next();
});
Conclusion
Building a great API requires careful planning and attention to detail. Follow REST principles, use consistent naming conventions, provide clear error messages, and document thoroughly. Remember: your API is a product that developers will use—make their experience great!