nodejs-backend-patterns
upstream

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.

ID: nodejs-backend-patterns
Preview
Codigo

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


DI Container:

`typescript

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


`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


PostgreSQL with Connection Pool


`typescript

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


`typescript

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


`typescript

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


`typescript

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


`typescript

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


`typescript

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


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.


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

Informacion

Estado
Activo
Origen upstream
Usos 3
Ultimo uso 23/01 16:57
Actualizado 09/01/2026 02:14

Archivo origen

Z:\Repositorios\core\upstream\plugins\javascript-typescript\skills\nodejs-backend-patterns\SKILL.md