Structuring Feature Modules

One Folder, One Concern

Structuring Feature Modules

Group a feature's controllers, services, DTOs, and entities under a single module. The Nest CLI's resource generator scaffolds the convention for you.

4 min read Level 2/5 #nestjs#modules#structure
What you'll learn
  • Use the resource generator for a full feature scaffold
  • Follow the standard feature-module layout
  • Re-export shared providers through the module's exports array

A Nest app is a tree of feature modules — UsersModule, OrdersModule, BillingModule — each owning its slice of the domain. Get the layout right and your codebase scales without becoming a maze.

The Resource Generator

The CLI scaffolds an entire feature in one command:

nest g resource users

You’ll be asked which transport (REST is the default) and whether to generate CRUD entry points. Pick yes and you get:

src/users/
  users.module.ts
  users.controller.ts
  users.service.ts
  dto/
    create-user.dto.ts
    update-user.dto.ts
  entities/
    user.entity.ts

Everything is pre-wired into users.module.ts. Add your business logic and move on.

The Standard Layout

After a year of Nest, every feature module starts to look the same. Lean into the convention:

// users.module.ts
@Module({
  imports: [DatabaseModule],          // what this feature needs
  controllers: [UsersController],     // HTTP surface
  providers: [UsersService, UsersRepository],
  exports: [UsersService],            // what other features can use
})
export class UsersModule {}

Four arrays, four jobs: incoming dependencies, HTTP routes, internal providers, outgoing API. Keep them in that order in every file and readability becomes free.

Exports — Your Public API

Anything not in exports is private to the module. Other features that import UsersModule can only see what’s exported.

// orders.module.ts
@Module({
  imports: [UsersModule],         // pulls in whatever UsersModule exports
  providers: [OrdersService],
})
export class OrdersModule {}

// orders.service.ts can now inject UsersService
@Injectable()
export class OrdersService {
  constructor(private readonly users: UsersService) {}
}

This is real encapsulation. UsersRepository is registered but not exported, so no other feature can reach past UsersService into the data layer. Refactor the repo whenever you like.

A Few Layout Rules That Help

  • One feature, one folder. Co-locate the controller, service, DTOs, entities, and tests.
  • No cross-feature reach-ins. If BillingService wants UsersService, it goes through UsersModule’s exports. Never import a file from another feature’s internals.
  • Shared utilities live in a shared/ or common/ module. Cross-cutting helpers (pipes, guards, decorators) sit there.
  • AppModule just composes. It imports feature modules and global modules; it shouldn’t own controllers or services of its own.

Stick to this and a new teammate can navigate the codebase on day one.

Middleware →