Skip to content
NestJS ns fundamentals 5 min read

Feature Modules

As an application grows, dumping every controller and provider into the root AppModule quickly becomes unmanageable. NestJS encourages you to slice the codebase along business domains — users, orders, billing — and give each one its own feature module. A feature module is a self-contained unit that bundles the controllers, providers, and any sub-imports for a single area of responsibility, then exposes only what the rest of the app needs. This is the foundation of separation of concerns in Nest, and it scales from a handful of routes to hundreds.

What a feature module is

A feature module is just a class decorated with @Module() that groups everything belonging to one domain. The four metadata keys you will use are controllers, providers, imports, and exports. The critical idea is encapsulation: a provider declared in a module is private to that module unless you explicitly list it in exports. Other modules can only consume a provider after the owning module exports it and the consuming module imports the owning module.

KeyPurposeVisibility effect
controllersHTTP route handlers owned by this moduleAlways active when the module is loaded
providersServices, repositories, factories for DIPrivate to this module by default
importsOther modules whose exports this module needsBrings in their exported providers
exportsSubset of providers made available to importersMakes a provider public

Generating a feature module

The Nest CLI scaffolds a module, controller, and service in one command and automatically wires the new module into AppModule for you.

nest generate module users
nest generate controller users
nest generate service users

Output:

CREATE src/users/users.module.ts (82 bytes)
CREATE src/users/users.controller.ts (99 bytes)
CREATE src/users/users.controller.spec.ts (478 bytes)
CREATE src/users/users.service.ts (89 bytes)
UPDATE src/app.module.ts (312 bytes)

The generated UsersModule registers its own controller and service. Note that UsersService is listed in providers but not in exports, so it stays private to the users domain.

// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

Here exports: [UsersService] is the deliberate choice that lets other domains — like orders — depend on user data without reaching into the users module’s internals.

Building the users domain

A realistic service holds the domain logic and data access. Keep it framework-light so it can be tested in isolation.

// src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';

export interface User {
  id: number;
  name: string;
  email: string;
}

@Injectable()
export class UsersService {
  private readonly users: User[] = [
    { id: 1, name: 'Ada Lovelace', email: '[email protected]' },
    { id: 2, name: 'Alan Turing', email: '[email protected]' },
  ];

  findOne(id: number): User {
    const user = this.users.find((u) => u.id === id);
    if (!user) throw new NotFoundException(`User ${id} not found`);
    return user;
  }
}
// src/users/users.controller.ts
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
import { UsersService, User } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number): User {
    return this.usersService.findOne(id);
  }
}

Composing modules through imports and exports

The orders domain needs to validate that a user exists before creating an order. Instead of duplicating user logic, OrdersModule imports UsersModule and injects the exported UsersService directly.

// src/orders/orders.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

export interface Order {
  id: number;
  userId: number;
  total: number;
}

@Injectable()
export class OrdersService {
  private orders: Order[] = [];

  constructor(private readonly usersService: UsersService) {}

  create(userId: number, total: number): Order {
    // Throws NotFoundException if the user does not exist.
    this.usersService.findOne(userId);
    const order: Order = { id: this.orders.length + 1, userId, total };
    this.orders.push(order);
    return order;
  }
}
// src/orders/orders.module.ts
import { Module } from '@nestjs/common';
import { OrdersController } from './orders.controller';
import { OrdersService } from './orders.service';
import { UsersModule } from '../users/users.module';

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

Because UsersModule exports UsersService and OrdersModule imports UsersModule, Nest’s dependency injection can satisfy the UsersService parameter in OrdersService’s constructor.

If you inject a provider that the owning module did not export, Nest fails at startup with a Nest can't resolve dependencies error. The fix is almost always to add the provider to the owning module’s exports array — not to re-declare it in the consuming module, which would create a second, unrelated instance.

Wiring everything into the root module

The root AppModule becomes thin: it simply composes the feature modules. It declares no domain controllers or providers of its own.

// src/app.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { OrdersModule } from './orders/orders.module';

@Module({
  imports: [UsersModule, OrdersModule],
})
export class AppModule {}

A request to POST /orders for a non-existent user now surfaces the users domain’s own error, proving the modules are correctly composed.

Output:

$ curl -s -X POST localhost:3000/orders -H 'content-type: application/json' -d '{"userId":99,"total":50}'
{"statusCode":404,"message":"User 99 not found","error":"Not Found"}

Best Practices

  • Organize one module per business domain (users, orders, billing) and keep each module’s files in a dedicated folder.
  • Export only the providers other modules genuinely need; everything else should stay private to enforce real encapsulation.
  • Compose feature modules in the root AppModule via imports and keep the root module free of domain controllers and providers.
  • Import the module, not the provider — never re-list another module’s service in your own providers, or you will get a duplicate instance.
  • When two feature modules need each other, use forwardRef() to resolve the circular dependency rather than merging the domains.
  • Promote shared, app-wide providers (config, logging) into a dedicated module and mark it @Global() instead of re-importing it everywhere.
Last updated June 14, 2026
Was this helpful?