Estructura de un Proyecto Backend en Node.js

Esta documentación describe la estructura estándar para proyectos backend en Node.js, basada en las mejores prácticas de la industria y patrones escalables para aplicaciones de producción.

Estructura del Proyecto

src/
├── app.ts                    # Punto de entrada principal de la aplicación
├── api/                      # Módulo principal de la API
│   ├── index.ts             # Configuración principal de rutas
│   ├── controllers/         # Controladores de la aplicación
│   │   ├── auth.controller.ts
│   │   ├── user.controller.ts
│   │   ├── product.controller.ts
│   │   └── order.controller.ts
│   ├── routes/              # Definición de rutas
│   │   ├── auth.routes.ts
│   │   ├── user.routes.ts
│   │   ├── product.routes.ts
│   │   └── order.routes.ts
│   ├── services/            # Lógica de negocio
│   │   ├── auth.service.ts
│   │   ├── user.service.ts
│   │   ├── product.service.ts
│   │   └── order.service.ts
│   ├── integrations/        # Servicios de integración externa
│   │   ├── payment.ts
│   │   ├── email.ts
│   │   ├── sms.ts
│   │   └── storage.ts
│   ├── utils/               # Utilidades y helpers
│   │   ├── http/            # Configuraciones HTTP
│   │   ├── formatters/      # Formateadores de datos
│   │   ├── queues/          # Manejo de colas
│   │   ├── logger/          # Sistema de logging
│   │   └── validators/      # Validadores personalizados
│   ├── validations/         # Validaciones de entrada
│   └── swagger/             # Documentación de API
├── config/                  # Configuraciones
│   ├── config.ts           # Configuración de Express
│   ├── cors.ts             # Configuración de CORS
│   ├── db.ts               # Configuración de base de datos
│   └── redis.ts            # Configuración de Redis
├── middleware/             # Middlewares personalizados
│   ├── auth.ts             # Autenticación y autorización
│   ├── validation.ts       # Validaciones de entrada
│   ├── sanitize.ts         # Sanitización de datos
│   ├── rateLimit.ts        # Rate limiting
│   └── errorHandler.ts     # Manejo de errores
├── models/                 # Modelos de base de datos
│   ├── index.ts
│   ├── User.ts
│   ├── Product.ts
│   └── Order.ts
├── interfaces/             # Definiciones de tipos TypeScript
│   ├── user.interface.ts
│   ├── product.interface.ts
│   └── common.interface.ts
├── types/                  # Tipos globales
│   └── global.d.ts
├── constants/              # Constantes de la aplicación
│   └── index.ts
└── public/                 # Archivos estáticos
    ├── images/
    └── documents/

Componentes Principales

1. app.ts - Punto de Entrada Principal

El archivo app.ts contiene toda la configuración inicial del backend:

// Load environment variables from .env file
import "dotenv/config";
import express, { Request, Response } from "express";
import http from "http";
 
// Import local modules for configuration, middleware, and routes
import { config, cors, testDatabase } from "./config";
import api from "./api";
import swagger from "./swagger";
import { errorHandler } from "./middleware/errorHandler";
 
const app = express();
const PORT = process.env.PORT || 3000;
 
// Apply app configurations
config(app);
 
// Enable Cross-Origin Resource Sharing (CORS)
cors(app);
 
// Serve static files
app.use(express.static("src/public"));
 
// API documentation
app.use(swagger);
 
// Mount API routes
app.use("/api", api);
 
// Health check endpoint
app.get("/health", (req: Request, res: Response) => {
	res.status(200).json({
		status: "success",
		message: "Service is running",
		timestamp: new Date().toISOString(),
		uptime: process.uptime(),
	});
});
 
// Global error handler
app.use(errorHandler);
 
// Create HTTP server
const server = http.createServer(app);
 
