On this page

Modules and project structure in NestJS

15 min read TextCh. 1 — NestJS Fundamentals

The module system

In NestJS, a module is a class decorated with @Module() that groups related components: controllers, providers (services), and their imports/exports. Every NestJS application has at least one module — the root AppModule.

Modules are the primary mechanism for organizing your codebase. Each feature of your application should have its own module. For the bookstore API, you will create separate modules for books, authors, categories, and authentication.

Why modules matter

Modules enforce encapsulation. A service defined inside BooksModule is not automatically available in AuthorsModule. You must explicitly export it from BooksModule and import BooksModule inside AuthorsModule. This explicit dependency graph makes your application easier to understand and prevents unintended coupling between features.

The @Module decorator

The @Module() decorator accepts an object with four optional properties:

@Module({
  imports: [],     // Other modules whose exports are needed here
  controllers: [], // Request handlers belonging to this module
  providers: [],   // Services, repositories, guards, etc.
  exports: [],     // Subset of providers to make available to importers
})

imports — List of modules that this module depends on. When you import a module, all of its exported providers become available for injection in the current module.

controllers — The controllers that handle HTTP routes for this feature. NestJS registers their routes when the module is loaded.

providers — Everything that can be injected. Services are the most common type, but guards, interceptors, pipes, and factories are also providers.

exports — Providers that should be visible to other modules that import this one. Not exporting means the provider is private to this module.

For a production-grade NestJS project, organize your code by feature:

src/
  books/
    dto/
      create-book.dto.ts
      update-book.dto.ts
    entities/
      book.entity.ts
    books.controller.ts
    books.controller.spec.ts
    books.module.ts
    books.service.ts
    books.service.spec.ts
  authors/
    dto/
      create-author.dto.ts
    entities/
      author.entity.ts
    authors.controller.ts
    authors.module.ts
    authors.service.ts
  auth/
    dto/
      login.dto.ts
      register.dto.ts
    strategies/
      jwt.strategy.ts
      local.strategy.ts
    auth.controller.ts
    auth.module.ts
    auth.service.ts
  shared/
    logger/
      logger.service.ts
    utils/
      utils.service.ts
    shared.module.ts
  config/
    database.config.ts
    jwt.config.ts
  app.module.ts
  main.ts

This structure scales well: adding a new feature means adding a new folder with the same internal pattern.

Generating a feature module

The NestJS CLI generates the module skeleton for you:

# Generates books folder with books.module.ts
nest g module books

# Generates books.controller.ts and registers it in BooksModule
nest g controller books

# Generates books.service.ts and registers it in BooksModule
nest g service books

# All-in-one: generates module, controller, service, and DTOs
nest g resource books

The CLI is smart enough to update existing files. When you run nest g controller books, it not only creates books.controller.ts but also adds BooksController to the controllers array in books.module.ts.

Importing a feature module

Once BooksModule is created, you register it in AppModule by adding it to the imports array:

@Module({
  imports: [BooksModule, AuthorsModule, AuthModule],
})
export class AppModule {}

NestJS will instantiate each module, wire up its providers via dependency injection, and register all controllers with the HTTP adapter.

The @Global decorator

Some services are needed everywhere in the application: a logger, a configuration service, or a utility library. Importing the module that provides them in every feature module is tedious. The @Global() decorator solves this.

A global module's exports are available to all other modules without explicit imports. You typically apply @Global() to infrastructure modules like LoggerModule or ConfigModule.

Important: use @Global() sparingly. Overusing it defeats the purpose of the module encapsulation system and makes dependency tracking difficult.

Dynamic modules

Some modules need to be configured at registration time. A common example is the database module — you need to pass the connection URL at startup. NestJS supports this with dynamic modules:

@Module({})
export class DatabaseModule {
  static forRoot(connectionString: string): DynamicModule {
    return {
      module: DatabaseModule,
      providers: [
        {
          provide: 'DATABASE_CONNECTION',
          useFactory: () => createConnection(connectionString),
        },
      ],
      exports: ['DATABASE_CONNECTION'],
      global: true,
    };
  }
}

Usage in AppModule:

@Module({
  imports: [
    DatabaseModule.forRoot(process.env['DATABASE_URL'] ?? ''),
  ],
})
export class AppModule {}

The official @nestjs/typeorm and @nestjs/config packages use this pattern. You call TypeOrmModule.forRoot(options) to configure the database connection once, and then TypeOrmModule.forFeature([BookEntity]) inside each feature module to register entity-specific repositories.

Module re-exporting

A module can re-export modules it imports. This is useful for creating "barrel" modules that group related functionality:

@Module({
  imports: [HttpModule, AxiosModule],
  exports: [HttpModule, AxiosModule], // re-export both
})
export class NetworkModule {}

Any module that imports NetworkModule also gains access to HttpModule and AxiosModule exports, without needing to import them separately.

Lazy-loading modules

In microservice architectures and CLI tools, you sometimes want to load certain modules only when needed. NestJS supports lazy module loading with LazyModuleLoader:

import { LazyModuleLoader } from '@nestjs/core';

@Injectable()
export class AppService {
  constructor(private readonly lazyModuleLoader: LazyModuleLoader) {}

  async triggerHeavyFeature(): Promise<void> {
    const { HeavyModule } = await import('./heavy/heavy.module');
    const moduleRef = await this.lazyModuleLoader.load(() => HeavyModule);
    const heavyService = moduleRef.get(HeavyService);
    await heavyService.run();
  }
}

This is not common in typical REST APIs but is a powerful tool for performance-critical scenarios.

The module reference

Inside any provider, you can inject ModuleRef to dynamically retrieve other providers:

import { ModuleRef } from '@nestjs/core';

@Injectable()
export class BooksService {
  constructor(private readonly moduleRef: ModuleRef) {}

  async someMethod(): Promise<void> {
    const authorsService = this.moduleRef.get(AuthorsService, { strict: false });
    // use authorsService
  }
}

strict: false allows retrieving providers from other modules. This is an escape hatch for complex scenarios — in most cases, proper imports/exports are the right approach.

One module per feature
Each feature (books, authors, auth) should live in its own folder with its own module. This is the standard NestJS convention and makes your codebase easy to navigate, test, and potentially extract into a microservice later.
Circular dependencies
Avoid circular module imports. If module A needs a service from module B and vice versa, extract the shared logic into a third SharedModule that both can import without circular references.
import { Module } from '@nestjs/common';
import { BooksController } from './books.controller';
import { BooksService } from './books.service';

@Module({
  imports: [],
  controllers: [BooksController],
  providers: [BooksService],
  exports: [BooksService],
})
export class BooksModule {}