NestJS is a progressive Node.js framework for building efficient, reliable, and scalable server-side applications. Combining the best ideas from OOP (Object-Oriented Programming), FP (Functional Programming), and FRP (Functional Reactive Programming), it gives you a fully-architected, batteries-included platform on top of Express (or Fastify).
If you’re coming from Angular, you’ll feel right at home with its module/controller/service structure and powerful dependency-injection system.
In this article we’ll cover both theory – why NestJS exists, how it’s structured, and when to reach for it –and practice, with bite-sized code snippets demonstrating how to bootstrap a project, define routes, inject dependencies, and more. Let’s start by understanding what NestJS is and where it came from.
Table of Contents
1. What is NestJS?
NestJS is a framework for building server-side applications in Node.js. It’s written in TypeScript (but supports plain JavaScript as well). At its core, it:
Wraps a mature HTTP server library (Express or Fastify)
Standardizes application architecture around modules, controllers, and providers
Leverages TypeScript’s type system for compile-time safety and clear APIs
Offers built-in support for things like validation, configuration, and testing
Rather than stitching together middleware by hand, NestJS encourages a declarative, layered approach. You define modules to group related functionality, controllers to handle incoming requests, and providers (often called “services”) for your business logic. Behind the scenes, NestJS resolves dependencies via an IoC container, so you can focus on writing clean, reusable classes.
To start up a project, run the following commands:
<span class="hljs-comment"># Install the Nest CLI globally</span>
npm install -g @nestjs/cli
<span class="hljs-comment"># Create a new project called 'my-app'</span>
nest new my-app
<span class="hljs-built_in">cd</span> my-app
npm run start:dev
Once it’s running, you have a ready-to-go HTTP server with hot reloading, strict typing, and a sensible folder layout.
1.1 History and Philosophy
NestJS first appeared in 2017, created by Kamil Myśliwiec. Its goal was to bring the architectural patterns of Angular to the backend world, providing:
Consistency: A single, opinionated way to structure applications.
Scalability: Clear boundaries (modules) make it easier to grow teams and codebases.
Testability: Built-in support for Jest and clear separation of concerns.
Extensibility: A pluggable module system makes it easy to integrate ORMs, WebSockets, GraphQL, microservices, and more.
Under the hood, NestJS embraces these principles:
Modularity: Everything lives in a module (
AppModule
,UsersModule
, and so on), which can import other modules or export providers.Dependency Injection: Services can be injected into controllers (and even into other services), which fosters loose coupling.
Decorators and Metadata: With TypeScript decorators (
@Module()
,@Controller()
,@Injectable()
), NestJS reads metadata at runtime to wire everything together.
Here’s a tiny example showing the interplay of these pieces:
<span class="hljs-comment">// users.service.ts</span>
<span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> UsersService {
<span class="hljs-keyword">private</span> users = [{ id: <span class="hljs-number">1</span>, name: <span class="hljs-string">'Alice'</span> }];
findAll() {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.users;
}
}
<span class="hljs-comment">// users.controller.ts</span>
<span class="hljs-keyword">import</span> { Controller, Get } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { UsersService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./users.service'</span>;
<span class="hljs-meta">@Controller</span>(<span class="hljs-string">'users'</span>)
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> UsersController {
<span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> usersService: UsersService</span>) {}
<span class="hljs-meta">@Get</span>()
getUsers() {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.usersService.findAll();
}
}
<span class="hljs-comment">// users.module.ts</span>
<span class="hljs-keyword">import</span> { Module } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { UsersController } <span class="hljs-keyword">from</span> <span class="hljs-string">'./users.controller'</span>;
<span class="hljs-keyword">import</span> { UsersService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./users.service'</span>;
<span class="hljs-meta">@Module</span>({
controllers: [UsersController],
providers: [UsersService],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> UsersModule {}
The
@Module
decorator groups controller + serviceThe controller injects the service via its constructor
A simple
GET /users
route returns an array of user objects
With that foundation laid, in the next section we’ll explore why you’d choose NestJS, comparing it to other popular Node frameworks and outlining common real-world use cases.
2. Why Choose NestJS?
NestJS isn’t just another Node.js framework – it brings a structured, enterprise-grade approach to building backend services. In this section we’ll cover benefits and real-world use cases, then compare NestJS to other popular Node frameworks so you can see where it fits best.
2.1 Benefits and Use Cases
Strong architectural patterns
Modularity: You break your app into focused modules (
AuthModule
,ProductsModule
, and so on), each responsible for a slice of functionality.Separation of concerns: Controllers handle HTTP, services encapsulate business logic, modules wire everything up.
Scalability: Growing teams map naturally onto modules—new features rarely touch existing code.
Built-in dependency injection (DI)
DI makes testing and swapping implementations trivial.
You can easily mock a service in a unit test:
<span class="hljs-comment">// products.controller.spec.ts</span>
<span class="hljs-keyword">import</span> { Test, TestingModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/testing'</span>;
<span class="hljs-keyword">import</span> { ProductsController } <span class="hljs-keyword">from</span> <span class="hljs-string">'./products.controller'</span>;
<span class="hljs-keyword">import</span> { ProductsService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./products.service'</span>;
describe(<span class="hljs-string">'ProductsController'</span>, <span class="hljs-function">() =></span> {
<span class="hljs-keyword">let</span> controller: ProductsController;
<span class="hljs-keyword">const</span> mockService = { findAll: <span class="hljs-function">() =></span> [<span class="hljs-string">'apple'</span>, <span class="hljs-string">'banana'</span>] };
beforeEach(<span class="hljs-keyword">async</span> () => {
<span class="hljs-keyword">const</span> <span class="hljs-keyword">module</span>: TestingModule = await Test.createTestingModule({
controllers: [ProductsController],
providers: [
{ provide: ProductsService, useValue: mockService },
],
}).compile();
controller = <span class="hljs-built_in">module</span>.get<ProductsController>(ProductsController);
});
it(<span class="hljs-string">'returns a list of products'</span>, <span class="hljs-function">() =></span> {
expect(controller.getAll()).toEqual([<span class="hljs-string">'apple'</span>, <span class="hljs-string">'banana'</span>]);
});
});
TypeScript-first
Full type safety at compile time.
Leverage interfaces and decorators (
@Body()
,@Param()
) to validate and transform data.
Rich ecosystem and extensibility
Official integrations for WebSockets, GraphQL, microservices (RabbitMQ, Kafka), and more.
Hundreds of community modules (for example
@nestjs/swagger
for OpenAPI docs).
Production-grade tooling
CLI generates boilerplate (
nest g module
,nest g service
).Support for hot-reload in development (
npm run start:dev
).Built-in testing setup with Jest.
Real-World Use Cases:
Enterprise APIs with strict module boundaries and RBAC.
Microservices architectures, where each service is a self-contained NestJS app.
Real-time applications (chat, live dashboards) using Nest’s WebSocket gateways.
GraphQL backends with code-first schemas.
Event-driven systems connecting to message brokers.
2.2 Comparison with Other Frameworks
Feature | Express | Koa | NestJS |
Architecture | Minimal, unopinionated | Minimal, middleware-based | Opinionated modules/controllers/services |
Dependency Injection | Manual wiring | Manual wiring | Built-in, reflect-metadata |
TypeScript Support | Via DefinitelyTyped | Via DefinitelyTyped | First-class, decorators |
CLI Tooling | None (3rd-party) | None | @nestjs/cli generates code |
Testing | User-configured | User-configured | Jest + DI makes mocking easy |
Ecosystem | Middleware library | Middleware library | Official microservices, GraphQL, Swagger modules |
Learning Curve | Low | Low | Medium (learning Nest idioms) |
Express is great if you want minimal layers and full control, but you’ll end up hand-rolling a lot (DI, validation, folder structure).
Koa offers a more modern middleware approach, but still leaves architecture decisions to you.
NestJS provides the full stack: structure, DI, validation, testing, and official integrations, which is ideal if you value consistency, type safety, and out-of-the-box best practices.
When to choose NestJS:
NextJS is great for various use cases. It’s particularly effective if you’re building a large-scale API or microservice suite, if you want a solid architecture from day one, and if you prefer TypeScript and DI to keep code testable and maintainable.
With these advantages in mind, you’ll find that NestJS can dramatically speed up development, especially on projects that need robust structure and clear boundaries.
In the next section, we’ll dive into getting started: installing the CLI, creating a project, and exploring the generated folder layout.
3. Getting Started
Let’s jump into the basics: installing the CLI, scaffolding a new project, and exploring the default folder layout.
3.1 Installing the CLI
Nest ships with an official command-line tool that helps you generate modules, controllers, services, and more. Under the hood it uses Yeoman templates to keep everything consistent.
<span class="hljs-comment"># Install the CLI globally (requires npm ≥ 6)</span>
npm install -g @nestjs/cli
Once installed, you can run nest --help
to see available commands:
nest --<span class="hljs-built_in">help</span>
Usage: nest <<span class="hljs-built_in">command</span>> [options]
Commands:
new <name> Scaffold a new project
generate|g <schematic> [options] Generate artifacts (modules, controllers, ...)
build Build project with webpack
...
Options:
-v, --version Show version number
-h, --<span class="hljs-built_in">help</span> Show <span class="hljs-built_in">help</span>
3.2 Creating Your First Project
Scaffolding a new app is a single command. The CLI will ask whether to use npm or yarn, and whether to enable strict TypeScript settings.
<span class="hljs-comment"># Create a new Nest app in the "my-nest-app" folder</span>
nest new my-nest-app
After answering the prompts, you’ll have:
<span class="hljs-built_in">cd</span> my-nest-app
npm run start:dev
This launches a development server on http://localhost:3000
with automatic reload on file changes.
3.3 Project Structure Overview
By default, you’ll see something like:
my-nest-app/
├── src/
│ ├── app.controller.ts <span class="hljs-comment"># example controller</span>
│ ├── app.controller.spec.ts <span class="hljs-comment"># unit test for controller</span>
│ ├── app.module.ts <span class="hljs-comment"># root application module</span>
│ ├── app.service.ts <span class="hljs-comment"># example provider</span>
│ └── main.ts <span class="hljs-comment"># entry point (bootstraps Nest)</span>
├── <span class="hljs-built_in">test</span>/ <span class="hljs-comment"># end-to-end tests</span>
├── node_modules/
├── package.json
├── tsconfig.json
└── nest-cli.json <span class="hljs-comment"># CLI configuration</span>
src/main.ts
The “bootstrap” script. It creates a Nest application instance and starts listening on a port:<span class="hljs-keyword">import</span> { NestFactory } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/core'</span>; <span class="hljs-keyword">import</span> { AppModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'./app.module'</span>; <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">bootstrap</span>(<span class="hljs-params"></span>) </span>{ <span class="hljs-keyword">const</span> app = <span class="hljs-keyword">await</span> NestFactory.create(AppModule); <span class="hljs-keyword">await</span> app.listen(<span class="hljs-number">3000</span>); <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`🚀 Application is running on: <span class="hljs-subst">${<span class="hljs-keyword">await</span> app.getUrl()}</span>`</span>); } bootstrap();
src/app.module.ts
The root module. It ties together controllers and providers:<span class="hljs-keyword">import</span> { Module } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { AppController } <span class="hljs-keyword">from</span> <span class="hljs-string">'./app.controller'</span>; <span class="hljs-keyword">import</span> { AppService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./app.service'</span>; <span class="hljs-meta">@Module</span>({ imports: [], <span class="hljs-comment">// other modules to import</span> controllers: [AppController], providers: [AppService], }) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppModule {}
src/app.controller.ts / app.service.ts
A simple example that shows dependency injection in action:<span class="hljs-comment">// app.controller.ts</span> <span class="hljs-keyword">import</span> { Controller, Get } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { AppService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./app.service'</span>; <span class="hljs-meta">@Controller</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppController { <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> appService: AppService</span>) {} <span class="hljs-meta">@Get</span>() getHello(): <span class="hljs-built_in">string</span> { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.appService.getHello(); } } <span class="hljs-comment">// app.service.ts</span> <span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-meta">@Injectable</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppService { getHello(): <span class="hljs-built_in">string</span> { <span class="hljs-keyword">return</span> <span class="hljs-string">'Hello, NestJS!'</span>; } }
With this scaffold in place, you have a minimal – but fully functional – NestJS application. From here, you can generate new modules, controllers, and services:
<span class="hljs-comment"># Generate a new module, controller, and service for "tasks"</span>
nest g module tasks
nest g controller tasks
nest g service tasks
Each command will drop a new .ts
file in the appropriate folder and update your module’s metadata. In the next section, we’ll dive into core Nest building blocks like modules, controllers, and providers in more detail.
4. Core NestJS Building Blocks
At the heart of every NestJS application are three pillars: Modules, Controllers, and Providers (often called Services). Let’s see what each one does, and how they fit together in theory and in practice.
4.1 Modules
A Module is a logical boundary – a container that groups related components (controllers, providers, and even other modules). Every NestJS app has at least one root module (usually AppModule
), and you create feature modules (UsersModule
, AuthModule
, and so on) to organize code by domain.
@Module() Decorator
imports
: other modules to usecontrollers
: controllers that handle incoming requestsproviders
: services or values available via DIexports
: providers that should be visible to importing modules
Here’s an example:
<span class="hljs-comment">// cats.module.ts</span>
<span class="hljs-keyword">import</span> { Module } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { CatsController } <span class="hljs-keyword">from</span> <span class="hljs-string">'./cats.controller'</span>;
<span class="hljs-keyword">import</span> { CatsService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./cats.service'</span>;
<span class="hljs-meta">@Module</span>({
imports: [], <span class="hljs-comment">// e.g. TypeOrmModule.forFeature([Cat])</span>
controllers: [CatsController],
providers: [CatsService],
<span class="hljs-built_in">exports</span>: [CatsService], <span class="hljs-comment">// makes CatsService available to other modules</span>
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> CatsModule {}
Then in your root module:
<span class="hljs-comment">// app.module.ts</span>
<span class="hljs-keyword">import</span> { Module } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { CatsModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'./cats/cats.module'</span>;
<span class="hljs-meta">@Module</span>({
imports: [CatsModule],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppModule {}
Now anything that injects CatsService
will resolve to the one defined inside CatsModule
.
4.2 Controllers
A Controller maps incoming HTTP requests to handler methods. It’s responsible for extracting request data (query parameters, body, headers) and returning a response. Controllers should remain thin – delegating business logic to providers.
@Controller(path?): Defines a route prefix
@Get, @Post, @Put, @Delete, and so on: Define method-level routes
@Param(), @Query(), @Body(), @Headers(), @Req(), @Res(): Decorators to extract request details
Here’s an example:
<span class="hljs-comment">// cats.controller.ts</span>
<span class="hljs-keyword">import</span> { Controller, Get, Post, Body, Param } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { CatsService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./cats.service'</span>;
<span class="hljs-keyword">import</span> { CreateCatDto } <span class="hljs-keyword">from</span> <span class="hljs-string">'./dto/create-cat.dto'</span>;
<span class="hljs-meta">@Controller</span>(<span class="hljs-string">'cats'</span>) <span class="hljs-comment">// prefix: /cats</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> CatsController {
<span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> catsService: CatsService</span>) {}
<span class="hljs-meta">@Get</span>()
findAll() {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.catsService.findAll(); <span class="hljs-comment">// GET /cats</span>
}
<span class="hljs-meta">@Get</span>(<span class="hljs-string">':id'</span>)
findOne(<span class="hljs-meta">@Param</span>(<span class="hljs-string">'id'</span>) id: <span class="hljs-built_in">string</span>) {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.catsService.findOne(+id); <span class="hljs-comment">// GET /cats/1</span>
}
<span class="hljs-meta">@Post</span>()
create(<span class="hljs-meta">@Body</span>() createCatDto: CreateCatDto) {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.catsService.create(createCatDto); <span class="hljs-comment">// POST /cats</span>
}
}
<span class="hljs-comment">// dto/create-cat.dto.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> CreateCatDto {
<span class="hljs-keyword">readonly</span> name: <span class="hljs-built_in">string</span>;
<span class="hljs-keyword">readonly</span> age: <span class="hljs-built_in">number</span>;
<span class="hljs-keyword">readonly</span> breed?: <span class="hljs-built_in">string</span>;
}
4.3 Providers (Services)
Providers are classes annotated with @Injectable()
that contain your business logic or data access. Anything you want to inject elsewhere must be a provider. You can provide plain values, factory functions, or classes.
@Injectable(): Marks a class as available for DI
Scope: Default is singleton, but you can change to request or transient
Custom Providers: Use
useClass
,useValue
,useFactory
, oruseExisting
for more control
Here’s an example:
<span class="hljs-comment">// cats.service.ts</span>
<span class="hljs-keyword">import</span> { Injectable, NotFoundException } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { CreateCatDto } <span class="hljs-keyword">from</span> <span class="hljs-string">'./dto/create-cat.dto'</span>;
<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> CatsService {
<span class="hljs-keyword">private</span> cats = [];
create(dto: CreateCatDto) {
<span class="hljs-keyword">const</span> newCat = { id: <span class="hljs-built_in">Date</span>.now(), ...dto };
<span class="hljs-built_in">this</span>.cats.push(newCat);
<span class="hljs-keyword">return</span> newCat;
}
findAll() {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.cats;
}
findOne(id: <span class="hljs-built_in">number</span>) {
<span class="hljs-keyword">const</span> cat = <span class="hljs-built_in">this</span>.cats.find(<span class="hljs-function"><span class="hljs-params">c</span> =></span> c.id === id);
<span class="hljs-keyword">if</span> (!cat) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotFoundException(<span class="hljs-string">`Cat #<span class="hljs-subst">${id}</span> not found`</span>);
}
<span class="hljs-keyword">return</span> cat;
}
}
Injecting a Custom Value:
<span class="hljs-comment">// logger.provider.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> LOGGER = {
provide: <span class="hljs-string">'LOGGER'</span>,
useValue: <span class="hljs-built_in">console</span>,
};
<span class="hljs-comment">// app.module.ts</span>
<span class="hljs-keyword">import</span> { Module } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { LOGGER } <span class="hljs-keyword">from</span> <span class="hljs-string">'./logger.provider'</span>;
<span class="hljs-meta">@Module</span>({
providers: [LOGGER],
<span class="hljs-built_in">exports</span>: [LOGGER],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppModule {}
<span class="hljs-comment">// some.service.ts</span>
<span class="hljs-keyword">import</span> { Inject, Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> SomeService {
<span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-meta">@Inject</span>(<span class="hljs-string">'LOGGER'</span>) <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> logger: Console</span>) {}
logMessage(msg: <span class="hljs-built_in">string</span>) {
<span class="hljs-built_in">this</span>.logger.log(<span class="hljs-string">`Custom log: <span class="hljs-subst">${msg}</span>`</span>);
}
}
With modules wiring up controllers and providers, NestJS gives you a scalable, testable foundation. In the next section, we’ll explore Dependency Injection in depth – how it works under the hood and how to create custom providers and factory-based injections.
5. Dependency Injection
Nest’s built-in Dependency Injection (DI) system is the heart of how components (controllers, services, and so on) talk to each other in a loosely-coupled, testable way.
5.1 How DI Works in NestJS
When your application boots, Nest builds a module-based IoC container. Each @Injectable()
provider is registered in the container under a token (by default, its class). When a class declares a dependency in its constructor, Nest looks up that token and injects the matching instance.
Singleton scope: One instance per application (default)
Request scope: New instance per incoming request
Transient scope: New instance every time it’s injected
Here’s an example:
<span class="hljs-comment">// cats.service.ts</span>
<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> CatsService {
<span class="hljs-comment">// ...</span>
}
<span class="hljs-comment">// cats.controller.ts</span>
<span class="hljs-meta">@Controller</span>(<span class="hljs-string">'cats'</span>)
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> CatsController {
<span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> catsService: CatsService</span>) {}
<span class="hljs-comment">// Nest sees CatsService in the constructor,</span>
<span class="hljs-comment">// finds its singleton instance, and injects it.</span>
}
Behind the scenes, Nest collects metadata from decorators (@Injectable()
, @Controller()
) and builds a graph of providers. When you call NestFactory.create(AppModule)
, it resolves that graph and wires everything together.
5.2 Custom Providers and Factory Providers
Sometimes you need to inject non-class values (APIs, constants) or run logic at registration time. Nest lets you define custom providers using the provide
syntax.
useValue
Inject a plain value or object:
<span class="hljs-comment">// config.constant.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> APP_NAME = {
provide: <span class="hljs-string">'APP_NAME'</span>,
useValue: <span class="hljs-string">'MyAwesomeApp'</span>,
};
<span class="hljs-comment">// app.module.ts</span>
<span class="hljs-meta">@Module</span>({
providers: [APP_NAME],
<span class="hljs-built_in">exports</span>: [<span class="hljs-string">'APP_NAME'</span>],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppModule {}
<span class="hljs-comment">// some.service.ts</span>
<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> SomeService {
<span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-meta">@Inject</span>(<span class="hljs-string">'APP_NAME'</span>) <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> name: <span class="hljs-built_in">string</span></span>) {}
whoAmI() {
<span class="hljs-keyword">return</span> <span class="hljs-string">`Running in <span class="hljs-subst">${<span class="hljs-built_in">this</span>.name}</span>`</span>;
}
}
useClass
Swap implementations easily (useful for testing or feature flags):
<span class="hljs-comment">// logger.interface.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> Logger {
log(msg: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">void</span>;
}
<span class="hljs-comment">// console-logger.ts</span>
<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ConsoleLogger <span class="hljs-keyword">implements</span> Logger {
log(msg: <span class="hljs-built_in">string</span>) { <span class="hljs-built_in">console</span>.log(msg); }
}
<span class="hljs-comment">// file-logger.ts</span>
<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> FileLogger <span class="hljs-keyword">implements</span> Logger {
log(msg: <span class="hljs-built_in">string</span>) { <span class="hljs-comment">/* write to file */</span> }
}
<span class="hljs-comment">// app.module.ts</span>
<span class="hljs-meta">@Module</span>({
providers: [
{ provide: <span class="hljs-string">'Logger'</span>, useClass: FileLogger },
],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppModule {}
<span class="hljs-comment">// any.service.ts</span>
<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AnyService {
<span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-meta">@Inject</span>(<span class="hljs-string">'Logger'</span>) <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> logger: Logger</span>) {}
}
useFactory
Run arbitrary factory logic (for example, async initialization, dynamic config):
<span class="hljs-comment">// database.provider.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> DATABASE = {
provide: <span class="hljs-string">'DATABASE'</span>,
useFactory: <span class="hljs-keyword">async</span> (configService: ConfigService) => {
<span class="hljs-keyword">const</span> opts = configService.getDbOptions();
<span class="hljs-keyword">const</span> connection = <span class="hljs-keyword">await</span> createConnection(opts);
<span class="hljs-keyword">return</span> connection;
},
inject: [ConfigService],
};
<span class="hljs-comment">// app.module.ts</span>
<span class="hljs-meta">@Module</span>({
imports: [ConfigModule],
providers: [DATABASE],
<span class="hljs-built_in">exports</span>: [<span class="hljs-string">'DATABASE'</span>],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppModule {}
<span class="hljs-comment">// users.service.ts</span>
<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> UsersService {
<span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-meta">@Inject</span>(<span class="hljs-string">'DATABASE'</span>) <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> db: Connection</span>) {}
}
With custom providers and the factory pattern, you can integrate external libraries, toggle implementations, or perform async setup – all while retaining the clear, testable structure NestJS provides.
In the next section we’ll look at Routing and Middleware, showing how to define route handlers, apply global or per-route middleware, and extend your HTTP pipeline.
6. Routing & Middleware
Routing in NestJS is built on top of your controllers and decorators, while middleware lets you hook into the request/response pipeline for cross-cutting concerns like logging, authentication checks, or CORS.
6.1 Defining Routes
First, a bit of theory:
@Controller(path?) sets a URL prefix for all routes in that class.
@Get, @Post, @Put, @Delete, etc. define HTTP-method handlers.
@Param(), @Query(), @Body(), @Headers(), @Req(), @Res() extract parts of the incoming request.
You can combine route decorators and parameter decorators to build expressive, type-safe endpoints.
Here’s an example:
<span class="hljs-comment">// products.controller.ts</span>
<span class="hljs-keyword">import</span> { Controller, Get, Post, Param, Query, Body } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { ProductsService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./products.service'</span>;
<span class="hljs-keyword">import</span> { CreateProductDto } <span class="hljs-keyword">from</span> <span class="hljs-string">'./dto/create-product.dto'</span>;
<span class="hljs-meta">@Controller</span>(<span class="hljs-string">'products'</span>) <span class="hljs-comment">// all routes here start with /products</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ProductsController {
<span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> productsService: ProductsService</span>) {}
<span class="hljs-meta">@Get</span>() <span class="hljs-comment">// GET /products</span>
findAll(
<span class="hljs-meta">@Query</span>(<span class="hljs-string">'limit'</span>) limit = <span class="hljs-string">'10'</span>, <span class="hljs-comment">// optional query ?limit=20</span>
) {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.productsService.findAll(+limit);
}
<span class="hljs-meta">@Get</span>(<span class="hljs-string">':id'</span>) <span class="hljs-comment">// GET /products/123</span>
findOne(<span class="hljs-meta">@Param</span>(<span class="hljs-string">'id'</span>) id: <span class="hljs-built_in">string</span>) {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.productsService.findOne(+id);
}
<span class="hljs-meta">@Post</span>() <span class="hljs-comment">// POST /products</span>
create(<span class="hljs-meta">@Body</span>() dto: CreateProductDto) {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.productsService.create(dto);
}
}
You can also nest controllers by importing a feature module, and use @Patch, @Put, @Delete, @Head, and so on for full RESTful coverage.
6.2 Applying Middleware
Middleware are functions that run before your routes handle a request. They’re useful for logging, body-parsing (though Nest provides built-ins), authentication guards at a lower level, rate limiting, and so on.
You can implement them either as a functional middleware or a class implementing NestMiddleware
.
Here’s an example (Functional Middleware):
<span class="hljs-comment">// logger.middleware.ts</span>
<span class="hljs-keyword">import</span> { Request, Response, NextFunction } <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">logger</span>(<span class="hljs-params">req: Request, res: Response, next: NextFunction</span>) </span>{
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">`[<span class="hljs-subst">${<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString()}</span>] <span class="hljs-subst">${req.method}</span> <span class="hljs-subst">${req.url}</span>`</span>);
next();
}
<span class="hljs-comment">// app.module.ts</span>
<span class="hljs-keyword">import</span> { Module, NestModule, MiddlewareConsumer } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { logger } <span class="hljs-keyword">from</span> <span class="hljs-string">'./logger.middleware'</span>;
<span class="hljs-keyword">import</span> { ProductsModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'./products/products.module'</span>;
<span class="hljs-meta">@Module</span>({
imports: [ProductsModule],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppModule <span class="hljs-keyword">implements</span> NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(logger) <span class="hljs-comment">// apply logger</span>
.forRoutes(<span class="hljs-string">'products'</span>); <span class="hljs-comment">// only for /products routes</span>
}
}
And here’s another example (Class-based Middleware):
<span class="hljs-comment">// auth.middleware.ts</span>
<span class="hljs-keyword">import</span> { Injectable, NestMiddleware } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { Request, Response, NextFunction } <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AuthMiddleware <span class="hljs-keyword">implements</span> NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
<span class="hljs-keyword">if</span> (!req.headers.authorization) {
<span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).send(<span class="hljs-string">'Unauthorized'</span>);
}
<span class="hljs-comment">// validate token...</span>
next();
}
}
<span class="hljs-comment">// security.module.ts</span>
<span class="hljs-keyword">import</span> { Module, NestModule, MiddlewareConsumer } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { AuthMiddleware } <span class="hljs-keyword">from</span> <span class="hljs-string">'./auth.middleware'</span>;
<span class="hljs-keyword">import</span> { UsersController } <span class="hljs-keyword">from</span> <span class="hljs-string">'./users.controller'</span>;
<span class="hljs-meta">@Module</span>({
controllers: [UsersController],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> SecurityModule <span class="hljs-keyword">implements</span> NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuthMiddleware)
.forRoutes(UsersController); <span class="hljs-comment">// apply to all routes in UsersController</span>
}
}
Tip: Global middleware can be applied in your main.ts
before the app.listen()
call via app.use(logger)
if you want it on every request.
With routing and middleware set up, you have full control over how requests flow through your application. Next up, we’ll dive into Request Lifecycle and Pipes, exploring how data transforms and validations happen as part of each request.
7. Request Lifecycle & Pipes
NestJS processes each incoming request through a defined “lifecycle” of steps – routing to the correct handler, applying pipes, guards, interceptors, and finally invoking your controller method. Pipes sit between the incoming request and your handler, transforming or validating data before it reaches your business logic.
7.1 What Are Pipes?
A Pipe is a class annotated with @Injectable()
that implements the PipeTransform
interface. It has a single method:
transform(value: <span class="hljs-built_in">any</span>, metadata: ArgumentMetadata): <span class="hljs-built_in">any</span>
Transformation: Convert input data (for example, a string
"123"
) into the desired type (number
123
).Validation: Check that incoming data meets certain rules and throw an exception (usually a
BadRequestException
) if it doesn’t.
By default, pipes run after middleware and before guards/interceptors, for each decorated parameter (@Body()
, @Param()
, and so on).
Here’s how it works:
Nest ships with a handy global validation pipe that integrates with class-validator:
<span class="hljs-comment">// main.ts</span>
<span class="hljs-keyword">import</span> { ValidationPipe } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { NestFactory } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/core'</span>;
<span class="hljs-keyword">import</span> { AppModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'./app.module'</span>;
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">bootstrap</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">await</span> NestFactory.create(AppModule);
<span class="hljs-comment">// Automatically validate and strip unknown properties</span>
app.useGlobalPipes(<span class="hljs-keyword">new</span> ValidationPipe({ whitelist: <span class="hljs-literal">true</span>, forbidNonWhitelisted: <span class="hljs-literal">true</span> }));
<span class="hljs-keyword">await</span> app.listen(<span class="hljs-number">3000</span>);
}
bootstrap();
With this in place, any DTO annotated with validation decorators will be checked before your handler runs:
<span class="hljs-comment">// dto/create-user.dto.ts</span>
<span class="hljs-keyword">import</span> { IsEmail, IsString, MinLength } <span class="hljs-keyword">from</span> <span class="hljs-string">'class-validator'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> CreateUserDto {
<span class="hljs-meta">@IsEmail</span>() <span class="hljs-comment">// must be a valid email</span>
email: <span class="hljs-built_in">string</span>;
<span class="hljs-meta">@IsString</span>() <span class="hljs-comment">// must be a string</span>
<span class="hljs-meta">@MinLength</span>(<span class="hljs-number">8</span>) <span class="hljs-comment">// at least 8 characters</span>
password: <span class="hljs-built_in">string</span>;
}
<span class="hljs-comment">// users.controller.ts</span>
<span class="hljs-meta">@Post</span>()
createUser(<span class="hljs-meta">@Body</span>() dto: CreateUserDto) {
<span class="hljs-comment">// If body.email isn't an email, or password is shorter,</span>
<span class="hljs-comment">// Nest throws a 400 Bad Request with details.</span>
<span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.usersService.create(dto);
}
7.2 Built-In vs. Custom Pipes
Built-In Pipes
Nest provides several out-of-the-box pipes:
ValidationPipe: Integrates with
class-validator
for DTO validation (shown above).ParseIntPipe: Converts a route parameter to
number
or throwsBadRequestException
.ParseBoolPipe, ParseUUIDPipe, ParseFloatPipe, and so on.
<span class="hljs-meta">@Get</span>(<span class="hljs-string">':id'</span>)
getById(<span class="hljs-meta">@Param</span>(<span class="hljs-string">'id'</span>, ParseIntPipe) id: <span class="hljs-built_in">number</span>) {
<span class="hljs-comment">// id is guaranteed to be a number here</span>
<span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.itemsService.findOne(id);
}
Custom Pipes
You can write your own to handle any transformation or validation logic:
<span class="hljs-keyword">import</span> { PipeTransform, Injectable, BadRequestException } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ParsePositiveIntPipe <span class="hljs-keyword">implements</span> PipeTransform<<span class="hljs-built_in">string</span>, <span class="hljs-built_in">number</span>> {
transform(value: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">number</span> {
<span class="hljs-keyword">const</span> val = <span class="hljs-built_in">parseInt</span>(value, <span class="hljs-number">10</span>);
<span class="hljs-keyword">if</span> (<span class="hljs-built_in">isNaN</span>(val) || val <= <span class="hljs-number">0</span>) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> BadRequestException(<span class="hljs-string">`"<span class="hljs-subst">${value}</span>" is not a positive integer`</span>);
}
<span class="hljs-keyword">return</span> val;
}
}
Use it just like a built-in pipe:
<span class="hljs-meta">@Get</span>(<span class="hljs-string">'order/:orderId'</span>)
getOrder(
<span class="hljs-meta">@Param</span>(<span class="hljs-string">'orderId'</span>, ParsePositiveIntPipe) orderId: <span class="hljs-built_in">number</span>
) {
<span class="hljs-comment">// orderId is a validated, positive integer</span>
<span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.ordersService.findById(orderId);
}
With pipes you ensure that every piece of data entering your handlers is correctly typed and valid, keeping your business logic clean and focused. In the next section, we’ll explore Guards and Authorization to control access to your endpoints.
8. Guards & Authorization
Guards sit in the request lifecycle after pipes and before interceptors/controllers. They determine whether a given request should be allowed to proceed based on custom logic. This is ideal for authentication, role checks, or feature flags.
8.1 Implementing Guards
A Guard is a class that implements the CanActivate
interface, with a single method:
canActivate(context: ExecutionContext): <span class="hljs-built_in">boolean</span> | <span class="hljs-built_in">Promise</span><<span class="hljs-built_in">boolean</span>> | Observable<<span class="hljs-built_in">boolean</span>>;
ExecutionContext gives you access to the underlying request/response and route metadata.
If
canActivate
returnstrue
, the request continues. Returningfalse
or throwing an exception (for example,UnauthorizedException
) blocks it.
You register guards either globally, at the controller level, or on individual routes with the @UseGuards()
decorator.
Here’s how guards work:
- Creating a simple auth guard:
<span class="hljs-comment">// auth.guard.ts</span>
<span class="hljs-keyword">import</span> { Injectable, CanActivate, ExecutionContext, UnauthorizedException } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AuthGuard <span class="hljs-keyword">implements</span> CanActivate {
canActivate(context: ExecutionContext): <span class="hljs-built_in">boolean</span> {
<span class="hljs-keyword">const</span> req = context.switchToHttp().getRequest();
<span class="hljs-keyword">const</span> authHeader = req.headers.authorization;
<span class="hljs-keyword">if</span> (!authHeader || !authHeader.startsWith(<span class="hljs-string">'Bearer '</span>)) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnauthorizedException(<span class="hljs-string">'Missing or invalid authorization header'</span>);
}
<span class="hljs-comment">// Basic token check (replace with real validation)</span>
<span class="hljs-keyword">const</span> token = authHeader.split(<span class="hljs-string">' '</span>)[<span class="hljs-number">1</span>];
<span class="hljs-keyword">if</span> (token !== <span class="hljs-string">'valid-token'</span>) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnauthorizedException(<span class="hljs-string">'Invalid token'</span>);
}
<span class="hljs-comment">// Attach user info if needed:</span>
req.user = { id: <span class="hljs-number">1</span>, name: <span class="hljs-string">'Alice'</span> };
<span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
}
}
- Applying the guard
Globally (in
main.ts
):<span class="hljs-keyword">import</span> { NestFactory } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/core'</span>; <span class="hljs-keyword">import</span> { AppModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'./app.module'</span>; <span class="hljs-keyword">import</span> { AuthGuard } <span class="hljs-keyword">from</span> <span class="hljs-string">'./auth.guard'</span>; <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">bootstrap</span>(<span class="hljs-params"></span>) </span>{ <span class="hljs-keyword">const</span> app = <span class="hljs-keyword">await</span> NestFactory.create(AppModule); <span class="hljs-comment">// every incoming request passes through AuthGuard</span> app.useGlobalGuards(<span class="hljs-keyword">new</span> AuthGuard()); <span class="hljs-keyword">await</span> app.listen(<span class="hljs-number">3000</span>); } bootstrap();
Controller-Level:
<span class="hljs-keyword">import</span> { Controller, Get, UseGuards } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { AuthGuard } <span class="hljs-keyword">from</span> <span class="hljs-string">'./auth.guard'</span>; <span class="hljs-meta">@Controller</span>(<span class="hljs-string">'profile'</span>) <span class="hljs-meta">@UseGuards</span>(AuthGuard) <span class="hljs-comment">// applies to all routes in this controller</span> <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ProfileController { <span class="hljs-meta">@Get</span>() getProfile(<span class="hljs-meta">@Req</span>() req) { <span class="hljs-keyword">return</span> req.user; } }
Route-Level:
<span class="hljs-meta">@Get</span>(<span class="hljs-string">'admin'</span>) <span class="hljs-meta">@UseGuards</span>(AdminGuard, AuthGuard) <span class="hljs-comment">// chain multiple guards</span> getAdminData() { <span class="hljs-comment">/* ... */</span> }
8.2 Role-Based Access Control
Beyond plain authentication, you often need authorization – ensuring a user has the correct role or permission. You can build a guard that reads metadata (for example, required roles) and verifies user claims.
Here’s how it works:
- Define a roles decorator:
<span class="hljs-comment">// roles.decorator.ts</span>
<span class="hljs-keyword">import</span> { SetMetadata } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Roles = <span class="hljs-function">(<span class="hljs-params">...roles: <span class="hljs-built_in">string</span>[]</span>) =></span> SetMetadata(<span class="hljs-string">'roles'</span>, roles);
- Create a roles guard:
<span class="hljs-comment">// roles.guard.ts</span>
<span class="hljs-keyword">import</span> { Injectable, CanActivate, ExecutionContext, ForbiddenException } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { Reflector } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/core'</span>;
<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> RolesGuard <span class="hljs-keyword">implements</span> CanActivate {
<span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> reflector: Reflector</span>) {}
canActivate(context: ExecutionContext): <span class="hljs-built_in">boolean</span> {
<span class="hljs-keyword">const</span> requiredRoles = <span class="hljs-built_in">this</span>.reflector.get<<span class="hljs-built_in">string</span>[]>(<span class="hljs-string">'roles'</span>, context.getHandler());
<span class="hljs-keyword">if</span> (!requiredRoles) {
<span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>; <span class="hljs-comment">// no roles metadata => open route</span>
}
<span class="hljs-keyword">const</span> { user } = context.switchToHttp().getRequest();
<span class="hljs-keyword">const</span> hasRole = requiredRoles.some(<span class="hljs-function"><span class="hljs-params">role</span> =></span> user.roles?.includes(role));
<span class="hljs-keyword">if</span> (!hasRole) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> ForbiddenException(<span class="hljs-string">'You do not have permission (roles)'</span>);
}
<span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
}
}
- Apply roles metadata and guard:
<span class="hljs-meta">@Controller</span>(<span class="hljs-string">'projects'</span>)
<span class="hljs-meta">@UseGuards</span>(AuthGuard, RolesGuard)
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ProjectsController {
<span class="hljs-meta">@Get</span>()
<span class="hljs-meta">@Roles</span>(<span class="hljs-string">'user'</span>, <span class="hljs-string">'admin'</span>) <span class="hljs-comment">// route requires either 'user' or 'admin'</span>
findAll() { <span class="hljs-comment">/* ... */</span> }
<span class="hljs-meta">@Post</span>()
<span class="hljs-meta">@Roles</span>(<span class="hljs-string">'admin'</span>) <span class="hljs-comment">// only 'admin' can create</span>
create() { <span class="hljs-comment">/* ... */</span> }
}
With this setup:
AuthGuard
ensures the request is authenticated and populatesreq.user
.RolesGuard
reads the@Roles()
metadata to enforce role-based access.
Guards give you a powerful, declarative way to enforce security and authorization policies. In the next section, we’ll cover Exception Filters – how to catch and format errors centrally, keeping your controllers clean.
9. Exception Filters
Exception filters let you centralize error handling, transforming thrown exceptions into consistent HTTP responses or other formats. You can rely on Nest’s built-in behavior for many cases, but custom filters give you control over logging, response shape, or handling non-HTTP errors.
9.1 Handling Errors Gracefully
By default, if a controller or service throws an HttpException
(or one of Nest’s built-in exceptions like NotFoundException
, BadRequestException
, and so on), Nest catches it and sends an appropriate HTTP response with status code and JSON body containing statusCode
, message
, and error
.
If an unexpected error (for example, a runtime error) bubbles up, Nest uses its default exception filter to return a 500 Internal Server Error with a generic message.
Controllers/services should throw exceptions rather than return error codes manually, so the framework can format consistently.
Here’s how it works:
<span class="hljs-comment">// users.service.ts</span>
<span class="hljs-keyword">import</span> { Injectable, NotFoundException } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> UsersService {
<span class="hljs-keyword">private</span> users = [{ id: <span class="hljs-number">1</span>, name: <span class="hljs-string">'Alice'</span> }];
findOne(id: <span class="hljs-built_in">number</span>) {
<span class="hljs-keyword">const</span> user = <span class="hljs-built_in">this</span>.users.find(<span class="hljs-function"><span class="hljs-params">u</span> =></span> u.id === id);
<span class="hljs-keyword">if</span> (!user) {
<span class="hljs-comment">// results in 404 with JSON { statusCode: 404, message: 'User #2 not found', error: 'Not Found' }</span>
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotFoundException(<span class="hljs-string">`User #<span class="hljs-subst">${id}</span> not found`</span>);
}
<span class="hljs-keyword">return</span> user;
}
}
<span class="hljs-comment">// users.controller.ts</span>
<span class="hljs-keyword">import</span> { Controller, Get, Param, ParseIntPipe } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-meta">@Controller</span>(<span class="hljs-string">'users'</span>)
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> UsersController {
<span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> usersService: UsersService</span>) {}
<span class="hljs-meta">@Get</span>(<span class="hljs-string">':id'</span>)
getUser(<span class="hljs-meta">@Param</span>(<span class="hljs-string">'id'</span>, ParseIntPipe) id: <span class="hljs-built_in">number</span>) {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.usersService.findOne(id);
}
}
If findOne
throws, Nest’s default filter sends a structured JSON error. For unexpected errors (like a thrown Error
), Nest wraps it into a 500 response.
9.2 Creating Custom Filters
You can implement the ExceptionFilter
interface or extend BaseExceptionFilter
. Just use the @Catch()
decorator to target specific exception types (or leave empty to catch all).
In catch(exception, host)
, you can extract context (HTTP request/response) and shape your response (for example, add metadata, custom fields, or a uniform envelope). You can also log exceptions or report to external systems here.
You can apply filters globally, to a controller, or to an individual route.
Here’s how it works:
Simple logging filter
Catch all exceptions, log details, then delegate to default behavior:<span class="hljs-comment">// logging-exception.filter.ts</span> <span class="hljs-keyword">import</span> { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger, } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { BaseExceptionFilter } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/core'</span>; <span class="hljs-meta">@Catch</span>() <span class="hljs-comment">// no args = catch every exception</span> <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> LoggingExceptionFilter <span class="hljs-keyword">extends</span> BaseExceptionFilter { <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> logger = <span class="hljs-keyword">new</span> Logger(LoggingExceptionFilter.name); <span class="hljs-keyword">catch</span>(exception: unknown, host: ArgumentsHost) { <span class="hljs-keyword">const</span> ctx = host.switchToHttp(); <span class="hljs-keyword">const</span> req = ctx.getRequest<Request>(); <span class="hljs-keyword">const</span> res = ctx.getResponse(); <span class="hljs-comment">// Log stack or message</span> <span class="hljs-keyword">if</span> (exception <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Error</span>) { <span class="hljs-built_in">this</span>.logger.error(<span class="hljs-string">`Error on <span class="hljs-subst">${req.method}</span> <span class="hljs-subst">${req.url}</span>`</span>, exception.stack); } <span class="hljs-keyword">else</span> { <span class="hljs-built_in">this</span>.logger.error(<span class="hljs-string">`Unknown exception on <span class="hljs-subst">${req.method}</span> <span class="hljs-subst">${req.url}</span>`</span>); } <span class="hljs-comment">// Delegate to default filter for HTTP exceptions or generic 500</span> <span class="hljs-built_in">super</span>.catch(exception, host); } }
Apply globally in
main.ts
:<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">bootstrap</span>(<span class="hljs-params"></span>) </span>{ <span class="hljs-keyword">const</span> app = <span class="hljs-keyword">await</span> NestFactory.create(AppModule); app.useGlobalFilters(<span class="hljs-keyword">new</span> LoggingExceptionFilter(app.get(HttpAdapterHost))); <span class="hljs-keyword">await</span> app.listen(<span class="hljs-number">3000</span>); }
(If extending
BaseExceptionFilter
, pass the adapter host to the constructor or super as needed.)Custom response shape
Suppose you want all errors to return{ success: false, error: { code, message } }
:<span class="hljs-comment">// custom-response.filter.ts</span> <span class="hljs-keyword">import</span> { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-meta">@Catch</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> CustomResponseFilter <span class="hljs-keyword">implements</span> ExceptionFilter { <span class="hljs-keyword">catch</span>(exception: unknown, host: ArgumentsHost) { <span class="hljs-keyword">const</span> ctx = host.switchToHttp(); <span class="hljs-keyword">const</span> response = ctx.getResponse(); <span class="hljs-keyword">const</span> request = ctx.getRequest<Request>(); <span class="hljs-keyword">let</span> status: <span class="hljs-built_in">number</span>; <span class="hljs-keyword">let</span> message: <span class="hljs-built_in">string</span> | <span class="hljs-built_in">object</span>; <span class="hljs-keyword">if</span> (exception <span class="hljs-keyword">instanceof</span> HttpException) { status = exception.getStatus(); <span class="hljs-keyword">const</span> res = exception.getResponse(); <span class="hljs-comment">// res might be a string or object</span> message = <span class="hljs-keyword">typeof</span> res === <span class="hljs-string">'string'</span> ? { message: res } : res; } <span class="hljs-keyword">else</span> { status = HttpStatus.INTERNAL_SERVER_ERROR; message = { message: <span class="hljs-string">'Internal server error'</span> }; } response.status(status).json({ success: <span class="hljs-literal">false</span>, error: { statusCode: status, ...( <span class="hljs-keyword">typeof</span> message === <span class="hljs-string">'object'</span> ? message : { message } ), }, timestamp: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString(), path: request.url, }); } }
Apply at controller or route level:
<span class="hljs-meta">@Controller</span>(<span class="hljs-string">'orders'</span>) <span class="hljs-meta">@UseFilters</span>(CustomResponseFilter) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> OrdersController { <span class="hljs-comment">// ...</span> }
Catching specific exceptions
If you have a custom exception class:<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> PaymentFailedException <span class="hljs-keyword">extends</span> HttpException { <span class="hljs-keyword">constructor</span>(<span class="hljs-params">details: <span class="hljs-built_in">string</span></span>) { <span class="hljs-built_in">super</span>({ message: <span class="hljs-string">'Payment failed'</span>, details }, HttpStatus.PAYMENT_REQUIRED); } }
You can write a filter that only catches that:
<span class="hljs-meta">@Catch</span>(PaymentFailedException) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> PaymentFailedFilter <span class="hljs-keyword">implements</span> ExceptionFilter { <span class="hljs-keyword">catch</span>(exception: PaymentFailedException, host: ArgumentsHost) { <span class="hljs-keyword">const</span> ctx = host.switchToHttp(); <span class="hljs-keyword">const</span> res = ctx.getResponse(); <span class="hljs-keyword">const</span> status = exception.getStatus(); <span class="hljs-keyword">const</span> { message, details } = exception.getResponse() <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span>; res.status(status).json({ error: { message, details, }, help: <span class="hljs-string">'Please verify your payment method and retry.'</span>, }); } }
Then apply only where payments occur:
<span class="hljs-meta">@Post</span>(<span class="hljs-string">'charge'</span>) <span class="hljs-meta">@UseFilters</span>(PaymentFailedFilter) charge() { <span class="hljs-comment">// ...</span> }
With exception filters in place, you ensure a consistent error contract, centralized logging or reporting, and tailored handling of different error types. Next up: Interceptors and Logging, where we’ll see how to transform responses, measure performance, and hook around method execution.
10. Interceptors & Logging
Interceptors wrap around method execution, letting you transform responses, bind extra logic before/after method calls, or measure performance. They’re ideal for cross-cutting concerns like logging, response shaping, caching, or timing metrics.
10.1 Transforming Responses
An Interceptor implements the NestInterceptor
interface with an intercept(context, next)
method.
Inside intercept
, you typically call next.handle()
which returns an Observable
of the handler’s result. You can then apply RxJS operators (like map
) to modify the data before it’s sent to the client.
Common uses are wrapping all responses in a uniform envelope, filtering out certain fields, or adding metadata.
Here’s how it works:
Basic response wrapper
Suppose you want every successful response to be{ success: true, data: <original> }
.<span class="hljs-comment">// response.interceptor.ts</span> <span class="hljs-keyword">import</span> { Injectable, NestInterceptor, ExecutionContext, CallHandler, } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { Observable } <span class="hljs-keyword">from</span> <span class="hljs-string">'rxjs'</span>; <span class="hljs-keyword">import</span> { map } <span class="hljs-keyword">from</span> <span class="hljs-string">'rxjs/operators'</span>; <span class="hljs-meta">@Injectable</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ResponseInterceptor <span class="hljs-keyword">implements</span> NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<<span class="hljs-built_in">any</span>> { <span class="hljs-keyword">return</span> next.handle().pipe( map(<span class="hljs-function"><span class="hljs-params">data</span> =></span> ({ success: <span class="hljs-literal">true</span>, data, })), ); } }
Apply globally in
main.ts
:<span class="hljs-keyword">import</span> { NestFactory } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/core'</span>; <span class="hljs-keyword">import</span> { AppModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'./app.module'</span>; <span class="hljs-keyword">import</span> { ResponseInterceptor } <span class="hljs-keyword">from</span> <span class="hljs-string">'./common/response.interceptor'</span>; <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">bootstrap</span>(<span class="hljs-params"></span>) </span>{ <span class="hljs-keyword">const</span> app = <span class="hljs-keyword">await</span> NestFactory.create(AppModule); app.useGlobalInterceptors(<span class="hljs-keyword">new</span> ResponseInterceptor()); <span class="hljs-keyword">await</span> app.listen(<span class="hljs-number">3000</span>); } bootstrap();
Now, if a controller method returns
{ id: 1, name: 'Alice' }
, the client sees:{ <span class="hljs-attr">"success"</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">"data"</span>: { <span class="hljs-attr">"id"</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Alice"</span> } }
Filtering sensitive fields
You might want to strip out fields likepassword
before sending a user object:<span class="hljs-comment">// sanitize.interceptor.ts</span> <span class="hljs-keyword">import</span> { Injectable, NestInterceptor, ExecutionContext, CallHandler, } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { Observable } <span class="hljs-keyword">from</span> <span class="hljs-string">'rxjs'</span>; <span class="hljs-keyword">import</span> { map } <span class="hljs-keyword">from</span> <span class="hljs-string">'rxjs/operators'</span>; <span class="hljs-meta">@Injectable</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> SanitizeInterceptor <span class="hljs-keyword">implements</span> NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<<span class="hljs-built_in">any</span>> { <span class="hljs-keyword">return</span> next.handle().pipe( map(<span class="hljs-function"><span class="hljs-params">data</span> =></span> { <span class="hljs-keyword">if</span> (data && <span class="hljs-keyword">typeof</span> data === <span class="hljs-string">'object'</span>) { <span class="hljs-keyword">const</span> { password, ...rest } = data; <span class="hljs-keyword">return</span> rest; } <span class="hljs-keyword">return</span> data; }), ); } }
Apply at controller or route:
<span class="hljs-meta">@Controller</span>(<span class="hljs-string">'users'</span>) <span class="hljs-meta">@UseInterceptors</span>(SanitizeInterceptor) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> UsersController { <span class="hljs-meta">@Get</span>(<span class="hljs-string">':id'</span>) getUser(<span class="hljs-meta">@Param</span>(<span class="hljs-string">'id'</span>) id: <span class="hljs-built_in">string</span>) { <span class="hljs-comment">// returns a user object with a password field internally,</span> <span class="hljs-comment">// but interceptor strips it before sending to client</span> <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.usersService.findOne(+id); } }
Serializing with
class-transformer
If you use classes with decorators, you can integrate withclass-transformer
:<span class="hljs-comment">// user.entity.ts</span> <span class="hljs-keyword">import</span> { Exclude, Expose } <span class="hljs-keyword">from</span> <span class="hljs-string">'class-transformer'</span>; <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> User { id: <span class="hljs-built_in">number</span>; name: <span class="hljs-built_in">string</span>; <span class="hljs-meta">@Exclude</span>() password: <span class="hljs-built_in">string</span>; <span class="hljs-meta">@Expose</span>() get displayName(): <span class="hljs-built_in">string</span> { <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-built_in">this</span>.name}</span> (#<span class="hljs-subst">${<span class="hljs-built_in">this</span>.id}</span>)`</span>; } }
<span class="hljs-comment">// class-transform.interceptor.ts</span> <span class="hljs-keyword">import</span> { Injectable, NestInterceptor, ExecutionContext, CallHandler, } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { plainToInstance } <span class="hljs-keyword">from</span> <span class="hljs-string">'class-transformer'</span>; <span class="hljs-keyword">import</span> { Observable } <span class="hljs-keyword">from</span> <span class="hljs-string">'rxjs'</span>; <span class="hljs-keyword">import</span> { map } <span class="hljs-keyword">from</span> <span class="hljs-string">'rxjs/operators'</span>; <span class="hljs-meta">@Injectable</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ClassTransformInterceptor<T> <span class="hljs-keyword">implements</span> NestInterceptor { <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> dto: <span class="hljs-keyword">new</span> (...args: <span class="hljs-built_in">any</span>[]) => T</span>) {} intercept(context: ExecutionContext, next: CallHandler): Observable<<span class="hljs-built_in">any</span>> { <span class="hljs-keyword">return</span> next.handle().pipe( map(<span class="hljs-function"><span class="hljs-params">data</span> =></span> { <span class="hljs-keyword">return</span> plainToInstance(<span class="hljs-built_in">this</span>.dto, data, { excludeExtraneousValues: <span class="hljs-literal">true</span>, }); }), ); } }
Apply with a DTO:
<span class="hljs-meta">@Controller</span>(<span class="hljs-string">'users'</span>) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> UsersController { <span class="hljs-meta">@Get</span>(<span class="hljs-string">':id'</span>) <span class="hljs-meta">@UseInterceptors</span>(<span class="hljs-keyword">new</span> ClassTransformInterceptor(User)) getUser(<span class="hljs-meta">@Param</span>(<span class="hljs-string">'id'</span>) id: <span class="hljs-built_in">string</span>) { <span class="hljs-comment">// service returns a plain object; interceptor transforms to User instance</span> <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.usersService.findOne(+id); } }
10.2 Logging and Performance Metrics
Interceptors can also measure execution time or log request/response details. You capture timestamps before and after next.handle()
, logging the difference. This helps monitor slow endpoints. Combined with a logging framework or Nest’s Logger
, you can standardize logs.
Here’s how it works:
Timing interceptor
Logs how long each request-handler takes:<span class="hljs-comment">// logging.interceptor.ts</span> <span class="hljs-keyword">import</span> { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger, } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { Observable } <span class="hljs-keyword">from</span> <span class="hljs-string">'rxjs'</span>; <span class="hljs-keyword">import</span> { tap } <span class="hljs-keyword">from</span> <span class="hljs-string">'rxjs/operators'</span>; <span class="hljs-meta">@Injectable</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> LoggingInterceptor <span class="hljs-keyword">implements</span> NestInterceptor { <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> logger = <span class="hljs-keyword">new</span> Logger(LoggingInterceptor.name); intercept(context: ExecutionContext, next: CallHandler): Observable<<span class="hljs-built_in">any</span>> { <span class="hljs-keyword">const</span> req = context.switchToHttp().getRequest(); <span class="hljs-keyword">const</span> method = req.method; <span class="hljs-keyword">const</span> url = req.url; <span class="hljs-keyword">const</span> now = <span class="hljs-built_in">Date</span>.now(); <span class="hljs-keyword">return</span> next.handle().pipe( tap(<span class="hljs-function">() =></span> { <span class="hljs-keyword">const</span> elapsed = <span class="hljs-built_in">Date</span>.now() - now; <span class="hljs-built_in">this</span>.logger.log(<span class="hljs-string">`<span class="hljs-subst">${method}</span> <span class="hljs-subst">${url}</span> - <span class="hljs-subst">${elapsed}</span>ms`</span>); }), ); } }
Apply globally:
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">bootstrap</span>(<span class="hljs-params"></span>) </span>{ <span class="hljs-keyword">const</span> app = <span class="hljs-keyword">await</span> NestFactory.create(AppModule); app.useGlobalInterceptors(<span class="hljs-keyword">new</span> LoggingInterceptor()); <span class="hljs-keyword">await</span> app.listen(<span class="hljs-number">3000</span>); }
Now each request logs something like:
[LoggingInterceptor] GET /users/1 - 35ms
Detailed request/response logging
For more detail, log request body or response size (careful with sensitive data):<span class="hljs-comment">// detailed-logging.interceptor.ts</span> <span class="hljs-keyword">import</span> { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger, } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { Observable } <span class="hljs-keyword">from</span> <span class="hljs-string">'rxjs'</span>; <span class="hljs-keyword">import</span> { tap, map } <span class="hljs-keyword">from</span> <span class="hljs-string">'rxjs/operators'</span>; <span class="hljs-meta">@Injectable</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> DetailedLoggingInterceptor <span class="hljs-keyword">implements</span> NestInterceptor { <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> logger = <span class="hljs-keyword">new</span> Logger(<span class="hljs-string">'HTTP'</span>); intercept(context: ExecutionContext, next: CallHandler): Observable<<span class="hljs-built_in">any</span>> { <span class="hljs-keyword">const</span> ctx = context.switchToHttp(); <span class="hljs-keyword">const</span> req = ctx.getRequest<Request>(); <span class="hljs-keyword">const</span> { method, url, body } = req; <span class="hljs-keyword">const</span> now = <span class="hljs-built_in">Date</span>.now(); <span class="hljs-built_in">this</span>.logger.log(<span class="hljs-string">`Incoming <span class="hljs-subst">${method}</span> <span class="hljs-subst">${url}</span> - body: <span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify(body)}</span>`</span>); <span class="hljs-keyword">return</span> next.handle().pipe( map(<span class="hljs-function"><span class="hljs-params">data</span> =></span> { <span class="hljs-keyword">const</span> elapsed = <span class="hljs-built_in">Date</span>.now() - now; <span class="hljs-built_in">this</span>.logger.log(<span class="hljs-string">`Response <span class="hljs-subst">${method}</span> <span class="hljs-subst">${url}</span> - <span class="hljs-subst">${elapsed}</span>ms - data: <span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify(data)}</span>`</span>); <span class="hljs-keyword">return</span> data; }), ); } }
Apply conditionally: perhaps only in development:
<span class="hljs-keyword">if</span> (process.env.NODE_ENV !== <span class="hljs-string">'production'</span>) { app.useGlobalInterceptors(<span class="hljs-keyword">new</span> DetailedLoggingInterceptor()); }
Combining with guards/pipes
Since interceptors run after guards and before the response is sent, logging time captures the full handler including service calls, but after validation/authorization. That ensures you measure only authorized requests and valid data flows.
Interceptors offer a flexible way to wrap your handlers with extra behavior: transforming outputs, sanitizing data, timing execution, or adding headers. In the next section, we’ll explore Database integration to see how you can integrate your data layer in Nest.
11. Database Integration
In many real-world applications, persisting data is essential. NestJS offers first-class support and integrations for several database technologies. In this section we cover three common approaches:
TypeORM with NestJS (relational databases, Active Record/Data Mapper style)
Mongoose (MongoDB) (NoSQL document store)
Prisma (Type-safe query builder/ORM alternative)
For each, we’ll explain the theory – when and why to choose it – and show concise practical examples of setup and usage in a NestJS context.
11.1 TypeORM with NestJS
TypeORM is a popular ORM for Node.js that supports multiple relational databases (PostgreSQL, MySQL, SQLite, SQL Server, and so on), offering both Active Record and Data Mapper patterns.
In NestJS, the @nestjs/typeorm
package wraps TypeORM to provide:
Automatic connection management via
TypeOrmModule.forRoot()
Module-scoped repositories/entities via
TypeOrmModule.forFeature()
Dependency injection for repositories and the
DataSource
/Connection
Entity decorators (
@Entity()
,@Column()
, and so on) for schema definitionMigrations and advanced features via TypeORM CLI or programmatic usage
When to choose TypeORM
Type ORM is useful in several scenarios. Use it when your data is relational and you want a full-featured ORM with decorators and built-in migrations. It’s also great if you prefer to work with classes/entities and automatically map them to tables. And it’s a great choice if you value built-in features like eager/lazy relations, cascading, query builders, and repository patterns.
Here’s how to use it:
Install dependencies:
npm install --save @nestjs/typeorm typeorm reflect-metadata <span class="hljs-comment"># Also install the database driver; e.g., for Postgres:</span> npm install --save pg
Configure the root module:
In
app.module.ts
, importTypeOrmModule.forRoot()
with connection options. These can come from environment variables (discussed later in Configuration Management).<span class="hljs-comment">// src/app.module.ts</span> <span class="hljs-keyword">import</span> { Module } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { TypeOrmModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/typeorm'</span>; <span class="hljs-keyword">import</span> { UsersModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'./users/users.module'</span>; <span class="hljs-meta">@Module</span>({ imports: [ TypeOrmModule.forRoot({ <span class="hljs-keyword">type</span>: <span class="hljs-string">'postgres'</span>, host: process.env.DB_HOST || <span class="hljs-string">'localhost'</span>, port: +process.env.DB_PORT || <span class="hljs-number">5432</span>, username: process.env.DB_USER || <span class="hljs-string">'postgres'</span>, password: process.env.DB_PASS || <span class="hljs-string">'password'</span>, database: process.env.DB_NAME || <span class="hljs-string">'mydb'</span>, entities: [__dirname + <span class="hljs-string">'/**/*.entity{.ts,.js}'</span>], synchronize: <span class="hljs-literal">false</span>, <span class="hljs-comment">// recommended false in production; use migrations</span> <span class="hljs-comment">// logging: true,</span> }), UsersModule, <span class="hljs-comment">// ...other modules</span> ], }) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppModule {}
synchronize: true
can auto-sync schema in development, but in production prefer migrations.Entities are auto-loaded via glob. Ensure path matches compiled output.
Define an entity:
Create an entity class with decorators:
<span class="hljs-comment">// src/users/user.entity.ts</span> <span class="hljs-keyword">import</span> { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>; <span class="hljs-meta">@Entity</span>({ name: <span class="hljs-string">'users'</span> }) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> User { <span class="hljs-meta">@PrimaryGeneratedColumn</span>() id: <span class="hljs-built_in">number</span>; <span class="hljs-meta">@Column</span>({ unique: <span class="hljs-literal">true</span> }) email: <span class="hljs-built_in">string</span>; <span class="hljs-meta">@Column</span>() password: <span class="hljs-built_in">string</span>; <span class="hljs-meta">@Column</span>({ nullable: <span class="hljs-literal">true</span> }) name?: <span class="hljs-built_in">string</span>; <span class="hljs-meta">@CreateDateColumn</span>() createdAt: <span class="hljs-built_in">Date</span>; <span class="hljs-meta">@UpdateDateColumn</span>() updatedAt: <span class="hljs-built_in">Date</span>; }
Set up the feature module:
<span class="hljs-comment">// src/users/users.module.ts</span> <span class="hljs-keyword">import</span> { Module } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { TypeOrmModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/typeorm'</span>; <span class="hljs-keyword">import</span> { UsersService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./users.service'</span>; <span class="hljs-keyword">import</span> { UsersController } <span class="hljs-keyword">from</span> <span class="hljs-string">'./users.controller'</span>; <span class="hljs-keyword">import</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">'./user.entity'</span>; <span class="hljs-meta">@Module</span>({ imports: [TypeOrmModule.forFeature([User])], providers: [UsersService], controllers: [UsersController], <span class="hljs-built_in">exports</span>: [UsersService], <span class="hljs-comment">// if other modules need UsersService</span> }) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> UsersModule {}
Inject the repository:
In the service, inject the
Repository<User>
:<span class="hljs-comment">// src/users/users.service.ts</span> <span class="hljs-keyword">import</span> { Injectable, NotFoundException } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { InjectRepository } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/typeorm'</span>; <span class="hljs-keyword">import</span> { Repository } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>; <span class="hljs-keyword">import</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">'./user.entity'</span>; <span class="hljs-keyword">import</span> { CreateUserDto } <span class="hljs-keyword">from</span> <span class="hljs-string">'./dto/create-user.dto'</span>; <span class="hljs-meta">@Injectable</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> UsersService { <span class="hljs-keyword">constructor</span>(<span class="hljs-params"> <span class="hljs-meta">@InjectRepository</span>(User) <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> userRepository: Repository<User>, </span>) {} <span class="hljs-keyword">async</span> create(dto: CreateUserDto): <span class="hljs-built_in">Promise</span><User> { <span class="hljs-keyword">const</span> user = <span class="hljs-built_in">this</span>.userRepository.create(dto); <span class="hljs-comment">// maps DTO fields to entity</span> <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.userRepository.save(user); } <span class="hljs-keyword">async</span> findAll(): <span class="hljs-built_in">Promise</span><User[]> { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.userRepository.find(); } <span class="hljs-keyword">async</span> findOne(id: <span class="hljs-built_in">number</span>): <span class="hljs-built_in">Promise</span><User> { <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.userRepository.findOne({ where: { id } }); <span class="hljs-keyword">if</span> (!user) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotFoundException(<span class="hljs-string">`User #<span class="hljs-subst">${id}</span> not found`</span>); } <span class="hljs-keyword">return</span> user; } <span class="hljs-keyword">async</span> update(id: <span class="hljs-built_in">number</span>, dto: Partial<CreateUserDto>): <span class="hljs-built_in">Promise</span><User> { <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.findOne(id); <span class="hljs-built_in">Object</span>.assign(user, dto); <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.userRepository.save(user); } <span class="hljs-keyword">async</span> remove(id: <span class="hljs-built_in">number</span>): <span class="hljs-built_in">Promise</span><<span class="hljs-built_in">void</span>> { <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.userRepository.delete(id); } }
Use in controller:
<span class="hljs-comment">// src/users/users.controller.ts</span> <span class="hljs-keyword">import</span> { Controller, Get, Post, Body, Param, ParseIntPipe, Put, Delete } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { UsersService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./users.service'</span>; <span class="hljs-keyword">import</span> { CreateUserDto } <span class="hljs-keyword">from</span> <span class="hljs-string">'./dto/create-user.dto'</span>; <span class="hljs-meta">@Controller</span>(<span class="hljs-string">'users'</span>) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> UsersController { <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> usersService: UsersService</span>) {} <span class="hljs-meta">@Post</span>() create(<span class="hljs-meta">@Body</span>() dto: CreateUserDto) { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.usersService.create(dto); } <span class="hljs-meta">@Get</span>() findAll() { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.usersService.findAll(); } <span class="hljs-meta">@Get</span>(<span class="hljs-string">':id'</span>) findOne(<span class="hljs-meta">@Param</span>(<span class="hljs-string">'id'</span>, ParseIntPipe) id: <span class="hljs-built_in">number</span>) { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.usersService.findOne(id); } <span class="hljs-meta">@Put</span>(<span class="hljs-string">':id'</span>) update( <span class="hljs-meta">@Param</span>(<span class="hljs-string">'id'</span>, ParseIntPipe) id: <span class="hljs-built_in">number</span>, <span class="hljs-meta">@Body</span>() dto: Partial<CreateUserDto>, ) { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.usersService.update(id, dto); } <span class="hljs-meta">@Delete</span>(<span class="hljs-string">':id'</span>) remove(<span class="hljs-meta">@Param</span>(<span class="hljs-string">'id'</span>, ParseIntPipe) id: <span class="hljs-built_in">number</span>) { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.usersService.remove(id); } }
Migrations (optional but recommended)
Use TypeORM CLI or programmatic migrations.
Configure a separate
ormconfig
or supply options in code.Generate and run migrations to evolve schema without data loss.
11.2 Mongoose (MongoDB)
Mongoose is a widely used ODM (Object Document Mapper) for MongoDB. In NestJS, @nestjs/mongoose
integrates Mongoose to:
Define schemas via classes and decorators (
@Schema()
,@Prop()
)Register models in modules with
MongooseModule.forFeature()
Manage the MongoDB connection with
MongooseModule.forRoot()
Inject Mongoose Model instances into services
Work with documents in a type-safe way (with interfaces/types)
Leverage features like hooks, virtuals, and validation at schema level
When to choose Mongoose
Mongoose is a good choice if you need a document-oriented, schema-less/ schematized NoSQL store. It’s also great if your data shapes may vary, or you prefer MongoDB’s flexible schema. And it’s helpful if you want features like middleware hooks in schema (pre/post save), virtuals, and so on.
Here’s how to use it:
Install dependencies:
npm install --save @nestjs/mongoose mongoose
Configure root module:
<span class="hljs-comment">// src/app.module.ts</span> <span class="hljs-keyword">import</span> { Module } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { MongooseModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/mongoose'</span>; <span class="hljs-keyword">import</span> { CatsModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'./cats/cats.module'</span>; <span class="hljs-meta">@Module</span>({ imports: [ MongooseModule.forRoot(process.env.MONGO_URI || <span class="hljs-string">'mongodb://localhost/nest'</span>), CatsModule, <span class="hljs-comment">// ...other modules</span> ], }) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppModule {}
Define a schema and document:
Use decorators and interfaces:
<span class="hljs-comment">// src/cats/schemas/cat.schema.ts</span> <span class="hljs-keyword">import</span> { Prop, Schema, SchemaFactory } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/mongoose'</span>; <span class="hljs-keyword">import</span> { Document } <span class="hljs-keyword">from</span> <span class="hljs-string">'mongoose'</span>; <span class="hljs-meta">@Schema</span>({ timestamps: <span class="hljs-literal">true</span> }) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> Cat <span class="hljs-keyword">extends</span> Document { <span class="hljs-meta">@Prop</span>({ required: <span class="hljs-literal">true</span> }) name: <span class="hljs-built_in">string</span>; <span class="hljs-meta">@Prop</span>() age: <span class="hljs-built_in">number</span>; <span class="hljs-meta">@Prop</span>() breed: <span class="hljs-built_in">string</span>; } <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> CatSchema = SchemaFactory.createForClass(Cat);
Extending
Document
gives the Mongoose document methods and properties.timestamps: true
auto-addscreatedAt
andupdatedAt
.You can add hooks:
CatSchema.pre<Cat>(<span class="hljs-string">'save'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">next</span>) </span>{ <span class="hljs-comment">// e.g., modify data or log before saving</span> next(); });
Set up feature module:
<span class="hljs-comment">// src/cats/cats.module.ts</span> <span class="hljs-keyword">import</span> { Module } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { MongooseModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/mongoose'</span>; <span class="hljs-keyword">import</span> { CatsService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./cats.service'</span>; <span class="hljs-keyword">import</span> { CatsController } <span class="hljs-keyword">from</span> <span class="hljs-string">'./cats.controller'</span>; <span class="hljs-keyword">import</span> { Cat, CatSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'./schemas/cat.schema'</span>; <span class="hljs-meta">@Module</span>({ imports: [ MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }]), ], controllers: [CatsController], providers: [CatsService], }) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> CatsModule {}
Inject the model:
In the service, inject
Model<Cat>
:<span class="hljs-comment">// src/cats/cats.service.ts</span> <span class="hljs-keyword">import</span> { Injectable, NotFoundException } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { InjectModel } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/mongoose'</span>; <span class="hljs-keyword">import</span> { Model } <span class="hljs-keyword">from</span> <span class="hljs-string">'mongoose'</span>; <span class="hljs-keyword">import</span> { Cat } <span class="hljs-keyword">from</span> <span class="hljs-string">'./schemas/cat.schema'</span>; <span class="hljs-keyword">import</span> { CreateCatDto } <span class="hljs-keyword">from</span> <span class="hljs-string">'./dto/create-cat.dto'</span>; <span class="hljs-keyword">import</span> { UpdateCatDto } <span class="hljs-keyword">from</span> <span class="hljs-string">'./dto/update-cat.dto'</span>; <span class="hljs-meta">@Injectable</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> CatsService { <span class="hljs-keyword">constructor</span>(<span class="hljs-params"> <span class="hljs-meta">@InjectModel</span>(Cat.name) <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> catModel: Model<Cat>, </span>) {} <span class="hljs-keyword">async</span> create(dto: CreateCatDto): <span class="hljs-built_in">Promise</span><Cat> { <span class="hljs-keyword">const</span> created = <span class="hljs-keyword">new</span> <span class="hljs-built_in">this</span>.catModel(dto); <span class="hljs-keyword">return</span> created.save(); } <span class="hljs-keyword">async</span> findAll(): <span class="hljs-built_in">Promise</span><Cat[]> { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.catModel.find().exec(); } <span class="hljs-keyword">async</span> findOne(id: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span><Cat> { <span class="hljs-keyword">const</span> cat = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.catModel.findById(id).exec(); <span class="hljs-keyword">if</span> (!cat) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotFoundException(<span class="hljs-string">`Cat <span class="hljs-subst">${id}</span> not found`</span>); } <span class="hljs-keyword">return</span> cat; } <span class="hljs-keyword">async</span> update(id: <span class="hljs-built_in">string</span>, dto: UpdateCatDto): <span class="hljs-built_in">Promise</span><Cat> { <span class="hljs-keyword">const</span> updated = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.catModel .findByIdAndUpdate(id, dto, { <span class="hljs-keyword">new</span>: <span class="hljs-literal">true</span> }) .exec(); <span class="hljs-keyword">if</span> (!updated) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotFoundException(<span class="hljs-string">`Cat <span class="hljs-subst">${id}</span> not found`</span>); } <span class="hljs-keyword">return</span> updated; } <span class="hljs-keyword">async</span> remove(id: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span><<span class="hljs-built_in">void</span>> { <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.catModel.findByIdAndDelete(id).exec(); <span class="hljs-keyword">if</span> (!res) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotFoundException(<span class="hljs-string">`Cat <span class="hljs-subst">${id}</span> not found`</span>); } } }
Use in controller:
<span class="hljs-comment">// src/cats/cats.controller.ts</span> <span class="hljs-keyword">import</span> { Controller, Get, Post, Body, Param, Put, Delete } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { CatsService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./cats.service'</span>; <span class="hljs-keyword">import</span> { CreateCatDto } <span class="hljs-keyword">from</span> <span class="hljs-string">'./dto/create-cat.dto'</span>; <span class="hljs-keyword">import</span> { UpdateCatDto } <span class="hljs-keyword">from</span> <span class="hljs-string">'./dto/update-cat.dto'</span>; <span class="hljs-meta">@Controller</span>(<span class="hljs-string">'cats'</span>) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> CatsController { <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> catsService: CatsService</span>) {} <span class="hljs-meta">@Post</span>() create(<span class="hljs-meta">@Body</span>() dto: CreateCatDto) { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.catsService.create(dto); } <span class="hljs-meta">@Get</span>() findAll() { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.catsService.findAll(); } <span class="hljs-meta">@Get</span>(<span class="hljs-string">':id'</span>) findOne(<span class="hljs-meta">@Param</span>(<span class="hljs-string">'id'</span>) id: <span class="hljs-built_in">string</span>) { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.catsService.findOne(id); } <span class="hljs-meta">@Put</span>(<span class="hljs-string">':id'</span>) update( <span class="hljs-meta">@Param</span>(<span class="hljs-string">'id'</span>) id: <span class="hljs-built_in">string</span>, <span class="hljs-meta">@Body</span>() dto: UpdateCatDto, ) { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.catsService.update(id, dto); } <span class="hljs-meta">@Delete</span>(<span class="hljs-string">':id'</span>) remove(<span class="hljs-meta">@Param</span>(<span class="hljs-string">'id'</span>) id: <span class="hljs-built_in">string</span>) { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.catsService.remove(id); } }
Advanced Mongoose features
Virtuals: define computed properties not stored in DB.
Indexes: via schema options or
@Prop({ index: true })
.Populate: reference other collections with
@Prop({ type: Types.ObjectId, ref: 'OtherModel' })
.Transactions: use MongoDB sessions for multi-document atomic operations.
11.3 Prisma
Prisma is a modern ORM/Query Builder that generates a type-safe client based on a schema definition. It supports relational databases (PostgreSQL, MySQL, SQLite, SQL Server, and more).
Here are some of its key features:
Type-safe queries: Autogenerated TypeScript definitions prevent many runtime errors.
Prisma schema: A declarative
.prisma
file to define models, relations, and enums.Migrations:
prisma migrate
for evolving schema.Performance: Lean query builder without heavy runtime overhead.
Flexibility: Supports raw queries when needed.
When to choose Prisma
Prisma is a great choice if you prefer a schema-first approach with a clear DSL and auto-generated type-safe client. It’s also great if you want modern features like efficient migrations, rich type inference, and a straightforward developer experience. And it’s a solid choice if you don’t need Active Record pattern. Instead, you use the Prisma client in services.
Here’s how it works:
Install dependencies and initialize:
npm install @prisma/client npm install -D prisma npx prisma init
This creates a
prisma/schema.prisma
file and a.env
withDATABASE_URL
.Define the schema:
In
prisma/schema.prisma
:datasource db { provider = <span class="hljs-string">"postgresql"</span> url = env(<span class="hljs-string">"DATABASE_URL"</span>) } generator client { provider = <span class="hljs-string">"prisma-client-js"</span> } model User { id Int @id @default(autoincrement()) email String @unique name String? posts Post[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Post { id Int @id @default(autoincrement()) title String content String? author User @relation(fields: [authorId], references: [id]) authorId Int published Boolean @default(<span class="hljs-literal">false</span>) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }
Run migrations and generate client:
npx prisma migrate dev --name init npx prisma generate
This updates the database schema and regenerates the TypeScript client.
Create a PrismaService in NestJS:
A common pattern is to wrap the
PrismaClient
in an injectable service, handling lifecycle hooks.<span class="hljs-comment">// src/prisma/prisma.service.ts</span> <span class="hljs-keyword">import</span> { Injectable, OnModuleInit, OnModuleDestroy } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { PrismaClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@prisma/client'</span>; <span class="hljs-meta">@Injectable</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> PrismaService <span class="hljs-keyword">extends</span> PrismaClient <span class="hljs-keyword">implements</span> OnModuleInit, OnModuleDestroy { <span class="hljs-keyword">async</span> onModuleInit() { <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.$connect(); } <span class="hljs-keyword">async</span> onModuleDestroy() { <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.$disconnect(); } }
Register PrismaService in a module:
<span class="hljs-comment">// src/prisma/prisma.module.ts</span> <span class="hljs-keyword">import</span> { Module } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { PrismaService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./prisma.service'</span>; <span class="hljs-meta">@Module</span>({ providers: [PrismaService], <span class="hljs-built_in">exports</span>: [PrismaService], }) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> PrismaModule {}
Then import
PrismaModule
in any feature module needing DB access.Use in a feature service:
<span class="hljs-comment">// src/users/users.service.ts</span> <span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { PrismaService } <span class="hljs-keyword">from</span> <span class="hljs-string">'../prisma/prisma.service'</span>; <span class="hljs-keyword">import</span> { CreateUserDto } <span class="hljs-keyword">from</span> <span class="hljs-string">'./dto/create-user.dto'</span>; <span class="hljs-meta">@Injectable</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> UsersService { <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> prisma: PrismaService</span>) {} <span class="hljs-keyword">async</span> create(dto: CreateUserDto) { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.prisma.user.create({ data: dto }); } <span class="hljs-keyword">async</span> findAll() { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.prisma.user.findMany(); } <span class="hljs-keyword">async</span> findOne(id: <span class="hljs-built_in">number</span>) { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.prisma.user.findUnique({ where: { id } }); } <span class="hljs-keyword">async</span> update(id: <span class="hljs-built_in">number</span>, dto: Partial<CreateUserDto>) { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.prisma.user.update({ where: { id }, data: dto, }); } <span class="hljs-keyword">async</span> remove(id: <span class="hljs-built_in">number</span>) { <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.prisma.user.delete({ where: { id } }); <span class="hljs-keyword">return</span> { deleted: <span class="hljs-literal">true</span> }; } }
Note: DTO fields must align with Prisma schema types. Prisma client methods return typed results.
Inject in controller:
<span class="hljs-comment">// src/users/users.controller.ts</span> <span class="hljs-keyword">import</span> { Controller, Get, Post, Body, Param, ParseIntPipe, Put, Delete } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { UsersService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./users.service'</span>; <span class="hljs-keyword">import</span> { CreateUserDto } <span class="hljs-keyword">from</span> <span class="hljs-string">'./dto/create-user.dto'</span>; <span class="hljs-meta">@Controller</span>(<span class="hljs-string">'users'</span>) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> UsersController { <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> usersService: UsersService</span>) {} <span class="hljs-meta">@Post</span>() create(<span class="hljs-meta">@Body</span>() dto: CreateUserDto) { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.usersService.create(dto); } <span class="hljs-meta">@Get</span>() findAll() { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.usersService.findAll(); } <span class="hljs-meta">@Get</span>(<span class="hljs-string">':id'</span>) findOne(<span class="hljs-meta">@Param</span>(<span class="hljs-string">'id'</span>, ParseIntPipe) id: <span class="hljs-built_in">number</span>) { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.usersService.findOne(id); } <span class="hljs-meta">@Put</span>(<span class="hljs-string">':id'</span>) update( <span class="hljs-meta">@Param</span>(<span class="hljs-string">'id'</span>, ParseIntPipe) id: <span class="hljs-built_in">number</span>, <span class="hljs-meta">@Body</span>() dto: Partial<CreateUserDto>, ) { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.usersService.update(id, dto); } <span class="hljs-meta">@Delete</span>(<span class="hljs-string">':id'</span>) remove(<span class="hljs-meta">@Param</span>(<span class="hljs-string">'id'</span>, ParseIntPipe) id: <span class="hljs-built_in">number</span>) { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.usersService.remove(id); } }
Advanced Prisma usage
Relations and nested writes: for example, create a post with nested author connect/create.
Transactions:
this.prisma.$transaction([...])
for atomic operations.Raw queries:
this.prisma.$queryRaw
when needed.Middleware: Prisma supports middlewares on the client side.
Performance tuning: select only needed fields, use pagination patterns.
With these three approaches, you can choose the database integration strategy that best fits your application’s needs:
TypeORM for a full-fledged ORM with decorators and migrations support in relational databases.
Mongoose for flexible document schemas in MongoDB.
Prisma for a modern, type-safe query builder/ORM alternative with excellent developer ergonomics.
In the next section, we’ll cover Configuration Management – how to handle environment variables and config modules in NestJS.
12. Configuration Management
Managing configuration cleanly is crucial for applications to behave correctly across environments (development, staging, production). NestJS provides the @nestjs/config
module to centralize configuration loading, validation, and injection.
12.1 @nestjs/config Module
The @nestjs/config
module is a powerful utility for managing application configuration settings. Here are some of its key features:
Centralized config: Instead of sprinkling
process.env
throughout your code, it uses a dedicated service that loads and validates configuration once at startup.Environment agnostic: It loads variables from
.env
files, environment variables, or other sources, with support for different files per environment.Validation: It integrates a schema (for example, via Joi) to ensure required variables are present and correctly typed, failing fast if misconfigured.
Config Namespacing: It organizes related settings into logical groups (for example, database, auth, third-party APIs) via configuration factories.
Injection: It injects a
ConfigService
to read config values in services or modules, with type safety when using custom typed wrappers.
Here’s how it works:
Install the package
npm install @nestjs/config npm install joi <span class="hljs-comment"># if you plan to validate via Joi schemas</span>
Import and initialize ConfigModule
In your root module (
AppModule
), importConfigModule.forRoot()
. Typical options:<span class="hljs-comment">// src/app.module.ts</span> <span class="hljs-keyword">import</span> { Module } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { ConfigModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/config'</span>; <span class="hljs-keyword">import</span> configuration <span class="hljs-keyword">from</span> <span class="hljs-string">'./config/configuration'</span>; <span class="hljs-keyword">import</span> { validationSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">'./config/validation'</span>; <span class="hljs-meta">@Module</span>({ imports: [ ConfigModule.forRoot({ <span class="hljs-comment">// Load .env automatically; specify envFilePath if custom:</span> isGlobal: <span class="hljs-literal">true</span>, <span class="hljs-comment">// makes ConfigService available app-wide</span> envFilePath: [<span class="hljs-string">'.env.development.local'</span>, <span class="hljs-string">'.env.development'</span>, <span class="hljs-string">'.env'</span>], load: [configuration], <span class="hljs-comment">// optional: load custom config factory(s)</span> validationSchema, <span class="hljs-comment">// optional: Joi schema to validate env vars</span> validationOptions: { allowUnknown: <span class="hljs-literal">true</span>, abortEarly: <span class="hljs-literal">true</span>, }, }), <span class="hljs-comment">// ...other modules</span> ], }) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppModule {}
isGlobal: true
avoids importingConfigModule
in every feature module.envFilePath
: an array lets you try multiple files (for example, local overrides before default).load
: array of functions returning partial config objects – see next step.validationSchema
: a Joi schema ensuring required variables exist and are correct type/format.
Define a configuration factory
Organize related settings into a typed object:
<span class="hljs-comment">// src/config/configuration.ts</span> <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> () => ({ port: <span class="hljs-built_in">parseInt</span>(process.env.PORT, <span class="hljs-number">10</span>) || <span class="hljs-number">3000</span>, database: { host: process.env.DB_HOST, port: <span class="hljs-built_in">parseInt</span>(process.env.DB_PORT, <span class="hljs-number">10</span>) || <span class="hljs-number">5432</span>, user: process.env.DB_USER, pass: process.env.DB_PASS, name: process.env.DB_NAME, }, jwt: { secret: process.env.JWT_SECRET, expiresIn: process.env.JWT_EXPIRES_IN || <span class="hljs-string">'1h'</span>, }, <span class="hljs-comment">// add other namespaces as needed</span> });
Validate environment variables
Using Joi for validation:
<span class="hljs-comment">// src/config/validation.ts</span> <span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> Joi <span class="hljs-keyword">from</span> <span class="hljs-string">'joi'</span>; <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> validationSchema = Joi.object({ NODE_ENV: Joi.string() .valid(<span class="hljs-string">'development'</span>, <span class="hljs-string">'production'</span>, <span class="hljs-string">'test'</span>, <span class="hljs-string">'staging'</span>) .default(<span class="hljs-string">'development'</span>), PORT: Joi.number().default(<span class="hljs-number">3000</span>), DB_HOST: Joi.string().required(), DB_PORT: Joi.number().default(<span class="hljs-number">5432</span>), DB_USER: Joi.string().required(), DB_PASS: Joi.string().required(), DB_NAME: Joi.string().required(), JWT_SECRET: Joi.string().min(<span class="hljs-number">32</span>).required(), JWT_EXPIRES_IN: Joi.string().default(<span class="hljs-string">'1h'</span>), <span class="hljs-comment">// add other variables...</span> });
If validation fails at startup, the application will error out with details, preventing misconfigured deployments.
Inject ConfigService
Anywhere you need config, inject
ConfigService
:<span class="hljs-comment">// src/some/some.service.ts</span> <span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { ConfigService } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/config'</span>; <span class="hljs-meta">@Injectable</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> SomeService { <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> configService: ConfigService</span>) {} getDbConfig() { <span class="hljs-keyword">const</span> host = <span class="hljs-built_in">this</span>.configService.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'database.host'</span>); <span class="hljs-keyword">const</span> port = <span class="hljs-built_in">this</span>.configService.get<<span class="hljs-built_in">number</span>>(<span class="hljs-string">'database.port'</span>); <span class="hljs-comment">// Use these values to configure a database client, etc.</span> <span class="hljs-keyword">return</span> { host, port }; } }
Use dot notation for nested config: for example,
'jwt.secret'
.You can also read raw env vars via
configService.get<string>('DB_HOST')
if needed, but preferring structured config is clearer.
Typed wrapper for ConfigService (optional)
For stronger typing, create an interface matching your configuration and a wrapper:
<span class="hljs-comment">// src/config/config.interface.ts</span> <span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> AppConfig { port: <span class="hljs-built_in">number</span>; database: { host: <span class="hljs-built_in">string</span>; port: <span class="hljs-built_in">number</span>; user: <span class="hljs-built_in">string</span>; pass: <span class="hljs-built_in">string</span>; name: <span class="hljs-built_in">string</span>; }; jwt: { secret: <span class="hljs-built_in">string</span>; expiresIn: <span class="hljs-built_in">string</span>; }; }
<span class="hljs-comment">// src/config/typed-config.service.ts</span> <span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { ConfigService } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/config'</span>; <span class="hljs-keyword">import</span> { AppConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'./config.interface'</span>; <span class="hljs-meta">@Injectable</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> TypedConfigService { <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> configService: ConfigService</span>) {} get appConfig(): AppConfig { <span class="hljs-keyword">return</span> { port: <span class="hljs-built_in">this</span>.configService.get<<span class="hljs-built_in">number</span>>(<span class="hljs-string">'port'</span>), database: { host: <span class="hljs-built_in">this</span>.configService.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'database.host'</span>), port: <span class="hljs-built_in">this</span>.configService.get<<span class="hljs-built_in">number</span>>(<span class="hljs-string">'database.port'</span>), user: <span class="hljs-built_in">this</span>.configService.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'database.user'</span>), pass: <span class="hljs-built_in">this</span>.configService.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'database.pass'</span>), name: <span class="hljs-built_in">this</span>.configService.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'database.name'</span>), }, jwt: { secret: <span class="hljs-built_in">this</span>.configService.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'jwt.secret'</span>), expiresIn: <span class="hljs-built_in">this</span>.configService.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'jwt.expiresIn'</span>), }, }; } }
Register
TypedConfigService
in a module if you prefer injecting it instead of rawConfigService
.Dynamic module registration using config
Many Nest modules accept dynamic options. For example, TypeORM:
<span class="hljs-comment">// src/database/database.module.ts</span> <span class="hljs-keyword">import</span> { Module } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { TypeOrmModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/typeorm'</span>; <span class="hljs-keyword">import</span> { ConfigService } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/config'</span>; <span class="hljs-meta">@Module</span>({ imports: [ TypeOrmModule.forRootAsync({ inject: [ConfigService], useFactory: <span class="hljs-function">(<span class="hljs-params">config: ConfigService</span>) =></span> ({ <span class="hljs-keyword">type</span>: <span class="hljs-string">'postgres'</span>, host: config.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'database.host'</span>), port: config.get<<span class="hljs-built_in">number</span>>(<span class="hljs-string">'database.port'</span>), username: config.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'database.user'</span>), password: config.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'database.pass'</span>), database: config.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'database.name'</span>), entities: [__dirname + <span class="hljs-string">'/../**/*.entity{.ts,.js}'</span>], synchronize: config.get(<span class="hljs-string">'NODE_ENV'</span>) !== <span class="hljs-string">'production'</span>, }), }), ], }) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> DatabaseModule {}
Using
forRootAsync
withuseFactory
ensures config is loaded before the module initializes.
12.2 Environment Variables
Environment variables serve as the bridge between code and its runtime environment, letting you decouple configuration (like database URLs, API keys, or feature flags) from your source.
By relying on environment variables, you ensure that the same application bundle can run safely across development, staging, and production – each providing its own sensitive or environment-specific settings without changing code. This is how it works:
12-Factor app principle: Stores config in the environment. Avoids hard-coding secrets or environment-specific settings in code.
Separation of concerns: Code remains the same across environments. Behavior is driven by env vars or config files.
Security: Keeps secrets (API keys, DB passwords) out of source control. Uses environment variables or secure vaults.
Overrides and precedence: You may have multiple
.env
files (for example,.env
,.env.local
,.env.production
) or CI/CD provided vars. It controls the order of loading.Defaults and fallbacks: Provides sensible defaults in code or config factories so the app can run in development without requiring every variable.
Here’s how to use it:
.env files
Create a
.env
file at project root with key-value pairs:PORT=<span class="hljs-number">3000</span> DB_HOST=localhost DB_PORT=<span class="hljs-number">5432</span> DB_USER=postgres DB_PASS=secret DB_NAME=mydb JWT_SECRET=supersecretjwtkey JWT_EXPIRES_IN=<span class="hljs-number">2</span>h
Optionally create
.env.development
,.env.test
,.env.production
, and load them based onNODE_ENV
.Ensure
.env
files are in.gitignore
to avoid committing secrets.
Loading order
With
@nestjs/config
, specifyenvFilePath
as an array, for example:ConfigModule.forRoot({ envFilePath: [ <span class="hljs-string">`.env.<span class="hljs-subst">${process.env.NODE_ENV}</span>.local`</span>, <span class="hljs-string">`.env.<span class="hljs-subst">${process.env.NODE_ENV}</span>`</span>, <span class="hljs-string">`.env`</span>, ], isGlobal: <span class="hljs-literal">true</span>, });
This tries
.env.development.local
, then.env.development
, then.env
. CI/CD can set actual environment variables that override values in files.
Accessing raw environment variables
While structured config is preferred, sometimes you need direct access:
<span class="hljs-keyword">const</span> raw = process.env.SOME_VAR;
Avoid scattering
process.env
in multiple places. Instead, prefer reading once in configuration factory and injecting viaConfigService
.
Default values
In configuration factory or when reading via
ConfigService
, provide defaults:<span class="hljs-keyword">const</span> port = configService.get<<span class="hljs-built_in">number</span>>(<span class="hljs-string">'PORT'</span>, <span class="hljs-number">3000</span>);
or in factory:
port: <span class="hljs-built_in">parseInt</span>(process.env.PORT, <span class="hljs-number">10</span>) || <span class="hljs-number">3000</span>
Type coercion
Environment variables are strings by default. Convert to numbers or booleans as needed:
<span class="hljs-keyword">const</span> isProd = configService.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'NODE_ENV'</span>) === <span class="hljs-string">'production'</span>; <span class="hljs-keyword">const</span> enableFeature = configService.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'FEATURE_FLAG'</span>) === <span class="hljs-string">'true'</span>; <span class="hljs-keyword">const</span> timeout = <span class="hljs-built_in">parseInt</span>(configService.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'TIMEOUT_MS'</span>), <span class="hljs-number">10</span>) || <span class="hljs-number">5000</span>;
Secret management
For sensitive data in production, consider using secret managers (AWS Secrets Manager, Vault) instead of plain
.env
. In that case, load secrets at startup (for example, via a custom provider or factory) and merge into the configuration.Example: in
useFactory
, asynchronously fetch secrets and return a config object including them.
Runtime configuration changes
- Generally configs are static at startup. If you need to reload config without restarting, implement a custom mechanism (for example, read from a database or remote config service periodically). Inject a service that fetches and caches values, but note this departs from 12-factor principles.
Validation in production
Always validate required env vars at startup so misconfigurations fail early. Use
validationSchema
with Joi or another validator.Example error: if
JWT_SECRET
is missing or too short, the app should refuse to start, logging a clear error.
With configuration managed via @nestjs/config
and environment variables, your NestJS app can adapt seamlessly across environments, keep secrets secure, and avoid environment-specific code changes. In the next section, we’ll cover Authentication strategies (JWT, OAuth2/social login).
13. Authentication
Handling authentication securely is a common requirement. In NestJS, you typically use Passport strategies alongside the @nestjs/jwt module for JWT-based flows, or OAuth2 strategies for social login.
Here, we’ll cover two common approaches:
JWT Strategy: token-based authentication for APIs.
OAuth2 / Social Login: integrating providers like Google or GitHub.
13.1 JWT Strategy
JSON Web Tokens (JWTs) are a compact, URL-safe means of representing claims between two parties. In an authentication context, the server issues a signed token containing user identity and possibly other claims, while the client stores and sends this token on subsequent requests (typically in the Authorization: Bearer <token>
header).
Because the token is signed (and optionally encrypted), the server can verify its integrity and authenticity without needing to maintain session state in memory or a database. This stateless nature simplifies scaling and decouples services.
Tokens include an expiration (exp
) so they automatically become invalid after a certain time. For longer-lived sessions, you can layer a refresh-token pattern on top.
In NestJS, we leverage @nestjs/jwt
to sign and verify tokens and @nestjs/passport
with passport-jwt
to integrate a guard that checks incoming tokens. Below is how it works.
JWT (JSON Web Token): a signed token containing claims (for example, user ID) that clients send in the
Authorization
header.Stateless: the server verifies the token signature without storing session state.
Expiration: embed expiry (
exp
) so tokens auto-expire; possibly use refresh tokens for long-lived sessions.In NestJS, you use
@nestjs/jwt
to sign/verify tokens and@nestjs/passport
withpassport-jwt
to implement the guard.
Here’s how to use it:
Install dependencies
npm install @nestjs/jwt passport-jwt @nestjs/passport passport
Configuration
Use
ConfigService
(from previous section) to load secrets and TTL:<span class="hljs-comment">// src/auth/auth.config.ts</span> <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> () => ({ jwt: { secret: process.env.JWT_SECRET || <span class="hljs-string">'default-secret'</span>, expiresIn: process.env.JWT_EXPIRES_IN || <span class="hljs-string">'1h'</span>, }, });
Ensure
ConfigModule.forRoot({ load: [authConfig], isGlobal: true, validationSchema: ... })
is set inAppModule
.AuthModule setup
<span class="hljs-comment">// src/auth/auth.module.ts</span> <span class="hljs-keyword">import</span> { Module } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { JwtModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/jwt'</span>; <span class="hljs-keyword">import</span> { PassportModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/passport'</span>; <span class="hljs-keyword">import</span> { ConfigService, ConfigModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/config'</span>; <span class="hljs-keyword">import</span> { JwtStrategy } <span class="hljs-keyword">from</span> <span class="hljs-string">'./jwt.strategy'</span>; <span class="hljs-keyword">import</span> { AuthService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./auth.service'</span>; <span class="hljs-keyword">import</span> { UsersModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'../users/users.module'</span>; <span class="hljs-comment">// assumes a UsersService</span> <span class="hljs-meta">@Module</span>({ imports: [ UsersModule, PassportModule.register({ defaultStrategy: <span class="hljs-string">'jwt'</span> }), JwtModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: <span class="hljs-function">(<span class="hljs-params">config: ConfigService</span>) =></span> ({ secret: config.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'jwt.secret'</span>), signOptions: { expiresIn: config.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'jwt.expiresIn'</span>) }, }), }), ], providers: [AuthService, JwtStrategy], <span class="hljs-built_in">exports</span>: [AuthService], }) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AuthModule {}
AuthService
Responsible for validating credentials and issuing tokens:
<span class="hljs-comment">// src/auth/auth.service.ts</span> <span class="hljs-keyword">import</span> { Injectable, UnauthorizedException } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { JwtService } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/jwt'</span>; <span class="hljs-keyword">import</span> { UsersService } <span class="hljs-keyword">from</span> <span class="hljs-string">'../users/users.service'</span>; <span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> bcrypt <span class="hljs-keyword">from</span> <span class="hljs-string">'bcrypt'</span>; <span class="hljs-meta">@Injectable</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AuthService { <span class="hljs-keyword">constructor</span>(<span class="hljs-params"> <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> usersService: UsersService, <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> jwtService: JwtService, </span>) {} <span class="hljs-comment">// Validate user credentials (email/password)</span> <span class="hljs-keyword">async</span> validateUser(email: <span class="hljs-built_in">string</span>, pass: <span class="hljs-built_in">string</span>) { <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.usersService.findByEmail(email); <span class="hljs-keyword">if</span> (user && (<span class="hljs-keyword">await</span> bcrypt.compare(pass, user.password))) { <span class="hljs-comment">// exclude password before returning</span> <span class="hljs-keyword">const</span> { password, ...result } = user; <span class="hljs-keyword">return</span> result; } <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>; } <span class="hljs-comment">// Called after validateUser succeeds</span> <span class="hljs-keyword">async</span> login(user: <span class="hljs-built_in">any</span>) { <span class="hljs-keyword">const</span> payload = { sub: user.id, email: user.email }; <span class="hljs-keyword">return</span> { access_token: <span class="hljs-built_in">this</span>.jwtService.sign(payload), }; } }
JwtStrategy
<span class="hljs-comment">// src/auth/jwt.strategy.ts</span> <span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { PassportStrategy } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/passport'</span>; <span class="hljs-keyword">import</span> { ExtractJwt, Strategy } <span class="hljs-keyword">from</span> <span class="hljs-string">'passport-jwt'</span>; <span class="hljs-keyword">import</span> { ConfigService } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/config'</span>; <span class="hljs-meta">@Injectable</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> JwtStrategy <span class="hljs-keyword">extends</span> PassportStrategy(Strategy) { <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> configService: ConfigService</span>) { <span class="hljs-built_in">super</span>({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: <span class="hljs-literal">false</span>, secretOrKey: configService.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'jwt.secret'</span>), }); } <span class="hljs-keyword">async</span> validate(payload: <span class="hljs-built_in">any</span>) { <span class="hljs-comment">// payload.sub is user ID</span> <span class="hljs-keyword">return</span> { userId: payload.sub, email: payload.email }; <span class="hljs-comment">// returned value is assigned to req.user</span> } }
Auth Controller
Expose login endpoint:
<span class="hljs-comment">// src/auth/auth.controller.ts</span> <span class="hljs-keyword">import</span> { Controller, Post, Body, Request, UseGuards } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { AuthService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./auth.service'</span>; <span class="hljs-keyword">import</span> { LocalAuthGuard } <span class="hljs-keyword">from</span> <span class="hljs-string">'./local-auth.guard'</span>; <span class="hljs-comment">// optional if using local strategy</span> <span class="hljs-meta">@Controller</span>(<span class="hljs-string">'auth'</span>) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AuthController { <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> authService: AuthService</span>) {} <span class="hljs-comment">// Example: using a local strategy for email/password</span> <span class="hljs-meta">@UseGuards</span>(LocalAuthGuard) <span class="hljs-meta">@Post</span>(<span class="hljs-string">'login'</span>) <span class="hljs-keyword">async</span> login(<span class="hljs-meta">@Request</span>() req) { <span class="hljs-comment">// LocalAuthGuard attaches user to req.user</span> <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.authService.login(req.user); } <span class="hljs-comment">// Alternatively, implement login logic directly:</span> <span class="hljs-meta">@Post</span>(<span class="hljs-string">'login-basic'</span>) <span class="hljs-keyword">async</span> loginBasic(<span class="hljs-meta">@Body</span>() body: { email: <span class="hljs-built_in">string</span>; password: <span class="hljs-built_in">string</span> }) { <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.authService.validateUser(body.email, body.password); <span class="hljs-keyword">if</span> (!user) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnauthorizedException(<span class="hljs-string">'Invalid credentials'</span>); } <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.authService.login(user); } }
- LocalAuthGuard would use a LocalStrategy to validate credentials via Passport.
Protecting routes
Use the JwtAuthGuard:
<span class="hljs-comment">// src/auth/jwt-auth.guard.ts</span> <span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { AuthGuard } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/passport'</span>; <span class="hljs-meta">@Injectable</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> JwtAuthGuard <span class="hljs-keyword">extends</span> AuthGuard(<span class="hljs-string">'jwt'</span>) {}
Apply to controllers or routes:
<span class="hljs-comment">// src/profile/profile.controller.ts</span> <span class="hljs-keyword">import</span> { Controller, Get, UseGuards, Request } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { JwtAuthGuard } <span class="hljs-keyword">from</span> <span class="hljs-string">'../auth/jwt-auth.guard'</span>; <span class="hljs-meta">@Controller</span>(<span class="hljs-string">'profile'</span>) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ProfileController { <span class="hljs-meta">@UseGuards</span>(JwtAuthGuard) <span class="hljs-meta">@Get</span>() getProfile(<span class="hljs-meta">@Request</span>() req) { <span class="hljs-keyword">return</span> req.user; <span class="hljs-comment">// { userId, email }</span> } }
Refresh Tokens (optional)
Issue a refresh token (longer expiry) and store it (for example, in DB or as HTTP-only cookie).
Create a separate endpoint to issue new access token when the access token expires.
Verify refresh token validity (for example, compare stored token or a hashed version).
Implementation details vary – consider security best practices (rotate tokens, revoke on logout).
13.2 OAuth2 / Social Login
Social login via OAuth2 lets users authenticate with third-party providers (Google, GitHub, Facebook, and so on) without creating a separate password for your service.
Under the Authorization Code Flow, the user is redirected to the provider’s consent screen. After granting permission, the provider redirects back with a temporary code. The backend exchanges this code for access (and optionally refresh) tokens, fetches the user’s profile, and then you can link or create a local user record. Finally, you typically issue your own JWT (or session) so the client can call your secured APIs.
Keeping OAuth client IDs/secrets in environment variables (via ConfigService
) ensures security and flexibility. Here’s how it works:
OAuth2 Authorization Code Flow: Redirect the user to the provider’s consent screen. The provider redirects back with a code. The back-end exchanges code for tokens and retrieves user info.
In server-side (NestJS) you use Passport strategies (for example,
passport-google-oauth20
,passport-github2
).After getting user profile from provider, you look up or create a matching local user record, then issue your own JWT or session.
Keep secrets (client ID/secret) in environment variables and load via
ConfigService
.
Here’s how to use it:
Install dependencies
npm install @nestjs/passport passport passport-google-oauth20 <span class="hljs-comment"># or passport-facebook, passport-github2, etc.</span>
Configuration
Add OAuth credentials to env and
ConfigModule
:GOOGLE_CLIENT_ID=your-google-client-id GOOGLE_CLIENT_SECRET=your-google-client-secret GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/callback
OAuth Strategy
Example: Google
<span class="hljs-comment">// src/auth/google.strategy.ts</span> <span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { PassportStrategy } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/passport'</span>; <span class="hljs-keyword">import</span> { Strategy, VerifyCallback } <span class="hljs-keyword">from</span> <span class="hljs-string">'passport-google-oauth20'</span>; <span class="hljs-keyword">import</span> { ConfigService } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/config'</span>; <span class="hljs-keyword">import</span> { AuthService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./auth.service'</span>; <span class="hljs-meta">@Injectable</span>() <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> GoogleStrategy <span class="hljs-keyword">extends</span> PassportStrategy(Strategy, <span class="hljs-string">'google'</span>) { <span class="hljs-keyword">constructor</span>(<span class="hljs-params">configService: ConfigService, <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> authService: AuthService</span>) { <span class="hljs-built_in">super</span>({ clientID: configService.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'GOOGLE_CLIENT_ID'</span>), clientSecret: configService.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'GOOGLE_CLIENT_SECRET'</span>), callbackURL: configService.get<<span class="hljs-built_in">string</span>>(<span class="hljs-string">'GOOGLE_CALLBACK_URL'</span>), scope: [<span class="hljs-string">'email'</span>, <span class="hljs-string">'profile'</span>], }); } <span class="hljs-keyword">async</span> validate(accessToken: <span class="hljs-built_in">string</span>, refreshToken: <span class="hljs-built_in">string</span>, profile: <span class="hljs-built_in">any</span>, done: VerifyCallback): <span class="hljs-built_in">Promise</span><<span class="hljs-built_in">any</span>> { <span class="hljs-keyword">const</span> { id, emails, displayName } = profile; <span class="hljs-keyword">const</span> email = emails && emails[<span class="hljs-number">0</span>]?.value; <span class="hljs-comment">// Delegate to AuthService to find or create local user</span> <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.authService.validateOAuthLogin(<span class="hljs-string">'google'</span>, id, email, displayName); done(<span class="hljs-literal">null</span>, user); } }
In
AuthService
:<span class="hljs-comment">// src/auth/auth.service.ts (add method)</span> <span class="hljs-keyword">async</span> validateOAuthLogin(provider: <span class="hljs-built_in">string</span>, providerId: <span class="hljs-built_in">string</span>, email: <span class="hljs-built_in">string</span>, name?: <span class="hljs-built_in">string</span>) { <span class="hljs-comment">// Find existing user by provider+providerId or email</span> <span class="hljs-keyword">let</span> user = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.usersService.findByProvider(provider, providerId); <span class="hljs-keyword">if</span> (!user) { <span class="hljs-comment">// Optionally check by email: if exists, link accounts; otherwise create new</span> user = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.usersService.createOAuthUser({ provider, providerId, email, name }); } <span class="hljs-comment">// Issue JWT or return user object; here we return minimal payload for login</span> <span class="hljs-keyword">return</span> user; }
AuthController endpoints
<span class="hljs-comment">// src/auth/auth.controller.ts</span> <span class="hljs-keyword">import</span> { Controller, Get, Req, UseGuards } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>; <span class="hljs-keyword">import</span> { AuthGuard } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/passport'</span>; <span class="hljs-keyword">import</span> { AuthService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./auth.service'</span>; <span class="hljs-meta">@Controller</span>(<span class="hljs-string">'auth'</span>) <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AuthController { <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> authService: AuthService</span>) {} <span class="hljs-meta">@Get</span>(<span class="hljs-string">'google'</span>) <span class="hljs-meta">@UseGuards</span>(AuthGuard(<span class="hljs-string">'google'</span>)) <span class="hljs-keyword">async</span> googleAuth(<span class="hljs-meta">@Req</span>() req) { <span class="hljs-comment">// Initiates Google OAuth2 flow</span> } <span class="hljs-meta">@Get</span>(<span class="hljs-string">'google/callback'</span>) <span class="hljs-meta">@UseGuards</span>(AuthGuard(<span class="hljs-string">'google'</span>)) <span class="hljs-keyword">async</span> googleAuthRedirect(<span class="hljs-meta">@Req</span>() req) { <span class="hljs-comment">// Google redirects here after consent; req.user set by GoogleStrategy.validate</span> <span class="hljs-keyword">const</span> user = req.user; <span class="hljs-comment">// Issue JWT or set a cookie, then redirect or return token</span> <span class="hljs-keyword">const</span> jwt = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.authService.login(user); <span class="hljs-comment">// E.g., redirect with token as query, or set cookie:</span> <span class="hljs-comment">// res.redirect(`http://frontend-app.com?token=${jwt.access_token}`);</span> <span class="hljs-keyword">return</span> { access_token: jwt.access_token }; } }
The first endpoint (
/auth/google
) triggers redirect to Google.The callback endpoint handles the response, then issues your JWT.
Session vs. Stateless
Many examples use sessions and
@nestjs/passport
session support, but for APIs you often skip sessions: Passport still invokesvalidate
, returns user, and you issue JWT immediately.Ensure you disable sessions in
PassportModule
registration:PassportModule.register({ session: false })
.
Multiple Providers
Repeat strategy setup for each provider (for example, GitHubStrategy).
In
validateOAuthLogin
, handleprovider
parameter to distinguish logic.You can store in your user entity fields like
googleId
,githubId
, and so on, or a separate table for OAuth accounts.
Protecting routes post-login
Clients use the issued JWT in
Authorization: Bearer <token>
to access protected endpoints viaJwtAuthGuard
.If you prefer sessions/cookies, configure Nest to use sessions and Passport’s session features, but for SPAs or mobile clients JWT is common.
Frontend considerations
Redirect URIs must match those configured in the OAuth provider console.
After receiving JWT, store it securely (for example, HTTP-only cookie or secure storage on client).
Handle token expiry: possibly combine OAuth refresh tokens or your own refresh token flow.
With JWT and OAuth2 strategies set up, your NestJS backend can support secured endpoints, user registration/login flows, and social logins.
Conclusion & Further Resources
Summary
We’ve walked through key aspects of building a NestJS application: its architectural patterns, core building blocks (modules, controllers, providers), dependency injection, routing and middleware, request lifecycle with pipes, guards, exception filters, interceptors, database integration options (TypeORM, Mongoose, Prisma), configuration management, authentication strategies (JWT, OAuth2), and strategies for migrating existing apps.
NestJS provides a structured, TypeScript-first framework that accelerates development of scalable, maintainable backends. By leveraging its module system and built-in integrations, you get consistency, testability, and clear separation of concerns out of the box.
Whether you choose a relational database via TypeORM, a document store with Mongoose, or Prisma’s type-safe client, you can plug these into Nest’s DI container and configuration module. Authentication flows – both JWT-based and social login – fit naturally into Nest’s Passport integration.
Overall, NestJS is well-suited for APIs, microservices, real-time apps, and enterprise backends where maintainability and developer experience matter.
Official Docs and Community Links
NestJS Official Documentation: Comprehensive guide and API reference for all core features.
- https://docs.nestjs.com
GitHub Repository: Source code, issue tracker, and community contributions.
Source: freeCodeCamp Programming Tutorials: Python, JavaScript, Git & MoreÂ