// Start server
server.listen(PORT, async () => {
	console.log(`🚀 Server is running on port ${PORT}`);
	console.log(`📚 API Documentation: http://localhost:${PORT}/api-docs`);
	await testDatabase();
});
 
export default app;

2. Routes - Definición de Rutas

Las rutas contienen toda la lógica de enrutamiento y middleware:

import { Router } from "express";
import {
	getUsers,
	getUserById,
	createUser,
	updateUser,
	deleteUser,
} from "../controllers/user.controller";
import { authenticate, authorize } from "../../middleware/auth";
import { validateUser } from "../validations/user.validation";
import { sanitize } from "../../middleware/sanitize";
import { rateLimit } from "../../middleware/rateLimit";
 
const router = Router();
 
/**
 * @route   GET /api/users
 * @desc    Get all users
 * @access  Private (Admin only)
 */
router.get("/", authenticate, authorize(["admin"]), getUsers);
 
/**
 * @route   GET /api/users/:id
 * @desc    Get user by ID
 * @access  Private
 */
router.get("/:id", authenticate, getUserById);
 
/**
 * @route   POST /api/users
 * @desc    Create new user
 * @access  Public
 */
router.post(
	"/",
	rateLimit({ windowMs: 15 * 60 * 1000, max: 5 }), // 5 requests per 15 minutes
	validateUser,
	sanitize,
	createUser
);
 
/**
 * @route   PUT /api/users/:id
 * @desc    Update user
 * @access  Private
 */
router.put("/:id", authenticate, validateUser, sanitize, updateUser);
 
/**
 * @route   DELETE /api/users/:id
 * @desc    Delete user
 * @access  Private (Admin only)
 */
router.delete("/:id", authenticate, authorize(["admin"]), deleteUser);
 
export default router;

3. Controllers - Controladores

Los controladores manejan las peticiones HTTP y coordinan con los servicios:

import { NextFunction, Request, Response } from "express";
import userService from "../services/user.service";
import { formatUserResponse } from "../utils/formatters/user";
import { logger } from "../utils/logger";
 
/**
 * @desc    Get all users
 * @route   GET /api/users
 * @access  Private (Admin only)
 */
export const getUsers = async (
	req: Request,
	res: Response,
	next: NextFunction
) => {
	try {
		const { page = 1, limit = 10, search } = req.query;
 
		const users = await userService.getAllUsers({
			page: Number(page),
			limit: Number(limit),
			search: search as string,
		});
 
		res.status(200).json({
			status: "success",
			message: "Users retrieved successfully",
			data: users,
			pagination: {
				page: Number(page),
				limit: Number(limit),
				total: users.length,
			},
		});
	} catch (error) {
		logger.error("Error getting users:", error);
		next(error);
	}
};
 
/**
 * @desc    Get user by ID
 * @route   GET /api/users/:id
 * @access  Private
 */
export const getUserById = async (
	req: Request,
	res: Response,
	next: NextFunction
) => {
	try {
		const { id } = req.params;
		const user = await userService.getUserById(id);
 
		if (!user) {
			return res.status(404).json({
				status: "error",
				message: "User not found",
			});
		}
 
		const formattedUser = formatUserResponse(user);
 
		res.status(200).json({
			status: "success",
			message: "User retrieved successfully",
			data: formattedUser,
		});
	} catch (error) {
		logger.error("Error getting user by ID:", error);
		next(error);
	}
};
 
/**
 * @desc    Create new user
 * @route   POST /api/users
 * @access  Public
 */
export const createUser = async (
	req: Request,
	res: Response,
	next: NextFunction
) => {
	try {
		const userData = req.body;
		const newUser = await userService.createUser(userData);
 
		res.status(201).json({
			status: "success",
			message: "User created successfully",
			data: newUser,
		});
	} catch (error) {
		logger.error("Error creating user:", error);
		next(error);
	}
};

4. Services - Lógica de Negocio

Los servicios contienen la lógica de negocio y ayudan a mantener los controladores limpios:

