Saltar al contenido principal

Inyección de Dependencias y Providers

En las guías anteriores exploramos los fundamentos de NestJS, creamos controladores y servicios en memoria, y modelamos entidades relacionales con TypeORM y PostgreSQL. Sin embargo, para que todos estos elementos operen armónicamente en un backend escalable y mantenible, es indispensable comprender cómo se comunican las capas entre sí: la Inversión de Control (IoC), la Inyección de Dependencias (DI) y los Providers.


1. La Arquitectura por Capas en NestJS

Una aplicación backend moderna desacopla responsabilidades en niveles bien delimitados para que cada pieza cumpla un propósito único. En NestJS, la información fluye a través de capas estructuradas:

Arquitectura por capas en NestJS
  • Cliente: Navegador, aplicación móvil o cliente HTTP que inicia la petición y consume la respuesta.
  • Controlador (@Controller): Punto de entrada de las solicitudes HTTP; enruta URLs, extrae parámetros y delega el trabajo al servicio.
  • Servicio (@Injectable): Contiene las reglas del negocio, validaciones de dominio y orquestación de operaciones.
  • Repositorio (Repository<T>): Abstrae el acceso a datos mediante TypeORM, traduciendo operaciones orientadas a objetos en consultas SQL seguras.
  • Base de Datos (PostgreSQL): Motor de persistencia física de tablas y registros.

2. Acoplamiento Fuerte frente a Inversión de Control

Ahora que conocemos las capas, la interrogante fundamental es: ¿cómo obtiene el controlador una instancia de su servicio o repositorio?

Enfoque tradicional acoplado (Instanciación manual con new)

En la programación tradicional sin contenedores de dependencias, una clase crea directamente a sus dependencias mediante new:

src/users/users.controller.ts (Sin Inversión de Control)
export class UsersController {
private usersService: UsersService;

constructor() {
// Acoplamiento rígido: el controlador crea la instancia directamente
this.usersService = new UsersService();
}

getUsers() {
return this.usersService.findAll();
}
}

Este esquema genera serios problemas arquitectónicos:

  • Acoplamiento rígido: Si UsersService cambia su constructor en el futuro (por ejemplo, para requerir una conexión de base de datos o un repositorio), todos los controladores que lo instanciaban con new dejarán de compilar y deberán reescribirse.
  • Dificultad extrema en pruebas unitarias: Resulta imposible sustituir el servicio real por un sustituto simulado (mock), impidiendo aislar el controlador para pruebas de unidad.
  • Duplicación ineficiente de memoria: Cada consumidor crea una nueva instancia de la dependencia, desperdiciando recursos.

Enfoque con Inversión de Control (IoC) e Inyección de Dependencias (DI)

En NestJS se adopta el patrón de Inversión de Control (IoC). La clase consumidora ya no se preocupa por fabricar lo que necesita; únicamente declara sus necesidades en los parámetros de su constructor.

El Contenedor IoC de NestJS se encarga de instanciar los componentes, resolver el grafo de dependencias en el orden correcto y suministrarlos listos para su uso:

Contenedor IoC de NestJS y suministro de dependencias
CriterioInstanciación Manual (new)Inyección de Dependencias (IoC)
Responsabilidad de creaciónLa clase consumidora crea sus dependenciasEl Contenedor IoC de NestJS las construye automáticamente
Nivel de acoplamientoAlto (depende de clases e implementaciones concretas)Bajo (las clases declaran contratos o dependencias desacopladas)
Pruebas unitariasComplejas (no permite inyectar mocks)Sencillas (basta suministrar objetos falsos en el constructor)
MantenimientoFrágil ante cambios en los constructoresCentralizado y gestionado por los módulos de NestJS

3. ¿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.

La gran mayoría de las clases con lógica operativa en NestJS son providers:

  • Servicios (Services): Para las reglas de negocio de la aplicación.
  • Repositorios (Repositories): Para interactuar con las tablas mediante TypeORM.
  • Clientes de API o adaptadores: Para consumir servicios externos (pasarelas de pago, correos, microservicios).
  • Helpers y utilidades: Clases con funciones de cálculo o transformación reutilizables.

