Email copiado — support@tuurt.com
Cargando experiencia
nestjs · 05 de mayo de 2026 · 9 min

Estructura de un proyecto NestJS escalable: módulos, capas y boundaries

Las decisiones de arquitectura de las primeras semanas determinan cuánto duele el proyecto en el mes 6. Cómo organizar módulos por dominio, separar capas y mantener boundaries limpios en NestJS.

Por Equipo Tuurt

Estructura de un proyecto NestJS escalable: módulos, capas y boundaries

NestJS da estructura desde el día uno, pero esa estructura inicial no escala sola. Un proyecto con 5 módulos y 20 endpoints tiene problemas distintos a uno con 30 módulos, equipos paralelos y lógica de negocio que crece semana a semana.

Este post es sobre las decisiones que tomás (o no tomás) en las primeras semanas y que determinan cuánto duele el proyecto en el mes 6.


El error más común: módulos por tipo, no por dominio

La estructura que aparece en todos los tutoriales:

src/
  controllers/
  services/
  repositories/
  dto/

Funciona para un tutorial. No funciona para un proyecto real. Cuando tenés 15 controllers, 15 services y 15 repos, encontrar todo lo relacionado con "Pagos" significa navegar 4 carpetas distintas.

La alternativa es organizar por dominio:

src/
  users/
    users.module.ts
    users.controller.ts
    users.service.ts
    users.repository.ts
    dto/
  orders/
    orders.module.ts
    orders.controller.ts
    ...
  payments/
    ...

Cada dominio es una unidad cohesiva. Todo lo que tiene que ver con Users vive en users/. Cuando alguien toca payments, sabe exactamente dónde mirar.


Las capas dentro de cada módulo

Dentro de cada módulo de dominio, la separación de capas es lo que mantiene la lógica manejable:

Entry layer (controllers): solo recibe el request HTTP, valida el DTO, llama al servicio, devuelve la respuesta. Cero lógica de negocio. Si tu controller hace más de 3 cosas, algo está mal.

Application layer (services): orquesta los casos de uso. Llama a repositorios, lanza eventos, coordina con otros servicios. Acá vive la lógica de negocio, pero no los detalles de persistencia.

Domain layer (entities, value objects): las reglas del negocio que no dependen de ninguna infraestructura. Un Order sabe si puede ser cancelado; no sabe nada de HTTP ni de SQL.

Infrastructure layer (repositories, adapters): los detalles de persistencia, llamadas externas, colas. Implementa interfaces definidas en la capa de aplicación.

En proyectos chicos, la separación entre application y domain puede ser excesiva. En proyectos que van a crecer, es la diferencia entre poder testear la lógica de negocio sin base de datos o no.


Boundaries: la regla que más se viola

Un boundary es el límite de lo que un módulo puede ver del resto del sistema. En NestJS, se define con qué exportás en cada módulo.

El error típico: exportar todo y que cualquier módulo importe lo que quiera de cualquier otro. Eso se ve así:

// orders.module.ts — exporta todo
@Module({
  exports: [OrdersService, OrdersRepository, OrdersMapper],
})

Y termina con PaymentsService usando directamente el OrdersRepository porque "era más fácil". Ahora Payments conoce los detalles de persistencia de Orders. Eso es un boundary roto.

La regla: exportar solo lo que otros módulos necesitan conocer. Casi siempre es solo el Service.

// orders.module.ts — boundary limpio
@Module({
  exports: [OrdersService], // solo la interfaz pública
})

Si otro módulo necesita hacer algo con Orders, lo hace a través del servicio. El repositorio y los detalles internos son privados al módulo.


El SharedModule: útil y peligroso a la vez

El SharedModule es donde van las cosas que todos necesitan: utilidades, guards, pipes globales, helpers. Es conveniente y por eso se abusa de él.

El síntoma de un SharedModule enfermo: crece sin control y termina siendo un cajón de sastre con 40 providers. Cuando todo está en Shared, nada tiene dueño claro.

Reglas para mantenerlo sano:

  • Solo entra lo que es verdaderamente transversal: auth guards, pipes de validación, utilidades de fecha/string, interceptors globales.
  • Si algo solo lo usan 2 o 3 módulos, no va a Shared — va al módulo que más sentido tenga como dueño y se exporta selectivamente.
  • Revisalo cada mes. Si está creciendo, es señal de que algo que debería tener su propio módulo se está escondiendo ahí.

Dependencias circulares: cómo evitarlas antes de que aparezcan

Las dependencias circulares en NestJS son un problema en tiempo de ejecución, no en compilación. Aparecen tarde, son difíciles de debuggear y casi siempre son síntoma de un diseño que mezcla responsabilidades.

La causa más común: UsersService necesita a OrdersService y OrdersService necesita a UsersService.

Soluciones antes de llegar a forwardRef():

Opción 1 — Eventos: en lugar de que A llame a B directamente, A emite un evento y B escucha. El EventEmitter de NestJS o un message bus interno rompe la dependencia directa.

Opción 2 — Módulo de coordinación: si A y B se necesitan mutuamente, probablemente hay una tercera responsabilidad que debería estar en un módulo C. Extraerla resuelve la circularidad.

Opción 3 — Mover la lógica compartida: si la lógica que genera la circularidad es pequeña, moverla al SharedModule o a un módulo common/ puede resolver el problema sin complicar la arquitectura.

forwardRef() es el último recurso, no la primera solución.


Hexagonal vs feature-folder: cuándo usar cada uno

Feature-folder (lo que describimos hasta acá) es la opción pragmática. Módulos por dominio, capas internas, boundaries claros. Funciona bien para la mayoría de los proyectos de producto.

Hexagonal (ports & adapters) tiene sentido cuando:

  • El dominio tiene lógica compleja que necesita testearse de forma aislada.
  • Hay múltiples adaptadores de entrada (HTTP, gRPC, eventos) o salida (distintas bases de datos, servicios externos intercambiables).
  • El equipo tiene experiencia con el patrón y el proyecto tiene tiempo de vida largo.

En la práctica: empezar con feature-folder bien estructurado. Migrar partes específicas a hexagonal cuando el dominio lo justifique. No arrancar con hexagonal puro desde el día uno a menos que tengas razones concretas.


La estructura que funciona para escalar

src/
  common/
    guards/
    pipes/
    interceptors/
    decorators/
  config/
  database/
  users/
    dto/
    entities/
    users.controller.ts
    users.service.ts
    users.repository.ts
    users.module.ts
  orders/
  payments/
  app.module.ts
  main.ts

Sin carpetas controllers/, services/, repositories/ en el nivel raíz. Sin SharedModule inflado. Cada dominio con su propio directorio y módulo.


Conclusión

NestJS te da las piezas. La arquitectura la ponés vos. Las decisiones de cómo organizar módulos, dónde poner los boundaries y cómo separar las capas no son cosméticas — determinan cuánto puede crecer el proyecto antes de que el costo de cada cambio empiece a subir.

Invertir un día en pensar la estructura al inicio vale más que dos semanas de refactor en el mes 8.

nestjs arquitectura node typescript backend buenas-practicas
← Volver al blog