import { User } from "../models/User";
import { logger } from "../utils/logger";
import { hashPassword, comparePassword } from "../utils/password";
import { generateToken } from "../utils/jwt";
import { sendEmail } from "../integrations/email";
 
interface CreateUserData {
	email: string;
	password: string;
	firstName: string;
	lastName: string;
	role?: string;
}
 
interface GetUsersOptions {
	page: number;
	limit: number;
	search?: string;
}
 
/**
 * Get all users with pagination and search
 */
const getAllUsers = async (options: GetUsersOptions) => {
	const { page, limit, search } = options;
	const offset = (page - 1) * limit;
 
	const whereClause = search
		? {
				$or: [
					{ firstName: { $like: `%${search}%` } },
					{ lastName: { $like: `%${search}%` } },
					{ email: { $like: `%${search}%` } },
				],
		  }
		: {};
 
	const users = await User.findAndCountAll({
		where: whereClause,
		limit,
		offset,
		attributes: { exclude: ["password"] },
		order: [["createdAt", "DESC"]],
	});
 
	return {
		users: users.rows,
		total: users.count,
		page,
		limit,
		totalPages: Math.ceil(users.count / limit),
	};
};
 
/**
 * Get user by ID
 */
const getUserById = async (id: string) => {
	const user = await User.findByPk(id, {
		attributes: { exclude: ["password"] },
	});
 
	if (!user) {
		throw new Error("User not found");
	}
 
	return user;
};
 
/**
 * Create new user
 */
const createUser = async (userData: CreateUserData) => {
	const { email, password, firstName, lastName, role = "user" } = userData;
 
	// Check if user already exists
	const existingUser = await User.findOne({ where: { email } });
	if (existingUser) {
		throw new Error("User with this email already exists");
	}
 
	// Hash password
	const hashedPassword = await hashPassword(password);
 
	// Create user
	const newUser = await User.create({
		email,
		password: hashedPassword,
		firstName,
		lastName,
		role,
	});
 
	// Send welcome email
	try {
		await sendEmail({
			to: email,
			subject: "Welcome to our platform!",
			template: "welcome",
			data: { firstName, lastName },
		});
	} catch (error) {
		logger.warn("Failed to send welcome email:", error);
	}
 
	return {
		id: newUser.id,
		email: newUser.email,
		firstName: newUser.firstName,
		lastName: newUser.lastName,
		role: newUser.role,
		createdAt: newUser.createdAt,
	};
};
 
/**
 * Update user
 */
const updateUser = async (id: string, updateData: Partial<CreateUserData>) => {
	const user = await User.findByPk(id);
	if (!user) {
		throw new Error("User not found");
	}
 
	// Hash password if provided
	if (updateData.password) {
		updateData.password = await hashPassword(updateData.password);
	}
 
	await user.update(updateData);
 
	return {
		id: user.id,
		email: user.email,
		firstName: user.firstName,
		lastName: user.lastName,
		role: user.role,
		updatedAt: user.updatedAt,
	};
};
 
/**
 * Delete user
 */
const deleteUser = async (id: string) => {
	const user = await User.findByPk(id);
	if (!user) {
		throw new Error("User not found");
	}
 
	await user.destroy();
	return { message: "User deleted successfully" };
};
 
export default {
	getAllUsers,
	getUserById,
	createUser,
	updateUser,
	deleteUser,
};

5. Utils - Utilidades

Las utilidades contienen funciones reutilizables:

// utils/formatters/user.ts
export const formatUserResponse = (user: any) => {
	return {
		id: user.id,
		email: user.email,
		firstName: user.firstName,
		lastName: user.lastName,
		role: user.role,
		createdAt: user.createdAt,
		updatedAt: user.updatedAt,
	};
};
 
// utils/password.ts
import bcrypt from "bcryptjs";
 
