RESTful API Design: Best Practices and Patterns
Well-designed APIs are the backbone of modern applications. Let’s explore the principles and patterns that make APIs intuitive, maintainable, and scalable.
Resource-Oriented Design
URL Structure and Naming
# Good: Noun-based resource URLs
GET /api/v1/users # Get all users
GET /api/v1/users/123 # Get specific user
POST /api/v1/users # Create new user
PUT /api/v1/users/123 # Update entire user
PATCH /api/v1/users/123 # Partial update
DELETE /api/v1/users/123 # Delete user
# Nested resources
GET /api/v1/users/123/posts # Get user's posts
POST /api/v1/users/123/posts # Create post for user
GET /api/v1/posts/456/comments # Get post comments
# Bad: Verb-based URLs
POST /api/v1/createUser
GET /api/v1/getUserById/123
POST /api/v1/deleteUser/123
HTTP Methods and Status Codes
// Express.js example with proper status codes
app.get('/api/v1/users/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
error: 'User not found',
code: 'USER_NOT_FOUND'
});
}
res.status(200).json({ data: user });
} catch (error) {
res.status(500).json({
error: 'Internal server error',
code: 'INTERNAL_ERROR'
});
}
});
app.post('/api/v1/users', async (req, res) => {
try {
const user = await User.create(req.body);
res.status(201).json({
data: user,
message: 'User created successfully'
});
} catch (error) {
if (error.name === 'ValidationError') {
return res.status(400).json({
error: 'Validation failed',
code: 'VALIDATION_ERROR',
details: error.details
});
}
res.status(500).json({
error: 'Internal server error',
code: 'INTERNAL_ERROR'
});
}
});
Request and Response Design
Consistent Response Format
// Standard response wrapper
class ApiResponse {
static success(data, message = 'Success', meta = {}) {
return {
success: true,
data,
message,
meta,
timestamp: new Date().toISOString()
};
}
static error(error, code = 'UNKNOWN_ERROR', details = null) {
return {
success: false,
error,
code,
details,
timestamp: new Date().toISOString()
};
}
static paginated(data, pagination) {
return {
success: true,
data,
pagination: {
page: pagination.page,
limit: pagination.limit,
total: pagination.total,
totalPages: Math.ceil(pagination.total / pagination.limit),
hasNext: pagination.page < Math.ceil(pagination.total / pagination.limit),
hasPrev: pagination.page > 1
},
timestamp: new Date().toISOString()
};
}
}
// Usage
app.get('/api/v1/users', async (req, res) => {
const { page = 1, limit = 10 } = req.query;
const users = await User.paginate({ page, limit });
res.json(ApiResponse.paginated(users.docs, {
page: parseInt(page),
limit: parseInt(limit),
total: users.totalDocs
}));
});
Input Validation and Sanitization
const Joi = require('joi');
// Validation schemas
const userSchemas = {
create: Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/).required(),
age: Joi.number().integer().min(13).max(120),
role: Joi.string().valid('user', 'admin').default('user')
}),
update: Joi.object({
name: Joi.string().min(2).max(50),
email: Joi.string().email(),
age: Joi.number().integer().min(13).max(120),
role: Joi.string().valid('user', 'admin')
}).min(1) // At least one field required
};
// Validation middleware
const validate = (schema) => {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true
});
if (error) {
const details = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message,
value: detail.context.value
}));
return res.status(400).json(
ApiResponse.error('Validation failed', 'VALIDATION_ERROR', details)
);
}
req.body = value; // Use sanitized data
next();
};
};
// Usage
app.post('/api/v1/users', validate(userSchemas.create), createUser);
app.patch('/api/v1/users/:id', validate(userSchemas.update), updateUser);
Advanced Query Patterns
Filtering, Sorting, and Pagination
class QueryBuilder {
constructor(model) {
this.model = model;
this.query = model.find();
}
filter(filters) {
Object.keys(filters).forEach(key => {
const value = filters[key];
if (value !== undefined && value !== '') {
if (typeof value === 'string' && value.includes(',')) {
// Handle comma-separated values (e.g., status=active,pending)
this.query = this.query.where(key).in(value.split(','));
} else if (key.endsWith('_gte')) {
// Greater than or equal (e.g., age_gte=18)
const field = key.replace('_gte', '');
this.query = this.query.where(field).gte(value);
} else if (key.endsWith('_lte')) {
// Less than or equal (e.g., age_lte=65)
const field = key.replace('_lte', '');
this.query = this.query.where(field).lte(value);
} else if (key.endsWith('_like')) {
// Partial match (e.g., name_like=john)
const field = key.replace('_like', '');
this.query = this.query.where(field).regex(new RegExp(value, 'i'));
} else {
this.query = this.query.where(key).equals(value);
}
}
});
return this;
}
sort(sortBy) {
if (sortBy) {
const sortFields = sortBy.split(',').map(field => {
if (field.startsWith('-')) {
return { [field.substring(1)]: -1 };
}
return { [field]: 1 };
});
this.query = this.query.sort(Object.assign({}, ...sortFields));
}
return this;
}
paginate(page = 1, limit = 10) {
const skip = (page - 1) * limit;
this.query = this.query.skip(skip).limit(parseInt(limit));
return this;
}
select(fields) {
if (fields) {
this.query = this.query.select(fields.split(',').join(' '));
}
return this;
}
async execute() {
return await this.query.exec();
}
async executeWithCount() {
const [data, total] = await Promise.all([
this.query.exec(),
this.model.countDocuments(this.query.getFilter())
]);
return { data, total };
}
}
// Usage in route handler
app.get('/api/v1/users', async (req, res) => {
try {
const {
page = 1,
limit = 10,
sort = '-createdAt',
fields,
...filters
} = req.query;
const queryBuilder = new QueryBuilder(User)
.filter(filters)
.sort(sort)
.select(fields)
.paginate(page, limit);
const { data, total } = await queryBuilder.executeWithCount();
res.json(ApiResponse.paginated(data, {
page: parseInt(page),
limit: parseInt(limit),
total
}));
} catch (error) {
res.status(500).json(ApiResponse.error('Failed to fetch users'));
}
});
Error Handling and Logging
Comprehensive Error Handling
// Custom error classes
class ApiError extends Error {
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR', details = null) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.details = details;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends ApiError {
constructor(message, details) {
super(message, 400, 'VALIDATION_ERROR', details);
}
}
class NotFoundError extends ApiError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404, 'NOT_FOUND');
}
}
class UnauthorizedError extends ApiError {
constructor(message = 'Unauthorized') {
super(message, 401, 'UNAUTHORIZED');
}
}
// Global error handler middleware
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// Log error
console.error(err);
// Mongoose bad ObjectId
if (err.name === 'CastError') {
const message = 'Invalid ID format';
error = new ApiError(message, 400, 'INVALID_ID');
}
// Mongoose duplicate key
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
const message = `${field} already exists`;
error = new ApiError(message, 400, 'DUPLICATE_FIELD');
}
// Mongoose validation error
if (err.name === 'ValidationError') {
const details = Object.values(err.errors).map(val => ({
field: val.path,
message: val.message
}));
error = new ValidationError('Validation failed', details);
}
res.status(error.statusCode || 500).json(
ApiResponse.error(
error.message || 'Internal Server Error',
error.code || 'INTERNAL_ERROR',
error.details
)
);
};
Security Best Practices
Rate Limiting and Security Headers
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');
// Rate limiting
const createRateLimiter = (windowMs, max, message) => {
return rateLimit({
windowMs,
max,
message: ApiResponse.error(message, 'RATE_LIMIT_EXCEEDED'),
standardHeaders: true,
legacyHeaders: false,
});
};
// Apply security middleware
app.use(helmet()); // Security headers
app.use(mongoSanitize()); // Prevent NoSQL injection
app.use(xss()); // Clean user input from malicious HTML
// Different rate limits for different endpoints
app.use('/api/v1/auth', createRateLimiter(15 * 60 * 1000, 5, 'Too many auth attempts'));
app.use('/api/v1', createRateLimiter(15 * 60 * 1000, 100, 'Too many requests'));
API Key Authentication
const apiKeyAuth = async (req, res, next) => {
try {
const apiKey = req.header('X-API-Key');
if (!apiKey) {
throw new UnauthorizedError('API key required');
}
const hashedKey = crypto.createHash('sha256').update(apiKey).digest('hex');
const keyRecord = await ApiKey.findOne({
hashedKey,
isActive: true,
expiresAt: { $gt: new Date() }
});
if (!keyRecord) {
throw new UnauthorizedError('Invalid or expired API key');
}
// Update last used
keyRecord.lastUsedAt = new Date();
await keyRecord.save();
req.apiKey = keyRecord;
next();
} catch (error) {
next(error);
}
};
API Documentation
OpenAPI/Swagger Documentation
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'User Management API',
version: '1.0.0',
description: 'A comprehensive user management API',
},
servers: [
{
url: 'http://localhost:3000/api/v1',
description: 'Development server',
},
],
components: {
securitySchemes: {
ApiKeyAuth: {
type: 'apiKey',
in: 'header',
name: 'X-API-Key'
}
}
}
},
apis: ['./routes/*.js'], // Path to the API docs
};
/**
* @swagger
* /users:
* get:
* summary: Get all users
* tags: [Users]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: Page number
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* description: Number of items per page
* responses:
* 200:
* description: List of users
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: array
* items:
* $ref: '#/components/schemas/User'
*/
const specs = swaggerJsdoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
Great API design is about creating interfaces that are intuitive for developers to use and maintain. Focus on consistency, clear error messages, and comprehensive documentation to build APIs that developers love to work with.