Cómo estructurar un backend en Node.js sin framework pesado
- node.js
- backend
- arquitectura
- desarrollo
Montar un backend en Node.js puede ser una trampa si no eliges bien la arquitectura. Muchos caen en la tentación de usar frameworks gigantes que prometen mucho pero acaban atando el proyecto a una forma de trabajar que no siempre es eficiente. Mi experiencia me dice que la clave está en una estructura limpia y modular, sin dependencias innecesarias.
La complejidad no es un valor, es una deuda. Un backend que empieza simple y crece de forma controlada es mucho más robusto que uno que nace con una capa de abstracción innecesaria desde el día uno.
Por qué evitar frameworks monolíticos en Node.js
Los frameworks como NestJS o AdonisJS resuelven muchos problemas, pero también imponen una forma de trabajar muy específica. Esto puede ser útil en equipos grandes o proyectos con requisitos muy claros desde el principio, pero en proyectos medianos o donde la flexibilidad es clave, terminan ralentizando. Añaden una capa de abstracción que, a menudo, no necesitas.
Mi regla es simple: usa la herramienta más ligera que resuelva el problema. Para un servidor HTTP, Express es una librería, no un framework pesado. Te da lo justo para montar rutas y middleware, sin meterse en cómo organizas tu lógica de negocio o tu capa de datos.
Una estructura modular para tu backend
La mantenibilidad de un backend depende de la separación de responsabilidades. Cada parte del código debe hacer una única cosa y hacerla bien. Cuando un cliente me llega con un problema de escalabilidad o bugs recurrentes, casi siempre encuentro un controlador haciendo lógica de negocio, acceso a datos y formateo de respuestas.
Propongo una estructura basada en módulos, donde cada directorio es responsable de una pieza de funcionalidad. Esto facilita el desarrollo en paralelo y la depuración.
.
├── src/
│ ├── api/
│ │ ├── routes/
│ │ │ ├── auth.routes.js
│ │ │ └── users.routes.js
│ │ ├── controllers/
│ │ │ ├── auth.controller.js
│ │ │ └── users.controller.js
│ │ └── middlewares/
│ │ └── auth.middleware.js
│ ├── services/
│ │ ├── auth.service.js
│ │ └── user.service.js
│ ├── repositories/
│ │ ├── user.repository.js
│ │ └── session.repository.js
│ ├── config/
│ │ └── index.js
│ ├── utils/
│ │ └── errorHandling.js
│ └── app.js
└── server.js
Funciones de cada capa de abstracción
Cada capa tiene su función definida. El server.js es el punto de entrada que inicializa Express y carga las rutas. Los routes definen los endpoints y delegan en los controllers. Los controllers gestionan la petición y la respuesta HTTP, pero la lógica de negocio la delegan en los services.
Los services contienen la lógica real de tu aplicación, orquestando operaciones y utilizando los repositories para interactuar con la base de datos. Finalmente, los repositories son los únicos que saben cómo hablar con tu base de datos o cualquier otro sistema de persistencia. Esta separación, similar a la arquitectura hexagonal, hace que tu código sea mucho más fácil de testear y modificar sin afectar otras partes.
// src/api/routes/users.routes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/users.controller');
router.get('/:id', userController.getUserById);
router.post('/', userController.createUser);
module.exports = router;
// src/api/controllers/users.controller.js
const userService = require('../../services/user.service');
async function getUserById(req, res) {
try {
const user = await userService.findUserById(req.params.id);
if (!user) {
return res.status(404).json({ message: 'Usuario no encontrado' });
}
res.status(200).json(user);
} catch (error) {
res.status(500).json({ message: error.message });
}
}
async function createUser(req, res) {
try {
const newUser = await userService.createNewUser(req.body);
res.status(201).json(newUser);
} catch (error) {
res.status(500).json({ message: error.message });
}
}
module.exports = {
getUserById,
createUser
};
Gestión de la configuración y secretos
Un error común es hardcodear credenciales o configuraciones. Esto es una receta para desastres en producción. La configuración debe cargarse desde variables de entorno.
Uso dotenv para cargar variables en desarrollo, y en producción dependo del sistema de orquestación (Docker, Kubernetes) para inyectarlas. Asegúrate de nunca subir archivos .env al control de versiones. Si necesitas más detalles, tengo un artículo sobre gestión de secretos en software que te puede interesar.
// src/config/index.js
require('dotenv').config();
const config = {
port: process.env.PORT || 3000,
databaseUrl: process.env.DATABASE_URL,
jwtSecret: process.env.JWT_SECRET
};
module.exports = config;
Ventajas y desventajas de un backend ligero
Ventajas:
- Flexibilidad total: No hay un framework que te imponga cómo hacer las cosas. Tú decides la mejor herramienta para cada problema.
- Menos código “boilerplate”: Solo añades las dependencias que realmente necesitas, reduciendo la superficie de ataque y el tamaño del proyecto.
- Mayor rendimiento: Al no cargar capas de abstracción innecesarias, tu aplicación puede ser más rápida y consumir menos recursos.
- Curva de aprendizaje controlada: Aprendes Node.js y las librerías específicas, no un framework completo con sus propias convenciones.
Desventajas:
- Necesitas más disciplina: No hay un framework que te obligue a seguir ciertas convenciones. La consistencia depende del equipo.
- Configuración inicial manual: Tienes que montar la estructura y las conexiones básicas tú mismo, lo que puede llevar más tiempo al principio.
- Menos “features out-of-the-box”: Tendrás que implementar funcionalidades comunes (validación, autenticación) o integrarlas con librerías específicas.
La elección de la base de datos es otro punto crítico, y si no tienes claro cuándo usar una relacional o no relacional, puedes leer mi artículo sobre la decisión entre SQL y NoSQL. Si estás pensando en cómo mantener tu API en el tiempo, te recomiendo revisar las estrategias para versionar una API sin romperla para evitar dolores de cabeza futuros.
Técnico freelance especializado en desarrollo a medida, automatizaciones con IA y gestión técnica para negocios en España. Más sobre mí →
¿Necesitas desarrollo a medida?
Desarrollo funcionalidades específicas, integraciones entre sistemas y herramientas internas. Si se puede programar, probablemente puedo hacerlo.