export const hashPassword = async (password: string): Promise<string> => {
	const saltRounds = 12;
	return await bcrypt.hash(password, saltRounds);
};
 
export const comparePassword = async (
	password: string,
	hashedPassword: string
): Promise<boolean> => {
	return await bcrypt.compare(password, hashedPassword);
};
 
// utils/jwt.ts
import jwt from "jsonwebtoken";
 
export const generateToken = (payload: any): string => {
	return jwt.sign(payload, process.env.JWT_SECRET!, {
		expiresIn: process.env.JWT_EXPIRES_IN || "24h",
	});
};
 
export const verifyToken = (token: string): any => {
	return jwt.verify(token, process.env.JWT_SECRET!);
};
 
// utils/logger/index.ts
import winston from "winston";
 
const logger = winston.createLogger({
	level: process.env.LOG_LEVEL || "info",
	format: winston.format.combine(
		winston.format.timestamp(),
		winston.format.errors({ stack: true }),
		winston.format.json()
	),
	transports: [
		new winston.transports.File({ filename: "logs/error.log", level: "error" }),
		new winston.transports.File({ filename: "logs/combined.log" }),
		new winston.transports.Console({
			format: winston.format.simple(),
		}),
	],
});
 
export { logger };

6. Middleware - Middlewares Personalizados

Los middlewares manejan autenticación, validación y sanitización:

// middleware/auth.ts
import { Request, Response, NextFunction } from "express";
import { verifyToken } from "../utils/jwt";
import { User } from "../models/User";
 
interface AuthRequest extends Request {
	user?: any;
}
 
export const authenticate = async (
	req: AuthRequest,
	res: Response,
	next: NextFunction
) => {
	try {
		const token = req.header("Authorization")?.replace("Bearer ", "");
 
		if (!token) {
			return res.status(401).json({
				status: "error",
				message: "Access denied. No token provided.",
			});
		}
 
		const decoded = verifyToken(token);
		const user = await User.findByPk(decoded.id, {
			attributes: { exclude: ["password"] },
		});
 
		if (!user) {
			return res.status(401).json({
				status: "error",
				message: "Invalid token. User not found.",
			});
		}
 
		req.user = user;
		next();
	} catch (error) {
		res.status(401).json({
			status: "error",
			message: "Invalid token.",
		});
	}
};
 
export const authorize = (...roles: string[]) => {
	return (req: AuthRequest, res: Response, next: NextFunction) => {
		if (!req.user) {
			return res.status(401).json({
				status: "error",
				message: "Access denied. Please authenticate first.",
			});
		}
 
		if (!roles.includes(req.user.role)) {
			return res.status(403).json({
				status: "error",
				message: "Access denied. Insufficient permissions.",
			});
		}
 
		next();
	};
};
 
// middleware/rateLimit.ts
import rateLimit from "express-rate-limit";
 
export const createRateLimit = (options: {
	windowMs: number;
	max: number;
	message?: string;
}) => {
	return rateLimit({
		windowMs: options.windowMs,
		max: options.max,
		message: {
			status: "error",
			message: options.message || "Too many requests, please try again later.",
		},
		standardHeaders: true,
		legacyHeaders: false,
	});
};
 
// middleware/errorHandler.ts
import { Request, Response, NextFunction } from "express";
import { logger } from "../utils/logger";
 
export const errorHandler = (
	err: any,
	req: Request,
	res: Response,
	next: NextFunction
) => {
	let error = { ...err };
	error.message = err.message;
 
	// Log error
	logger.error(err);
 
	// Mongoose bad ObjectId
	if (err.name === "CastError") {
		const message = "Resource not found";
		error = { message, statusCode: 404 };
	}
 
	// Mongoose duplicate key
	if (err.code === 11000) {
		const message = "Duplicate field value entered";
		error = { message, statusCode: 400 };
	}
 
	// Mongoose validation error
	if (err.name === "ValidationError") {
		const message = Object.values(err.errors).map((val: any) => val.message);
		error = { message, statusCode: 400 };
	}
 
	res.status(error.statusCode || 500).json({
		status: "error",
		message: error.message || "Server Error",
		...(process.env.NODE_ENV === "development" && { stack: err.stack }),
	});
};