El decorador @Injectable()

El decorador @Injectable() adjunta metadatos de reflexión que le indican al motor de NestJS que esta clase puede ser inyectada automáticamente donde sea requerida:

src/users/users.service.ts
import { Injectable } from '@nestjs/common';

export interface UserItem {
id: number;
email: string;
}

@Injectable()
export class UsersService {
private readonly users: UserItem[] = [
{ id: 1, email: 'ada@icesi.edu.co' },
{ id: 2, email: 'alan@icesi.edu.co' },
];

findAll(): UserItem[] {
return this.users;
}

findById(id: number): UserItem | undefined {
return this.users.find((u) => u.id === id);
}
}

4. Inyección Basada en Constructor

NestJS utiliza principalmente la inyección basada en constructor. Las dependencias requeridas se declaran como parámetros del constructor.

Al anteponer el modificador private readonly en TypeScript, la propiedad de la clase se declara y se asigna de manera simultánea en una sola línea:

src/users/users.controller.ts
import { Controller, Get, Param, ParseIntPipe, NotFoundException } from '@nestjs/common';
import { UsersService, UserItem } from './users.service';

@Controller('users')
export class UsersController {
// Inyección por constructor:
// NestJS identifica el tipo UsersService y suministra la instancia automáticamente
constructor(private readonly usersService: UsersService) {}

@Get()
getAll(): UserItem[] {
return this.usersService.findAll();
}

@Get(':id')
getById(@Param('id', ParseIntPipe) id: number): UserItem {
const user = this.usersService.findById(id);
if (!user) {
throw new NotFoundException(`Usuario con ID ${id} no encontrado`);
}
return user;
}
}
Buenas prácticas: Modificador readonly

Declarar las dependencias inyectadas como readonly impide que sean reasignadas accidentalmente en los métodos de la clase a lo largo de su ciclo de vida.


5. Organización Modular: Estructura de @Module()

Para que el contenedor IoC sepa qué providers existen y cuáles componentes tienen permiso para consumirlos, deben registrarse dentro de un Módulo.

Un módulo es una clase decorada con @Module() que delimita el alcance y encapsulación de sus elementos:

Estructura de propiedades de @Module en NestJS

Propiedades Principales de @Module()

  1. controllers: [ ... ]: Controladores que pertenecen a este módulo y deben ser instanciados para escuchar rutas HTTP.
  2. providers: [ ... ]: Servicios y providers instanciados y administrados por el contenedor IoC dentro del módulo. Por defecto, son privados al módulo.
  3. imports: [ ... ]: Lista de otros módulos cuyas exportaciones son requeridas dentro del módulo actual.
  4. exports: [ ... ]: Subconjunto de providers de este módulo que se hacen públicos para que otros módulos que importen a este puedan inyectarlos.
src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
imports: [], // Módulos importados
controllers: [UsersController], // Controladores que escuchan rutas
providers: [UsersService], // Providers registrados en el módulo
exports: [UsersService], // Hace público a UsersService para otros módulos
})
export class UsersModule {}

6. Diagnóstico de Errores Frecuentes

Error: Nest can't resolve dependencies of the UsersController (?)

Este es el error más recurrente cuando se trabaja con inyección de dependencias en NestJS. Ocurre cuando un componente solicita un provider en su constructor pero el contenedor IoC no encuentra instrucciones para resolverlo en el módulo actual:

Consola de Error de NestJS
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 solucionarlo:

  1. Verificar @Injectable(): Confirma que la clase del servicio tenga el decorador @Injectable() sobre su declaración.
  2. Verificar providers: Comprueba que el servicio esté incluido en el arreglo providers del módulo (UsersModule).
  3. Verificar exports e imports: Si el servicio proviene de otro módulo (por ejemplo AuthModule), verifica que AuthModule tenga al servicio en su lista de exports y que UsersModule tenga a AuthModule en su lista de imports.

Autoevaluación

Cargando cuestionario...