---
name: nestjs-module-architecture
description: Applies NestJS module boundaries per bounded context, thin controllers over use-case services, DTOs in and response mappers out, typed config, and the dependency-injection scope choices that quietly halve throughput. Use when structuring a Nest application, resolving a circular module dependency, reviewing Nest code, or deciding what to unit test versus end-to-end.
license: MIT
compatibility: NestJS 10+ with TypeScript. Requires permission to run the project's build and test commands.
metadata:
  category: coding
  version: "1.0.0"
---

# NestJS Module Architecture

Nest gives you modules, providers and decorators, and no opinion at all about how to use them. Two
teams with the same framework produce a clean bounded-context application and an inextricable web of
`forwardRef` calls. The difference is a handful of decisions made early, all of them reversible only
at great cost later.

## Modules are bounded contexts

One module per business capability — `OrdersModule`, `BillingModule` — not per technical layer. A
module owns its controllers, its services, its repositories and its DTOs, and exports the narrowest
surface another module could need.

**The `SharedModule` anti-pattern.** A module named after its lack of a boundary accumulates
everything and is imported by everything, so it becomes the cycle magnet and the reason your test
bootstrap takes nine seconds. Two shapes replace it:

- **Infrastructure modules** with a real name — `DatabaseModule`, `HttpClientModule`, `ClockModule`.
  Stateless, no business logic, imported freely.
- **A `@Global()` module used exactly once**, for genuinely cross-cutting config and logging. If you
  reach for a second one, the boundary is wrong.

## forwardRef is a smell, not a solution

`forwardRef` silences a circular dependency; it does not remove one, and it will resurface as an
`undefined` provider at runtime in an order that depends on file layout. When you reach for it, pick
one of these instead:

1. **Emit an event.** If `Orders` needs `Billing` to do something after the fact, publish
   `OrderPlaced` and let `Billing` subscribe. Most cycles are a notification wearing a method call.
2. **Extract the shared interface** into a third module both depend on. The cycle was two modules
   sharing a concept neither owns.
3. **Extract the shared behaviour** into a module both import. Slower, and correct when the shared
   thing is logic rather than a type.

## Layers inside the module

Controller → use-case service → repository.

- **Controllers are thin.** Route, validate, delegate, map the result. No business logic, no
  repository access, no transaction handling.
- **Services hold one use case each** where practical. A `OrdersService` with nineteen public methods
  is a namespace, not a service.
- **DTOs in, mappers out.** A request DTO with `class-validator` decorators comes in; an explicit
  response object goes out. **Never return an ORM entity from a controller** — it leaks the schema,
  serialises lazy relations unpredictably, and turns a column rename into a breaking API change.

## Wire the pipeline once, globally

- A global `ValidationPipe` with `whitelist: true`, `forbidNonWhitelisted: true` and
  `transform: true`. Without `whitelist`, unexpected properties pass silently into your handlers.
- A typed config module validating `process.env` at boot against a schema, so a missing variable fails
  at startup rather than at 3am. **Ban `process.env` everywhere else** — it is the easiest rule to
  enforce with a lint rule and one of the most valuable.
- A global exception filter mapping domain errors to HTTP status codes in one place.
- An interceptor attaching a request id and structured logging.

## Dependency-injection scopes

Providers are singletons by default, which is what you want. `Scope.REQUEST` is the trap.

A request-scoped provider forces Nest to instantiate a new instance **per request** — and the scope
**bubbles up the entire injection chain**: everything that depends on it becomes request-scoped too,
often including your controller. A single innocuous request-scoped logger can turn a whole module
transient and measurably cut throughput.

Use it when you genuinely need per-request state and cannot pass it as an argument. Prefer
`AsyncLocalStorage` for request context, which gives you the same thing without the scope cascade.

## Testing

- **Unit** — `Test.createTestingModule` with the collaborators mocked. Fast, and the right place for
  business rules and branching.
- **End-to-end** — the real application against a real database in testcontainers, hitting HTTP.
  The right place for validation pipes, guards, serialisation and transactions, none of which a unit
  test exercises.
- Do not mock the repository and then claim the query works. That test asserts your mock.

## Verification gate

- [ ] Build passes and the application boots — a DI error only appears at boot, never at compile time.
- [ ] No `forwardRef` was added. If one survives, the reason is written next to it.
- [ ] No controller returns an ORM entity.
- [ ] `grep -rn "process.env" src/ --include=*.ts` hits only the config module.
- [ ] Every new `Scope.REQUEST` provider is justified in a comment naming what it holds.
- [ ] Unit and e2e suites both pass; paste the output rather than describing it.
