NestJS Module Boundaries and Request Lifecycles

NestJS brings an opinionated, Angular-inspired architecture to Node.js back ends: modules group related providers and controllers, a dependency injection container wires them together, and a defined request lifecycle runs middleware, guards, interceptors, pipes, and exception filters around every handler. This structure is why teams choose Nest — and misunderstanding it is why Nest codebases become tangled.

Two things matter most. First, module boundaries: providers are private to their module unless exported, so modules can enforce real encapsulation — or be undermined by global modules and circular imports. Second, lifecycle order: knowing exactly when a guard runs relative to a pipe, or why an interceptor does not see an error, determines where authentication, validation, logging, and error mapping belong.

TL;DR

Quick Example

A feature module with an explicit boundary, a role guard, a parameter pipe, and a timing interceptor:

A test that crosses the module boundary with a fake dependency:

Core Concepts

Modules, Providers, and Exports

Design modules around business capabilities, export a small service interface, and keep repositories and internal helpers private. A forwardRef() between two modules usually means responsibilities are split wrongly; extract the shared part or communicate through events.

The Request Lifecycle

Guards run before pipes, so authorization logic cannot rely on transformed DTOs. Interceptors see errors only if they handle them in the observable (catchError, finalize).

Validation and Errors

Enable a global ValidationPipe with whitelist: true, forbidNonWhitelisted: true, and transform: true to reject unknown fields and convert types. Throw domain exceptions from services and map them to HTTP in an exception filter, so services stay transport-independent. See TypeScript.

Injection Scopes

For per-request data such as tenant IDs or user context, prefer AsyncLocalStorage-based context (for example nestjs-cls) or durable providers over making large dependency chains request-scoped. See Angular Dependency Injection for the model Nest borrows.

Platforms

Nest runs on Express by default or Fastify for higher throughput; middleware written for one platform's request objects may not work on the other. See Express and Node.js.

Best Practices

Export Narrow Interfaces

Export services that represent the module's capability, not repositories or internal helpers.

Put Each Concern in the Right Enhancer

Authentication and authorization in guards, validation in pipes, cross-cutting timing and mapping in interceptors, error translation in filters.

Default to Singletons

Keep providers stateless and singleton; carry request context explicitly or with async local storage.

Test Modules as Units of Boundary

Use testing modules that import the real feature module and override only its external dependencies. See Integration Testing.

Common Mistakes

Everything Global

Authorization in an Interceptor or Pipe

Accidental Request Scope

FAQ

What is the order of the NestJS request lifecycle?

Middleware, then guards, then interceptors (before the handler), then pipes, then the route handler, then interceptors (after the handler), with exception filters handling any thrown exceptions before the response is sent.

Why can't my module inject a provider from another module?

Providers are encapsulated: the other module must list the provider in its exports, and your module must import that module.

When should I use a global module?

Rarely — for cross-cutting infrastructure such as configuration or logging. Global modules hide dependencies and weaken module boundaries.

What does request scope cost?

Request-scoped providers are instantiated per request, and any provider that depends on them also becomes request-scoped, increasing allocations and latency.

Where should validation happen in NestJS?

In pipes — typically a global ValidationPipe with DTO classes — so handlers receive validated, transformed input.

Related Topics

References