logo-andres-saumet

logo-andres-saumet
Node.js Best Practices: Cómo Escribir Código Backend Limpio
7 de febrero de 2026

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.

AS
Andres Saumet
nodejsbackendjavascriptbest-practices

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.

Escrito por Andrés Saumet - Ingeniero full-stack especializado en arquitectura backend escalable y segura.

Andres Saumet

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.

Compartir:
Volver al Blog

Todos los derechos reservados Andres Saumet 2026 ©