Menu
CoddyTech

NestJS Modules, Controllers and Providers Questions

How modules group code, what they export, dynamic modules, lifecycle hooks and route order.

11 questions
  1. What is a module in NestJS?

    A module is a class decorated with @Module() that groups related controllers and providers and declares what it needs and what it shares.

    @Module({
      imports: [UsersModule],          // modules whose exports we use
      controllers: [OrdersController], // route handlers in this module
      providers: [OrdersService],      // injectables created here
      exports: [OrdersService],        // what importers may inject
    })
    export class OrdersModule {}
    

    Every app has one root module (AppModule) passed to NestFactory.create. Nest builds a graph from the imports, and each module is a scope: a provider is visible inside its own module and in modules that import it, only if it is exported.

  2. What is a provider in NestJS?

    A provider is anything Nest's container can create and inject: usually a class marked @Injectable() (a service, repository, helper), but also a value, a factory result or an alias registered under a token.

    You register it in a module's providers array and receive it through a constructor:

    @Injectable()
    export class PricingService {
      total(items: { price: number }[]) {
        return items.reduce((sum, i) => sum + i.price, 0);
      }
    }
    
    @Controller('cart')
    export class CartController {
      constructor(private readonly pricing: PricingService) {}
    }
    

    By default a provider is a singleton: one instance for the whole app, created at startup.

  3. What goes in a controller and what goes in a service?

    A controller handles HTTP: it maps routes, reads params and the body, and returns a response. A service holds the business logic and data access, with no knowledge of HTTP.

    Keep controllers thin: one or two lines that call a service. The same service can then run from a queue consumer, a GraphQL resolver or a test without faking a request.

    A practical check: if the logic would make sense in a cron job too, it belongs in a service.

  4. Why does Nest say it "can't resolve dependencies" when a service from another module is injected?

    Because the provider is not exported, or its module is not imported. Providers are private to their module. To use UsersService inside OrdersModule, both sides must agree:

    @Module({ providers: [UsersService], exports: [UsersService] })
    export class UsersModule {}
    
    @Module({ imports: [UsersModule], providers: [OrdersService] })
    export class OrdersModule {}
    

    Otherwise startup fails with a message that begins "Nest can't resolve dependencies of the OrdersService (?)" and names the module it searched. The ? marks the parameter it could not find. Do not fix it by adding UsersService to OrdersModule's providers: that creates a second instance with its own state.

  5. Are modules singletons in NestJS?

    Yes. A module imported by ten other modules is created once, and its exported providers are the same instances everywhere. That is how a DatabaseModule shares one connection pool across the app.

    A module can also re-export a module it imports, so importers get its providers without importing it themselves:

    @Module({
      imports: [DatabaseModule, CacheModule],
      exports: [DatabaseModule, CacheModule],
    })
    export class CoreModule {}
    

    The exception is a dynamic module configured differently: HttpModule.register({ timeout: 1000 }) and HttpModule.register({ timeout: 5000 }) give two separate instances.

  6. What does @Global() do, and when should you use it?

    @Global() makes a module's exported providers available in every module without importing it. Register the module once, usually in AppModule.

    @Global()
    @Module({ providers: [AppConfig], exports: [AppConfig] })
    export class ConfigCoreModule {}
    

    Use it for truly app-wide infrastructure: configuration, logging, a database connection. Avoid it for feature services: explicit imports document what a module depends on, and a global module hides that, which makes modules harder to test and to extract later. ConfigModule.forRoot({ isGlobal: true }) from @nestjs/config is the usual example.

  7. What is a dynamic module in NestJS?

    A dynamic module is a module configured at import time: a static method (by convention forRoot, register or forFeature) returns a DynamicModule object with providers built from the arguments. TypeOrmModule.forRoot(options) and JwtModule.register({ secret }) are examples.

    NestJS
    import { Controller, DynamicModule, Get, Inject, Injectable, Module, Param } from '@nestjs/common';
    import { NestFactory } from '@nestjs/core';
    
    @Injectable()
    class GreetingService {
      constructor(@Inject('GREETING') private word: string) {}
      greet(name: string) { return `${this.word}, ${name}`; }
    }
    
    @Module({})
    class GreetingModule {
      static forRoot(word: string): DynamicModule {
        return {
          module: GreetingModule,
          providers: [{ provide: 'GREETING', useValue: word }, GreetingService],
          exports: [GreetingService],
        };
      }
    }
    
    // GET /hello/Ada
    @Controller('hello')
    class HelloController {
      constructor(private greeting: GreetingService) {}
      @Get(':name')
      hi(@Param('name') name: string) { return this.greeting.greet(name); }
    }
    
    @Module({ imports: [GreetingModule.forRoot('Hola')], controllers: [HelloController] })
    class AppModule {}
    
    async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); }
    bootstrap();

    The first request answers Hola, Ada: the word passed to forRoot became a provider that GreetingService injects. Change 'Hola' to 'Hi' and run it again. For options that come from config or an async source, libraries add forRootAsync({ useFactory, inject }).

  8. What is the difference between forRoot, register and forFeature?

    They are naming conventions, not framework rules, but libraries follow them:

    MethodMeaningExample
    forRootConfigure once for the whole app, shared everywhereTypeOrmModule.forRoot({...}) in AppModule
    forFeatureUse the forRoot setup for one module's needsTypeOrmModule.forFeature([User]) in UsersModule
    registerConfigure a fresh instance for this importerHttpModule.register({ timeout: 2000 })

    Each has an Async variant (forRootAsync) that takes useFactory and inject, so the options can come from ConfigService instead of literals.

  9. What are lifecycle hooks in NestJS, and when does onModuleInit run?

    Lifecycle hooks are methods Nest calls on providers, controllers and modules at fixed points: onModuleInit, onApplicationBootstrap, and on shutdown onModuleDestroy, beforeApplicationShutdown, onApplicationShutdown. An async hook is awaited before Nest continues.

    NestJS
    import { Controller, Get, Injectable, Module, OnApplicationBootstrap, OnModuleInit } from '@nestjs/common';
    import { NestFactory } from '@nestjs/core';
    
    const events: string[] = [];
    
    @Injectable()
    class DbService implements OnModuleInit, OnApplicationBootstrap {
      constructor() { events.push('DbService constructor'); }
      async onModuleInit() {
        await new Promise((r) => setTimeout(r, 50));
        events.push('DbService onModuleInit (connected)');
      }
      onApplicationBootstrap() { events.push('DbService onApplicationBootstrap'); }
    }
    
    @Module({ providers: [DbService], exports: [DbService] })
    class DbModule {}
    
    // GET /events
    @Controller('events')
    class EventsController implements OnModuleInit {
      constructor(private db: DbService) { events.push('EventsController constructor'); }
      onModuleInit() { events.push('EventsController onModuleInit'); }
      @Get() list() { return events; }
    }
    
    @Module({ imports: [DbModule], controllers: [EventsController] })
    class AppModule {}
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      events.push('before listen');
      await app.listen(3000);
    }
    bootstrap();

    The response shows the order: constructors run inside NestFactory.create, but onModuleInit only runs during app.init(), which listen() calls, so it comes after before listen. The imported DbModule finishes its async onModuleInit before AppModule's hooks start. Shutdown hooks run only after app.enableShutdownHooks() (or an explicit app.close()).

  10. Why does GET /users/stats call the :id handler?

    Because routes match in the order they are declared, and @Get(':id') declared first also matches stats.

    @Controller('users')
    export class UsersController {
      @Get(':id')   // matches /users/stats too, with id = 'stats'
      findOne(@Param('id') id: string) {}
    
      @Get('stats') // never reached
      stats() {}
    }
    

    Declare static paths before parameterized ones (the same rule as Express routing), or constrain the parameter with a pipe (ParseIntPipe turns stats into a 400, but still does not reach stats()). Across controllers, the order follows module and controller registration, which is easy to break by reordering imports. NestJS 12 adds opt-in route conflict checks (routeConflictPolicy) that report shadowed routes at startup.

  11. How do you split a NestJS application into modules?

    One module per business feature (UsersModule, OrdersModule, PaymentsModule), plus a few infrastructure modules (database, config, mail) that features import.

    Rules that keep it healthy:

    • A feature module exports a small API (usually one service), not its repositories.
    • Dependencies point one way. If Orders needs Users and Users needs Orders, extract the shared part into a third module rather than reaching for forwardRef.
    • Controllers only call their own module's services.
    • Cross-feature side effects (send an email after an order) go through events, so the order module does not import the mail module.

    A module you could move into its own service later is a good sign.

Coddy programming languages illustration

Learn to code with Coddy

Get started