DEV Community

Cover image for NestJS Fundamentals Part 1: Modularity in NestJS
Amir Ehsan Ahmadzadeh
Amir Ehsan Ahmadzadeh

Posted on

NestJS Fundamentals Part 1: Modularity in NestJS

When building scalable applications with NestJS, understanding its modular architecture is the first and most critical step. Modules are at the heart of every NestJS application, organizing your code into manageable, reusable, and maintainable components.

NestJS’s modular architecture works closely with its Dependency Injection (DI) system, which enables the decoupling of application logic and improves testability and maintainability. If you’re unfamiliar with concepts like DI, IoC (Inversion of Control), or the Dependency Inversion principle, I’ve created a blog post to help you understand these foundational ideas. You can check it out here.

This guide is Part 1 of the NestJS Fundamentals series, where we’ll explore how to leverage the @Module decorator to design modular applications. Future parts of this series will cover controllers, dependency injection in practice, middleware, testing, and more. By the end of this series, you’ll have a deep understanding of NestJS and how to use it effectively.

Let’s dive into modularity in NestJS and master the inputs of the @Module decorator.


Table of Contents

  1. What is the Module Decorator?
  2. Inputs of the Module Decorator
  3. Practical Example: A Modular NestJS Application
  4. FAQs About NestJS Modules

What is the Module Decorator?

The @Module decorator in NestJS is used to define the metadata of a module. A module organizes your application into smaller, reusable building blocks, each responsible for a specific piece of functionality. This modular approach enhances scalability and maintainability in applications.

For more details, visit the official NestJS documentation on modules.


Inputs of the Module Decorator

The @Module decorator accepts several inputs that define the structure and behavior of a module. These inputs include:

Providers

Providers are the backbone of NestJS’s dependency injection system. They encapsulate reusable business logic, services, or utilities that can be injected into other parts of your application.

  • Default Scope: Providers are module-scoped by default, meaning they are available only within the module they are defined in.

Example:

// src/services/my.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class MyService {
  private data = 'Hello, NestJS!';

  getData(): string {
    return this.data;
  }
}
Enter fullscreen mode Exit fullscreen mode

Controllers

Controllers define routes and handle incoming HTTP requests. They are specified in the controllers array within a module.

Example:

// src/controllers/user.controller.ts
import { Controller, Get } from '@nestjs/common';

@Controller('users')
export class UserController {
  @Get()
  getAllUsers() {
    return 'List of users';
  }
}

// src/modules/user.module.ts
import { Module } from '@nestjs/common';
import { UserController } from '../controllers/user.controller';

@Module({
  controllers: [UserController],
})
export class UserModule {}
Enter fullscreen mode Exit fullscreen mode

Imports

The imports array allows a module to include and use other modules. By importing a module, you gain access to its exported providers.

Example:

// src/modules/app.module.ts
import { Module } from '@nestjs/common';
import { UserModule } from './user.module';

