Imported from Omnipro-Solutions/loyalty (
.agents/skills/nodejs-backend-patterns/SKILL.md). Install upstream withnpx skills add Omnipro-Solutions/loyalty --skill nodejs-backend-patterns. Copyright stays with the author.
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
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 —
pgPool configuration and graceful shutdown - MongoDB with Mongoose — connection management and schema definition
- Transaction pattern —
BEGIN/COMMIT/ROLLBACKwithpgclient
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.