API Best Practices
This section covers essential best practices for designing, implementing, and maintaining robust and scalable APIs.
Security Practices
Authentication
API Keys
- Simple implementation: Easy to implement and manage
- Rate limiting: Track usage per key
- Revocation: Disable compromised keys
- Scope limitation: Restrict key permissions
Implementation:
// Express.js API key middleware
const apiKeys = new Map([
['key123', { id: 1, scopes: ['read', 'write'] }],
['key456', { id: 2, scopes: ['read'] }]
]);
function apiKeyMiddleware(req, res, next) {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ error: 'API key required' });
}
const keyInfo = apiKeys.get(apiKey);
if (!keyInfo) {
return res.status(401).json({ error: 'Invalid API key' });
}
req.apiKey = keyInfo;
next();
}
OAuth 2.0
- Industry standard: Widely adopted
- Token-based: JWT or opaque tokens
- Scopes and permissions: Granular access control
- Refresh tokens: Long-lived access
JWT (JSON Web Tokens)
- Stateless: No server-side session storage
- Self-contained: Contains user information
- Signature verification: Security guarantee
- Expiration: Built-in token lifetime
const jwt = require('jsonwebtoken');
function generateToken(user) {
return jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
}
function verifyToken(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Token required' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
}
Authorization
Role-Based Access Control (RBAC)
- User roles: Admin, user, guest
- Resource permissions: Read, write, delete
- Hierarchical roles: Role inheritance
- Dynamic permissions: Context-based access
function requireRole(role) {
return (req, res, next) => {
if (!req.user || req.user.role !== role) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Usage
app.post('/admin/users', verifyToken, requireRole('admin'), createUser);
Attribute-Based Access Control (ABAC)
- Fine-grained control: Attribute-based decisions
- Context awareness: Time, location, device
- Dynamic policies: Rule-based access
- Complex scenarios: Enterprise requirements
Rate Limiting
- Prevent abuse: Protect against DDoS
- Fair usage: Ensure equal access
- Tiered limits: Different limits per user tier
- Sliding windows: Time-based rate limiting
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: 'Too many requests from this IP'
});
app.use('/api/', limiter);
Input Validation
- Sanitize inputs: Prevent injection attacks
- Validate types: Ensure correct data types
- Length limits: Prevent buffer overflows
- Whitelist approach: Allow only known good inputs
const { body, validationResult } = require('express-validator');
const userValidation = [
body('email').isEmail().normalizeEmail(),
body('name').isLength({ min: 2, max: 50 }).trim(),
body('age').isInt({ min: 18, max: 120 })
];
function handleValidationErrors(req, res, next) {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
}
app.post('/users', userValidation, handleValidationErrors, createUser);
API Versioning
URL Path Versioning
// Version 1
app.get('/v1/users', getUsersV1);
// Version 2
app.get('/v2/users', getUsersV2);
Header Versioning
app.get('/users', (req, res) => {
const version = req.headers['api-version'] || 'v1';
switch (version) {
case 'v1':
return getUsersV1(req, res);
case 'v2':
return getUsersV2(req, res);
default:
return res.status(400).json({ error: 'Unsupported version' });
}
});
Query Parameter Versioning
app.get('/users', (req, res) => {
const version = req.query.version || 'v1';
if (version === 'v1') {
return getUsersV1(req, res);
} else if (version === 'v2') {
return getUsersV2(req, res);
}
res.status(400).json({ error: 'Unsupported version' });
});
Performance Optimization
Caching Strategies
Response Caching
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600 }); // 10 minutes
function cacheMiddleware(duration = 600) {
return (req, res, next) => {
const key = req.originalUrl;
const cached = cache.get(key);
if (cached) {
return res.json(cached);
}
const originalSend = res.json;
res.json = function(data) {
cache.set(key, data, duration);
originalSend.call(this, data);
};
next();
};
}
app.get('/users', cacheMiddleware(300), getUsers);
Database Query Caching
const queryCache = new Map();
async function getCachedUser(id) {
const cacheKey = `user:${id}`;
if (queryCache.has(cacheKey)) {
return queryCache.get(cacheKey);
}
const user = await User.findById(id);
queryCache.set(cacheKey, user);
// Remove from cache after 5 minutes
setTimeout(() => {
queryCache.delete(cacheKey);
}, 5 * 60 * 1000);
return user;
}
Connection Pooling
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'myapp',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
async function getUsers() {
const connection = await pool.getConnection();
try {
const [rows] = await connection.execute('SELECT * FROM users');
return rows;
} finally {
connection.release();
}
}
Compression
const compression = require('compression');
app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
},
level: 6,
threshold: 1024
}));
Documentation
OpenAPI/Swagger
openapi: 3.0.0
info:
title: User API
version: 1.0.0
description: API for managing users
paths:
/users:
get:
summary: Get all users
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
post:
summary: Create a new user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: User created successfully
components:
schemas:
User:
type: object
properties:
id:
type: string
name:
type: string
email:
type: string
createdAt:
type: string
format: date-time
API Documentation Tools
- Swagger UI: Interactive API documentation
- Redoc: Clean API documentation
- Postman: API testing and documentation
- API Blueprint: Markdown-based documentation
Error Handling
Consistent Error Format
class ApiError extends Error {
constructor(statusCode, message, details = null) {
super(message);
this.statusCode = statusCode;
this.details = details;
}
}
function errorHandler(err, req, res, next) {
if (err instanceof ApiError) {
return res.status(err.statusCode).json({
error: {
code: err.code,
message: err.message,
details: err.details
}
});
}
// Log unexpected errors
console.error('Unexpected error:', err);
res.status(500).json({
error: {
code: 'INTERNAL_SERVER_ERROR',
message: 'An unexpected error occurred'
}
});
}
// Usage
try {
const user = await User.findById(id);
if (!user) {
throw new ApiError(404, 'User not found');
}
res.json({ data: user });
} catch (error) {
next(error);
}
HTTP Status Codes
- 200 OK: Successful request
- 201 Created: Resource created
- 400 Bad Request: Invalid input
- 401 Unauthorized: Authentication required
- 403 Forbidden: Insufficient permissions
- 404 Not Found: Resource not found
- 429 Too Many Requests: Rate limit exceeded
- 500 Internal Server Error: Server error
Monitoring and Analytics
API Metrics
- Request count: Total requests per endpoint
- Response time: Average and percentile response times
- Error rate: Percentage of failed requests
- Active users: Number of unique API consumers
const prometheus = require('prom-client');
// Create metrics
const httpRequestDuration = new prometheus.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status']
});
const httpRequestTotal = new prometheus.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status']
});
// Middleware to record metrics
function metricsMiddleware(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
const labels = {
method: req.method,
route: req.route?.path || req.path,
status: res.statusCode
};
httpRequestDuration.observe(labels, duration);
httpRequestTotal.inc(labels);
});
next();
}
app.use(metricsMiddleware);
Logging
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
function requestLogger(req, res, next) {
logger.info({
method: req.method,
url: req.url,
userAgent: req.get('User-Agent'),
ip: req.ip,
timestamp: new Date().toISOString()
});
next();
}
app.use(requestLogger);
Testing
Unit Testing
const request = require('supertest');
const app = require('../app');
describe('Users API', () => {
test('GET /users should return users list', async () => {
const response = await request(app)
.get('/api/users')
.expect(200);
expect(response.body).toHaveProperty('data');
expect(Array.isArray(response.body.data)).toBe(true);
});
test('POST /users should create new user', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
expect(response.body.data).toMatchObject(userData);
});
});
Integration Testing
describe('Users Integration Tests', () => {
test('Complete user lifecycle', async () => {
// Create user
const createResponse = await request(app)
.post('/api/users')
.send({ name: 'Jane Doe', email: 'jane@example.com' })
.expect(201);
const userId = createResponse.body.data.id;
// Get user
const getResponse = await request(app)
.get(`/api/users/${userId}`)
.expect(200);
expect(getResponse.body.data.name).toBe('Jane Doe');
// Update user
await request(app)
.put(`/api/users/${userId}`)
.send({ name: 'Jane Smith' })
.expect(200);
// Delete user
await request(app)
.delete(`/api/users/${userId}`)
.expect(204);
});
});
Best Practices Summary
Security
- Implement proper authentication and authorization
- Use HTTPS for all API communications
- Validate and sanitize all inputs
- Implement rate limiting
- Log security events
Performance
- Implement caching strategies
- Use connection pooling
- Enable compression
- Monitor performance metrics
- Optimize database queries
Reliability
- Implement proper error handling
- Use appropriate HTTP status codes
- Add comprehensive logging
- Implement health checks
- Design for scalability
Documentation
- Provide clear API documentation
- Include examples and use cases
- Document error responses
- Keep documentation up to date
- Provide interactive documentation
Key Takeaway: Building robust APIs requires attention to security, performance, reliability, and documentation throughout the development lifecycle.