Building Scalable APIs: Best Practices
Building APIs that can scale to handle millions of requests requires careful planning and implementation. In this guide, we'll explore the key principles and patterns for building robust, scalable APIs.
API Design Principles
RESTful Design
A well-designed REST API follows consistent patterns:
// Express.js route handlers with proper REST conventions
import { Router } from 'express';
import { z } from 'zod';
const router = Router();
// GET /api/users - List users with pagination
router.get('/users', async (req, res) => {
const { page = 1, limit = 20 } = req.query;
const users = await db.users.findMany({
skip: (Number(page) - 1) * Number(limit),
take: Number(limit),
});
const total = await db.users.count();
res.json({
data: users,
pagination: {
page: Number(page),
limit: Number(limit),
total,
totalPages: Math.ceil(total / Number(limit)),
},
});
});
// POST /api/users - Create a new user
router.post('/users', async (req, res) => {
const schema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.issues });
}
const user = await db.users.create({ data: result.data });
res.status(201).json({ data: user });
});Caching Strategies
Effective caching is crucial for API performance:
// Redis caching middleware
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
interface CacheOptions {
ttl?: number; // Time to live in seconds
key?: (req: Request) => string;
}
function cacheMiddleware(options: CacheOptions = {}) {
const { ttl = 300, key = (req) => req.url } = options;
return async (req, res, next) => {
const cacheKey = `api:${key(req)}`;
// Try to get from cache
const cached = await redis.get(cacheKey);
if (cached) {
return res.json(JSON.parse(cached));
}
// Store original json method
const originalJson = res.json.bind(res);
// Override json to cache response
res.json = (data) => {
redis.setex(cacheKey, ttl, JSON.stringify(data));
return originalJson(data);
};
next();
};
}
// Usage
router.get('/products', cacheMiddleware({ ttl: 600 }), getProducts);Rate Limiting
Protect your API from abuse with rate limiting:
// Rate limiting with sliding window
import { RateLimiter } from 'rate-limiter-flexible';
const rateLimiter = new RateLimiter({
points: 100, // Number of requests
duration: 60, // Per 60 seconds
blockDuration: 60, // Block for 60 seconds if exceeded
});
async function rateLimitMiddleware(req, res, next) {
try {
const key = req.ip || req.headers['x-forwarded-for'];
await rateLimiter.consume(key);
next();
} catch (error) {
res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil(error.msBeforeNext / 1000),
});
}
}Database Optimization
Connection Pooling
// Prisma with connection pooling
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
// Connection pool settings
log: ['query', 'info', 'warn', 'error'],
});
// Graceful shutdown
process.on('SIGTERM', async () => {
await prisma.$disconnect();
process.exit(0);
});Query Optimization
-- Create indexes for frequently queried columns
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_created_at ON orders(created_at DESC);
-- Composite index for common query patterns
CREATE INDEX idx_orders_user_status ON orders(user_id, status);Error Handling
Consistent error responses improve API usability:
// Centralized error handling
class APIError extends Error {
constructor(
public statusCode: number,
public code: string,
message: string,
public details?: unknown
) {
super(message);
this.name = 'APIError';
}
}
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_ERROR',
message: 'An unexpected error occurred',
},
});
}Monitoring and Observability
Track API performance with proper instrumentation:
// Request timing middleware
function timingMiddleware(req, res, next) {
const start = process.hrtime.bigint();
res.on('finish', () => {
const duration = Number(process.hrtime.bigint() - start) / 1e6;
metrics.histogram('api_request_duration_ms', duration, {
method: req.method,
path: req.route?.path || req.path,
status: res.statusCode,
});
});
next();
}Summary
Building scalable APIs requires attention to:
- Design - Follow REST conventions and use proper HTTP methods
- Caching - Implement multi-layer caching strategies
- Rate Limiting - Protect against abuse and ensure fair usage
- Database - Optimize queries and use connection pooling
- Error Handling - Provide consistent, helpful error responses
- Monitoring - Track performance and identify bottlenecks
By following these practices, you can build APIs that scale gracefully as your user base grows.