7. Config - Configuraciones

// config/config.ts
import express, { Application } from "express";
import compression from "compression";
 
export default (app: Application) => {
	app.use(express.json({ limit: "50mb" }));
	app.use(express.urlencoded({ extended: true }));
	app.enable("trust proxy");
	app.use(compression());
};
 
// config/cors.ts
import { Application } from "express";
 
export default (app: Application) => {
	const whitelist = [
		"http://localhost:3000",
		"https://yourdomain.com",
		// Agregar más dominios permitidos
	];
 
	app.use((req, res, next) => {
		const origin = req.headers.origin;
 
		if (whitelist.includes(origin as string)) {
			res.header("Access-Control-Allow-Origin", origin);
		}
 
		res.header(
			"Access-Control-Allow-Methods",
			"GET, POST, PUT, DELETE, OPTIONS"
		);
		res.header(
			"Access-Control-Allow-Headers",
			"Origin, X-Requested-With, Content-Type, Accept, Authorization"
		);
 
		if (req.method === "OPTIONS") {
			res.sendStatus(200);
		} else {
			next();
		}
	});
};

ORM - Object-Relational Mapping

Sequelize

// models/User.ts
import { Model, BuildOptions, Optional, DataTypes } from "sequelize";
import db from "../config/db";
 
interface UserI {
	id?: number;
	email: string;
	password: string;
	firstName: string;
	lastName: string;
	role?: string;
	isActive?: boolean;
	lastLogin?: Date;
	createdAt?: Date;
	updatedAt?: Date;
}
 
export type UserAttributesI = Optional<
	UserI,
	"id" | "role" | "isActive" | "lastLogin"
>;
 
interface UserInstance extends Model<UserAttributesI, UserI>, UserAttributesI {}
 
type UserStatic = typeof Model & {
	new (values?: object, options?: BuildOptions): UserInstance;
};
 
export const User = <UserStatic>db.define(
	"User",
	{
		id: {
			type: DataTypes.INTEGER,
			primaryKey: true,
			autoIncrement: true,
			allowNull: false,
		},
		email: {
			type: DataTypes.STRING,
			allowNull: false,
			unique: true,
			validate: {
				isEmail: true,
			},
		},
		password: {
			type: DataTypes.STRING,
			allowNull: false,
			validate: {
				len: [6, 255],
			},
		},
		firstName: {
			type: DataTypes.STRING,
			allowNull: false,
			validate: {
				len: [1, 50],
			},
		},
		lastName: {
			type: DataTypes.STRING,
			allowNull: false,
			validate: {
				len: [1, 50],
			},
		},
		role: {
			type: DataTypes.ENUM("user", "admin", "moderator"),
			defaultValue: "user",
			allowNull: false,
		},
		isActive: {
			type: DataTypes.BOOLEAN,
			defaultValue: true,
			allowNull: false,
		},
		lastLogin: {
			type: DataTypes.DATE,
			allowNull: true,
		},
	},
	{
		tableName: "users",
		timestamps: true,
		freezeTableName: true,
		indexes: [
			{
				unique: true,
				fields: ["email"],
			},
			{
				fields: ["role"],
			},
			{
				fields: ["isActive"],
			},
		],
	}
);

Prisma (Alternativa)

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  password  String
  firstName String   @map("first_name")
  lastName  String   @map("last_name")
  role      Role     @default(USER)
  isActive  Boolean  @default(true) @map("is_active")
  lastLogin DateTime? @map("last_login")
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")
 
  @@map("users")
}
 
