ENGINEERING NOTES
NestJS Architecture: Modules, DI, Guards, Pipes and Interceptors
Place dependency wiring, authorization, validation and cross-cutting behavior at the right NestJS boundary.
What problem does this solve?
As services grow, manually wiring dependencies and repeating authorization or validation in every handler makes behavior inconsistent. Framework structure helps when responsibilities remain explicit rather than hidden behind decorators.
What is it?
Modules define provider visibility. Dependency injection constructs and supplies collaborators. Guards decide access, pipes transform or validate input, interceptors wrap execution, and exception filters translate failures.
Mental model
A module is a room with declared doors: providers must be exported and the room imported before another room can use them. Guards check entry, pipes inspect the package, and interceptors wrap the handler's execution and result.
How it works internally
Nest resolves provider tokens from module context. TypeScript interfaces disappear at runtime, so interfaces need an explicit token such as a symbol. In the request lifecycle, middleware precedes guards, then inbound interceptors, pipes and the handler; interceptors observe the return path. Scope choices affect provider lifetime and should be deliberate.
Production example
In the Pharma project, the gateway establishes trusted identity and services enforce organisation-scoped permissions. DTO validation protects transport input, while database constraints protect persistent invariants. NestJS is used there; the GCP e-commerce backend remains Express.js.
Node.js / TypeScript implementation
A module excerpt for a Nest TypeScript project with decorator metadata enabled. A symbol token makes the persistence dependency replaceable in focused tests. App bootstrap and HTTP transport are outside this excerpt.
import { Inject, Injectable, Module } from "@nestjs/common";
const PRODUCTS = Symbol("PRODUCTS");
interface Products { count(): Promise<number> }
@Injectable()
class Catalog {
constructor(@Inject(PRODUCTS) private readonly products: Products) {}
count() { return this.products.count(); }
}
@Module({
providers: [Catalog, {
provide: PRODUCTS, useValue: { count: async () => 0 }
}],
exports: [Catalog]
})
export class CatalogModule {}Common mistakes
Decorators do not replace business rules or runtime validation. An interface alone is not an injection token. A guard that trusts browser-supplied roles is unsafe. Circular dependencies often signal an ownership problem rather than a need to add forwardRef everywhere.
Performance / reliability considerations
Singleton providers must not store per-user mutable state. Request scope can increase object construction and propagate through dependencies. Test guards, pipes and transport behavior with integration tests; service mocks alone cannot prove the whole request pipeline is secure.
Interview questions
Why must a provider be exported? Why cannot an interface be injected directly? How do guards and pipes differ? Where does an interceptor run? What goes wrong when a singleton keeps request-specific identity?
Quick Revision
Modules define visibility; DI resolves tokens. Guards authorize, pipes validate/transform, interceptors wrap execution and filters handle exceptions. Interfaces need runtime tokens. Keep controllers thin and invariants in services/database constraints. Avoid mutable per-request state in singleton providers. Test the complete boundary as well as individual classes.