Skip to content

nest_05_modules - Modules & Module Patterns

Modules are where NestJS earns its keep Without them your app is a flat namespace where everything imports everything and you can't tell which feature depends on which Modules create boundaries , and boundaries create security - you can lock down what each part of your app can access

what's in here

  • @Module decorator structure
  • Feature modules and encapsulation
  • Global modules (use sparingly)
  • Dynamic modules with forRoot/forFeature patterns
  • Module re-export and shared modules

the @Module decorator

Every NestJS app has at least one module - the root AppModule Modules declare what belongs together and what they expose to others

import { Module } from '@nestjs/common'
import { UsersController } from './users.controller'
import { UsersService } from './users.service'
import { UsersRepository } from './users.repository'

@Module({
  imports: [],         // other modules this module needs
  controllers: [UsersController],   // routes this module registers
  providers: [UsersService, UsersRepository],  // DI providers
  exports: [UsersService]   // providers visible to importing modules
})
export class UsersModule {}
  • imports - modules whose exported providers become available here
  • controllers - route handlers for this feature
  • providers - services , repositories , factories registered in DI
  • exports - subset of providers that importing modules can inject

feature modules

Split your app by domain , not by file type

// src/auth/auth.module.ts
@Module({
  imports: [UsersModule],   // Auth needs UsersService
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService]
})
export class AuthModule {}
src/
  auth/
    auth.module.ts
    auth.controller.ts
    auth.service.ts
    strategies/
      jwt.strategy.ts
    guards/
      jwt-auth.guard.ts
  users/
    users.module.ts
    users.controller.ts
    users.service.ts
    users.repository.ts
    dto/
      create-user.dto.ts
      update-user.dto.ts

Each feature module encapsulates its domain The auth module doesn't need to know how users are stored - it imports UsersModule and uses the exported UsersService

module encapsulation

By default , providers in a module are private They can be injected within the module but not by external modules Only providers listed in exports: [] are visible outside

@Module({
  providers: [
    UsersService,          // private - only this module can use it
    UsersRepository,       // private
    EmailService           // private
  ],
  exports: [UsersService]  // only UsersService is visible to importing modules
})
export class UsersModule {}

This encapsulation is your security boundary If someone accidentally tries to inject UsersRepository in the AuthModule , Nest throws at compile time - not at 3AM in production when an auth bypass exploits a leaked repository

global modules

Sometimes you need a provider available everywhere without importing its module in every feature ConfigService is the classic example

import { Module, Global } from '@nestjs/common'
import { ConfigService } from './config.service'

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

With @Global() , ConfigService is available in every module without importing ConfigModule Use globals sparingly - they're convenient but they break the explicit dependency tracking that makes NestJS modular Good candidates: config , logging , database connections Bad candidates: business logic services

dynamic modules

Dynamic modules accept configuration at import time

import { Module, DynamicModule } from '@nestjs/common'

@Module({})
export class DatabaseModule {
  static forRoot(config: DatabaseConfig): DynamicModule {
    return {
      module: DatabaseModule,
      providers: [
        {
          provide: 'DATABASE_OPTIONS',
          useValue: config
        },
        DatabaseService
      ],
      exports: [DatabaseService]
    }
  }

  static forFeature(entity: Function): DynamicModule {
    return {
      module: DatabaseModule,
      providers: [
        {
          provide: 'ENTITY_REPOSITORY',
          useFactory: (connection: Connection) =>
            connection.getRepository(entity),
          inject: ['DATABASE_CONNECTION']
        }
      ],
      exports: ['ENTITY_REPOSITORY']
    }
  }
}

Usage:

@Module({
  imports: [
    DatabaseModule.forRoot({
      host: process.env.DB_HOST,
      port: parseInt(process.env.DB_PORT || '5432')
    })
  ]
})
export class AppModule {}

@Module({
  imports: [
    DatabaseModule.forFeature(User)
  ],
  providers: [UsersService]
})
export class UsersModule {}

forRoot() sets up the module globally - called once in AppModule forFeature() configures per-feature functionality - called in each feature module This pattern comes from @nestjs/typeorm , @nestjs/mongoose , and @nestjs/config

module re-export

Export modules to make their exports transitively available:

@Module({
  imports: [CommonModule],
  exports: [CommonModule]  // re-export CommonModule's exports
})
export class SharedModule {}

Modules importing SharedModule also get access to CommonModule's exports Useful for creating "barrel" modules that group related functionality

multi-module architecture for security

app.module.ts
  imports:
    ConfigModule.forRoot({ isGlobal: true })
    CoreModule          // guards , interceptors , filters
    AuthModule          // auth strategies , guards
    UsersModule         // user CRUD
    AdminModule         // admin-only features
    HealthModule        // public health check (no auth)

The AdminModule imports AuthModule and uses guards to restrict access The HealthModule imports nothing auth-related - it can't accidentally expose admin data Module boundaries create clear attack surface visibility

prerequisites

nest_04_providers - Providers & Dependency Injection


next -> nest_06_middleware - Middleware