
Node.js Best Practices: Cómo Escribir Código Backend Limpio
Domina las mejores prácticas de Node.js. Estructura escalable, error handling, seguridad y optimización de performance.
Node.js Best Practices: Cómo Escribir Código Backend Limpio y Escalable
Node.js es increíblemente poderoso, pero con gran poder viene gran responsabilidad. Escribir código backend limpio, mantenible y escalable requiere disciplina y conocimiento de mejores prácticas probadas en batalla. Esta guía te enseña exactamente cómo estructurar, asegurar y optimizar tus aplicaciones Node.js para producción.
¿Por qué las mejores prácticas importan en Node.js?
Node.js es no-bloqueante y asincrónico, lo que lo hace increíblemente eficiente pero también peligroso si no entiendes qué estás haciendo. Sin mejores prácticas, terminas con:
- Memory leaks que causan crashes en producción
- Callbacks hell haciendo código imposible de mantener
- Race conditions difíciles de debuggear
- Vulnerabilidades de seguridad críticas
Las mejores prácticas previenen todos estos problemas desde el inicio.
1. Arquitectura y Estructura de Proyecto
El patrón MVC/Service Layer
Separa tu código en capas claras de responsabilidad:
src/
├── controllers/ # Maneja requests/responses
│ ├── userController.js
│ └── productController.js
│
├── services/ # Lógica de negocio
│ ├── userService.js
│ └── productService.js
│
├── routes/ # Definiciones de rutas
│ ├── userRoutes.js
│ └── productRoutes.js
│
├── models/ # Esquemas y acceso a BD
│ ├── User.js
│ └── Product.js
│
├── middleware/ # Middleware personalizado
│ ├── auth.js
│ ├── errorHandler.js
│ └── validation.js
│
├── utils/ # Funciones helper
│ ├── logger.js
│ ├── validators.js
│ └── response.js
│
├── config/ # Configuración
│ ├── database.js
│ ├── env.js
│ └── constants.js
│
├── app.js # Inicialización Express
└── index.js # Punto de entrada
Beneficios de esta estructura
- Testabilidad: Cada componente es independiente y testeable
- Mantenibilidad: Fácil encontrar y modificar lógica
- Escalabilidad: Agregar nuevas features es simple
- Reutilización: Services pueden usarse en múltiples controllers
2. Error Handling - Manejando errores correctamente
Crear una clase de error personalizada
// utils/AppError.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AppError;
Middleware de error global
// middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
// Errores operacionales (previstos)
if (err.isOperational) {
return res.status(err.statusCode).json({
status: 'error',
message: err.message
});
}
// Errores de programación (no esperados)
console.error('Error no manejado:', err);
// No revelar detalles de errores internos en producción
res.status(500).json({
status: 'error',
message: 'Algo salió mal en el servidor'
});
};
// Usar en app.js (siempre al final)
app.use(errorHandler);
Try-catch con async/await
// controllers/userController.js
const catchAsync = (fn) => (req, res, next) => {
fn(req, res, next).catch(next); // Pasa error al middleware
};
exports.getUser = catchAsync(async (req, res, next) => {
const user = await User.findById(req.params.id);
if (!user) {
return next(new AppError('Usuario no encontrado', 404));
}
res.json({ status: 'success', data: user });
});
3. Middleware - Concerns transversales
Middleware de autenticación
// middleware/auth.js
const jwt = require('jsonwebtoken');
exports.protect = catchAsync(async (req, res, next) => {
// 1. Obtener token del header
let token;
if (req.headers.authorization?.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return next(new AppError('No autenticado', 401));
}
// 2. Verificar token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// 3. Cargar usuario
const user = await User.findById(decoded.id);
if (!user) {
return next(new AppError('Usuario no existe', 401));
}
req.user = user;
next();
});
// Uso en rutas
router.get('/me', protect, userController.getMe);
Middleware de validación
// middleware/validation.js
const { body, validationResult } = require('express-validator');
exports.validateCreateUser = [
body('name').notEmpty().withMessage('Nombre es requerido'),
body('email').isEmail().withMessage('Email inválido'),
body('password')
.isLength({ min: 8 })
.withMessage('Contraseña debe tener 8 caracteres'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return next(new AppError(errors.array()[0].msg, 400));
}
next();
}
];
// Uso
router.post('/users', validateCreateUser, userController.create);
Middleware de logging
// middleware/logger.js
const morgan = require('morgan');
// Morgan para logs HTTP
app.use(morgan('combined'));
// Logger personalizado
const logger = {
info: (msg) => console.log(`[INFO] ${new Date().toISOString()}: ${msg}`),
error: (msg) => console.error(`[ERROR] ${new Date().toISOString()}: ${msg}`),
warn: (msg) => console.warn(`[WARN] ${new Date().toISOString()}: ${msg}`)
};
module.exports = logger;
4. Seguridad - Protege tu API
Validación de entrada
// ❌ MAL - Vulnerable a SQL injection
const user = await User.findOne({ email: req.body.email });
// ✅ BIEN - Usar ORM que prepara statements
const user = await User.findOne({ email: req.body.email });
Manejo de secretos
// ❌ MAL - Secrets en código
const API_KEY = 'sk_live_1234567890abcdef';
// ✅ BIEN - Variables de entorno
const API_KEY = process.env.STRIPE_API_KEY;
// .env
STRIPE_API_KEY=sk_live_1234567890abcdef
JWT_SECRET=your_secret_key_here
Rate Limiting
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100, // máximo 100 requests
message: 'Demasiadas requests, intenta luego'
});
// Aplicar a todas las rutas o selectivas
app.use(limiter);
// O solo a login (más restrictivo)
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5
});
app.post('/login', loginLimiter, authController.login);
Headers de seguridad
const helmet = require('helmet');
// Agrega múltiples headers de seguridad
app.use(helmet());
// Esto incluye:
// - Content-Security-Policy
// - X-Frame-Options
// - X-Content-Type-Options
// - Strict-Transport-Security
CORS apropiado
const cors = require('cors');
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(','),
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
5. Asincronía - Maneja callbacks y promises correctamente
Evita callback hell
// ❌ Callback hell
getData(function(a) {
getMoreData(a, function(b) {
getMoreData(b, function(c) {
getMoreData(c, function(d) {
getMoreData(d, function(e) {
console.log(e);
});
});
});
});
});
// ✅ Con promises
getData()
.then(a => getMoreData(a))
.then(b => getMoreData(b))
.then(c => getMoreData(c))
.catch(err => console.error(err));
// ✅ Con async/await (la mejor opción)
async function processData() {
try {
const a = await getData();
const b = await getMoreData(a);
const c = await getMoreData(b);
console.log(c);
} catch(err) {
console.error(err);
}
}
Promise.all para operaciones paralelas
// ✅ BIEN - Ejecuta en paralelo
const [user, posts, comments] = await Promise.all([
User.findById(id),
Post.find({ userId: id }),
Comment.find({ userId: id })
]);
// ❌ MAL - Ejecución secuencial (más lenta)
const user = await User.findById(id);
const posts = await Post.find({ userId: id });
const comments = await Comment.find({ userId: id });
6. Performance - Optimización de velocidad
Caching con Redis
const redis = require('redis');
const client = redis.createClient();
exports.getUser = catchAsync(async (req, res, next) => {
const cacheKey = `user:${req.params.id}`;
// Verificar cache
let user = await client.get(cacheKey);
if (user) {
return res.json({ status: 'success', data: JSON.parse(user) });
}
// Si no está en cache, traer de BD
user = await User.findById(req.params.id);
// Guardar en cache por 1 hora
await client.setex(cacheKey, 3600, JSON.stringify(user));
res.json({ status: 'success', data: user });
});
Paginación
exports.getAllUsers = catchAsync(async (req, res, next) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const users = await User.find()
.skip(skip)
.limit(limit)
.sort({ createdAt: -1 });
const total = await User.countDocuments();
res.json({
status: 'success',
data: users,
pagination: {
currentPage: page,
totalPages: Math.ceil(total / limit),
totalDocs: total
}
});
});
Monitoreo de memoria
setInterval(() => {
const used = process.memoryUsage();
logger.info(`
Heap Used: ${Math.round(used.heapUsed / 1024 / 1024)} MB
Heap Total: ${Math.round(used.heapTotal / 1024 / 1024)} MB
`);
// Alerta si memoria es alta
if (used.heapUsed / used.heapTotal > 0.9) {
logger.warn('Memoria crítica detectada');
}
}, 60000); // Cada minuto
Checklist final - ¿Estás listo para producción?
✓ Arquitectura
- ✅ Código organizado en capas (controllers, services, models)
- ✅ Separación de concerns clara
- ✅ Dependencias inyectadas
✓ Seguridad
- ✅ Todas las entradas validadas
- ✅ Secretos en variables de entorno
- ✅ Rate limiting implementado
- ✅ Helmet para headers de seguridad
- ✅ CORS configurado correctamente
✓ Error Handling
- ✅ Middleware de error global
- ✅ Try-catch o .catch() en promesas
- ✅ Logging de errores
✓ Performance
- ✅ Caching implementado donde es apropiado
- ✅ Paginación en endpoints que retornan listas
- ✅ Índices en base de datos
- ✅ Monitoreo de memoria
✓ Escalabilidad
- ✅ Pronto para dockerización
- ✅ Puede escalar horizontalmente
- ✅ Sesiones en redis si es necesario
Node.js Backend Profesional
Seguir estas mejores prácticas no solo hace tu código mejor, sino que te transforma en un desarrollador backend profesional. Los beneficios incluyen menos bugs, mejor performance, código que otros pueden mantener, y aplicaciones que pueden escalar.
Comienza a aplicar una práctica a la vez en tu próximo proyecto. Verás la diferencia inmediatamente en la calidad y mantenibilidad de tu código.

Sobre el autor
Andres Saumet
Desarrollador Web & Móvil Full Stack · Colombia
Hay mil desarrolladores que pueden hacer que algo funcione. Yo me obsesiono con hacer que funcione y genere dinero.
Soy Andres Saumet, desarrollador Web y Móvil con foco en rentabilidad. Trabajo con startups, emprendedores y empresas que ya tienen una visión clara y necesitan a alguien que la convierta en un producto digital real — uno que los usuarios quieran usar y que el negocio quiera escalar.
Domino React, Next.js, React Native y Node.js. Pero más allá del stack, entiendo cómo piensan los usuarios, cómo fluye un negocio y qué decisiones técnicas impactan directamente en los ingresos.
Cada línea de código que escribo tiene un propósito: que tu producto web o móvil sea más rápido, más usable y más rentable.
¿Tienes un producto que necesita crecer? Construyámoslo juntos.
