Lesson 43 of 49 intermediate

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

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. 1. users.service.ts — A complete service
  2. 2. Importing Injectable and error class
  3. 3. Importing Prisma for database access
  4. 4. Importing DTO for creating users
  5. 5. Importing DTO for updating users
  6. 6.
  7. 7. Injectable marks this as a DI-managed class
  8. 8. Exporting the service
  9. 9. PrismaService is injected automatically via constructor
  10. 10. Constructor with private readonly for immutability
  11. 11.
  12. 12. Find all users with pagination
  13. 13. Destructure page and limit from params
  14. 14. Calculate how many records to skip
  15. 15.
  16. 16. Run both queries in parallel for speed
  17. 17. Get paginated users
  18. 18. Count total users
  19. 19. Close Promise.all
  20. 20.
  21. 21. Return data with metadata
  22. 22. The user records
  23. 23. Pagination metadata
  24. 24. Close return
  25. 25. Close method
  26. 26.
  27. 27. Find a single user by ID
  28. 28. Query the database
  29. 29. Filter by ID
  30. 30. Close query
  31. 31.
  32. 32. If no user found, throw 404
  33. 33. Throw a NotFoundException
  34. 34. Close if block
  35. 35. Return the found user
  36. 36. Close method
  37. 37.
  38. 38. Create a new user
  39. 39. Use Prisma to insert the data
  40. 40. Close method
  41. 41.
  42. 42. Update a user
  43. 43. Check if user exists first
  44. 44. Update in the database
  45. 45. Filter by ID
  46. 46. Set new data from DTO
  47. 47. Close update
  48. 48. Close method
  49. 49.
  50. 50. Remove a user
  51. 51. Check if user exists first
  52. 52. Delete from the database
  53. 53. Close method
  54. 54. Close class
  55. 55.
  56. 56. Custom provider example
  57. 57. app.module.ts
  58. 58. Module decorator
  59. 59. Providers array
  60. 60. Regular class provider
  61. 61. Value provider — inject a constant
  62. 62. Factory provider — custom creation
  63. 63. Factory function with injected config
  64. 64. Create a MailerService with config
  65. 65. Close factory
  66. 66. Tell NestJS to inject ConfigService into the factory
  67. 67. Close factory provider
  68. 68. Close providers
  69. 69. Close decorator
  70. 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

Open interactive version (quiz + challenge) ← Back to course: Full-Stack Playbook