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: bridgeMejores 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=100Scripts 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.