Learning Guides
Menu

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:

AreaPatterns
Error HandlingCustom errors, centralized handler, graceful degradation
TestingUnit mocks, integration tests, async testing
SecurityInput validation, rate limiting, auth best practices
PerformanceCaching, pooling, request coalescing
OperationsStructured logging, health checks, configuration

Key takeaways:

  1. Distinguish operational from programming errors
  2. Test at multiple levels (unit, integration, e2e)
  3. Security is not optional—validate, sanitize, limit
  4. Cache strategically, invalidate correctly
  5. Log structured data for searchability
  6. 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.