model Product {
  id          Int      @id @default(autoincrement())
  name        String
  description String?
  price       Decimal  @db.Decimal(10, 2)
  category    String
  isActive    Boolean  @default(true) @map("is_active")
  createdAt   DateTime @default(now()) @map("created_at")
  updatedAt   DateTime @updatedAt @map("updated_at")
 
  @@map("products")
}
 
model Order {
  id        Int      @id @default(autoincrement())
  userId    Int      @map("user_id")
  total     Decimal  @db.Decimal(10, 2)
  status    OrderStatus @default(PENDING)
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")
 
  user User @relation(fields: [userId], references: [id])
 
  @@map("orders")
}
 
enum Role {
  USER
  ADMIN
  MODERATOR
}
 
enum OrderStatus {
  PENDING
  PROCESSING
  SHIPPED
  DELIVERED
  CANCELLED
}

Docker

Dockerfile

# Use Node.js 20+ image
FROM node:20-alpine
 
# Set working directory
WORKDIR /app
 
# Install system dependencies
RUN apk add --no-cache dumb-init
 
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
 
# Copy package files
COPY package*.json ./
COPY yarn.lock ./
 
# Install dependencies
RUN yarn install --frozen-lockfile --production
 
# Copy source code
COPY . .
 
# Build TypeScript
RUN yarn build
 
# Create logs directory
RUN mkdir -p logs && chown -R nodejs:nodejs logs
 
# Change ownership of the app directory
RUN chown -R nodejs:nodejs /app
 
# Switch to non-root user
USER nodejs
 
# Expose port
EXPOSE 3000
 
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1
 
# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
 
# Run the application
CMD ["yarn", "start"]

docker-compose.yml

version: "3.8"
 
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - PORT=3000
      - DATABASE_URL=postgresql://user:password@db:5432/database
      - REDIS_URL=redis://redis:6379
      - JWT_SECRET=your-super-secret-jwt-key
    depends_on:
      - db
      - redis
    volumes:
      - ./uploads:/app/uploads
      - ./logs:/app/logs
    restart: unless-stopped
    networks:
      - app-network
 
  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=database
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    restart: unless-stopped
    networks:
      - app-network
 
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    restart: unless-stopped
    networks:
      - app-network
 
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - app
    restart: unless-stopped
    networks:
      - app-network
 
volumes:
  postgres_data:
  redis_data:
 
networks:
  app-network:
    driver: bridge

Mejores Prácticas

1. Manejo de Archivos

import multer from "multer";
import path from "path";
import { v4 as uuidv4 } from "uuid";
 
// Configure storage
const storage = multer.diskStorage({
	destination: (req, file, cb) => {
		cb(null, "uploads/");
	},
	filename: (req, file, cb) => {
		const uniqueName = `${uuidv4()}-${Date.now()}${path.extname(
			file.originalname
		)}`;
		cb(null, uniqueName);
	},
});
 
// File filter
const fileFilter = (
	req: any,
	file: Express.Multer.File,
	cb: multer.FileFilterCallback
) => {
	const allowedTypes = /jpeg|jpg|png|gif|pdf|doc|docx/;
	const extname = allowedTypes.test(
		path.extname(file.originalname).toLowerCase()
	);
	const mimetype = allowedTypes.test(file.mimetype);
 
	if (mimetype && extname) {
		return cb(null, true);
	} else {
		cb(new Error("Only images and documents are allowed"));
	}
};
 
// Configure multer
const upload = multer({
	storage,
	limits: {
		fileSize: 10 * 1024 * 1024, // 10MB limit
		files: 5, // Maximum 5 files
	},
	fileFilter,
});
 
// Middleware for single file upload
export const uploadSingle = (fieldName: string) => upload.single(fieldName);
 
// Middleware for multiple files upload
export const uploadMultiple = (fieldName: string, maxCount: number = 5) =>
	upload.array(fieldName, maxCount);
 
// Middleware for mixed fields
export const uploadFields = (fields: multer.Field[]) => upload.fields(fields);

