Objetivo de una buena estructura
Una estructura de carpetas clara te ayuda a escalar una API sin que el proyecto se vuelva difícil de mantener. La idea es separar responsabilidades (cada archivo hace una cosa bien) y reducir dependencias innecesarias entre partes del sistema. Esto mejora la legibilidad, facilita pruebas y evita que un solo archivo crezca hasta volverse “monolítico”.
Estructura de carpetas propuesta (escalable)
Una base común para una API con Express puede verse así:
project/ src/ app/ app.js server.js routes/ index.js users.routes.js controllers/ users.controller.js services/ users.service.js config/ env.js db.js middleware/ auth.middleware.js error.middleware.js utils/ logger.js asyncHandler.js tests/ users.test.jsNotas rápidas: tests/ es opcional, pero recomendable desde temprano. La carpeta src/ agrupa el código de la aplicación (evita mezclarlo con archivos de configuración del repositorio).
Responsabilidades por capa (qué va en cada carpeta)
src/app/
- app.js: crea y configura la instancia de Express (middlewares globales, rutas, manejo de errores). No debería “escuchar” un puerto.
- server.js: punto de arranque. Importa
appy llama alisten. Mantenerlo separado facilita pruebas (puedes importarappsin abrir un puerto).
src/routes/
Define endpoints y conecta cada ruta con su controlador. Aquí se decide el “mapa” de la API, pero no la lógica de negocio.
- Archivos por recurso:
users.routes.js,products.routes.js, etc. routes/index.jspuede centralizar el montaje de rutas para mantenerapp.jslimpio.
src/controllers/
Orquesta la petición HTTP: lee req, valida lo mínimo necesario para el flujo, llama a servicios y construye la respuesta (res). Evita meter aquí consultas a base de datos o reglas de negocio complejas.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
- Un controlador por recurso:
users.controller.js. - Funciones pequeñas por endpoint:
listUsers,getUserById,createUser.
src/services/
Contiene la lógica de negocio y casos de uso. Un servicio no debería depender de Express (ni de req/res). Esto permite reutilizarlo y probarlo con facilidad.
- Ejemplos: reglas de creación de usuario, validaciones de dominio, coordinación de múltiples repositorios/consultas.
- Si más adelante incorporas una capa de acceso a datos (por ejemplo
repositories/omodels/), el servicio se apoya en ella.
src/config/
Configuración centralizada: variables de entorno, conexión a base de datos, constantes por entorno. Evita leer process.env disperso por todo el proyecto.
env.js: carga y expone configuración tipada/normalizada.db.js: inicializa conexión (si aplica).
src/middleware/
Middlewares reutilizables: autenticación, autorización, validación, logging, manejo de errores. Mantenerlos aquí evita duplicación en rutas/controladores.
auth.middleware.js: verifica token/sesión.error.middleware.js: captura errores y responde con un formato consistente.
src/utils/
Utilidades genéricas sin dependencia del dominio: helpers, formateadores, wrappers para async, logger, etc. Si una utilidad empieza a conocer demasiado del negocio, probablemente debería vivir en services/ o en un módulo del dominio.
asyncHandler.js: wrapper para evitartry/catchrepetidos en controladores.logger.js: interfaz simple de logging.
src/tests/ (opcional)
Pruebas unitarias e integración. Una convención útil es reflejar la estructura de src/ o agrupar por recurso.
users.test.jspara endpoints o servicios de usuarios.
Criterios para separar código: cohesión y acoplamiento
Cohesión (alta)
Un módulo debe agrupar cosas que cambian por la misma razón. Ejemplos:
- Alta cohesión:
users.service.jscontiene reglas relacionadas con usuarios. - Baja cohesión: un archivo
helpers.jscon funciones de fechas, usuarios, pagos y strings mezcladas.
Acoplamiento (bajo)
Evita que una capa dependa de detalles de otra. Reglas prácticas:
- Controladores pueden depender de servicios, pero los servicios no deberían depender de Express.
- La configuración se importa desde
config/, no se leeprocess.enven cada archivo. - Evita importaciones “cruzadas” entre recursos (por ejemplo,
users.controllerimportandoorders.routes).
Convenciones de nombres recomendadas
- Archivos:
users.routes.js,users.controller.js,users.service.js(sufijo por responsabilidad). - Funciones: verbos claros:
listUsers,getUser,createUser,updateUser,deleteUser. - Rutas: sustantivos en plural:
/users,/users/:id. - Constantes:
UPPER_SNAKE_CASEcuando aplique. - Clases (si las usas):
PascalCase.
Uso de archivos index.js (cuándo sí y cuándo no)
Un index.js puede aportar claridad cuando centraliza exportaciones o montaje de rutas. Úsalo si reduce ruido y evita imports largos.
Ejemplo: routes/index.js para montar recursos
// src/routes/index.js const express = require('express'); const usersRoutes = require('./users.routes'); const router = express.Router(); router.use('/users', usersRoutes); module.exports = router;Así, app.js queda más limpio:
// src/app/app.js const express = require('express'); const routes = require('../routes'); const { notFound, errorHandler } = require('../middleware/error.middleware'); const app = express(); app.use(express.json()); app.use('/api', routes); app.use(notFound); app.use(errorHandler); module.exports = app;Evita index.js si solo añade un salto extra sin mejorar legibilidad (por ejemplo, un index.js que exporta una sola cosa sin motivo).
Cómo evitar archivos monolíticos
1) Divide por recurso y por responsabilidad
Si users.controller.js empieza a crecer demasiado, suele ser señal de que:
- Hay lógica de negocio dentro del controlador (muévela a
users.service.js). - Hay demasiados endpoints en un solo recurso (considera sub-recursos o módulos:
users.profile.controller.js,users.admin.controller.jssi realmente son contextos distintos).
2) Extrae middlewares específicos
Validaciones repetidas o checks de permisos suelen convertirse en middlewares reutilizables:
// src/middleware/auth.middleware.js function requireAuth(req, res, next) { // validar token... next(); } module.exports = { requireAuth };3) Usa helpers para async y errores
Un wrapper reduce repetición en controladores:
// src/utils/asyncHandler.js module.exports = function asyncHandler(fn) { return function (req, res, next) { Promise.resolve(fn(req, res, next)).catch(next); }; };Controlador más limpio:
// src/controllers/users.controller.js const asyncHandler = require('../utils/asyncHandler'); const usersService = require('../services/users.service'); const listUsers = asyncHandler(async (req, res) => { const users = await usersService.listUsers(); res.json({ data: users }); }); module.exports = { listUsers };Guía práctica paso a paso: montar la plantilla base
Paso 1: crea la estructura de carpetas
mkdir -p src/app src/routes src/controllers src/services src/config src/middleware src/utils src/testsPaso 2: crea app.js y server.js
// src/app/app.js const express = require('express'); const routes = require('../routes'); const { notFound, errorHandler } = require('../middleware/error.middleware'); const app = express(); app.use(express.json()); app.use('/api', routes); app.use(notFound); app.use(errorHandler); module.exports = app;// src/app/server.js const app = require('./app'); const { env } = require('../config/env'); app.listen(env.port, () => { console.log(`API listening on port ${env.port}`); });Paso 3: agrega configuración centralizada
// src/config/env.js const env = { port: Number(process.env.PORT || 3000), nodeEnv: process.env.NODE_ENV || 'development' }; module.exports = { env };Paso 4: define rutas y controlador de ejemplo
// src/routes/users.routes.js const express = require('express'); const { listUsers } = require('../controllers/users.controller'); const router = express.Router(); router.get('/', listUsers); module.exports = router;// src/routes/index.js const express = require('express'); const usersRoutes = require('./users.routes'); const router = express.Router(); router.use('/users', usersRoutes); module.exports = router;// src/controllers/users.controller.js const asyncHandler = require('../utils/asyncHandler'); const usersService = require('../services/users.service'); const listUsers = asyncHandler(async (req, res) => { const users = await usersService.listUsers(); res.json({ data: users }); }); module.exports = { listUsers };Paso 5: crea el servicio (lógica de negocio)
// src/services/users.service.js async function listUsers() { // En una app real, aquí consultarías la base de datos. return [{ id: 1, name: 'Ada' }, { id: 2, name: 'Linus' }]; } module.exports = { listUsers };Paso 6: manejo de errores consistente
// src/middleware/error.middleware.js function notFound(req, res, next) { res.status(404).json({ error: { message: 'Not Found' } }); } function errorHandler(err, req, res, next) { const status = err.statusCode || 500; res.status(status).json({ error: { message: err.message || 'Internal Server Error' } }); } module.exports = { notFound, errorHandler };Paso 7: utilidades comunes
// src/utils/logger.js function logInfo(message, meta) { console.log(message, meta || ''); } function logError(message, meta) { console.error(message, meta || ''); } module.exports = { logInfo, logError };Plantilla base lista para crecer
Esta plantilla resume una base mínima con separación por capas, lista para añadir más recursos, middlewares, configuración y pruebas:
src/ app/ app.js server.js routes/ index.js users.routes.js controllers/ users.controller.js services/ users.service.js config/ env.js db.js middleware/ auth.middleware.js error.middleware.js utils/ asyncHandler.js logger.js tests/ users.test.js| Capa | Pregunta guía | Ejemplo |
|---|---|---|
| routes | ¿Qué endpoint existe y a qué controlador apunta? | GET /api/users -> listUsers |
| controllers | ¿Cómo traduzco HTTP a una llamada de negocio y respuesta? | req.query -> service -> res.json |
| services | ¿Cuál es la regla/caso de uso? | listar usuarios, crear usuario |
| middleware | ¿Qué lógica transversal se aplica? | auth, errores, validación |
| config | ¿Qué parámetros cambian por entorno? | PORT, DB_URL |
| utils | ¿Qué helper es genérico y reutilizable? | asyncHandler, logger |