Estructura y organización de un backend Node.js

Capítulo 3

Tiempo estimado de lectura: 8 minutos

+ Ejercicio

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.js

Notas 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 app y llama a listen. Mantenerlo separado facilita pruebas (puedes importar app sin 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.js puede centralizar el montaje de rutas para mantener app.js limpio.

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.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

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/ o models/), 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 evitar try/catch repetidos 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.js para 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.js contiene reglas relacionadas con usuarios.
  • Baja cohesión: un archivo helpers.js con 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 lee process.env en cada archivo.
  • Evita importaciones “cruzadas” entre recursos (por ejemplo, users.controller importando orders.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_CASE cuando 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.js si 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/tests

Paso 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
CapaPregunta guíaEjemplo
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

Ahora responde el ejercicio sobre el contenido:

¿Cuál es la organización correcta de responsabilidades entre app.js y server.js en un backend con Express para facilitar pruebas y mantener una estructura escalable?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Separar responsabilidades permite importar app en pruebas sin abrir un puerto. app.js configura Express (middlewares, rutas y errores) y server.js es el punto de arranque que ejecuta listen.

Siguiente capítulo

Servidor HTTP con Express en Node.js

Arrow Right Icon
Portada de libro electrónico gratuitaNode.js para principiantes: crea un backend simple con Express
25%

Node.js para principiantes: crea un backend simple con Express

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.