2. Validaciones

import { body, param, query, validationResult } from "express-validator";
import { Request, Response, NextFunction } from "express";
 
// User validation
export const validateUser = [
	body("email")
		.isEmail()
		.normalizeEmail()
		.withMessage("Please provide a valid email"),
	body("password")
		.isLength({ min: 6 })
		.withMessage("Password must be at least 6 characters long"),
	body("firstName")
		.trim()
		.isLength({ min: 1, max: 50 })
		.withMessage("First name must be between 1 and 50 characters"),
	body("lastName")
		.trim()
		.isLength({ min: 1, max: 50 })
		.withMessage("Last name must be between 1 and 50 characters"),
	body("role")
		.optional()
		.isIn(["user", "admin", "moderator"])
		.withMessage("Role must be user, admin, or moderator"),
	(req: Request, res: Response, next: NextFunction) => {
		const errors = validationResult(req);
		if (!errors.isEmpty()) {
			return res.status(400).json({
				status: "error",
				message: "Validation failed",
				errors: errors.array(),
			});
		}
		next();
	},
];
 
// ID parameter validation
export const validateId = [
	param("id").isInt({ min: 1 }).withMessage("ID must be a positive integer"),
	(req: Request, res: Response, next: NextFunction) => {
		const errors = validationResult(req);
		if (!errors.isEmpty()) {
			return res.status(400).json({
				status: "error",
				message: "Invalid ID parameter",
				errors: errors.array(),
			});
		}
		next();
	},
];
 
// Query parameters validation
export const validatePagination = [
	query("page")
		.optional()
		.isInt({ min: 1 })
		.withMessage("Page must be a positive integer"),
	query("limit")
		.optional()
		.isInt({ min: 1, max: 100 })
		.withMessage("Limit must be between 1 and 100"),
	(req: Request, res: Response, next: NextFunction) => {
		const errors = validationResult(req);
		if (!errors.isEmpty()) {
			return res.status(400).json({
				status: "error",
				message: "Invalid query parameters",
				errors: errors.array(),
			});
		}
		next();
	},
];

3. Logging y Telemetría

import { logger } from "./utils/logger";
import { Request, Response } from "express";
 
// Request logging middleware
export const requestLogger = (req: Request, res: Response, next: Function) => {
	const start = Date.now();
 
	res.on("finish", () => {
		const duration = Date.now() - start;
		logger.info("HTTP Request", {
			method: req.method,
			url: req.url,
			status: res.statusCode,
			duration: `${duration}ms`,
			userAgent: req.get("User-Agent"),
			ip: req.ip,
		});
	});
 
	next();
};
 
// Error logging
export const logError = (error: Error, context?: any) => {
	logger.error("Application Error", {
		message: error.message,
		stack: error.stack,
		context,
	});
};
 
// Success logging
export const logSuccess = (message: string, data?: any) => {
	logger.info("Success", {
		message,
		data,
	});
};
 
// Performance monitoring
export const performanceMonitor = (operation: string) => {
	const start = Date.now();
 
	return {
		end: (success: boolean = true) => {
			const duration = Date.now() - start;
			logger.info("Performance", {
				operation,
				duration: `${duration}ms`,
				success,
			});
		},
	};
};
 
// Usage in controllers
export const exampleController = async (req: Request, res: Response) => {
	const monitor = performanceMonitor("getUsers");
 
	try {
		const result = await userService.getAllUsers();
		monitor.end(true);
		logSuccess("Users retrieved successfully", { count: result.length });
		res.json(result);
	} catch (error) {
		monitor.end(false);
		logError(error as Error, { userId: req.user?.id });
		res.status(500).json({ error: "Internal server error" });
	}
};

4. Manejo de Colas

import { Queue, Worker, Job } from "bullmq";
import Redis from "ioredis";
import { sendEmail } from "./integrations/email";
import { logger } from "./utils/logger";
 
