NestJS Guide EP10: Dependency Injection and Providers
Video
Watch the full video on YouTube:
Introduction
In the previous chapter we formally closed the Controllers module, having seen the @Req and @Res decorators to interact with request attributes and modify the response format. Now it’s time to dive into Services and understand one of NestJS’s fundamental design patterns: Dependency Injection.
NestJS uses a design pattern heavily oriented towards IOC (Inversion of Control), where the framework is responsible for managing the entire API flow, data transformations, and conversions. In this IOC container, a dependency map is created with a very special way of being registered.
Creating the First Service
Let’s start by creating our first service using the NestJS CLI:
pnpm nest g service pets
This command creates three files:
pets.service.spec.ts- testing filepets.service.ts- where we’ll work- Updates
pets.module.ts- importing the service
The updated module imports PetService and injects it into the providers attribute:
// pets.module.ts
import { PetService } from "./pets.service"
@Module({
controllers: [PetsController],
providers: [PetService], // NestJS registers the dependency here
})
export class PetsModule {}
The @Injectable Decorator
The pets.service.ts file comes empty, but this is where the first important decorator comes in: @Injectable. This decorator registers the class PetService to add it to NestJS’s IOC container dependency map.
import { Injectable } from "@nestjs/common"
@Injectable()
export class PetService {
// NestJS can inject this class into controllers or other services
}
The @Injectable decorator allows PetService to be injected into our controllers and even other services. To inject it, we need to register it in the module’s providers attribute.
Separating Responsibilities: Controllers vs Services
As a best practice, controllers should not contain business logic. They should simply be an entry point for frontends to connect to the API. The service is responsible for:
- Connecting to the database
- Consuming third-party services
- Building all the business logic
Let’s create methods in the service to handle CRUD operations:
@Injectable()
export class PetService {
public pets = [
{ id: 1, name: "Firulais", type: "dog" },
{ id: 2, name: "Michi", type: "cat" },
]
public findAll(name?: string) {
if (name) {
return this.pets.filter((pet) => pet.name === name)
}
return this.pets
}
public find(id: number) {
return this.pets.find((pet) => pet.id === id)
}
public store(input: any) {
// Logic to save
}
// update, partiallyUpdate, delete - as homework
}
Injecting Dependencies in the Constructor
In NestJS, we inject dependencies using the constructor. The syntax is:
@Controller("pets")
export class PetsController {
constructor(
private readonly petService: PetService, // Dependency injection
) {}
}
The modifiers are important:
- private - we don’t want external use outside the controller
- readonly - we won’t modify elements with it from here
- The
PetServicetype tells NestJS which class to inject
If PetService weren’t in the module’s providers array, this injection would fail.
Class Providers (useClass)
The way we’ve been saying providers: [PetService] is simply shorthand. The explicit form is:
@Module({
providers: [
{
provide: PetService,
useClass: PetService,
},
],
})
export class PetsModule {}
NestJS detects that when PetService is injected into a constructor, it should use the PetService class as the value for that dependency.
Working with Interfaces
When we work with classes we work with values, but when we work with interfaces we work with types. Types belong to the TypeScript side, they exist at compile time and disappear once the project is compiled.
Value Providers (useValue)
useValue allows us to inject plain objects, like static configurations. It’s useful for unit tests or when we need to override configurations.
First, we create our constants:
// pets.constants.ts
export const valueConfig = {
DSN: "postgresql://localhost:5432/mydb",
version: 1,
}
export type ValueConfig = typeof valueConfig
export const VALUE_PROVIDER = Symbol("VALUE_PROVIDER")
Then we register it in the module:
@Module({
providers: [
{
provide: VALUE_PROVIDER,
useValue: valueConfig,
},
],
})
export class PetsModule {}
To inject values that aren’t classes, we use the @Inject decorator:
@Injectable()
export class PetService {
constructor(
@Inject(VALUE_PROVIDER)
private readonly providedConfig: ValueConfig,
) {}
findAll() {
console.log(this.providedConfig) // { DSN: '...', version: 1 }
return this.pets
}
}
Recommendation: Always use Symbols as injection tokens instead of strings to avoid dependency conflicts, since Symbols have a unique identifier on each execution.
Factory Providers (useFactory)
useFactory allows us to create dependencies in a structured way. Unlike useValue, it receives a function that may or may not be asynchronous.
@Module({
providers: [
{
provide: FACTORY_PROVIDER,
useFactory: () => {
return factoryConfig
},
},
],
})
export class PetsModule {}
Injecting Dependencies in useFactory
The powerful thing about useFactory is that we can inject other dependencies using the inject attribute:
@Module({
providers: [
{
provide: VALUE_PROVIDER,
useValue: valueConfig,
},
{
provide: FACTORY_PROVIDER,
useFactory: (valueConfig: ValueConfig) => {
console.log("Injected:", valueConfig)
return factoryConfig
},
inject: [VALUE_PROVIDER], // Inject dependencies into the factory
},
],
})
export class PetsModule {}
The function arguments are resolved by position: what we inject at index zero will be available in the first position of the arguments.
Use case: Injecting third-party libraries that need an API key or secret to function.
Existing Providers (useExisting)
useExisting allows us to create aliases of existing singletons. It’s useful for separating responsibilities while maintaining a single instance.
First, we create an injectable class and two interfaces:
// pets.constants.ts
export interface IReadable {
read(): string
}
export interface IWritable {
write(): void
}
@Injectable()
export class StorageManager implements IReadable, IWritable {
read() {
return "I am reading"
}
write() {
console.log("I am writing")
}
}
Now we register the aliases in the module:
@Module({
providers: [
StorageManager,
{
provide: READABLE_PROVIDER,
useExisting: StorageManager,
},
{
provide: WRITABLE_PROVIDER,
useExisting: StorageManager,
},
],
})
export class PetsModule {}
And we inject them separately in the service:
@Injectable()
export class PetService {
constructor(
@Inject(READABLE_PROVIDER)
private readonly readable: IReadable,
@Inject(WRITABLE_PROVIDER)
private readonly writable: IWritable,
) {}
findAll() {
this.writable.write() // I am writing
console.log(this.readable.read()) // I am reading
return this.pets
}
}
Key point: We’re not instantiating StorageManager three times. It’s only instantiated once and useExisting creates aliases that resolve to the same instance.
Best Practices
- Controllers should not contain business logic - only map endpoints to service methods.
- Use Symbols as injection tokens to avoid dependency conflicts.
- Use useValue for static configurations and unit tests.
- Use useFactory when you need to instantiate third-party libraries with configuration.
- Use useExisting to create aliases and separate responsibilities while maintaining singletons.
- Import types with the
typekeyword when working with interfaces.
Conclusion
Dependency Injection is one of NestJS’s fundamental pillars. Understanding how the IOC container works and the different provider types will allow you to create more flexible, maintainable, and robust applications. As your projects grow, these injection strategies will be essential to keep your code organized.
Repository
💻 Repository: https://github.com/radagva/guia-de-nestjs-example
