Tercer post de la serie NestJS! el tercero de tropecientosmil que hay planeados! Y ya sabemos montar el entorno con Docker (post 1) y manejar Controllers con rutas, parámetros y DTOs (post 2). Hoy toca entender el mecanismo que hace que todo funcione por debajo que es la inyección de dependencias.
En el post anterior, cuando hacíamos constructor(private readonly usersService: UsersService) en el controller, ¿quién creaba esa instancia de UsersService? ¿De dónde salía? No hicimos new UsersService() en ningún lado. Pues detrás de esa magia hay un sistema entero de resolución de dependencias que es el core de NestJS.
EA, amo a verlo.

1. ¿Qué es un Provider?
Un 💡 Provider Cualquier clase decorada con @Injectable() que puede ser inyectada como dependencia en otras clases. Services, repositories, factories, helpers... todo son providers en NestJS. El contenedor IoC los gestiona automáticamente. Más info → es cualquier clase que puede ser inyectada en otra. Servicios, repositorios, factories, helpers, configuraciones… en NestJS todo eso son providers.
Lo que hace que una clase sea un provider es el decorador @Injectable():
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
private users: User[] = [];
findAll(): User[] {
return this.users;
}
}
@Injectable() le dice a NestJS que la clase está disponible para el sistema de inyección de dependencias, sin esta clase, NestJS no sabe que la clase existe y no puede inyectarla.
2. Inyección por constructor
La principal forma recomendada de inyectar dependencias en NestJS es a través del constructor:
@Controller('users')
export class UsersController {
constructor(
private readonly usersService: UsersService,
) {}
}
¿Qué pasa aquí por debajo?
- NestJS ve que
UsersControllernecesita unUsersService, porque está declarado como parámetro del constructor con su tipo. - Busca en el 💡 contenedor IoC Inversion of Control Container. El sistema de NestJS que almacena y gestiona todas las instancias de los providers. Se encarga de crearlos, inyectarlos donde hagan falta y controlar su ciclo de vida (singleton por defecto).
Más info →
si ya tiene una instancia de
UsersService. - Si no la tiene, la crea y la guarda (por defecto es singleton singleton Una única instancia compartida en toda la aplicación. La primera vez que se necesita, se crea. Las siguientes veces, se reutiliza la misma. ).
- Se la pasa al controller.
Tú no tienes que hacer new UsersService(), porque se hace de forma automática.
El controller no sabe cómo se crea el service, solo sabe que lo necesita. Si mañana cambias la implementación, al controller le da igual.
En tests puedes inyectar un mock en vez del service real. Como el controller recibe la dependencia desde fuera, sustituirla es super sencillo.
Por defecto, una sola instancia compartida. Si 5 controllers necesitan UsersService, todos comparten la misma instancia.
NestJS controla cuándo se crea y cuándo se destruye cada provider.
El truco del private readonly
Esto es TypeScript, no de NestJS, pero cuando escribes private readonly usersService: UsersService en el constructor, TypeScript automáticamente:
- Declara una propiedad
usersServiceen la clase. - La inicializa con el valor que recibe el constructor.
- La marca como
privateque significa que será solo accesible dentro de la clase yreadonly, solo lectura y no se puede reasignar el valor.
Es lo mismo que hacer esto pero en una línea:
export class UsersController {
private readonly usersService: UsersService;
constructor(usersService: UsersService) {
this.usersService = usersService;
}
}
Esto ya por defecto lo escribo así, en una línea, verlo a la forma antigua me da toc. 😂
3. Registrar providers en un módulo
Para que NestJS sepa que un provider existe, hay que registrarlo en el array providers de un módulo:
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
Esto que parece tan simple en realidad es un atajo. Cuando escribes providers: [UsersService], NestJS lo interpreta como:
providers: [
{
provide: UsersService, // El token (la "clave" para buscar)
useClass: UsersService, // La clase que se instanciará
},
]
El token es lo que NestJS usa para buscar el provider cuando alguien lo necesita, por defecto es la propia clase. Pero como veremos ahora, puedes cambiar tanto el token como la implementación.
4. Custom Providers
NestJS te da cuatro formas de definir un provider, y cada una tiene su caso de uso, vamos a verlos:
4.1. useClass — Cambiar la implementación
Permite que un token resuelva a una clase diferente. Por ejemplo usar una implementación distinta según el entorno:
const configProvider = {
provide: ConfigService,
useClass:
process.env.NODE_ENV === 'development'
? DevConfigService
: ProdConfigService,
};
@Module({
providers: [configProvider],
})
export class AppModule {}
Cuando alguien inyecta ConfigService, recibe DevConfigService en desarrollo y ProdConfigService en producción, el código que consume el service no cambia.
4.2. useValue — Inyectar un valor constante
Inyecta un valor fijo como un objeto de configuración, una constante, o un mock para testing:
// Inyectar configuración
@Module({
providers: [
{
provide: 'API_CONFIG',
useValue: {
baseUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
},
},
],
})
export class AppModule {}
Para inyectarlo usas @Inject() con el token string:
@Injectable()
export class ApiService {
constructor(
@Inject('API_CONFIG') private readonly config: ApiConfig,
) {}
}
También es perfecto para mocks en testing:
const mockUsersService = {
findAll: () => [{ id: 1, name: 'Test User' }],
findOne: (id: number) => ({ id, name: 'Test User' }),
};
// En el test module
providers: [
{
provide: UsersService,
useValue: mockUsersService,
},
]
4.3. useFactory — Provider dinámico
Cuando necesitas lógica para crear el provider, dependencias asíncronas, o el valor depende de otros providers:
{
provide: 'DATABASE_CONNECTION',
useFactory: async (configService: ConfigService) => {
const connection = await createConnection({
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
database: configService.get('DB_NAME'),
});
return connection;
},
inject: [ConfigService], // Dependencias que la factory necesita
}
El array inject le dice a NestJS qué providers tiene que pasar como argumentos a la función factory. El orden de inject corresponde al orden de los parámetros de la función.
Las factories pueden ser async, NestJS espera a que la Promise se resuelva antes de usar el provider.
4.4. useExisting — Alias
Crea un alias, dos tokens que apuntan a la misma instancia:
@Module({
providers: [
LoggerService,
{
provide: 'LOGGER',
useExisting: LoggerService,
},
],
})
export class AppModule {}
Ahora puedes inyectar LoggerService directamente o con @Inject('LOGGER'), y ambos te dan la misma instancia. Ojo que useExisting reutiliza la instancia, useClass crearía una nueva.
Esto puede ir perfecto para ir implementando nuevo código sin romper el viejo que funciona correctamente.
Cuándo usar cada uno
| Tipo | Cuándo usarlo | Ejemplo |
|---|---|---|
useClass | Intercambiar implementaciones según contexto | Dev vs Prod config, estrategias |
useValue | Constantes, objetos de config, mocks | API keys, feature flags, testing |
useFactory | Creación dinámica, async, depende de otros providers | Conexiones DB, clientes HTTP |
useExisting | Alias, múltiples tokens → misma instancia | Retrocompatibilidad, interfaces |
5. Tokens de inyección
El token es la clave que NestJS usa para buscar un provider en el contenedor y hay tres tipos:
Referencia a clase (lo más común)
providers: [UsersService]
// Token: UsersService (la clase)
// Se inyecta directamente por tipo en el constructor
String
providers: [{ provide: 'API_KEY', useValue: 'abc123' }]
// Se inyecta con @Inject('API_KEY')
Symbol (token único garantizado)
export const CACHE_MANAGER = Symbol('CACHE_MANAGER');
providers: [{ provide: CACHE_MANAGER, useClass: RedisCacheService }]
// Se inyecta con @Inject(CACHE_MANAGER)
Los Symbols son útiles cuando tienes muchos string tokens y quieres evitar colisiones. Una buena práctica es definir los tokens como const en un archivo aparte:
// constants/tokens.ts
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
export const CACHE_MANAGER = Symbol('CACHE_MANAGER');
6. @Inject() y @Optional()
@Inject() — Para tokens no-clase
Cuando el token no es una clase (es un string o Symbol), necesitas @Inject() para decirle a NestJS cuál buscar:
@Injectable()
export class NotificationService {
constructor(
@Inject('MAILER_CONFIG') private readonly mailerConfig: MailerConfig,
) {}
}
@Optional() — Dependencia opcional
Si un provider puede no estar disponible y no quieres que NestJS lance un error:
@Injectable()
export class ReportService {
constructor(
@Optional() @Inject('ANALYTICS') private readonly analytics?: AnalyticsClient,
) {}
generateReport(): Report {
// analytics puede ser undefined si no está registrado
this.analytics?.track('report_generated');
return { /* ... */ };
}
}
Sin @Optional(), si el provider 'ANALYTICS' no está registrado, NestJS lanza un error al arrancar. Con @Optional() será undefined.
7. Injection Scopes: el ciclo de vida de un provider
Por defecto, todos los providers son singleton, es decir, una única instancia para toda la aplicación. Pero NestJS te da tres scopes:
Una sola instancia compartida en toda la app. Se crea al arrancar y vive hasta que se para la aplicación. Es el scope por defecto y el más eficiente.
Se crea una instancia nueva para cada petición HTTP. Cuando la petición termina, la instancia se destruye. Útil para datos que dependen del request (ej: usuario autenticado, tenant).
Cada vez que se inyecta, se crea una instancia nueva. Cada consumidor tiene su propia instancia dedicada. Útil para loggers con contexto.
Cómo se declaran
import { Injectable, Scope } from '@nestjs/common';
// Singleton (por defecto, no hace falta declararlo)
@Injectable()
export class CacheService {}
// Request scope
@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {}
// Transient scope
@Injectable({ scope: Scope.TRANSIENT })
export class LoggerService {}
Scope en custom providers
{
provide: 'CACHE',
useClass: RedisCacheService,
scope: Scope.TRANSIENT,
}
Scope bubbling: cuidado con esto
El scope REQUEST burbujea hacia arriba en la cadena de dependencias, si tienes:
UsersController → UsersService → RequestLoggerService (REQUEST)
UsersService depende de RequestLoggerService que es REQUEST-scoped. Esto hace que UsersService también se convierta en REQUEST-scoped automáticamente. Y a su vez, UsersController también. Toda la cadena hacia arriba se vuelve request-scoped.
Esto tiene impacto en rendimiento porque se crean instancias nuevas en cada petición, por eso la recomendación es:
- Singleton (DEFAULT) para el 99% de tus providers. Es el más eficiente y el más predecible
- REQUEST scope solo cuando realmente necesitas datos del request (multi-tenancy, usuario autenticado por petición)
- TRANSIENT para loggers con contexto o providers que necesitan estado independiente por consumidor
- REQUEST scope innecesariamente. Arrastra a toda la cadena de dependencias y crea instancias en cada petición
- Ignorar el scope bubbling. Un solo provider REQUEST puede hacer que medio árbol de dependencias deje de ser singleton
- REQUEST scope en WebSocket Gateways, Passport strategies o Cron controllers (deben ser singleton)
Acceder al Request en un provider REQUEST-scoped
import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { type Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class TenantService {
constructor(@Inject(REQUEST) private readonly request: Request) {}
getTenantId(): string {
return this.request.headers['x-tenant-id'] as string;
}
}
8. Ejemplo práctico: ampliando el módulo Users
Vamos a aplicar todo esto al proyecto que construimos en los posts anteriores. Vamos a añadir un provider de configuración y un logger con scope transient al módulo de usuarios.
8.1. Token y config provider
// src/users/constants.ts
export const USERS_CONFIG = 'USERS_CONFIG';
export interface UsersModuleConfig {
maxUsersPerPage: number;
allowRegistration: boolean;
}
8.2. Logger transient
// src/users/users-logger.service.ts
import { Injectable, Scope, Inject } from '@nestjs/common';
import { INQUIRER } from '@nestjs/core';
@Injectable({ scope: Scope.TRANSIENT })
export class UsersLogger {
private context: string;
constructor(@Inject(INQUIRER) parentClass: object) {
this.context = parentClass?.constructor?.name ?? 'Unknown';
}
log(message: string): void {
const timestamp = new Date().toISOString();
console.warn(`[${timestamp}] [${this.context}] ${message}`);
}
}
Échale un ogt al INQUIRER: es un token especial de NestJS que te da la referencia a la clase donde se inyectó el provider. Así el logger sabe automáticamente desde qué clase se está llamando, sin que tengas que pasarle el nombre a mano.
8.3. Registrando los providers en el módulo
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UsersLogger } from './users-logger.service';
import { USERS_CONFIG, type UsersModuleConfig } from './constants';
@Module({
controllers: [UsersController],
providers: [
UsersService,
UsersLogger,
{
provide: USERS_CONFIG,
useValue: {
maxUsersPerPage: 20,
allowRegistration: true,
} satisfies UsersModuleConfig,
},
],
})
export class UsersModule {}
8.4. Usando los providers en el service
// src/users/users.service.ts
import { Injectable, NotFoundException, Inject } from '@nestjs/common';
import { type CreateUserDto } from './dto/create-user.dto';
import { type UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';
import { UsersLogger } from './users-logger.service';
import { USERS_CONFIG, type UsersModuleConfig } from './constants';
@Injectable()
export class UsersService {
private users: User[] = [];
private idCounter: number = 1;
constructor(
private readonly logger: UsersLogger,
@Inject(USERS_CONFIG) private readonly config: UsersModuleConfig,
) {}
create(createUserDto: CreateUserDto): User {
if (!this.config.allowRegistration) {
throw new Error('Registration is disabled');
}
const user: User = {
id: this.idCounter++,
...createUserDto,
active: true,
};
this.users.push(user);
this.logger.log(`Usuario creado: ${user.name} (id: ${user.id})`);
return user;
}
findAll(): User[] {
this.logger.log(`Listando ${this.users.length} usuarios`);
return this.users;
}
findOne(id: number): User {
const user = this.users.find((u) => u.id === id);
if (!user) {
throw new NotFoundException(`Usuario con id ${id} no encontrado`);
}
return user;
}
update(id: number, updateUserDto: UpdateUserDto): User {
const user = this.findOne(id);
Object.assign(user, updateUserDto);
this.logger.log(`Usuario actualizado: id ${id}`);
return user;
}
remove(id: number): void {
const index = this.users.findIndex((u) => u.id === id);
if (index === -1) {
throw new NotFoundException(`Usuario con id ${id} no encontrado`);
}
this.users.splice(index, 1);
this.logger.log(`Usuario eliminado: id ${id}`);
}
}
Ahora el UsersService recibe automáticamente el logger, que sabe que está siendo usado desde UsersService gracias al INQUIRER y la configuración del módulo, inyectado, tipado y limpio.
9. Recapitulando
Marca una clase como provider gestionado por el contenedor IoC de NestJS. Sin esto, NestJS no la inyecta.
Declaras la dependencia en el constructor con su tipo y NestJS la resuelve automáticamente. Nunca haces new.
useClass, useValue, useFactory y useExisting te dan control total sobre qué se inyecta y cómo se crea.
La clave para buscar un provider: clase (automático), string (@Inject necesario) o Symbol (único garantizado).
DEFAULT (singleton), REQUEST (por petición) y TRANSIENT (por inyección). Usa singleton salvo que necesites otro.
REQUEST scope burbujea hacia arriba. Un provider REQUEST hace que toda su cadena de dependencias también lo sea.
En el próximo post vamos a ver los Modules, que son los que organizan y encapsulan todo este tinglado. Veremos feature modules, shared modules, @Global(), y los dynamic modules con forRoot() y forRootAsync() que son la forma profesional de configurar módulos reutilizables.
EA, nos vemos en los bares!! 🍺
Pon a prueba lo aprendido
1. ¿Qué decorador marca una clase como provider inyectable en NestJS?
2. ¿Cuál es el scope por defecto de un provider en NestJS?
3. ¿Qué tipo de custom provider usarías para inyectar un mock en un test?
4. ¿Qué pasa con el scope si un provider singleton depende de un provider REQUEST-scoped?
5. ¿Cuándo necesitas usar el decorador @Inject()?