@Module({
  imports: [UserModule], // Importing UserModule
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Exports

The exports array specifies which providers from the current module are available for use in other modules. Without exporting, providers remain private to the module.

Example:

// src/modules/user.module.ts
import { Module } from '@nestjs/common';
import { MyService } from '../services/my.service';

@Module({
  providers: [MyService],
  exports: [MyService], // Making MyService accessible to other modules
})
export class UserModule {}
Enter fullscreen mode Exit fullscreen mode

Global

The global property makes a module automatically available across the application without needing to explicitly import it in other modules.

Example:

// src/modules/global.module.ts
import { Module } from '@nestjs/common';
import { MyService } from '../services/my.service';

@Module({
  providers: [MyService],
  exports: [MyService],
  global: true, // Marks the module as globally available
})
export class GlobalModule {}
Enter fullscreen mode Exit fullscreen mode

Scope

The scope property allows you to customize the lifecycle of a provider. The available scopes are:

  • Singleton (default): A single instance is shared across the application.
  • Request: A new instance is created for each HTTP request.
  • Transient: A new instance is created each time it is injected.

Example:

// src/services/request-scoped.service.ts
import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {}
Enter fullscreen mode Exit fullscreen mode

Practical Example: A Modular NestJS Application

Here’s a real-world scenario demonstrating how the module decorator inputs work together:

Step 1: Define a Shared Provider

// src/services/shared.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class SharedService {
  private message = 'Shared Message';

  getMessage() {
    return this.message;
  }

  setMessage(newMessage: string) {
    this.message = newMessage;
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create and Export a Module

// src/modules/shared.module.ts
import { Module } from '@nestjs/common';
import { SharedService } from '../services/shared.service';

@Module({
  providers: [SharedService],
  exports: [SharedService], // Export SharedService to make it accessible
})
export class SharedModule {}
Enter fullscreen mode Exit fullscreen mode

Step 3: Import the Module and Use the Provider

// src/modules/app.module.ts
import { Module } from '@nestjs/common';
import { SharedModule } from './shared.module';
import { AppController } from '../controllers/app.controller';

@Module({
  imports: [SharedModule], // Importing SharedModule
  controllers: [AppController],
})
export class AppModule {}

// src/controllers/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { SharedService } from '../services/shared.service';

@Controller()
export class AppController {
  constructor(private readonly sharedService: SharedService) {}

  @Get()
  getSharedData() {
    return this.sharedService.getMessage();
  }
}
Enter fullscreen mode Exit fullscreen mode

Here’s the improved FAQs About NestJS Modules section with better, practical, and engaging questions that address common challenges and curiosities about modularity in NestJS:


FAQs About NestJS Modules

1. What is the purpose of modularity in NestJS?

Modularity allows you to divide your application into feature-specific units called modules. This:

  • Promotes code reusability.
  • Enhances maintainability by isolating features.
  • Simplifies scaling by making it easier to manage and extend modules.

2. How do I decide what should go into a module?

Each module should encapsulate a specific feature or closely related functionalities. Examples:

  • UserModule for user management (authentication, profiles).
  • ProductModule for product operations.
  • SharedModule for utilities like logging or configuration.

3. What’s the difference between providers, exports, and imports?

  • Providers: Internal services or logic available within the module.
  • Exports: Providers made available to other modules that import the module.
  • Imports: A way for a module to access the exports of another module.

Example:

@Module({
  providers: [FeatureService],
  exports: [FeatureService], // Makes FeatureService reusable
})
export class FeatureModule {}

@Module({
  imports: [FeatureModule], // Accessing exports of FeatureModule
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

4. When should I use global: true for a module?

Use global: true when you need application-wide accessibility for a module (e.g., ConfigModule, LoggerModule). Avoid overuse to prevent unintended dependencies.


5. How does the scope of providers affect their lifecycle?

  • Singleton (default): One shared instance across the app.
  • Request: A new instance for each HTTP request.
  • Transient: A new instance every time it’s injected.

Example:

@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {}
Enter fullscreen mode Exit fullscreen mode

6. Can I import the same module into multiple other modules?

Yes! NestJS ensures that exported providers remain singletons unless explicitly scoped otherwise.


7. How do I handle circular dependencies between modules?

Use forwardRef() to resolve circular dependencies:

@Injectable()
export class AService {
  constructor(@Inject(forwardRef(() => BService)) private bService: BService) {}
}
Enter fullscreen mode Exit fullscreen mode

8. How do I test a module in isolation?

Use Test.createTestingModule() to create an isolated testing environment. Override providers to use mock implementations.

Example:

const module = await Test.createTestingModule({
  imports: [UserModule],
})
  .overrideProvider(UserService)
  .useValue(mockUserService)
  .compile();
Enter fullscreen mode Exit fullscreen mode

9. How do shared modules work in NestJS?

A shared module exports providers to be reused across multiple modules. Exported providers remain singletons.

Example:

@Module({
  providers: [LoggingService],
  exports: [LoggingService],
})
export class SharedModule {}
Enter fullscreen mode Exit fullscreen mode

10. How should I organize modules in a large NestJS application?

Structure modules by feature or domain for scalability:

src/
├── app.module.ts
├── modules/
│   ├── auth/
│   │   ├── auth.module.ts
│   │   ├── auth.service.ts
│   │   ├── auth.controller.ts
│   ├── users/
│   │   ├── users.module.ts
│   │   ├── users.service.ts
│   │   ├── users.controller.ts
│   ├── shared/
│       ├── shared.module.ts
│       ├── logging.service.ts
Enter fullscreen mode Exit fullscreen mode

The @Module decorator is the foundation of NestJS’s modular architecture. By understanding and leveraging its inputs—providers, controllers, imports, exports, global, and scope—you can create scalable, maintainable, and reusable application structures.

If this guide helped you, don’t forget to share it or leave a comment. Let’s discuss your experience with NestJS modules below!

Top comments (0)