Agents nodejs-backend-patterns
Build production-ready Node.js backend services with Express/Fastify, implementing middleware patterns, error handling, authentication, database integration, and API design best practices. Use when creating Node.js servers, REST APIs, GraphQL backends, or microservices architectures.
git clone https://github.com/wshobson/agents
T=$(mktemp -d) && git clone --depth=1 https://github.com/wshobson/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/javascript-typescript/skills/nodejs-backend-patterns" ~/.claude/skills/wshobson-agents-nodejs-backend-patterns && rm -rf "$T"
plugins/javascript-typescript/skills/nodejs-backend-patterns/SKILL.mdNode.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
Use a DI container to wire up repositories, services, and controllers. For a full container implementation, see references/advanced-patterns.md.
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
Node.js supports both SQL and NoSQL databases. Use connection pooling for all production databases.
Key patterns covered in references/advanced-patterns.md:
- PostgreSQL with connection pool —
Pool configuration and graceful shutdownpg - MongoDB with Mongoose — connection management and schema definition
- Transaction pattern —
/BEGIN
/COMMIT
withROLLBACK
clientpg
Authentication & Authorization
JWT-based auth with access tokens (short-lived, 15m) and refresh tokens (7d). Full
AuthService implementation with bcrypt password comparison in references/advanced-patterns.md.
Caching Strategies
Redis-backed
CacheService with get/set/delete/invalidatePattern, plus a @Cacheable decorator for method-level caching. See references/advanced-patterns.md.
API Response Format
Standardized
ApiResponse helper with success, error, and paginated static methods. See references/advanced-patterns.md.
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.