En esta página

Providers y servicios — inyección de dependencias

15 min lectura TextoCap. 2 — Servicios e inyección

¿Qué es un provider en NestJS?

Un provider es cualquier clase que puede ser inyectada como dependencia en otras clases por el sistema de IoC (Inversion of Control) de NestJS. Los servicios son el tipo más común de provider, pero también lo son los repositorios, las factories, los helpers y prácticamente cualquier clase que necesite ser compartida.

La inyección de dependencias (DI, Dependency Injection) es un patrón de diseño donde las dependencias de una clase se le proporcionan desde el exterior en lugar de que la clase las cree por sí misma. NestJS tiene un contenedor de DI integrado que gestiona la creación y el ciclo de vida de todos los providers automáticamente.

El decorador @Injectable

Cualquier clase que quieras que el contenedor de NestJS pueda inyectar debe estar decorada con @Injectable:

import { Injectable } from '@nestjs/common';

@Injectable()
export class UsuariosService {
  private usuarios: Usuario[] = [];

  findAll(): Usuario[] {
    return this.usuarios;
  }
}

Este decorador hace dos cosas: marca la clase como "inyectable" y emite metadatos de TypeScript que NestJS usa para resolver las dependencias del constructor automáticamente mediante reflect-metadata.

Inyectando un servicio en un controlador

Para usar un servicio en un controlador, simplemente decláralo en el constructor:

@Controller('usuarios')
export class UsuariosController {
  constructor(private readonly usuariosService: UsuariosService) {}

  @Get()
  findAll() {
    return this.usuariosService.findAll();
  }
}

NestJS resolverá automáticamente la dependencia gracias a los metadatos de TypeScript. El modificador readonly y private es una convención que garantiza que el servicio no sea reasignado accidentalmente.

Excepciones HTTP integradas

NestJS proporciona un conjunto de excepciones HTTP listas para usar que automáticamente generan respuestas de error apropiadas:

import {
  NotFoundException,         // 404
  BadRequestException,       // 400
  UnauthorizedException,     // 401
  ForbiddenException,        // 403
  ConflictException,         // 409
  InternalServerErrorException, // 500
  UnprocessableEntityException, // 422
} from '@nestjs/common';

@Injectable()
export class ProductosService {
  findOne(id: number): Producto {
    const producto = this.db.find(id);
    if (!producto) {
      throw new NotFoundException(`No se encontró el producto con ID ${id}`);
    }
    return producto;
  }
}

La respuesta automática tendrá este formato JSON:

{
  "statusCode": 404,
  "message": "No se encontró el producto con ID 42",
  "error": "Not Found"
}

Proveedores personalizados

La forma más simple de registrar un provider es simplemente añadir la clase a la lista providers de un módulo. Pero NestJS también ofrece una sintaxis extendida para casos más complejos:

useClass — Implementaciones intercambiables

import { Module } from '@nestjs/common';
import { AnalyticsService } from './analytics.service';
import { MockAnalyticsService } from './mock-analytics.service';

@Module({
  providers: [
    {
      provide: AnalyticsService,
      useClass: process.env['NODE_ENV'] === 'test'
        ? MockAnalyticsService
        : AnalyticsService,
    },
  ],
})
export class AnalyticsModule {}

useValue — Constantes y valores estáticos

Ideal para configuración, clientes de librerías externas o datos mock para testing:

const mockProductosService = {
  findAll: () => [{ id: 1, nombre: 'Producto mock' }],
  findOne: (id: number) => ({ id, nombre: 'Producto mock' }),
};

@Module({
  providers: [
    {
      provide: ProductosService,
      useValue: mockProductosService,
    },
  ],
})
export class TestModule {}

useFactory — Creación asíncrona con lógica

El patrón más flexible, permite crear providers con lógica de inicialización asíncrona:

@Module({
  providers: [
    {
      provide: 'REDIS_CLIENT',
      useFactory: async (): Promise<Redis> => {
        const client = new Redis({
          host: process.env['REDIS_HOST'],
          port: parseInt(process.env['REDIS_PORT'] ?? '6379', 10),
        });
        await client.ping(); // verifica la conexión antes de continuar
        return client;
      },
    },
  ],
})
export class CacheModule {}

useExisting — Alias de providers

