Advanced Recipes and Best Practices
10 min read•Node.js Design Patterns
Advanced Recipes and Best Practices
This final chapter covers essential patterns and practices for production Node.js applications—error handling, testing, security, performance, and operational excellence.
Error Handling
Custom Error Classes
JAVASCRIPT
class AppError extends Error {
constructor(message, statusCode = 500, code = "INTERNAL_ERROR") {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.code = code;
this.isOperational = true; // Expected errors
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message, errors = []) {
super(message, 400, "VALIDATION_ERROR");
this.errors = errors;
}
}
class NotFoundError extends AppError {
constructor(resource, id) {
super(`${resource} not found: ${id}`, 404, "NOT_FOUND");
this.resource = resource;
this.resourceId = id;
}
}
class AuthenticationError extends AppError {
constructor(message = "Authentication required") {
super(message, 401, "AUTHENTICATION_ERROR");
}
}
class AuthorizationError extends AppError {
constructor(message = "Access denied") {
super(message, 403, "AUTHORIZATION_ERROR");
}
}
// Usage
throw new ValidationError("Invalid input", [
{ field: "email", message: "Invalid email format" },
{ field: "age", message: "Must be 18 or older" },
]);Centralized Error Handler
JAVASCRIPT
// Express error handler middleware
function errorHandler(err, req, res, next) {
// Log error
logger.error({
error: {
message: err.message,
stack: err.stack,
code: err.code,
},
request: {
method: req.method,
path: req.path,
body: req.body,
user: req.user?.id,
},
});
// Operational errors - safe to expose
if (err.isOperational) {
return res.status(err.statusCode).json({
error: {
code: err.code,
message: err.message,
...(err.errors && { errors: err.errors }),
},
});
}
// Programming errors - hide details
res.status(500).json({
error: {
code: "INTERNAL_ERROR",
message: "Something went wrong",
},
});
}
// Catch async errors
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// Usage
app.get(
"/users/:id",
asyncHandler(async (req, res) => {
const user = await userService.findById(req.params.id);
if (!user) {
throw new NotFoundError("User", req.params.id);
}
res.json(user);
}),
);
app.use(errorHandler);Unhandled Errors
JAVASCRIPT
// Catch unhandled promise rejections
process.on("unhandledRejection", (reason, promise) => {
logger.error("Unhandled Rejection:", reason);
// Optionally exit to restart clean
// process.exit(1);
});
// Catch uncaught exceptions
process.on("uncaughtException", (error) => {
logger.error("Uncaught Exception:", error);
// Must exit - process is in undefined state
process.exit(1);
});Testing Patterns
Unit Testing with Mocks
JAVASCRIPT
// user-service.test.js
const UserService = require("./user-service");
describe("UserService", () => {
let userService;
let mockDb;
let mockEmailService;
beforeEach(() => {
mockDb = {
find: jest.fn(),
insert: jest.fn(),
update: jest.fn(),
};
mockEmailService = {
send: jest.fn().mockResolvedValue(true),
};
userService = new UserService({
db: mockDb,
emailService: mockEmailService,
});
});
describe("createUser", () => {
it("should create user and send welcome email", async () => {
const userData = { email: "test@example.com", name: "Test" };
const createdUser = { id: "123", ...userData };
mockDb.insert.mockResolvedValue(createdUser);
const result = await userService.createUser(userData);
expect(mockDb.insert).toHaveBeenCalledWith("users", userData);
expect(mockEmailService.send).toHaveBeenCalledWith({
to: userData.email,
subject: "Welcome!",
template: "welcome",
});
expect(result).toEqual(createdUser);
});
it("should rollback on email failure", async () => {
mockDb.insert.mockResolvedValue({ id: "123" });
mockEmailService.send.mockRejectedValue(new Error("SMTP error"));
await expect(
userService.createUser({ email: "test@example.com" }),
).rejects.toThrow("SMTP error");
expect(mockDb.update).toHaveBeenCalledWith(
"users",
{ id: "123" },
{ status: "pending_email" },
);
});
});
});Integration Testing
JAVASCRIPT
// tests/integration/api.test.js
const request = require("supertest");
const app = require("../../src/app");
const db = require("../../src/db");
describe("User API", () => {
beforeEach(async () => {
await db.migrate.latest();
await db.seed.run();
});
afterEach(async () => {
await db.migrate.rollback();
});
describe("GET /api/users/:id", () => {
it("should return user by id", async () => {
const response = await request(app).get("/api/users/1").expect(200);
expect(response.body).toMatchObject({
id: 1,
email: expect.any(String),
name: expect.any(String),
});
});
it("should return 404 for non-existent user", async () => {
const response = await request(app).get("/api/users/99999").expect(404);
expect(response.body.error.code).toBe("NOT_FOUND");
});
});
describe("POST /api/users", () => {
it("should create new user", async () => {
const userData = {
email: "new@example.com",
name: "New User",
password: "secure123",
};
const response = await request(app)
.post("/api/users")
.send(userData)
.expect(201);
expect(response.body.id).toBeDefined();
expect(response.body.email).toBe(userData.email);
expect(response.body.password).toBeUndefined(); // Not exposed
});
});
});Testing Async Flows
JAVASCRIPT
// Testing event-driven code
describe("OrderProcessor", () => {
it("should emit events during processing", async () => {
const processor = new OrderProcessor();
const events = [];
processor.on("step", (step) => events.push(step));
await processor.process({ orderId: "123" });
expect(events).toEqual([
"validating",
"charging",
"fulfilling",
"completed",
]);
});
it("should handle timeouts", async () => {
jest.useFakeTimers();
const processor = new OrderProcessor({ timeout: 5000 });
const promise = processor.processWithTimeout({ orderId: "123" });
jest.advanceTimersByTime(6000);
await expect(promise).rejects.toThrow("Timeout");
jest.useRealTimers();
});
});
// Testing streams
describe("FileProcessor", () => {
it("should process file stream", async () => {
const input = Readable.from(["line1\n", "line2\n", "line3\n"]);
const output = [];
const processor = new FileProcessor();
await new Promise((resolve, reject) => {
input
.pipe(processor)
.on("data", (line) => output.push(line.toString()))
.on("end", resolve)
.on("error", reject);
});
expect(output).toHaveLength(3);
});
});Security Patterns
Input Validation
JAVASCRIPT
const Joi = require("joi");
const userSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).max(100).required(),
name: Joi.string().max(100).required(),
age: Joi.number().integer().min(18).max(150),
});
function validate(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true, // Remove unknown fields
});
if (error) {
throw new ValidationError(
"Validation failed",
error.details.map((d) => ({
field: d.path.join("."),
message: d.message,
})),
);
}
req.body = value; // Use sanitized data
next();
};
}
app.post("/api/users", validate(userSchema), createUser);Rate Limiting
JAVASCRIPT
const rateLimit = require("express-rate-limit");
const RedisStore = require("rate-limit-redis");
const Redis = require("ioredis");
// Basic rate limiting
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: { error: { code: "RATE_LIMITED", message: "Too many requests" } },
});
// Strict limiting for auth endpoints
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 attempts
skipSuccessfulRequests: true,
store: new RedisStore({
sendCommand: (...args) => redisClient.call(...args),
}),
});
app.use("/api/", generalLimiter);
app.use("/api/auth/login", authLimiter);SQL Injection Prevention
JAVASCRIPT
// NEVER do this
const query = `SELECT * FROM users WHERE email = '${userInput}'`;
// Always use parameterized queries
const query = "SELECT * FROM users WHERE email = $1";
const result = await db.query(query, [userInput]);
// Or use an ORM with proper escaping
const user = await User.findOne({ where: { email: userInput } });XSS Prevention
JAVASCRIPT
const helmet = require("helmet");
const sanitizeHtml = require("sanitize-html");
// Security headers
app.use(helmet());
// Sanitize user-generated HTML
function sanitizeInput(html) {
return sanitizeHtml(html, {
allowedTags: ["b", "i", "em", "strong", "a", "p", "br"],
allowedAttributes: {
a: ["href"],
},
allowedSchemes: ["http", "https"],
});
}
// Content Security Policy
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
},
}),
);Authentication Best Practices
JAVASCRIPT
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
// Password hashing
async function hashPassword(password) {
const saltRounds = 12;
return bcrypt.hash(password, saltRounds);
}
async function verifyPassword(password, hash) {
return bcrypt.compare(password, hash);
}
// JWT with refresh tokens
function generateTokens(user) {
const accessToken = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: "15m" },
);
const refreshToken = jwt.sign(
{ userId: user.id, tokenVersion: user.tokenVersion },
process.env.REFRESH_SECRET,
{ expiresIn: "7d" },
);
return { accessToken, refreshToken };
}
// Token refresh
async function refreshAccessToken(refreshToken) {
const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
const user = await User.findById(payload.userId);
// Check token version to allow invalidation
if (user.tokenVersion !== payload.tokenVersion) {
throw new AuthenticationError("Token revoked");
}
return generateTokens(user);
}Performance Patterns
Caching
JAVASCRIPT
const Redis = require("ioredis");
class CacheService {
constructor(redis) {
this.redis = redis;
this.defaultTTL = 3600; // 1 hour
}
async get(key) {
const data = await this.redis.get(key);
return data ? JSON.parse(data) : null;
}
async set(key, value, ttl = this.defaultTTL) {
await this.redis.setex(key, ttl, JSON.stringify(value));
}
async getOrSet(key, fetchFn, ttl) {
let value = await this.get(key);
if (value === null) {
value = await fetchFn();
await this.set(key, value, ttl);
}
return value;
}
async invalidate(pattern) {
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.del(...keys);
}
}
}
// Usage with memoization pattern
class UserService {
async getUser(id) {
return cache.getOrSet(`user:${id}`, () => db.users.findById(id), 3600);
}
async updateUser(id, data) {
const user = await db.users.update(id, data);
await cache.invalidate(`user:${id}*`);
return user;
}
}Connection Pooling
JAVASCRIPT
const { Pool } = require("pg");
const pool = new Pool({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
max: 20, // Max connections
idleTimeoutMillis: 30000, // Close idle connections after 30s
connectionTimeoutMillis: 5000, // Timeout for getting connection
});
// Monitor pool
pool.on("connect", () => console.log("New connection"));
pool.on("remove", () => console.log("Connection removed"));
pool.on("error", (err) => console.error("Pool error:", err));
// Query wrapper
async function query(sql, params) {
const client = await pool.connect();
try {
return await client.query(sql, params);
} finally {
client.release();
}
}
// Graceful shutdown
process.on("SIGTERM", async () => {
await pool.end();
process.exit(0);
});Request Coalescing
Avoid duplicate concurrent requests:
JAVASCRIPT
class RequestCoalescer {
constructor() {
this.pending = new Map();
}
async request(key, fetchFn) {
// If request is in flight, wait for it
if (this.pending.has(key)) {
return this.pending.get(key);
}
// Start new request
const promise = fetchFn().finally(() => this.pending.delete(key));
this.pending.set(key, promise);
return promise;
}
}
const coalescer = new RequestCoalescer();
// Multiple simultaneous requests for same data = 1 actual request
await Promise.all([
coalescer.request("user:123", () => fetchUser(123)),
coalescer.request("user:123", () => fetchUser(123)),
coalescer.request("user:123", () => fetchUser(123)),
]);Logging and Monitoring
Structured Logging
JAVASCRIPT
const pino = require("pino");
const logger = pino({
level: process.env.LOG_LEVEL || "info",
formatters: {
level: (label) => ({ level: label }),
},
base: {
service: "api-server",
environment: process.env.NODE_ENV,
},
});
// Request logging middleware
function requestLogger(req, res, next) {
const start = Date.now();
const requestId = req.headers["x-request-id"] || crypto.randomUUID();
req.log = logger.child({ requestId });
res.setHeader("X-Request-ID", requestId);
res.on("finish", () => {
req.log.info({
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: Date.now() - start,
userAgent: req.headers["user-agent"],
});
});
next();
}Health Checks
JAVASCRIPT
app.get("/health", async (req, res) => {
const health = {
status: "healthy",
timestamp: new Date().toISOString(),
uptime: process.uptime(),
checks: {},
};
// Database check
try {
await db.query("SELECT 1");
health.checks.database = "ok";
} catch (err) {
health.status = "unhealthy";
health.checks.database = "failed";
}
// Redis check
try {
await redis.ping();
health.checks.redis = "ok";
} catch (err) {
health.status = "unhealthy";
health.checks.redis = "failed";
}
// Memory check
const memUsage = process.memoryUsage();
health.checks.memory = {
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024) + "MB",
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024) + "MB",
};
res.status(health.status === "healthy" ? 200 : 503).json(health);
});
// Kubernetes readiness
app.get("/ready", (req, res) => {
if (isReady) {
res.status(200).send("Ready");
} else {
res.status(503).send("Not ready");
}
});
// Kubernetes liveness
app.get("/live", (req, res) => {
res.status(200).send("Alive");
});Configuration Management
JAVASCRIPT
// config.js
const convict = require("convict");
const config = convict({
env: {
doc: "Application environment",
format: ["production", "development", "test"],
default: "development",
env: "NODE_ENV",
},
port: {
doc: "Server port",
format: "port",
default: 3000,
env: "PORT",
},
db: {
host: {
doc: "Database host",
format: String,
default: "localhost",
env: "DB_HOST",
},
port: {
doc: "Database port",
format: "port",
default: 5432,
env: "DB_PORT",
},
name: {
doc: "Database name",
format: String,
default: "app_development",
env: "DB_NAME",
},
},
jwt: {
secret: {
doc: "JWT secret",
format: String,
default: "",
env: "JWT_SECRET",
sensitive: true,
},
expiresIn: {
doc: "JWT expiration",
format: String,
default: "1h",
env: "JWT_EXPIRES_IN",
},
},
});
// Validate configuration
config.validate({ allowed: "strict" });
module.exports = config;Summary
Production patterns for Node.js:
| Area | Patterns |
|---|---|
| Error Handling | Custom errors, centralized handler, graceful degradation |
| Testing | Unit mocks, integration tests, async testing |
| Security | Input validation, rate limiting, auth best practices |
| Performance | Caching, pooling, request coalescing |
| Operations | Structured logging, health checks, configuration |
Key takeaways:
- Distinguish operational from programming errors
- Test at multiple levels (unit, integration, e2e)
- Security is not optional—validate, sanitize, limit
- Cache strategically, invalidate correctly
- Log structured data for searchability
- Health checks enable reliable deployments
Note
These patterns aren't just "nice to have"—they're essential for production systems. Start with error handling and logging, then add security, testing, and performance optimization as your application grows.