Node.js Backend Patterns
Comprehensive guidance for building scalable, maintainable, and production-ready Node.js backend applications with modern frameworks, architectural patterns, and best practices.
When to Use This Skill
-
Building REST APIs or GraphQL servers
-
Creating microservices with Node.js
-
Implementing authentication and authorization
-
Designing scalable backend architectures
-
Setting up middleware and error handling
-
Integrating databases (SQL and NoSQL)
-
Building real-time applications with WebSockets
-
Implementing background job processing
Core Frameworks
Express.js - Minimalist Framework
Basic Setup:
import express, { Request, Response, NextFunction } from "express"; import helmet from "helmet"; import cors from "cors"; import compression from "compression";
const app = express();
// Security middleware app.use(helmet()); app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(",") })); app.use(compression());
// Body parsing app.use(express.json({ limit: "10mb" })); app.use(express.urlencoded({ extended: true, limit: "10mb" }));
// Request logging
app.use((req: Request, res: Response, next: NextFunction) => {
console.log(${req.method} ${req.path});
next();
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(Server running on port ${PORT});
});
Fastify - High Performance Framework
Basic Setup:
import Fastify from "fastify"; import helmet from "@fastify/helmet"; import cors from "@fastify/cors"; import compress from "@fastify/compress";
const fastify = Fastify({ logger: { level: process.env.LOG_LEVEL || "info", transport: { target: "pino-pretty", options: { colorize: true }, }, }, });
// Plugins await fastify.register(helmet); await fastify.register(cors, { origin: true }); await fastify.register(compress);
// Type-safe routes with schema validation fastify.post<{ Body: { name: string; email: string }; Reply: { id: string; name: string }; }>( "/users", { schema: { body: { type: "object", required: ["name", "email"], properties: { name: { type: "string", minLength: 1 }, email: { type: "string", format: "email" }, }, }, }, }, async (request, reply) => { const { name, email } = request.body; return { id: "123", name }; }, );
await fastify.listen({ port: 3000, host: "0.0.0.0" });
Architectural Patterns
Pattern 1: Layered Architecture
Structure:
src/ ├── controllers/ # Handle HTTP requests/responses ├── services/ # Business logic ├── repositories/ # Data access layer ├── models/ # Data models ├── middleware/ # Express/Fastify middleware ├── routes/ # Route definitions ├── utils/ # Helper functions ├── config/ # Configuration └── types/ # TypeScript types
Controller Layer:
// controllers/user.controller.ts import { Request, Response, NextFunction } from "express"; import { UserService } from "../services/user.service"; import { CreateUserDTO, UpdateUserDTO } from "../types/user.types";
export class UserController { constructor(private userService: UserService) {}
async createUser(req: Request, res: Response, next: NextFunction) { try { const userData: CreateUserDTO = req.body; const user = await this.userService.createUser(userData); res.status(201).json(user); } catch (error) { next(error); } }
async getUser(req: Request, res: Response, next: NextFunction) { try { const { id } = req.params; const user = await this.userService.getUserById(id); res.json(user); } catch (error) { next(error); } }
async updateUser(req: Request, res: Response, next: NextFunction) { try { const { id } = req.params; const updates: UpdateUserDTO = req.body; const user = await this.userService.updateUser(id, updates); res.json(user); } catch (error) { next(error); } }
async deleteUser(req: Request, res: Response, next: NextFunction) { try { const { id } = req.params; await this.userService.deleteUser(id); res.status(204).send(); } catch (error) { next(error); } } }
Service Layer:
// services/user.service.ts import { UserRepository } from "../repositories/user.repository"; import { CreateUserDTO, UpdateUserDTO, User } from "../types/user.types"; import { NotFoundError, ValidationError } from "../utils/errors"; import bcrypt from "bcrypt";
export class UserService { constructor(private userRepository: UserRepository) {}
async createUser(userData: CreateUserDTO): Promise<User> { // Validation const existingUser = await this.userRepository.findByEmail(userData.email); if (existingUser) { throw new ValidationError("Email already exists"); }
// Hash password
const hashedPassword = await bcrypt.hash(userData.password, 10);
// Create user
const user = await this.userRepository.create({
...userData,
password: hashedPassword,
});
// Remove password from response
const { password, ...userWithoutPassword } = user;
return userWithoutPassword as User;
}
async getUserById(id: string): Promise<User> { const user = await this.userRepository.findById(id); if (!user) { throw new NotFoundError("User not found"); } const { password, ...userWithoutPassword } = user; return userWithoutPassword as User; }
async updateUser(id: string, updates: UpdateUserDTO): Promise<User> { const user = await this.userRepository.update(id, updates); if (!user) { throw new NotFoundError("User not found"); } const { password, ...userWithoutPassword } = user; return userWithoutPassword as User; }
async deleteUser(id: string): Promise<void> { const deleted = await this.userRepository.delete(id); if (!deleted) { throw new NotFoundError("User not found"); } } }
Repository Layer:
// repositories/user.repository.ts import { Pool } from "pg"; import { CreateUserDTO, UpdateUserDTO, UserEntity } from "../types/user.types";
export class UserRepository { constructor(private db: Pool) {}
async create(
userData: CreateUserDTO & { password: string },
): Promise<UserEntity> {
const query = INSERT INTO users (name, email, password) VALUES ($1, $2, $3) RETURNING id, name, email, password, created_at, updated_at ;
const { rows } = await this.db.query(query, [
userData.name,
userData.email,
userData.password,
]);
return rows[0];
}
async findById(id: string): Promise<UserEntity | null> { const query = "SELECT * FROM users WHERE id = $1"; const { rows } = await this.db.query(query, [id]); return rows[0] || null; }
async findByEmail(email: string): Promise<UserEntity | null> { const query = "SELECT * FROM users WHERE email = $1"; const { rows } = await this.db.query(query, [email]); return rows[0] || null; }
async update(id: string, updates: UpdateUserDTO): Promise<UserEntity | null> { const fields = Object.keys(updates); const values = Object.values(updates);
const setClause = fields
.map((field, idx) => `${field} = $${idx + 2}`)
.join(", ");
const query = `
UPDATE users
SET ${setClause}, updated_at = CURRENT_TIMESTAMP
WHERE id = $1
RETURNING *
`;
const { rows } = await this.db.query(query, [id, ...values]);
return rows[0] || null;
}
async delete(id: string): Promise<boolean> { const query = "DELETE FROM users WHERE id = $1"; const { rowCount } = await this.db.query(query, [id]); return rowCount > 0; } }
Pattern 2: Dependency Injection
DI Container:
// di-container.ts import { Pool } from "pg"; import { UserRepository } from "./repositories/user.repository"; import { UserService } from "./services/user.service"; import { UserController } from "./controllers/user.controller"; import { AuthService } from "./services/auth.service";
class Container { private instances = new Map<string, any>();
register<T>(key: string, factory: () => T): void { this.instances.set(key, factory); }
resolve<T>(key: string): T {
const factory = this.instances.get(key);
if (!factory) {
throw new Error(No factory registered for ${key});
}
return factory();
}
singleton<T>(key: string, factory: () => T): void { let instance: T; this.instances.set(key, () => { if (!instance) { instance = factory(); } return instance; }); } }
export const container = new Container();
// Register dependencies container.singleton( "db", () => new Pool({ host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT || "5432"), database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, max: 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, }), );
container.singleton( "userRepository", () => new UserRepository(container.resolve("db")), );
container.singleton( "userService", () => new UserService(container.resolve("userRepository")), );
container.register( "userController", () => new UserController(container.resolve("userService")), );
container.singleton( "authService", () => new AuthService(container.resolve("userRepository")), );
Middleware Patterns
Authentication Middleware
// middleware/auth.middleware.ts import { Request, Response, NextFunction } from "express"; import jwt from "jsonwebtoken"; import { UnauthorizedError } from "../utils/errors";
interface JWTPayload { userId: string; email: string; }
declare global { namespace Express { interface Request { user?: JWTPayload; } } }
export const authenticate = async ( req: Request, res: Response, next: NextFunction, ) => { try { const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) {
throw new UnauthorizedError("No token provided");
}
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
req.user = payload;
next();
} catch (error) { next(new UnauthorizedError("Invalid token")); } };
export const authorize = (...roles: string[]) => { return async (req: Request, res: Response, next: NextFunction) => { if (!req.user) { return next(new UnauthorizedError("Not authenticated")); }
// Check if user has required role
const hasRole = roles.some((role) => req.user?.roles?.includes(role));
if (!hasRole) {
return next(new UnauthorizedError("Insufficient permissions"));
}
next();
}; };
Validation Middleware
// middleware/validation.middleware.ts import { Request, Response, NextFunction } from "express"; import { AnyZodObject, ZodError } from "zod"; import { ValidationError } from "../utils/errors";
export const validate = (schema: AnyZodObject) => { return async (req: Request, res: Response, next: NextFunction) => { try { await schema.parseAsync({ body: req.body, query: req.query, params: req.params, }); next(); } catch (error) { if (error instanceof ZodError) { const errors = error.errors.map((err) => ({ field: err.path.join("."), message: err.message, })); next(new ValidationError("Validation failed", errors)); } else { next(error); } } }; };
// Usage with Zod import { z } from "zod";
const createUserSchema = z.object({ body: z.object({ name: z.string().min(1), email: z.string().email(), password: z.string().min(8), }), });
router.post("/users", validate(createUserSchema), userController.createUser);
Rate Limiting Middleware
// middleware/rate-limit.middleware.ts import rateLimit from "express-rate-limit"; import RedisStore from "rate-limit-redis"; import Redis from "ioredis";
const redis = new Redis({ host: process.env.REDIS_HOST, port: parseInt(process.env.REDIS_PORT || "6379"), });
export const apiLimiter = rateLimit({ store: new RedisStore({ client: redis, prefix: "rl:", }), windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: "Too many requests from this IP, please try again later", standardHeaders: true, legacyHeaders: false, });
export const authLimiter = rateLimit({ store: new RedisStore({ client: redis, prefix: "rl:auth:", }), windowMs: 15 * 60 * 1000, max: 5, // Stricter limit for auth endpoints skipSuccessfulRequests: true, });
Request Logging Middleware
// middleware/logger.middleware.ts import { Request, Response, NextFunction } from "express"; import pino from "pino";
const logger = pino({ level: process.env.LOG_LEVEL || "info", transport: { target: "pino-pretty", options: { colorize: true }, }, });
export const requestLogger = ( req: Request, res: Response, next: NextFunction, ) => { const start = Date.now();
// Log response when finished
res.on("finish", () => {
const duration = Date.now() - start;
logger.info({
method: req.method,
url: req.url,
status: res.statusCode,
duration: ${duration}ms,
userAgent: req.headers["user-agent"],
ip: req.ip,
});
});
next(); };
export { logger };
Error Handling
Custom Error Classes
// utils/errors.ts export class AppError extends Error { constructor( public message: string, public statusCode: number = 500, public isOperational: boolean = true, ) { super(message); Object.setPrototypeOf(this, AppError.prototype); Error.captureStackTrace(this, this.constructor); } }
export class ValidationError extends AppError { constructor( message: string, public errors?: any[], ) { super(message, 400); } }
export class NotFoundError extends AppError { constructor(message: string = "Resource not found") { super(message, 404); } }
export class UnauthorizedError extends AppError { constructor(message: string = "Unauthorized") { super(message, 401); } }
export class ForbiddenError extends AppError { constructor(message: string = "Forbidden") { super(message, 403); } }
export class ConflictError extends AppError { constructor(message: string) { super(message, 409); } }
Global Error Handler
// middleware/error-handler.ts import { Request, Response, NextFunction } from "express"; import { AppError } from "../utils/errors"; import { logger } from "./logger.middleware";
export const errorHandler = ( err: Error, req: Request, res: Response, next: NextFunction, ) => { if (err instanceof AppError) { return res.status(err.statusCode).json({ status: "error", message: err.message, ...(err instanceof ValidationError && { errors: err.errors }), }); }
// Log unexpected errors logger.error({ error: err.message, stack: err.stack, url: req.url, method: req.method, });
// Don't leak error details in production const message = process.env.NODE_ENV === "production" ? "Internal server error" : err.message;
res.status(500).json({ status: "error", message, }); };
// Async error wrapper export const asyncHandler = ( fn: (req: Request, res: Response, next: NextFunction) => Promise<any>, ) => { return (req: Request, res: Response, next: NextFunction) => { Promise.resolve(fn(req, res, next)).catch(next); }; };
Database Patterns
PostgreSQL with Connection Pool
// config/database.ts import { Pool, PoolConfig } from "pg";
const poolConfig: PoolConfig = { host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT || "5432"), database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, max: 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, };
export const pool = new Pool(poolConfig);
// Test connection pool.on("connect", () => { console.log("Database connected"); });
pool.on("error", (err) => { console.error("Unexpected database error", err); process.exit(-1); });
// Graceful shutdown export const closeDatabase = async () => { await pool.end(); console.log("Database connection closed"); };
MongoDB with Mongoose
// config/mongoose.ts import mongoose from "mongoose";
const connectDB = async () => { try { await mongoose.connect(process.env.MONGODB_URI!, { maxPoolSize: 10, serverSelectionTimeoutMS: 5000, socketTimeoutMS: 45000, });
console.log("MongoDB connected");
} catch (error) { console.error("MongoDB connection error:", error); process.exit(1); } };
mongoose.connection.on("disconnected", () => { console.log("MongoDB disconnected"); });
mongoose.connection.on("error", (err) => { console.error("MongoDB error:", err); });
export { connectDB };
// Model example import { Schema, model, Document } from "mongoose";
interface IUser extends Document { name: string; email: string; password: string; createdAt: Date; updatedAt: Date; }
const userSchema = new Schema<IUser>( { name: { type: String, required: true }, email: { type: String, required: true, unique: true }, password: { type: String, required: true }, }, { timestamps: true, }, );
// Indexes userSchema.index({ email: 1 });
export const User = model<IUser>("User", userSchema);
Transaction Pattern
// services/order.service.ts import { Pool } from "pg";
export class OrderService { constructor(private db: Pool) {}
async createOrder(userId: string, items: any[]) { const client = await this.db.connect();
try {
await client.query("BEGIN");
// Create order
const orderResult = await client.query(
"INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id",
[userId, calculateTotal(items)],
);
const orderId = orderResult.rows[0].id;
// Create order items
for (const item of items) {
await client.query(
"INSERT INTO order_items (order_id, product_id, quantity, price) VALUES ($1, $2, $3, $4)",
[orderId, item.productId, item.quantity, item.price],
);
// Update inventory
await client.query(
"UPDATE products SET stock = stock - $1 WHERE id = $2",
[item.quantity, item.productId],
);
}
await client.query("COMMIT");
return orderId;
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
} }
Authentication & Authorization
JWT Authentication
// services/auth.service.ts import jwt from "jsonwebtoken"; import bcrypt from "bcrypt"; import { UserRepository } from "../repositories/user.repository"; import { UnauthorizedError } from "../utils/errors";
export class AuthService { constructor(private userRepository: UserRepository) {}
async login(email: string, password: string) { const user = await this.userRepository.findByEmail(email);
if (!user) {
throw new UnauthorizedError("Invalid credentials");
}
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
throw new UnauthorizedError("Invalid credentials");
}
const token = this.generateToken({
userId: user.id,
email: user.email,
});
const refreshToken = this.generateRefreshToken({
userId: user.id,
});
return {
token,
refreshToken,
user: {
id: user.id,
name: user.name,
email: user.email,
},
};
}
async refreshToken(refreshToken: string) { try { const payload = jwt.verify( refreshToken, process.env.REFRESH_TOKEN_SECRET!, ) as { userId: string };
const user = await this.userRepository.findById(payload.userId);
if (!user) {
throw new UnauthorizedError("User not found");
}
const token = this.generateToken({
userId: user.id,
email: user.email,
});
return { token };
} catch (error) {
throw new UnauthorizedError("Invalid refresh token");
}
}
private generateToken(payload: any): string { return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: "15m", }); }
private generateRefreshToken(payload: any): string { return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET!, { expiresIn: "7d", }); } }
Caching Strategies
// utils/cache.ts import Redis from "ioredis";
const redis = new Redis({ host: process.env.REDIS_HOST, port: parseInt(process.env.REDIS_PORT || "6379"), retryStrategy: (times) => { const delay = Math.min(times * 50, 2000); return delay; }, });
export class CacheService { async get<T>(key: string): Promise<T | null> { const data = await redis.get(key); return data ? JSON.parse(data) : null; }
async set(key: string, value: any, ttl?: number): Promise<void> { const serialized = JSON.stringify(value); if (ttl) { await redis.setex(key, ttl, serialized); } else { await redis.set(key, serialized); } }
async delete(key: string): Promise<void> { await redis.del(key); }
async invalidatePattern(pattern: string): Promise<void> { const keys = await redis.keys(pattern); if (keys.length > 0) { await redis.del(...keys); } } }
// Cache decorator export function Cacheable(ttl: number = 300) { return function ( target: any, propertyKey: string, descriptor: PropertyDescriptor, ) { const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const cache = new CacheService();
const cacheKey = `${propertyKey}:${JSON.stringify(args)}`;
const cached = await cache.get(cacheKey);
if (cached) {
return cached;
}
const result = await originalMethod.apply(this, args);
await cache.set(cacheKey, result, ttl);
return result;
};
return descriptor;
}; }
API Response Format
// utils/response.ts import { Response } from "express";
export class ApiResponse { static success<T>( res: Response, data: T, message?: string, statusCode = 200, ) { return res.status(statusCode).json({ status: "success", message, data, }); }
static error(res: Response, message: string, statusCode = 500, errors?: any) { return res.status(statusCode).json({ status: "error", message, ...(errors && { errors }), }); }
static paginated<T>( res: Response, data: T[], page: number, limit: number, total: number, ) { return res.json({ status: "success", data, pagination: { page, limit, total, pages: Math.ceil(total / limit), }, }); } }
Best Practices
-
Use TypeScript: Type safety prevents runtime errors
-
Implement proper error handling: Use custom error classes
-
Validate input: Use libraries like Zod or Joi
-
Use environment variables: Never hardcode secrets
-
Implement logging: Use structured logging (Pino, Winston)
-
Add rate limiting: Prevent abuse
-
Use HTTPS: Always in production
-
Implement CORS properly: Don't use * in production
-
Use dependency injection: Easier testing and maintenance
-
Write tests: Unit, integration, and E2E tests
-
Handle graceful shutdown: Clean up resources
-
Use connection pooling: For databases
-
Implement health checks: For monitoring
-
Use compression: Reduce response size
-
Monitor performance: Use APM tools
Testing Patterns
See javascript-testing-patterns skill for comprehensive testing guidance.
Resources
-
Node.js Best Practices: https://github.com/goldbergyoni/nodebestpractices
-
Express.js Guide: https://expressjs.com/en/guide/
-
Fastify Documentation: https://www.fastify.io/docs/
-
TypeScript Node Starter: https://github.com/microsoft/TypeScript-Node-Starter