// Redis connection
const connection = new Redis(process.env.REDIS_URL!);
 
// Email queue
export const emailQueue = new Queue("email", { connection });
 
// Job types
interface EmailJobData {
	to: string;
	subject: string;
	template: string;
	data: any;
}
 
interface NotificationJobData {
	userId: string;
	message: string;
	type: string;
}
 
// Email worker
const emailWorker = new Worker(
	"email",
	async (job: Job<EmailJobData>) => {
		const { to, subject, template, data } = job.data;
 
		try {
			await sendEmail({ to, subject, template, data });
			logger.info("Email sent successfully", { to, subject });
		} catch (error) {
			logger.error("Failed to send email", error);
			throw error;
		}
	},
	{ connection }
);
 
// Notification worker
const notificationWorker = new Worker(
	"notification",
	async (job: Job<NotificationJobData>) => {
		const { userId, message, type } = job.data;
 
		try {
			// Process notification logic
			logger.info("Notification processed", { userId, type });
		} catch (error) {
			logger.error("Failed to process notification", error);
			throw error;
		}
	},
	{ connection }
);
 
// Queue management functions
export const addEmailJob = async (data: EmailJobData, options?: any) => {
	return await emailQueue.add("send-email", data, {
		removeOnComplete: 10,
		removeOnFail: 5,
		attempts: 3,
		backoff: {
			type: "exponential",
			delay: 2000,
		},
		...options,
	});
};
 
export const addNotificationJob = async (
	data: NotificationJobData,
	options?: any
) => {
	return await notificationQueue.add("send-notification", data, {
		removeOnComplete: 10,
		removeOnFail: 5,
		attempts: 3,
		...options,
	});
};
 
// Queue monitoring
export const getQueueStats = async () => {
	const emailStats = await emailQueue.getJobCounts();
	const notificationStats = await notificationQueue.getJobCounts();
 
	return {
		email: emailStats,
		notification: notificationStats,
	};
};
 
// Graceful shutdown
export const closeQueues = async () => {
	await emailQueue.close();
	await notificationQueue.close();
	await connection.quit();
};

Configuración de Variables de Entorno

.env.example

# Application
NODE_ENV=development
PORT=3000

# Database
DATABASE_URL=postgresql://user:password@localhost:5432/database

# Redis
REDIS_URL=redis://localhost:6379

# JWT
JWT_SECRET=your-super-secret-jwt-key
JWT_EXPIRES_IN=24h

# Email
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password

# File Upload
MAX_FILE_SIZE=10485760
UPLOAD_PATH=uploads

# Logging
LOG_LEVEL=info
LOG_FILE=logs/app.log

# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100

Scripts de Package.json

{
	"scripts": {
		"dev": "nodemon src/app.ts",
		"start": "node dist/app.js",
		"build": "tsc",
		"test": "jest",
		"test:watch": "jest --watch",
		"test:coverage": "jest --coverage",
		"lint": "eslint src/**/*.ts",
		"lint:fix": "eslint src/**/*.ts --fix",
		"format": "prettier --write src/**/*.ts",
		"db:migrate": "sequelize db:migrate",
		"db:migrate:undo": "sequelize db:migrate:undo",
		"db:seed": "sequelize db:seed:all",
		"db:seed:undo": "sequelize db:seed:undo:all"
	}
}

Estructura de Archivos Adicionales

nginx.conf

events {
    worker_connections 1024;
}

http {
    upstream app {
        server app:3000;
    }

    server {
        listen 80;
        server_name localhost;

        location / {
            proxy_pass http://app;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

Esta estructura genérica proporciona una base sólida y escalable para cualquier proyecto backend en Node.js, siguiendo las mejores prácticas de la industria y patrones probados en producción. Es adaptable a diferentes tipos de aplicaciones (e-commerce, SaaS, APIs, microservicios) y puede ser personalizada según las necesidades específicas del proyecto.