Estructura de la Aplicación y Providers en NestJS
Hasta este punto, hemos explorado cómo diseñar bases de datos relacionales y cómo modelar tablas en el código mediante entidades de TypeORM. Sin embargo, en una aplicación web profesional de backend, las entidades son solo una pieza dentro de un engranaje mucho más amplio.
Para construir un sistema escalable, mantenible y profesional, es indispensable organizar el código siguiendo una arquitectura por capas y comprender el mecanismo que conecta estas capas sin acoplarlas rígidamente: la Inversión de Control (IoC) y los Providers.
La Arquitectura por Capas en NestJS
Una aplicación moderna no conecta directamente la interfaz visual con la base de datos. En su lugar, distribuye las responsabilidades en niveles bien delimitados para que cada parte cumpla un único propósito.
Para comprender cómo viaja la información, podemos observar el sistema desde dos niveles de abstracción:
El problema del acoplamiento fuerte frente a la Inversión de Control
Ahora que conocemos las capas, surge una pregunta clave: ¿cómo se conectan el controlador, el servicio y el repositorio entre sí?
Enfoque tradicional acoplado (Instanciación manual con new)
En la programación tradicional, cuando una clase necesita a otra, la crea directamente usando el operador new:
export class UsersController {
private usersService: UsersService;
constructor() {
// Acoplamiento directo: el controlador se encarga de crear el servicio
this.usersService = new UsersService();
}
getUsers() {
return this.usersService.findAll();
}
}
Este enfoque presenta serios inconvenientes:
- Acoplamiento rígido: Si el constructor de
UsersServicecambia en el futuro para requerir una conexión o un repositorio, todos los controladores que lo instanciaban connewse romperán y deberán ser modificados manualmente. - Imposibilidad de realizar pruebas unitarias: No es posible aislar el controlador para probarlo con un servicio falso o simulado (mock), ya que el controlador crea forzosamente la instancia real.
- Duplicación ineficiente de memoria: Cada vez que se crea un objeto, se instancian nuevamente todas sus dependencias en cascada.
Enfoque con Inversión de Control (IoC) e Inyección de Dependencias
Para resolver esto, NestJS implementa el patrón de Inversión de Control (IoC - Inversion of Control). La clase ya no se encarga de "fabricar" lo que necesita; en su lugar, simplemente declara sus necesidades en su constructor.
El Contenedor IoC de NestJS es el encargado de instanciar los componentes, resolver el árbol de dependencias y suministrarlos listos para su uso:
| Criterio | Instanciación Manual (new) | Inyección de Dependencias (IoC) |
|---|---|---|
| Responsabilidad de creación | Cada clase consumidora crea sus dependencias | El Contenedor IoC de NestJS las construye automáticamente |
| Grado de acoplamiento | Alto (las clases dependen de implementaciones concretas) | Bajo (las clases declaran contratos o dependencias desacopladas) |
| Pruebas unitarias | Muy difíciles (no permite inyectar mocks) | Sencillas (basta suministrar objetos simulados en el constructor) |
| Mantenimiento | Frágil ante cambios en los constructores | Centralizado y gestionado por los módulos de NestJS |
¿Qué es un Provider en NestJS?
En NestJS, un Provider es cualquier clase simple de TypeScript decorada con @Injectable() que puede ser gestionada por el contenedor IoC y suministrada como dependencia a otros componentes del sistema.
La inmensa mayoría de las clases que contienen lógica en NestJS son providers:
- Servicios (
Services): Para la lógica de negocio de la aplicación. - Repositorios (
Repositories): Para interactuar con TypeORM y las tablas. - Adaptadores o Clientes de API: Para comunicarse con servicios externos (pasarelas de pago, correos).
- Helpers o Utilidades: Clases con funciones auxiliares compartidas.
El decorador @Injectable()
El decorador @Injectable() adjunta metadatos de reflexión a la clase. Esto le indica al motor de NestJS: "esta clase es un provider y debe poder inyectarse en los componentes que la soliciten".
import { Injectable } from "@nestjs/common";
export interface User {
id: number;
name: string;
email: string;
}
// Marcamos la clase como un provider gestionado por NestJS
@Injectable()
export class UsersService {
private readonly users: User[] = [
{ id: 1, name: "Ada Lovelace", email: "ada@icesi.edu.co" },
{ id: 2, name: "Alan Turing", email: "alan@icesi.edu.co" },
];
findAll(): User[] {
return this.users;
}
findById(id: number): User | undefined {
return this.users.find((user) => user.id === id);
}
}
Cómo funciona la Inyección por Constructor
NestJS utiliza principalmente la inyección basada en constructor. En este esquema, las dependencias requeridas se declaran como parámetros dentro del constructor de la clase consumidora.
Aprovechando las capacidades de TypeScript, al anteponer un modificador de acceso como private readonly en el constructor, se crea y asigna la propiedad de forma automática:
import { Controller, Get, Param, ParseIntPipe, NotFoundException } from "@nestjs/common";
import { UsersService, User } from "./users.service";
@Controller("users")
export class UsersController {
// Inyección por constructor:
// NestJS analiza el tipo 'UsersService' y le suministra la instancia automáticamente
constructor(private readonly usersService: UsersService) {}
@Get()
getAll(): User[] {
return this.usersService.findAll();
}
@Get(":id")
getById(@Param("id", ParseIntPipe) id: number): User {
const user = this.usersService.findById(id);
if (!user) {
throw new NotFoundException(`Usuario con ID ${id} no encontrado`);
}
return user;
}
}
Es una buena práctica declarar las dependencias inyectadas como readonly. Esto previene reasignaciones accidentales de la variable a lo largo del código de la clase.
Organización en Módulos: La Estructura de @Module()
Para que el contenedor IoC de NestJS sepa qué providers existen y quiénes pueden usarlos, debemos registrarlos dentro de un Módulo.
Un módulo en NestJS es una clase decorada con @Module(). Funciona como una frontera de encapsulación que agrupa controladores y providers relacionados:
Las cuatro propiedades de @Module()
El decorador @Module() recibe un objeto con cuatro propiedades principales:
-
controllers: [ ... ]:- Declara los controladores que pertenecen a este módulo.
- NestJS los instancia y los registra en el enrutador HTTP para que comiencen a escuchar peticiones.
-
providers: [ ... ]:- Declara los servicios y clases auxiliares que el contenedor IoC del módulo debe instanciar.
- Regla de encapsulación: Por defecto, los providers registrados aquí son privados al módulo. Solo pueden ser inyectados en los controladores o en otros providers de este mismo módulo.
-
imports: [ ... ]:- Lista de otros módulos cuyas capacidades exportadas son necesarias dentro de este módulo.
- Por ejemplo, si necesitamos interactuar con TypeORM, importamos
TypeOrmModule.forFeature([User]).
-
exports: [ ... ]:- Lista de providers de este módulo que deseamos hacer públicos.
- Cualquier otro módulo que incluya a este módulo en su arreglo
importspodrá inyectar los providers que hayan sido exportados aquí.
Ejemplo de Módulo Completo
import { Module } from "@nestjs/common";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";
@Module({
imports: [], // Otros módulos que aportan dependencias externas
controllers: [UsersController], // Controladores que escuchan rutas HTTP
providers: [UsersService], // Providers instanciados en este módulo
exports: [UsersService], // Hace público a UsersService para otros módulos
})
export class UsersModule {}
Diagnóstico y Solución de Errores Frecuentes
Error: Nest can't resolve dependencies of the X (?)
Este es el mensaje de error más común al trabajar con dependencias en NestJS. Ocurre cuando un componente solicita un provider en su constructor pero NestJS no encuentra ninguna instrucción para construirlo dentro del contexto del módulo actual.
Error: Nest can't resolve dependencies of the UsersController (?).
Please make sure that the argument UsersService at index [0] is available in the UsersModule context.
Potential solutions:
- If UsersService is a provider, is it part of the current UsersModule?
- If UsersService is exported from a separate @Module, is that module imported within UsersModule?
Lista de verificación para resolverlo:
- ¿Decorador presente?: Revisa que la clase del servicio tenga el decorador
@Injectable()sobre su declaración. - ¿Registrado en
providers?: Verifica que el servicio esté incluido en el arregloprovidersdel módulo actual (UsersModule). - ¿Proviene de otro módulo?: Si el servicio pertenece a otro módulo (por ejemplo
AuthModule), confirma queAuthModulelo haya listado enexports, y que el módulo actual haya agregadoAuthModuleen su lista deimports.