---
name: nodejs-backend-patterns
description: 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.
---

# 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:**

```typescript
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:**

```typescript
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:**

```typescript
// 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:**

```typescript
// 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:**

```typescript
// 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](references/advanced-patterns.md).

## Middleware Patterns

### Authentication Middleware

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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](references/advanced-patterns.md):

- **PostgreSQL with connection pool** — `pg` Pool configuration and graceful shutdown
- **MongoDB with Mongoose** — connection management and schema definition
- **Transaction pattern** — `BEGIN`/`COMMIT`/`ROLLBACK` with `pg` client

## 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](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](references/advanced-patterns.md).

## API Response Format

Standardized `ApiResponse` helper with `success`, `error`, and `paginated` static methods. See [references/advanced-patterns.md](references/advanced-patterns.md).

## Best Practices

1. **Use TypeScript**: Type safety prevents runtime errors
2. **Implement proper error handling**: Use custom error classes
3. **Validate input**: Use libraries like Zod or Joi
4. **Use environment variables**: Never hardcode secrets
5. **Implement logging**: Use structured logging (Pino, Winston)
6. **Add rate limiting**: Prevent abuse
7. **Use HTTPS**: Always in production
8. **Implement CORS properly**: Don't use `*` in production
9. **Use dependency injection**: Easier testing and maintenance
10. **Write tests**: Unit, integration, and E2E tests
11. **Handle graceful shutdown**: Clean up resources
12. **Use connection pooling**: For databases
13. **Implement health checks**: For monitoring
14. **Use compression**: Reduce response size
15. **Monitor performance**: Use APM tools

## Testing Patterns

See `javascript-testing-patterns` skill for comprehensive testing guidance.
