🚨 ¡Nueva review! 🔇 Los mejores cascos con ANC del mercado: los Sony WH-1000XM4 . ¡Échale un ojo! 👀

Providers y Dependency Injection: el core de NestJS

Serie NestJS #3 — Inyección de dependencias, custom providers y scopes

Escrito por domin el 22 de marzo de 2026

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.

Diagrama de inyección de dependencias en NestJS con flechas conectando providers.

1. ¿Qué es un Provider?

Un 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?

  1. NestJS ve que UsersController necesita un UsersService, porque está declarado como parámetro del constructor con su tipo.
  2. Busca en el si ya tiene una instancia de UsersService.
  3. Si no la tiene, la crea y la guarda (por defecto es singleton ).
  4. Se la pasa al controller.

Tú no tienes que hacer new UsersService(), porque se hace de forma automática.

🔗 Desacoplamiento

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.

🧪 Testabilidad

En tests puedes inyectar un mock en vez del service real. Como el controller recibe la dependencia desde fuera, sustituirla es super sencillo.

♻️ Singleton automático

Por defecto, una sola instancia compartida. Si 5 controllers necesitan UsersService, todos comparten la misma instancia.

⏱️ Gestión del ciclo de vida

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:

  1. Declara una propiedad usersService en la clase.
  2. La inicializa con el valor que recibe el constructor.
  3. La marca como private que significa que será solo accesible dentro de la clase y readonly, 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

TipoCuándo usarloEjemplo
useClassIntercambiar implementaciones según contextoDev vs Prod config, estrategias
useValueConstantes, objetos de config, mocksAPI keys, feature flags, testing
useFactoryCreación dinámica, async, depende de otros providersConexiones DB, clientes HTTP
useExistingAlias, múltiples tokens → misma instanciaRetrocompatibilidad, 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:

🔷 DEFAULT (Singleton)

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.

🔄 REQUEST

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).

🆕 TRANSIENT

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:

Úsalo cuando...
  • 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
Evítalo cuando...
  • 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

💉 @Injectable()

Marca una clase como provider gestionado por el contenedor IoC de NestJS. Sin esto, NestJS no la inyecta.

🏗️ Constructor DI

Declaras la dependencia en el constructor con su tipo y NestJS la resuelve automáticamente. Nunca haces new.

🔧 Custom Providers

useClass, useValue, useFactory y useExisting te dan control total sobre qué se inyecta y cómo se crea.

🔑 Tokens

La clave para buscar un provider: clase (automático), string (@Inject necesario) o Symbol (único garantizado).

🔄 Scopes

DEFAULT (singleton), REQUEST (por petición) y TRANSIENT (por inyección). Usa singleton salvo que necesites otro.

⚠️ Scope Bubbling

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()?