Crea un alias para un provider existente, útil cuando quieres que una interfaz resuelva a una implementación concreta:

@Module({
  providers: [
    LoggerService,
    {
      provide: 'ILogger',
      useExisting: LoggerService, // 'ILogger' y LoggerService apuntan a la misma instancia
    },
  ],
})
export class AppModule {}

Scopes de vida de los providers

Por defecto, todos los providers en NestJS son singletons: se crean una vez cuando la aplicación arranca y se reutiliza la misma instancia en toda la vida de la aplicación. Pero puedes cambiar este comportamiento:

import { Injectable, Scope } from '@nestjs/common';

// DEFAULT — singleton (una instancia para toda la app)
@Injectable({ scope: Scope.DEFAULT })
export class ProductosService {}

// REQUEST — nueva instancia por cada petición HTTP
@Injectable({ scope: Scope.REQUEST })
export class AuditoriaService {}

// TRANSIENT — nueva instancia en cada punto de inyección
@Injectable({ scope: Scope.TRANSIENT })
export class ContadorService {}

Cuándo usar cada scope:

  • DEFAULT (singleton): el 95% de los casos. Servicios stateless que acceden a bases de datos, APIs externas, etc.
  • REQUEST: cuando necesitas datos específicos de la petición dentro del service (usuario actual, tenant ID, trace ID).
  • TRANSIENT: cuando necesitas garantizar que cada consumidor tenga su propia instancia (e.g. un buffer de escritura que no debe compartirse).

El scope se propaga

Una cosa importante: el scope se propaga hacia arriba. Si un servicio con scope REQUEST es inyectado en un servicio con scope DEFAULT, el servicio DEFAULT también se convierte en REQUEST. Esto puede tener implicaciones de rendimiento no deseadas.

// ⚠️ AuditoriaService (REQUEST) inyectado en ProductosService (DEFAULT)
// → ProductosService también pasa a ser REQUEST implícitamente
@Injectable()
export class ProductosService {
  constructor(private readonly auditoriaService: AuditoriaService) {}
}

Servicios asíncronos con OnModuleInit

Cuando un servicio necesita inicializarse de forma asíncrona al arrancar la aplicación (conexión a BD, carga de caché, etc.), implementa la interfaz OnModuleInit:

import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';

@Injectable()
export class DatabaseService implements OnModuleInit, OnModuleDestroy {
  private connection: DatabaseConnection | null = null;

  async onModuleInit(): Promise<void> {
    this.connection = await createDatabaseConnection();
    console.log('Base de datos conectada correctamente');
  }

  async onModuleDestroy(): Promise<void> {
    await this.connection?.close();
    console.log('Conexión a base de datos cerrada');
  }
}

Exportar servicios entre módulos

Un servicio solo puede ser inyectado en providers del mismo módulo, a menos que sea exportado. Para compartir un servicio entre módulos:

@Module({
  providers: [EmailService],
  exports: [EmailService], // ← ahora EmailService puede ser inyectado en otros módulos
})
export class EmailModule {}

// En otro módulo
@Module({
  imports: [EmailModule], // ← importar el módulo da acceso a sus exports
  providers: [UsuariosService],
})
export class UsuariosModule {}

// UsuariosService puede ahora inyectar EmailService
@Injectable()
export class UsuariosService {
  constructor(private readonly emailService: EmailService) {}
}

El sistema de DI de NestJS es uno de sus diferenciadores más importantes respecto a Express puro. Permite escribir código altamente desacoplado, testeable y mantenible. En la próxima lección, veremos cómo los Pipes complementan a los servicios validando los datos antes de que lleguen a la lógica de negocio.

Prefiere inject() de @nestjs/core en lugar de @Inject() del constructor
En NestJS moderno puedes usar `inject()` de `@nestjs/core` fuera del constructor para inyectar dependencias, similar a Angular. Esto hace el código más funcional y testeable: `private readonly service = inject(UsuariosService);`
Scope REQUEST para APIs multi-tenant
El scope `REQUEST` crea una nueva instancia del provider por cada petición HTTP. Es útil cuando necesitas acceder al objeto `Request` dentro de un service (como para multi-tenancy o para logs con contexto de usuario). Sin embargo, tiene un costo de rendimiento —úsalo solo cuando sea necesario.