Services and Dependency Injection in NestJS

Table of Contents

  1. Introduction to Services
  2. Why Use Services?
  3. Creating Your First Service
  4. Understanding Dependency Injection in NestJS
  5. Service Scope: Default, Transient, and Request-Scoped
  6. Injecting Services into Controllers
  7. Using Interfaces for Service Contracts
  8. Dependency Injection Tokens
  9. Global Services and Singleton Behavior
  10. Testing Services with Mocks and Providers
  11. Best Practices for Writing Maintainable Services
  12. Summary and What’s Next

1. Introduction to Services

In NestJS, services are fundamental building blocks that encapsulate business logic and communicate with repositories, external APIs, or other services. By keeping business logic in services and separating it from controllers, your code becomes cleaner, more testable, and more modular.


2. Why Use Services?

Services are important for:

  • Encapsulation of logic
  • Code reuse across modules and controllers
  • Better separation of concerns
  • Easier testing and mocking

For instance, if you’re building a blog API, the logic for fetching, creating, updating, or deleting posts should reside in a PostsService, not directly in the controller.


3. Creating Your First Service

NestJS provides the CLI command to create services easily:

bashCopyEditnest generate service users
# or shorthand
nest g s users

This will generate a file like:

tsCopyEdit@Injectable()
export class UsersService {
  private users = [{ id: 1, name: 'Alice' }];

  findAll() {
    return this.users;
  }

  findOne(id: number) {
    return this.users.find(user => user.id === id);
  }
}

4. Understanding Dependency Injection in NestJS

NestJS uses a dependency injection (DI) container, powered by TypeScript decorators and metadata reflection, to manage the lifecycle of providers (classes that can be injected).

When you add @Injectable() to a class, you’re telling NestJS that this class can be injected into other classes via its constructor.

Example of injecting UsersService into a controller:

tsCopyEdit@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  getAllUsers() {
    return this.usersService.findAll();
  }
}

5. Service Scope: Default, Transient, and Request-Scoped

Services can be singleton (default), transient, or request-scoped.

  • Singleton (default): One instance shared across all consumers
  • Transient: A new instance is created each time it’s injected
  • Request-scoped: A new instance per HTTP request
tsCopyEdit@Injectable({ scope: Scope.REQUEST })
export class ScopedService {
  // scoped logic
}

6. Injecting Services into Controllers

The controller constructor is where services are injected. NestJS’s DI system resolves all dependencies recursively.

Example:

tsCopyEdit@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  @Get()
  getPosts() {
    return this.postsService.findAll();
  }
}

7. Using Interfaces for Service Contracts

It’s good practice to define interfaces for your services. This allows better decoupling, especially for testing or when using multiple implementations.

tsCopyEditexport interface IUserService {
  findAll(): User[];
}

Then you can implement it in a class:

tsCopyEdit@Injectable()
export class UsersService implements IUserService {
  findAll(): User[] {
    return [{ id: 1, name: 'Alice' }];
  }
}

8. Dependency Injection Tokens

In some cases, especially with dynamic providers or multiple implementations, you’ll need to use DI tokens:

tsCopyEdit{
  provide: 'CustomService',
  useClass: CustomService,
}

You then inject it with:

tsCopyEditconstructor(@Inject('CustomService') private readonly service: CustomService) {}

9. Global Services and Singleton Behavior

Services are singleton by default and shared across your application. If you define a service in a module, it’s available only within that module unless exported and imported elsewhere.

If you want a global service:

tsCopyEdit@Global()
@Module({
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule {}

10. Testing Services with Mocks and Providers

Services are very testable. You can use NestJS’s testing utilities and dependency injection to provide mock services.

tsCopyEditdescribe('UsersService', () => {
  let service: UsersService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [UsersService],
    }).compile();

    service = module.get<UsersService>(UsersService);
  });

  it('should return users', () => {
    expect(service.findAll()).toEqual([{ id: 1, name: 'Alice' }]);
  });
});

11. Best Practices for Writing Maintainable Services

  • Keep logic atomic and focused
  • Avoid bloated services — split by domain
  • Use interfaces for abstraction
  • Use scopes only when necessary
  • Reuse services via shared modules

12. Summary and What’s Next

Services in NestJS are the core units for business logic and are designed to work seamlessly with Nest’s DI system. Mastering them helps you build clean, scalable, and testable apps.