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 toNestFactory.create. Nest builds a graph from theimports, 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.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
providersarray 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.
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.
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
UsersServiceinsideOrdersModule, 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 addingUsersServicetoOrdersModule'sproviders: that creates a second instance with its own state.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 inAppModule.@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
importsdocument 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/configis the usual example.What is a dynamic module in NestJS?
A dynamic module is a module configured at import time: a static method (by convention
forRoot,registerorforFeature) returns aDynamicModuleobject with providers built from the arguments.TypeOrmModule.forRoot(options)andJwtModule.register({ secret })are examples.NestJSimport { 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 toforRootbecame a provider thatGreetingServiceinjects. Change'Hola'to'Hi'and run it again. For options that come from config or an async source, libraries addforRootAsync({ useFactory, inject }).What is the difference between
forRoot,registerandforFeature?They are naming conventions, not framework rules, but libraries follow them:
Method Meaning Example forRootConfigure once for the whole app, shared everywhere TypeOrmModule.forRoot({...})inAppModuleforFeatureUse the forRootsetup for one module's needsTypeOrmModule.forFeature([User])inUsersModuleregisterConfigure a fresh instance for this importer HttpModule.register({ timeout: 2000 })Each has an
Asyncvariant (forRootAsync) that takesuseFactoryandinject, so the options can come fromConfigServiceinstead of literals.What are lifecycle hooks in NestJS, and when does
onModuleInitrun?Lifecycle hooks are methods Nest calls on providers, controllers and modules at fixed points:
onModuleInit,onApplicationBootstrap, and on shutdownonModuleDestroy,beforeApplicationShutdown,onApplicationShutdown. An async hook is awaited before Nest continues.NestJSimport { 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, butonModuleInitonly runs duringapp.init(), whichlisten()calls, so it comes afterbefore listen. The importedDbModulefinishes its asynconModuleInitbeforeAppModule's hooks start. Shutdown hooks run only afterapp.enableShutdownHooks()(or an explicitapp.close()).Why does
GET /users/statscall the:idhandler?Because routes match in the order they are declared, and
@Get(':id')declared first also matchesstats.@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 (
ParseIntPipeturnsstatsinto a400, but still does not reachstats()). 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.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
OrdersneedsUsersandUsersneedsOrders, extract the shared part into a third module rather than reaching forforwardRef. - 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.
NestJS Modules, Controllers and Providers Questions
How modules group code, what they export, dynamic modules, lifecycle hooks and route order.