NestJS Services & Dependency Injection
The Brain Behind Every Controller
Open interactive version (quiz + challenge)Real-world analogy
A service is like a mechanic in an auto shop. The front desk clerk (controller) writes up the work order but never touches the engine. The mechanic (service) is the expert who actually diagnoses and fixes cars. Dependency Injection is like the shop owner who automatically assigns mechanics to each service bay — no clerk has to wander the garage looking for an available mechanic.
What is it?
Services are classes marked with @Injectable() that contain business logic. Dependency Injection (DI) is NestJS's system for automatically creating and providing service instances where they're needed. Together, they enforce separation of concerns — controllers handle HTTP, services handle logic.
Real-world relevance
Every enterprise framework uses DI: Spring (Java), ASP.NET (C#), Angular (TypeScript). It makes code testable (swap real services for mocks), modular (replace one service without touching others), and clean (no manual object creation scattered everywhere).
Key points
- @Injectable() Decorator — Adding @Injectable() to a class tells NestJS: 'This class can be managed by the DI container.' It can then be injected into controllers, other services, or any provider. Without @Injectable(), NestJS can't create or inject the class.
- Constructor Injection — The main way to inject dependencies: `constructor(private usersService: UsersService) {}`. NestJS reads the type (UsersService), finds the matching provider, creates it (or reuses a singleton), and passes it in. Zero manual wiring needed!
- Providers Array — Services must be registered in a module's `providers` array to be available for injection. `providers: [UsersService]` is shorthand for `providers: [{ provide: UsersService, useClass: UsersService }]`. The module is the registry that connects everything.
- Singleton by Default — By default, every service is a singleton — one instance shared across the entire app. This means the first time UsersService is needed, NestJS creates it, and every subsequent injection gets the SAME instance. Great for caching, connection pools, and shared state.
- Service-to-Service Injection — Services can inject other services. An OrdersService might inject UsersService and ProductsService. This creates a dependency tree that NestJS resolves automatically. Just add them to the constructor — NestJS handles the rest.
- Custom Providers — Beyond simple classes, you can provide values (useValue), factories (useFactory), or aliases (useExisting). `{ provide: 'API_KEY', useValue: 'abc123' }` lets you inject config. `{ provide: Logger, useFactory: () => new WinstonLogger() }` for custom creation.
- Async Providers — Need to wait for a DB connection before creating a service? Use useFactory with async: `{ provide: 'DB', useFactory: async () => await connectDB() }`. NestJS waits for the promise to resolve before injecting the result. Essential for real-world apps.
- Provider Scopes — Change the scope with @Injectable({ scope: Scope.REQUEST }) to create a new instance per request — useful for multi-tenant apps. Scope.TRANSIENT creates a new instance per injection point. Default (Scope.DEFAULT) is singleton. Choose wisely — singletons are faster!
Code example
// users.service.ts — A complete service
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Injectable()
export class UsersService {
// PrismaService is injected automatically
constructor(private readonly prisma: PrismaService) {}
async findAll(params: { page: number; limit: number }) {
const { page, limit } = params;
const skip = (page - 1) * limit;
const [users, total] = await Promise.all([
this.prisma.user.findMany({ skip, take: limit }),
this.prisma.user.count(),
]);
return {
data: users,
meta: { total, page, limit, pages: Math.ceil(total / limit) },
};
}
async findOne(id: number) {
const user = await this.prisma.user.findUnique({
where: { id },
});
if (!user) {
throw new NotFoundException(`User #${id} not found`);
}
return user;
}
async create(dto: CreateUserDto) {
return this.prisma.user.create({ data: dto });
}
async update(id: number, dto: UpdateUserDto) {
await this.findOne(id); // Throws if not found
return this.prisma.user.update({
where: { id },
data: dto,
});
}
async remove(id: number) {
await this.findOne(id); // Throws if not found
return this.prisma.user.delete({ where: { id } });
}
}
// Custom provider example
// app.module.ts
@Module({
providers: [
UsersService,
// Value provider — inject config
{ provide: 'APP_NAME', useValue: 'My Awesome API' },
// Factory provider — complex creation
{
provide: 'MAILER',
useFactory: (config: ConfigService) => {
return new MailerService(config.get('SMTP_HOST'));
},
inject: [ConfigService],
},
],
})
export class AppModule {}Line-by-line walkthrough
- 1. users.service.ts — A complete service
- 2. Importing Injectable and error class
- 3. Importing Prisma for database access
- 4. Importing DTO for creating users
- 5. Importing DTO for updating users
- 6.
- 7. Injectable marks this as a DI-managed class
- 8. Exporting the service
- 9. PrismaService is injected automatically via constructor
- 10. Constructor with private readonly for immutability
- 11.
- 12. Find all users with pagination
- 13. Destructure page and limit from params
- 14. Calculate how many records to skip
- 15.
- 16. Run both queries in parallel for speed
- 17. Get paginated users
- 18. Count total users
- 19. Close Promise.all
- 20.
- 21. Return data with metadata
- 22. The user records
- 23. Pagination metadata
- 24. Close return
- 25. Close method
- 26.
- 27. Find a single user by ID
- 28. Query the database
- 29. Filter by ID
- 30. Close query
- 31.
- 32. If no user found, throw 404
- 33. Throw a NotFoundException
- 34. Close if block
- 35. Return the found user
- 36. Close method
- 37.
- 38. Create a new user
- 39. Use Prisma to insert the data
- 40. Close method
- 41.
- 42. Update a user
- 43. Check if user exists first
- 44. Update in the database
- 45. Filter by ID
- 46. Set new data from DTO
- 47. Close update
- 48. Close method
- 49.
- 50. Remove a user
- 51. Check if user exists first
- 52. Delete from the database
- 53. Close method
- 54. Close class
- 55.
- 56. Custom provider example
- 57. app.module.ts
- 58. Module decorator
- 59. Providers array
- 60. Regular class provider
- 61. Value provider — inject a constant
- 62. Factory provider — custom creation
- 63. Factory function with injected config
- 64. Create a MailerService with config
- 65. Close factory
- 66. Tell NestJS to inject ConfigService into the factory
- 67. Close factory provider
- 68. Close providers
- 69. Close decorator
- 70. Export the module
Spot the bug
export class UsersService {
constructor(private prisma: PrismaService) {}
findAll() {
return this.prisma.user.findMany();
}
}
@Module({
controllers: [UsersController],
providers: [UsersController],
})
export class UsersModule {}Need a hint?
Two issues: one with the service class and one with the module's providers array.
Show answer
1) UsersService is missing @Injectable() decorator — NestJS can't inject PrismaService without it. 2) The providers array has UsersController instead of UsersService. Fix: add @Injectable() above the class and change providers to [UsersService, PrismaService].
Explain like I'm 5
Imagine you need a calculator for math class. Instead of building one yourself every morning, your teacher (NestJS) keeps one calculator in the classroom and gives it to anyone who needs it. That's Dependency Injection — you just say 'I need a calculator' and it appears in your hands. You never worry about where it came from!
Fun fact
The Dependency Injection pattern was popularized by Martin Fowler in 2004. He called it 'Inversion of Control' because instead of your code creating its dependencies (the normal flow), the framework creates them and gives them to you (inverted flow). NestJS's DI is inspired by Angular's, which was inspired by Java Spring!
Hands-on challenge
Create a ProductsService with full CRUD operations. Inject it into ProductsController. Then create a separate OrdersService that injects BOTH ProductsService and UsersService to create orders that reference both a user and a product. Register everything in the module providers.
More resources
- NestJS Providers (NestJS Official)
- NestJS Custom Providers (NestJS Official)
- NestJS Injection Scopes (NestJS Official)