Building APIs that can handle millions of requests while remaining maintainable is one of the most rewarding challenges in backend development. Over the past few years, I’ve had the opportunity to architect and scale Node.js APIs from handling hundreds of requests per day to millions per hour.
In this post, I’ll share the key principles, patterns, and practical techniques that have proven most valuable in production environments.
The Foundation: Core Principles#
1. Design for Failure#
Every external dependency will fail at some point. Your API should gracefully handle:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| // Circuit breaker pattern implementation
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureCount = 0;
this.threshold = threshold;
this.timeout = timeout;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
}
async execute(operation) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
}
}
|
2. Embrace Asynchronous Processing#
Not every operation needs to block the request-response cycle:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Event-driven architecture for background processing
const EventEmitter = require('events');
class APIService extends EventEmitter {
async createUser(userData) {
// Synchronous: Create user in database
const user = await User.create(userData);
// Asynchronous: Send welcome email, update analytics, etc.
this.emit('user:created', user);
return { id: user.id, email: user.email };
}
}
// Background processors
apiService.on('user:created', async (user) => {
await emailService.sendWelcomeEmail(user.email);
await analyticsService.trackUserCreation(user);
});
|
3. Cache Strategically#
Implement caching at multiple levels:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| // Multi-layer caching strategy
class CacheService {
constructor() {
this.memoryCache = new Map();
this.redis = redis.createClient();
}
async get(key, fallbackFn) {
// L1: Memory cache (fastest)
if (this.memoryCache.has(key)) {
return this.memoryCache.get(key);
}
// L2: Redis cache
const redisValue = await this.redis.get(key);
if (redisValue) {
const parsed = JSON.parse(redisValue);
this.memoryCache.set(key, parsed);
return parsed;
}
// L3: Database/API call
const value = await fallbackFn();
// Cache in both layers
await this.redis.setex(key, 3600, JSON.stringify(value));
this.memoryCache.set(key, value);
return value;
}
}
|
Architecture Patterns That Scale#
API Gateway Pattern#
Centralize cross-cutting concerns:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // API Gateway with rate limiting and authentication
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
// Rate limiting middleware
const createRateLimit = (windowMs, max) => rateLimit({
windowMs,
max,
message: { error: 'Too many requests' },
standardHeaders: true,
legacyHeaders: false,
});
// Different limits for different endpoints
app.use('/api/auth', createRateLimit(15 * 60 * 1000, 5)); // 5 requests per 15 minutes
app.use('/api/users', createRateLimit(15 * 60 * 1000, 100)); // 100 requests per 15 minutes
// Authentication middleware
app.use('/api', async (req, res, next) => {
try {
const token = req.headers.authorization?.split(' ')[1];
const user = await authService.verifyToken(token);
req.user = user;
next();
} catch (error) {
res.status(401).json({ error: 'Unauthorized' });
}
});
|
Database Connection Pooling#
Manage database connections efficiently:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // Connection pool configuration
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
// Pool configuration
max: 20, // Maximum number of connections
idleTimeoutMillis: 30000, // Close idle connections after 30s
connectionTimeoutMillis: 2000, // Timeout for new connections
// Health check
keepAlive: true,
keepAliveInitialDelayMillis: 10000,
});
// Graceful shutdown
process.on('SIGTERM', async () => {
await pool.end();
process.exit(0);
});
|
Request Validation and Sanitization#
Validate early, fail fast:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| const Joi = require('joi');
// Schema definitions
const schemas = {
createUser: Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
name: Joi.string().min(2).max(50).required(),
age: Joi.number().integer().min(13).max(120),
}),
updateUser: Joi.object({
email: Joi.string().email(),
name: Joi.string().min(2).max(50),
age: Joi.number().integer().min(13).max(120),
}).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,
}));
return res.status(400).json({
error: 'Validation failed',
details,
});
}
req.validatedBody = value;
next();
};
};
|
Response Compression#
Reduce payload sizes:
1
2
3
4
5
6
7
8
9
10
11
12
| const compression = require('compression');
app.use(compression({
level: 6, // Compression level (1-9)
threshold: 1024, // Only compress responses > 1KB
filter: (req, res) => {
// Don't compress images or videos
const contentType = res.getHeader('content-type');
return !contentType?.startsWith('image/') &&
!contentType?.startsWith('video/');
},
}));
|
Database Query Optimization#
Optimize your data access patterns:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // N+1 problem solution with batching
class UserService {
async getUsersWithProfiles(userIds) {
// Bad: N+1 queries
// const users = await User.findByIds(userIds);
// for (const user of users) {
// user.profile = await Profile.findByUserId(user.id);
// }
// Good: 2 queries with batching
const [users, profiles] = await Promise.all([
User.findByIds(userIds),
Profile.findByUserIds(userIds),
]);
const profileMap = new Map(
profiles.map(profile => [profile.userId, profile])
);
return users.map(user => ({
...user,
profile: profileMap.get(user.id),
}));
}
}
|
Memory Management#
Prevent memory leaks:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // Streaming large datasets
const stream = require('stream');
const { pipeline } = require('stream/promises');
app.get('/api/users/export', async (req, res) => {
const query = `
SELECT id, email, name, created_at
FROM users
WHERE created_at >= $1
ORDER BY created_at DESC
`;
const readable = pool.query(new QueryStream(query, [req.query.since]));
const csvTransform = new stream.Transform({
objectMode: true,
transform(chunk, encoding, callback) {
const csv = `${chunk.id},${chunk.email},${chunk.name},${chunk.created_at}\n`;
callback(null, csv);
},
});
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename="users.csv"');
await pipeline(readable, csvTransform, res);
});
|
Monitoring and Observability#
Health Checks#
Implement comprehensive health monitoring:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| // Health check endpoint
app.get('/health', async (req, res) => {
const checks = {
database: false,
redis: false,
external_api: false,
timestamp: new Date().toISOString(),
};
try {
// Database health
await pool.query('SELECT 1');
checks.database = true;
} catch (error) {
console.error('Database health check failed:', error);
}
try {
// Redis health
await redis.ping();
checks.redis = true;
} catch (error) {
console.error('Redis health check failed:', error);
}
try {
// External API health (with timeout)
await axios.get('https://api.external-service.com/health', {
timeout: 5000,
});
checks.external_api = true;
} catch (error) {
console.error('External API health check failed:', error);
}
const isHealthy = Object.values(checks)
.filter(value => typeof value === 'boolean')
.every(Boolean);
res.status(isHealthy ? 200 : 503).json(checks);
});
|
Request Logging#
Log effectively for debugging and analytics:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
});
// Request logging middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info({
method: req.method,
url: req.url,
status: res.statusCode,
duration,
user_id: req.user?.id,
user_agent: req.get('User-Agent'),
ip: req.ip,
});
});
next();
});
|
Deployment and Scaling#
Graceful Shutdown#
Handle shutdown signals properly:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| // Graceful shutdown handler
let server;
const gracefulShutdown = async (signal) => {
console.log(`Received ${signal}. Starting graceful shutdown...`);
// Stop accepting new connections
server.close(async () => {
console.log('HTTP server closed.');
try {
// Close database connections
await pool.end();
console.log('Database pool closed.');
// Close Redis connections
redis.quit();
console.log('Redis connection closed.');
// Any other cleanup...
console.log('Graceful shutdown completed.');
process.exit(0);
} catch (error) {
console.error('Error during graceful shutdown:', error);
process.exit(1);
}
});
// Force shutdown after 30 seconds
setTimeout(() => {
console.error('Could not close connections in time, forcefully shutting down');
process.exit(1);
}, 30000);
};
// Handle shutdown signals
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
server = app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
|
Use tools like Artillery or k6 to test your API under load:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| # artillery-config.yml
config:
target: 'http://localhost:3000'
phases:
- duration: 60
arrivalRate: 10
name: "Warm up"
- duration: 300
arrivalRate: 50
name: "Load test"
- duration: 60
arrivalRate: 100
name: "Spike test"
scenarios:
- name: "User creation flow"
weight: 70
requests:
- post:
url: "/api/users"
json:
email: "user{{ $randomString() }}@example.com"
password: "password123"
name: "Test User"
- name: "User retrieval"
weight: 30
requests:
- get:
url: "/api/users/{{ $randomInt(1, 1000) }}"
|
Key Takeaways#
- Plan for Scale Early: It’s much easier to build scalable patterns from the start than to refactor later
- Monitor Everything: You can’t optimize what you don’t measure
- Cache Intelligently: Multiple cache layers with appropriate TTLs
- Design for Failure: Circuit breakers, retries, and fallbacks
- Validate Early: Catch bad data at the API boundary
- Stream Large Data: Don’t load massive datasets into memory
- Test Under Load: Regular performance testing prevents surprises
Building scalable APIs is an iterative process. Start with solid foundations, measure performance regularly, and optimize based on real usage patterns rather than assumptions.
Have you implemented any of these patterns in your Node.js APIs? What challenges have you faced when scaling? Share your experiences in the comments below!
Further Reading#