This document is ´written´ by a proud Script Kiddy using the cat
command so all the power and all the merit of this hard work is dedicated to the Cat Person! Tanks to the MIT License I have been able to ´write´ this document all by myself, without copy-pasting code or scripts from anyone else, Like Real Script Kiddies always do†. In fact I have used my own script: cat-docs.sh
to colligate the documentation.
†Codez kiddies just don't seem to understand that those scripts had to come from somewhere. ― Cosmos@yabbs (Wed Jun 1 22:30:55 1994)
This document itself is under MIT License Copyright © 2020 Benjamin Vincent (Luxcium). We all know (or your just learning now), @Luxcium is a cute and nice little Script Kiddy but also, we all know that the MIT License require to include the copyright notice and blah blah blah, so without further ado let me tell you the truth, the text contained in this document is the result of the hard work of Kamil Myśliwiec
- Please refer to the original NestJS Documentation for more acurate and up to date informations.
- Please make sure you have 'Stared🌟' the original Nest Project.
- The information used to compile this documentation comme from nestjs/docs.nestjs.com/content the most recent version is available here: docs.nestjs.com/.../master/content
[-- Script kiddies cut here -- ]
- Introduction
- First steps
- Controllers
- Providers
- Modules
- Middleware
- Exception filters
- Pipes
- Guards
- Interceptors
- Custom decorators
Nest (NestJS) is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with and fully supports TypeScript (yet still enables developers to code in pure JavaScript) and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Reactive Programming).
Under the hood, Nest makes use of robust HTTP Server frameworks like Express (the default) and optionally can be configured to use Fastify as well!
Nest provides a level of abstraction above these common Node.js frameworks (Express/Fastify), but also exposes their APIs directly to the developer. This gives developers the freedom to use the myriad of third-party modules which are available for the underlying platform.
In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front and backend applications. This has given rise to awesome projects like Angular, React and Vue, which improve developer productivity and enable the creation of fast, testable, and extensible frontend applications. However, while plenty of superb libraries, helpers, and tools exist for Node (and server-side JavaScript), none of them effectively solve the main problem of - Architecture.
Nest provides an out-of-the-box application architecture which allows developers and teams to create highly testable, scalable, loosely coupled, and easily maintainable applications. The architecture is heavily inspired by Angular.
To get started, you can either scaffold the project with the Nest CLI, or clone a starter project (both will produce the same outcome).
To scaffold the project with the Nest CLI, run the following commands. This will create a new project directory, and populate the directory with the initial core Nest files and supporting modules, creating a conventional base structure for your project. Creating a new project with the Nest CLI is recommended for first-time users. We'll continue with this approach in First Steps.
$ npm i -g @nestjs/cli
$ nest new project-name
Alternatively, to install the TypeScript starter project with Git:
$ git clone https://github.com/nestjs/typescript-starter.git project
$ cd project
$ npm install
$ npm run start
Open your browser and navigate to http://localhost:3000/
.
To install the JavaScript flavor of the starter project, use javascript-starter.git
in the command sequence above.
You can also manually create a new project from scratch by installing the core and supporting files with npm (or yarn). In this case, of course, you'll be responsible for creating the project boilerplate files yourself.
$ npm i --save @nestjs/core @nestjs/common rxjs reflect-metadata
In this set of articles, you'll learn the core fundamentals of Nest. To get familiar with the essential building blocks of Nest applications, we'll build a basic CRUD application with features that cover a lot of ground at an introductory level.
We're in love with TypeScript, but above all - we love Node.js. That's why Nest is compatible with both TypeScript and pure JavaScript. Nest takes advantage of the latest language features, so to use it with vanilla JavaScript we need a Babel compiler.
We'll mostly use TypeScript in the examples we provide, but you can always switch the code snippets to vanilla JavaScript syntax (simply click to toggle the language button in the upper right hand corner of each snippet).
Please make sure that Node.js (>= 10.13.0) is installed on your operating system.
Setting up a new project is quite simple with the Nest CLI. With npm installed, you can create a new Nest project with the following commands in your OS terminal:
$ npm i -g @nestjs/cli
$ nest new project-name
The project
directory will be created, node modules and a few other boilerplate files will be installed, and a src/
directory will be created and populated with several core files.
Here's a brief overview of those core files:
app.controller.ts |
Basic controller sample with a single route. |
app.module.ts |
The root module of the application. |
main.ts |
The entry file of the application which uses the core function NestFactory to create a Nest application instance. |
The main.ts
includes an async function, which will bootstrap our application:
@@filename(main)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
@@switch
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
To create a Nest application instance, we use the core NestFactory
class. NestFactory
exposes a few static methods that allow creating an application instance. The create()
method returns an application object, which fulfills the INestApplication
interface. This object provides a set of methods which are described in the coming chapters. In the main.ts
example above, we simply start up our HTTP listener, which lets the application await inbound HTTP requests.
Note that a project scaffolded with the Nest CLI creates an initial project structure that encourages developers to follow the convention of keeping each module in its own dedicated directory.
Nest aims to be a platform-agnostic framework. Platform independence makes it possible to create reusable logical parts that developers can take advantage of across several different types of applications. Technically, Nest is able to work with any Node HTTP framework once an adapter is created. There are two HTTP platforms supported out-of-the-box: express and fastify. You can choose the one that best suits your needs.
platform-express |
Express is a well-known minimalist web framework for node. It's a battle tested, production-ready library with lots of resources implemented by the community. The @nestjs/platform-express package is used by default. Many users are well served with Express, and need take no action to enable it. |
platform-fastify |
Fastify is a high performance and low overhead framework highly focused on providing maximum efficiency and speed. Read how to use it here. |
Whichever platform is used, it exposes its own application interface. These are seen respectively as NestExpressApplication
and NestFastifyApplication
.
When you pass a type to the NestFactory.create()
method, as in the example below, the app
object will have methods available exclusively for that specific platform. Note, however, you don't need to specify a type unless you actually want to access the underlying platform API.
const app = await NestFactory.create<NestExpressApplication>(AppModule);
Once the installation process is complete, you can run the following command at your OS command prompt to start the application listening for inbound HTTP requests:
$ npm run start
This command starts the app with the HTTP server listening on the port defined in the src/main.ts
file. Once the application is running, open your browser and navigate to http://localhost:3000/
. You should see the Hello World!
message.
Controllers are responsible for handling incoming requests and returning responses to the client.
A controller's purpose is to receive specific requests for the application. The routing mechanism controls which controller receives which requests. Frequently, each controller has more than one route, and different routes can perform different actions.
In order to create a basic controller, we use classes and decorators. Decorators associate classes with required metadata and enable Nest to create a routing map (tie requests to the corresponding controllers).
In the following example we'll use the @Controller()
decorator, which is required to define a basic controller. We'll specify an optional route path prefix of cats
. Using a path prefix in a @Controller()
decorator allows us to easily group a set of related routes, and minimize repetitive code. For example, we may choose to group a set of routes that manage interactions with a customer entity under the route /customers
. In that case, we could specify the path prefix customers
in the @Controller()
decorator so that we don't have to repeat that portion of the path for each route in the file.
@@filename(cats.controller)
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return 'This action returns all cats';
}
}
@@switch
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll() {
return 'This action returns all cats';
}
}
info Hint To create a controller using the CLI, simply execute the
$ nest g controller cats
command.
The @Get()
HTTP request method decorator before the findAll()
method tells Nest to create a handler for a specific endpoint for HTTP requests. The endpoint corresponds to the HTTP request method (GET in this case) and the route path. What is the route path? The route path for a handler is determined by concatenating the (optional) prefix declared for the controller, and any path specified in the request decorator. Since we've declared a prefix for every route ( cats
), and haven't added any path information in the decorator, Nest will map GET /cats
requests to this handler. As mentioned, the path includes both the optional controller path prefix and any path string declared in the request method decorator. For example, a path prefix of customers
combined with the decorator @Get('profile')
would produce a route mapping for requests like GET /customers/profile
.
In our example above, when a GET request is made to this endpoint, Nest routes the request to our user-defined findAll()
method. Note that the method name we choose here is completely arbitrary. We obviously must declare a method to bind the route to, but Nest doesn't attach any significance to the method name chosen.
This method will return a 200 status code and the associated response, which in this case is just a string. Why does that happen? To explain, we'll first introduce the concept that Nest employs two different options for manipulating responses:
Standard (recommended) |
Using this built-in method, when a request handler returns a JavaScript object or array, it will automatically
be serialized to JSON. When it returns a JavaScript primitive type (e.g., string , number , boolean ), however, Nest will send just the value without attempting to serialize it. This makes response handling simple: just return the value, and Nest takes care of the rest.
Furthermore, the response's status code is always 200 by default, except for POST requests which use 201. We can easily change this behavior by adding the @HttpCode(...)
decorator at a handler-level (see Status codes).
|
Library-specific |
We can use the library-specific (e.g., Express) response object, which can be injected using the @Res() decorator in the method handler signature (e.g., findAll(@Res() response) ). With this approach, you have the ability (and the responsibility), to use the native response handling methods exposed by that object. For example, with Express, you can construct responses using code like response.status(200).send()
|
warning Warning You cannot use both approaches at the same time. Nest detects when the handler is using either
@Res()
or@Next()
, indicating you have chosen the library-specific option. If both approaches are used at the same time, the Standard approach is automatically disabled for this single route and will no longer work as expected.
Handlers often need access to the client request details. Nest provides access to the request object of the underlying platform (Express by default). We can access the request object by instructing Nest to inject it by adding the @Req()
decorator to the handler's signature.
@@filename(cats.controller)
import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';
@Controller('cats')
export class CatsController {
@Get()
findAll(@Req() request: Request): string {
return 'This action returns all cats';
}
}
@@switch
import { Controller, Bind, Get, Req } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
@Bind(Req())
findAll(request) {
return 'This action returns all cats';
}
}
info Hint In order to take advantage of
express
typings (as in therequest: Request
parameter example above), install@types/express
package.
The request object represents the HTTP request and has properties for the request query string, parameters, HTTP headers, and body (read more here). In most cases, it's not necessary to grab these properties manually. We can use dedicated decorators instead, such as @Body()
or @Query()
, which are available out of the box. Below is a list of the provided decorators and the plain platform-specific objects they represent.
@Request(), @Req() |
req |
@Response(), @Res() * |
res |
@Next() |
next |
@Session() |
req.session |
@Param(key?: string) |
req.params / req.params[key] |
@Body(key?: string) |
req.body / req.body[key] |
@Query(key?: string) |
req.query / req.query[key] |
@Headers(name?: string) |
req.headers / req.headers[name] |
@Ip() |
req.ip |
@HostParam() |
req.hosts |
* For compatibility with typings across underlying HTTP platforms (e.g., Express and Fastify), Nest provides @Res()
and @Response()
decorators. @Res()
is simply an alias for @Response()
. Both directly expose the underlying native platform response
object interface. When using them, you should also import the typings for the underlying library (e.g., @types/express
) to take full advantage. Note that when you inject either @Res()
or @Response()
in a method handler, you put Nest into Library-specific mode for that handler, and you become responsible for managing the response. When doing so, you must issue some kind of response by making a call on the response
object (e.g., res.json(...)
or res.send(...)
), or the HTTP server will hang.
info Hint To learn how to create your own custom decorators, visit this chapter.
Earlier, we defined an endpoint to fetch the cats resource (GET route). We'll typically also want to provide an endpoint that creates new records. For this, let's create the POST handler:
@@filename(cats.controller)
import { Controller, Get, Post } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Post()
create(): string {
return 'This action adds a new cat';
}
@Get()
findAll(): string {
return 'This action returns all cats';
}
}
@@switch
import { Controller, Get, Post } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Post()
create() {
return 'This action adds a new cat';
}
@Get()
findAll() {
return 'This action returns all cats';
}
}
It's that simple. Nest provides decorators for all of the standard HTTP methods: @Get
, @Post
, @Put()
, @Delete()
, @Patch()
, @Options()
, and @Head()
. In addition, @All()
defines an endpoint that handles all of them.
Pattern based routes are supported as well. For instance, the asterisk is used as a wildcard, and will match any combination of characters.
@Get('ab*cd')
findAll() {
return 'This route uses a wildcard';
}
The 'ab*cd'
route path will match abcd
, ab_cd
, abecd
, and so on. The characters ?
, +
, *
, and ()
may be used in a route path, and are subsets of their regular expression counterparts. The hyphen ( -
) and the dot (.
) are interpreted literally by string-based paths.
As mentioned, the response status code is always 200 by default, except for POST requests which are 201. We can easily change this behavior by adding the @HttpCode(...)
decorator at a handler level.
@Post()
@HttpCode(204)
create() {
return 'This action adds a new cat';
}
info Hint Import
HttpCode
from the@nestjs/common
package.
Often, your status code isn't static but depends on various factors. In that case, you can use a library-specific response (inject using @Res()
) object (or, in case of an error, throw an exception).
To specify a custom response header, you can either use a @Header()
decorator or a library-specific response object (and call res.header()
directly).
@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat';
}
info Hint Import
Header
from the@nestjs/common
package.
To redirect a response to a specific URL, you can either use a @Redirect()
decorator or a library-specific response object (and call res.redirect()
directly).
@Redirect()
takes a required url
argument, and an optional statusCode
argument. The statusCode
defaults to 302
(Found
) if omitted.
@Get()
@Redirect('https://nestjs.com', 301)
Sometimes you may want to determine the HTTP status code or the redirect URL dynamically. Do this by returning an object from the route handler method with the shape:
{
"url": string,
"statusCode": number
}
Returned values will override any arguments passed to the @Redirect()
decorator. For example:
@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version) {
if (version && version === '5') {
return { url: 'https://docs.nestjs.com/v5/' };
}
}
Routes with static paths won't work when you need to accept dynamic data as part of the request (e.g., GET /cats/1
to get cat with id 1
). In order to define routes with parameters, we can add route parameter tokens in the path of the route to capture the dynamic value at that position in the request URL. The route parameter token in the @Get()
decorator example below demonstrates this usage. Route parameters declared in this way can be accessed using the @Param()
decorator, which should be added to the method signature.
@@filename()
@Get(':id')
findOne(@Param() params): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
@@switch
@Get(':id')
@Bind(Param())
findOne(params) {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
@Param()
is used to decorate a method parameter (params
in the example above), and makes the route parameters available as properties of that decorated method parameter inside the body of the method. As seen in the code above, we can access the id
parameter by referencing params.id
. You can also pass in a particular parameter token to the decorator, and then reference the route parameter directly by name in the method body.
info Hint Import
Param
from the@nestjs/common
package.
@@filename()
@Get(':id')
findOne(@Param('id') id): string {
return `This action returns a #${id} cat`;
}
@@switch
@Get(':id')
@Bind(Param('id'))
findOne(id) {
return `This action returns a #${id} cat`;
}
The @Controller
decorator can take a host
option to require that the HTTP host of the incoming requests matches some specific value.
@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index(): string {
return 'Admin page';
}
}
Warning Since Fastify lacks support for nested routers, when using sub-domain routing, the (default) Express adapter should be used instead.
Similar to a route path
, the hosts
option can use tokens to capture the dynamic value at that position in the host name. The host parameter token in the @Controller()
decorator example below demonstrates this usage. Host parameters declared in this way can be accessed using the @HostParam()
decorator, which should be added to the method signature.
@Controller({ host: ':account.example.com' })
export class AccountController {
@Get()
getInfo(@HostParam('account') account: string) {
return account;
}
}
For people coming from different programming language backgrounds, it might be unexpected to learn that in Nest, almost everything is shared across incoming requests. We have a connection pool to the database, singleton services with global state, etc. Remember that Node.js doesn't follow the request/response Multi-Threaded Stateless Model in which every request is processed by a separate thread. Hence, using singleton instances is fully safe for our applications.
However, there are edge-cases when request-based lifetime of the controller may be the desired behavior, for instance per-request caching in GraphQL applications, request tracking or multi-tenancy. Learn how to control scopes here.
We love modern JavaScript and we know that data extraction is mostly asynchronous. That's why Nest supports and works well with async
functions.
info Hint Learn more about
async / await
feature here
Every async function has to return a Promise
. This means that you can return a deferred value that Nest will be able to resolve by itself. Let's see an example of this:
@@filename(cats.controller)
@Get()
async findAll(): Promise<any[]> {
return [];
}
@@switch
@Get()
async findAll() {
return [];
}
The above code is fully valid. Furthermore, Nest route handlers are even more powerful by being able to return RxJS observable streams. Nest will automatically subscribe to the source underneath and take the last emitted value (once the stream is completed).
@@filename(cats.controller)
@Get()
findAll(): Observable<any[]> {
return of([]);
}
@@switch
@Get()
findAll() {
return of([]);
}
Both of the above approaches work and you can use whatever fits your requirements.
Our previous example of the POST route handler didn't accept any client params. Let's fix this by adding the @Body()
decorator here.
But first (if you use TypeScript), we need to determine the DTO (Data Transfer Object) schema. A DTO is an object that defines how the data will be sent over the network. We could determine the DTO schema by using TypeScript interfaces, or by simple classes. Interestingly, we recommend using classes here. Why? Classes are part of the JavaScript ES6 standard, and therefore they are preserved as real entities in the compiled JavaScript. On the other hand, since TypeScript interfaces are removed during the transpilation, Nest can't refer to them at runtime. This is important because features such as Pipes enable additional possibilities when they have access to the metatype of the variable at runtime.
Let's create the CreateCatDto
class:
@@filename(create-cat.dto)
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
It has only three basic properties. Thereafter we can use the newly created DTO inside the CatsController
:
@@filename(cats.controller)
@Post()
async create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
@@switch
@Post()
@Bind(Body())
async create(createCatDto) {
return 'This action adds a new cat';
}
There's a separate chapter about handling errors (i.e., working with exceptions) here.
Below is an example that makes use of several of the available decorators to create a basic controller. This controller exposes a couple of methods to access and manipulate internal data.
@@filename(cats.controller)
import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';
@Controller('cats')
export class CatsController {
@Post()
create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
@Get()
findAll(@Query() query: ListAllEntities) {
return `This action returns all cats (limit: ${query.limit} items)`;
}
@Get(':id')
findOne(@Param('id') id: string) {
return `This action returns a #${id} cat`;
}
@Put(':id')
update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
return `This action updates a #${id} cat`;
}
@Delete(':id')
remove(@Param('id') id: string) {
return `This action removes a #${id} cat`;
}
}
@@switch
import { Controller, Get, Query, Post, Body, Put, Param, Delete, Bind } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Post()
@Bind(Body())
create(createCatDto) {
return 'This action adds a new cat';
}
@Get()
@Bind(Query())
findAll(query) {
console.log(query);
return `This action returns all cats (limit: ${query.limit} items)`;
}
@Get(':id')
@Bind(Param('id'))
findOne(id) {
return `This action returns a #${id} cat`;
}
@Put(':id')
@Bind(Param('id'), Body())
update(id, updateCatDto) {
return `This action updates a #${id} cat`;
}
@Delete(':id')
@Bind(Param('id'))
remove(id) {
return `This action removes a #${id} cat`;
}
}
With the above controller fully defined, Nest still doesn't know that CatsController
exists and as a result won't create an instance of this class.
Controllers always belong to a module, which is why we include the controllers
array within the @Module()
decorator. Since we haven't yet defined any other modules except the root AppModule
, we'll use that to introduce the CatsController
:
@@filename(app.module)
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
@Module({
controllers: [CatsController],
})
export class AppModule {}
We attached the metadata to the module class using the @Module()
decorator, and Nest can now easily reflect which controllers have to be mounted.
So far we've discussed the Nest standard way of manipulating responses. The second way of manipulating the response is to use a library-specific response object. In order to inject a particular response object, we need to use the @Res()
decorator. To show the differences, let's rewrite the CatsController
to the following:
@@filename()
import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
@Controller('cats')
export class CatsController {
@Post()
create(@Res() res: Response) {
res.status(HttpStatus.CREATED).send();
}
@Get()
findAll(@Res() res: Response) {
res.status(HttpStatus.OK).json([]);
}
}
@@switch
import { Controller, Get, Post, Bind, Res, Body, HttpStatus } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Post()
@Bind(Res(), Body())
create(res, createCatDto) {
res.status(HttpStatus.CREATED).send();
}
@Get()
@Bind(Res())
findAll(res) {
res.status(HttpStatus.OK).json([]);
}
}
Though this approach works, and does in fact allow for more flexibility in some ways by providing full control of the response object (headers manipulation, library-specific features, and so on), it should be used with care. In general, the approach is much less clear and does have some disadvantages. The main disadvantages are that you lose compatibility with Nest features that depend on Nest standard response handling, such as Interceptors and the @HttpCode()
decorator. Also, your code can become platform-dependent (as underlying libraries may have different APIs on the response object), and harder to test (you'll have to mock the response object, etc.).
As a result, the Nest standard approach should always be preferred when possible.
Providers are a fundamental concept in Nest. Many of the basic Nest classes may be treated as a provider – services, repositories, factories, helpers, and so on. The main idea of a provider is that it can inject dependencies; this means objects can create various relationships with each other, and the function of "wiring up" instances of objects can largely be delegated to the Nest runtime system. A provider is simply a class annotated with an @Injectable()
decorator.
In the previous chapter, we built a simple CatsController
. Controllers should handle HTTP requests and delegate more complex tasks to providers. Providers are plain JavaScript classes with an @Injectable()
decorator preceding their class declaration.
info Hint Since Nest enables the possibility to design and organize dependencies in a more OO-way, we strongly recommend following the SOLID principles.
Let's start by creating a simple CatsService
. This service will be responsible for data storage and retrieval, and is designed to be used by the CatsController
, so it's a good candidate to be defined as a provider. Thus, we decorate the class with @Injectable()
.
@@filename(cats.service)
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
create(cat: Cat) {
this.cats.push(cat);
}
findAll(): Cat[] {
return this.cats;
}
}
@@switch
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatsService {
constructor() {
this.cats = [];
}
create(cat) {
this.cats.push(cat);
}
findAll() {
return this.cats;
}
}
info Hint To create a service using the CLI, simply execute the
$ nest g service cats
command.
Our CatsService
is a basic class with one property and two methods. The only new feature is that it uses the @Injectable()
decorator. The @Injectable()
decorator attaches metadata, which tells Nest that this class is a Nest provider. By the way, this example also uses a Cat
interface, which probably looks something like this:
@@filename(interfaces/cat.interface)
export interface Cat {
name: string;
age: number;
breed: string;
}
Now that we have a service class to retrieve cats, let's use it inside the CatsController
:
@@filename(cats.controller)
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
@@switch
import { Controller, Get, Post, Body, Bind, Dependencies } from '@nestjs/common';
import { CatsService } from './cats.service';
@Controller('cats')
@Dependencies(CatsService)
export class CatsController {
constructor(catsService) {
this.catsService = catsService;
}
@Post()
@Bind(Body())
async create(createCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll() {
return this.catsService.findAll();
}
}
The CatsService
is injected through the class constructor. Notice the use of the private
syntax. This shorthand allows us to both declare and initialize the catsService
member immediately in the same location.
Nest is built around the strong design pattern commonly known as Dependency injection. We recommend reading a great article about this concept in the official Angular documentation.
In Nest, thanks to TypeScript capabilities, it's extremely easy to manage dependencies because they are resolved just by type. In the example below, Nest will resolve the catsService
by creating and returning an instance of CatsService
(or, in the normal case of a singleton, returning the existing instance if it has already been requested elsewhere). This dependency is resolved and passed to your controller's constructor (or assigned to the indicated property):
constructor(private catsService: CatsService) {}
Providers normally have a lifetime ("scope") synchronized with the application lifecycle. When the application is bootstrapped, every dependency must be resolved, and therefore every provider has to be instantiated. Similarly, when the application shuts down, each provider will be destroyed. However, there are ways to make your provider lifetime request-scoped as well. You can read more about these techniques here.
Nest has a built-in inversion of control ("IoC") container that resolves relationships between providers. This feature underlies the dependency injection feature described above, but is in fact far more powerful than what we've described so far. The @Injectable()
decorator is only the tip of the iceberg, and is not the only way to define providers. In fact, you can use plain values, classes, and either asynchronous or synchronous factories. More examples are provided here.
Occasionally, you might have dependencies which do not necessarily have to be resolved. For instance, your class may depend on a configuration object, but if none is passed, the default values should be used. In such a case, the dependency becomes optional, because lack of the configuration provider wouldn't lead to errors.
To indicate a provider is optional, use the @Optional()
decorator in the constructor's signature.
import { Injectable, Optional, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}
Note that in the example above we are using a custom provider, which is the reason we include the HTTP_OPTIONS
custom token. Previous examples showed constructor-based injection indicating a dependency through a class in the constructor. Read more about custom providers and their associated tokens here.
The technique we've used so far is called constructor-based injection, as providers are injected via the constructor method. In some very specific cases, property-based injection might be useful. For instance, if your top-level class depends on either one or multiple providers, passing them all the way up by calling super()
in sub-classes from the constructor can be very tedious. In order to avoid this, you can use the @Inject()
decorator at the property level.
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}
warning Warning If your class doesn't extend another provider, you should always prefer using constructor-based injection.
Now that we have defined a provider (CatsService
), and we have a consumer of that service (CatsController
), we need to register the service with Nest so that it can perform the injection. We do this by editing our module file (app.module.ts
) and adding the service to the providers
array of the @Module()
decorator.
@@filename(app.module)
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}
Nest will now be able to resolve the dependencies of the CatsController
class.
This is how our directory structure should look now:
Thus far, we've discussed how Nest automatically handles most of the details of resolving dependencies. In certain circumstances, you may need to step outside of the built-in Dependency Injection system and manually retrieve or instantiate providers. We briefly discuss two such topics below.
To get existing instances, or instantiate providers dynamically, you can use Module reference.
To get providers within the bootstrap()
function (for example for standalone applications without controllers, or to utilize a configuration service during bootstrapping) see Standalone applications.
A module is a class annotated with a @Module()
decorator. The @Module()
decorator provides metadata that Nest makes use of to organize the application structure.
Each application has at least one module, a root module. The root module is the starting point Nest uses to build the application graph - the internal data structure Nest uses to resolve module and provider relationships and dependencies. While very small applications may theoretically have just the root module, this is not the typical case. We want to emphasize that modules are strongly recommended as an effective way to organize your components. Thus, for most applications, the resulting architecture will employ multiple modules, each encapsulating a closely related set of capabilities.
The @Module()
decorator takes a single object whose properties describe the module:
providers |
the providers that will be instantiated by the Nest injector and that may be shared at least across this module |
controllers |
the set of controllers defined in this module which have to be instantiated |
imports |
the list of imported modules that export the providers which are required in this module |
exports |
the subset of providers that are provided by this module and should be available in other modules which import this module |
The module encapsulates providers by default. This means that it's impossible to inject providers that are neither directly part of the current module nor exported from the imported modules. Thus, you may consider the exported providers from a module as the module's public interface, or API.
The CatsController
and CatsService
belong to the same application domain. As they are closely related, it makes sense to move them into a feature module. A feature module simply organizes code relevant for a specific feature, keeping code organized and establishing clear boundaries. This helps us manage complexity and develop with SOLID principles, especially as the size of the application and/or team grow.
To demonstrate this, we'll create the CatsModule
.
@@filename(cats/cats.module)
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
info Hint To create a module using the CLI, simply execute the
$ nest g module cats
command.
Above, we defined the CatsModule
in the cats.module.ts
file, and moved everything related to this module into the cats
directory. The last thing we need to do is import this module into the root module (the AppModule
, defined in the app.module.ts
file).
@@filename(app.module)
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule {}
Here is how our directory structure looks now:
In Nest, modules are singletons by default, and thus you can share the same instance of any provider between multiple modules effortlessly.
Every module is automatically a shared module. Once created it can be reused by any module. Let's imagine that we want to share an instance of the CatsService
between several other modules. In order to do that, we first need to export the CatsService
provider by adding it to the module's exports
array, as shown below:
@@filename(cats.module)
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService]
})
export class CatsModule {}
Now any module that imports the CatsModule
has access to the CatsService
and will share the same instance with all other modules that import it as well.
As seen above, Modules can export their internal providers. In addition, they can re-export modules that they import. In the example below, the CommonModule
is both imported into and exported from the CoreModule
, making it available for other modules which import this one.
@Module({
imports: [CommonModule],
exports: [CommonModule],
})
export class CoreModule {}
A module class can inject providers as well (e.g., for configuration purposes):
@@filename(cats.module)
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {
constructor(private catsService: CatsService) {}
}
@@switch
import { Module, Dependencies } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
@Dependencies(CatsService)
export class CatsModule {
constructor(catsService) {
this.catsService = catsService;
}
}
However, module classes themselves cannot be injected as providers due to circular dependency .
If you have to import the same set of modules everywhere, it can get tedious. Unlike in Nest, Angular providers
are registered in the global scope. Once defined, they're available everywhere. Nest, however, encapsulates providers inside the module scope. You aren't able to use a module's providers elsewhere without first importing the encapsulating module.
When you want to provide a set of providers which should be available everywhere out-of-the-box (e.g., helpers, database connections, etc.), make the module global with the @Global()
decorator.
import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Global()
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
The @Global()
decorator makes the module global-scoped. Global modules should be registered only once, generally by the root or core module. In the above example, the CatsService
provider will be ubiquitous, and modules that wish to inject the service will not need to import the CatsModule
in their imports array.
info Hint Making everything global is not a good design decision. Global modules are available to reduce the amount of necessary boilerplate. The
imports
array is generally the preferred way to make the module's API available to consumers.
The Nest module system includes a powerful feature called dynamic modules. This feature enables you to easily create customizable modules that can register and configure providers dynamically. Dynamic modules are covered extensively here. In this chapter, we'll give a brief overview to complete the introduction to modules.
Following is an example of a dynamic module definition for a DatabaseModule
:
@@filename()
import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';
@Module({
providers: [Connection],
})
export class DatabaseModule {
static forRoot(entities = [], options?): DynamicModule {
const providers = createDatabaseProviders(options, entities);
return {
module: DatabaseModule,
providers: providers,
exports: providers,
};
}
}
@@switch
import { Module } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';
@Module({
providers: [Connection],
})
export class DatabaseModule {
static forRoot(entities = [], options?) {
const providers = createDatabaseProviders(options, entities);
return {
module: DatabaseModule,
providers: providers,
exports: providers,
};
}
}
info Hint The
forRoot()
method may return a dynamic module either synchronously or asynchronously (i.e., via aPromise
).
This module defines the Connection
provider by default (in the @Module()
decorator metadata), but additionally - depending on the entities
and options
objects passed into the forRoot()
method - exposes a collection of providers, for example, repositories. Note that the properties returned by the dynamic module extend (rather than override) the base module metadata defined in the @Module()
decorator. That's how both the statically declared Connection
provider and the dynamically generated repository providers are exported from the module.
If you want to register a dynamic module in the global scope, set the global
property to true
.
{
global: true,
module: DatabaseModule,
providers: providers,
exports: providers,
}
warning Warning As mentioned above, making everything global is not a good design decision.
The DatabaseModule
can be imported and configured in the following manner:
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';
@Module({
imports: [DatabaseModule.forRoot([User])],
})
export class AppModule {}
If you want to in turn re-export a dynamic module, you can omit the forRoot()
method call in the exports array:
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';
@Module({
imports: [DatabaseModule.forRoot([User])],
exports: [DatabaseModule],
})
export class AppModule {}
The Dynamic modules chapter covers this topic in greater detail, and includes a working example.
Middleware is a function which is called before the route handler. Middleware functions have access to the request and response objects, and the next()
middleware function in the application’s request-response cycle. The next middleware function is commonly denoted by a variable named next
.
Nest middleware are, by default, equivalent to express middleware. The following description from the official express documentation describes the capabilities of middleware:
Middleware functions can perform the following tasks:
- execute any code.
- make changes to the request and the response objects.
- end the request-response cycle.
- call the next middleware function in the stack.
- if the current middleware function does not end the request-response cycle, it must call
next()
to pass control to the next middleware function. Otherwise, the request will be left hanging.
You implement custom Nest middleware in either a function, or in a class with an @Injectable()
decorator. The class should implement the NestMiddleware
interface, while the function does not have any special requirements. Let's start by implementing a simple middleware feature using the class method.
@@filename(logger.middleware)
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: Function) {
console.log('Request...');
next();
}
}
@@switch
import { Injectable } from '@nestjs/common';
@Injectable()
export class LoggerMiddleware {
use(req, res, next) {
console.log('Request...');
next();
}
}
Nest middleware fully supports Dependency Injection. Just as with providers and controllers, they are able to inject dependencies that are available within the same module. As usual, this is done through the constructor
.
There is no place for middleware in the @Module()
decorator. Instead, we set them up using the configure()
method of the module class. Modules that include middleware have to implement the NestModule
interface. Let's set up the LoggerMiddleware
at the AppModule
level.
@@filename(app.module)
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('cats');
}
}
@@switch
import { Module } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule {
configure(consumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('cats');
}
}
In the above example we have set up the LoggerMiddleware
for the /cats
route handlers that were previously defined inside the CatsController
. We may also further restrict a middleware to a particular request method by passing an object containing the route path
and request method
to the forRoutes()
method when configuring the middleware. In the example below, notice that we import the RequestMethod
enum to reference the desired request method type.
@@filename(app.module)
import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: 'cats', method: RequestMethod.GET });
}
}
@@switch
import { Module, RequestMethod } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule {
configure(consumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: 'cats', method: RequestMethod.GET });
}
}
info Hint The
configure()
method can be made asynchronous usingasync/await
(e.g., you canawait
completion of an asynchronous operation inside theconfigure()
method body).
Pattern based routes are supported as well. For instance, the asterisk is used as a wildcard, and will match any combination of characters:
forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });
The 'ab*cd'
route path will match abcd
, ab_cd
, abecd
, and so on. The characters ?
, +
, *
, and ()
may be used in a route path, and are subsets of their regular expression counterparts. The hyphen ( -
) and the dot (.
) are interpreted literally by string-based paths.
warning Warning The
fastify
package uses the latest version of thepath-to-regexp
package, which no longer supports wildcard asterisks*
. Instead, you must use parameters (e.g.,(.*)
,:splat*
).
The MiddlewareConsumer
is a helper class. It provides several built-in methods to manage middleware. All of them can be simply chained in the fluent style. The forRoutes()
method can take a single string, multiple strings, a RouteInfo
object, a controller class and even multiple controller classes. In most cases you'll probably just pass a list of controllers separated by commas. Below is an example with a single controller:
@@filename(app.module)
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
import { CatsController } from './cats/cats.controller.ts';
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes(CatsController);
}
}
@@switch
import { Module } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
import { CatsController } from './cats/cats.controller.ts';
@Module({
imports: [CatsModule],
})
export class AppModule {
configure(consumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes(CatsController);
}
}
info Hint The
apply()
method may either take a single middleware, or multiple arguments to specify multiple middlewares.
At times we want to exclude certain routes from having the middleware applied. We can easily exclude certain routes with the exclude()
method. This method can take a single string, multiple strings, or a RouteInfo
object identifying routes to be excluded, as shown below:
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'cats', method: RequestMethod.GET },
{ path: 'cats', method: RequestMethod.POST },
'cats/(.*)',
)
.forRoutes(CatsController);
info Hint The
exclude()
method supports wildcard parameters using the path-to-regexp package.
With the example above, LoggerMiddleware
will be bound to all routes defined inside CatsController
except the three passed to the exclude()
method.
The LoggerMiddleware
class we've been using is quite simple. It has no members, no additional methods, and no dependencies. Why can't we just define it in a simple function instead of a class? In fact, we can. This type of middleware is called functional middleware. Let's transform the logger middleware from class-based into functional middleware to illustrate the difference:
@@filename(logger.middleware)
import { Request, Response } from 'express';
export function logger(req: Request, res: Response, next: Function) {
console.log(`Request...`);
next();
};
@@switch
export function logger(req, res, next) {
console.log(`Request...`);
next();
};
And use it within the AppModule
:
@@filename(app.module)
consumer
.apply(logger)
.forRoutes(CatsController);
info Hint Consider using the simpler functional middleware alternative any time your middleware doesn't need any dependencies.
As mentioned above, in order to bind multiple middleware that are executed sequentially, simply provide a comma separated list inside the apply()
method:
consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);
If we want to bind middleware to every registered route at once, we can use the use()
method that is supplied by the INestApplication
instance:
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
Nest comes with a built-in exceptions layer which is responsible for processing all unhandled exceptions across an application. When an exception is not handled by your application code, it is caught by this layer, which then automatically sends an appropriate user-friendly response.
Out of the box, this action is performed by a built-in global exception filter, which handles exceptions of type HttpException
(and subclasses of it). When an exception is unrecognized (is neither HttpException
nor a class that inherits from HttpException
), the built-in exception filter generates the following default JSON response:
{
"statusCode": 500,
"message": "Internal server error"
}
Nest provides a built-in HttpException
class, exposed from the @nestjs/common
package. For typical HTTP REST/GraphQL API based applications, it's best practice to send standard HTTP response objects when certain error conditions occur.
For example, in the CatsController
, we have a findAll()
method (a GET
route handler). Let's assume that this route handler throws an exception for some reason. To demonstrate this, we'll hard-code it as follows:
@@filename(cats.controller)
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
info Hint We used the
HttpStatus
here. This is a helper enum imported from the@nestjs/common
package.
When the client calls this endpoint, the response looks like this:
{
"statusCode": 403,
"message": "Forbidden"
}
The HttpException
constructor takes two required arguments which determine the
response:
- The
response
argument defines the JSON response body. It can be astring
or anobject
as described below. - The
status
argument defines the HTTP status code.
By default, the JSON response body contains two properties:
statusCode
: defaults to the HTTP status code provided in thestatus
argumentmessage
: a short description of the HTTP error based on thestatus
To override just the message portion of the JSON response body, supply a string
in the response
argument. To override the entire JSON response body, pass an object in the response
argument. Nest will serialize the object and return it as the JSON response body.
The second constructor argument - status
- should be a valid HTTP status code.
Best practice is to use the HttpStatus
enum imported from @nestjs/common
.
Here's an example overriding the entire response body:
@@filename(cats.controller)
@Get()
async findAll() {
throw new HttpException({
status: HttpStatus.FORBIDDEN,
error: 'This is a custom message',
}, HttpStatus.FORBIDDEN);
}
Using the above, this is how the response would look:
{
"status": 403,
"error": "This is a custom message"
}
In many cases, you will not need to write custom exceptions, and can use the built-in Nest HTTP exception, as described in the next section. If you do need to create customized exceptions, it's good practice to create your own exceptions hierarchy, where your custom exceptions inherit from the base HttpException
class. With this approach, Nest will recognize your exceptions, and automatically take care of the error responses. Let's implement such a custom exception:
@@filename(forbidden.exception)
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}
Since ForbiddenException
extends the base HttpException
, it will work seamlessly with the built-in exception handler, and therefore we can use it inside the findAll()
method.
@@filename(cats.controller)
@Get()
async findAll() {
throw new ForbiddenException();
}
Nest provides a set of standard exceptions that inherit from the base HttpException
. These are exposed from the @nestjs/common
package, and represent many of the most common HTTP exceptions:
BadRequestException
UnauthorizedException
NotFoundException
ForbiddenException
NotAcceptableException
RequestTimeoutException
ConflictException
GoneException
HttpVersionNotSupportedException
PayloadTooLargeException
UnsupportedMediaTypeException
UnprocessableEntityException
InternalServerErrorException
NotImplementedException
ImATeapotException
MethodNotAllowedException
BadGatewayException
ServiceUnavailableException
GatewayTimeoutException
PreconditionFailedException
While the base (built-in) exception filter can automatically handle many cases for you, you may want full control over the exceptions layer. For example, you may want to add logging or use a different JSON schema based on some dynamic factors. Exception filters are designed for exactly this purpose. They let you control the exact flow of control and the content of the response sent back to the client.
Let's create an exception filter that is responsible for catching exceptions which are an instance of the HttpException
class, and implementing custom response logic for them. To do this, we'll need to access the underlying platform Request
and Response
objects. We'll access the Request
object so we can pull out the original url
and include that in the logging information. We'll use the Response
object to take direct control of the response that is sent, using the response.json()
method.
@@filename(http-exception.filter)
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
@@switch
import { Catch, HttpException } from '@nestjs/common';
@Catch(HttpException)
export class HttpExceptionFilter {
catch(exception, host) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception.getStatus();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
info Hint All exception filters should implement the generic
ExceptionFilter<T>
interface. This requires you to provide thecatch(exception: T, host: ArgumentsHost)
method with its indicated signature.T
indicates the type of the exception.
The @Catch(HttpException)
decorator binds the required metadata to the exception filter, telling Nest that this particular filter is looking for exceptions of type HttpException
and nothing else. The @Catch()
decorator may take a single parameter, or a comma-separated list. This lets you set up the filter for several types of exceptions at once.
Let's look at the parameters of the catch()
method. The exception
parameter is the exception object currently being processed. The host
parameter is an ArgumentsHost
object. ArgumentsHost
is a powerful utility object that we'll examine further in the execution context chapter*. In this code sample, we use it to obtain a reference to the Request
and Response
objects that are being passed to the original request handler (in the controller where the exception originates). In this code sample, we've used some helper methods on ArgumentsHost
to get the desired Request
and Response
objects. Learn more about ArgumentsHost
here.
*The reason for this level of abstraction is that ArgumentsHost
functions in all contexts (e.g., the HTTP server context we're working with now, but also Microservices and WebSockets). In the execution context chapter we'll see how we can access the appropriate underlying arguments for any execution context with the power of ArgumentsHost
and its helper functions. This will allow us to write generic exception filters that operate across all contexts.
Let's tie our new HttpExceptionFilter
to the CatsController
's create()
method.
@@filename(cats.controller)
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
@@switch
@Post()
@UseFilters(new HttpExceptionFilter())
@Bind(Body())
async create(createCatDto) {
throw new ForbiddenException();
}
info Hint The
@UseFilters()
decorator is imported from the@nestjs/common
package.
We have used the @UseFilters()
decorator here. Similar to the @Catch()
decorator, it can take a single filter instance, or a comma-separated list of filter instances. Here, we created the instance of HttpExceptionFilter
in place. Alternatively, you may pass the class (instead of an instance), leaving responsibility for instantiation to the framework, and enabling dependency injection.
@@filename(cats.controller)
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
@@switch
@Post()
@UseFilters(HttpExceptionFilter)
@Bind(Body())
async create(createCatDto) {
throw new ForbiddenException();
}
info Hint Prefer applying filters by using classes instead of instances when possible. It reduces memory usage since Nest can easily reuse instances of the same class across your entire module.
In the example above, the HttpExceptionFilter
is applied only to the single create()
route handler, making it method-scoped. Exception filters can be scoped at different levels: method-scoped, controller-scoped, or global-scoped. For example, to set up a filter as controller-scoped, you would do the following:
@@filename(cats.controller)
@UseFilters(new HttpExceptionFilter())
export class CatsController {}
This construction sets up the HttpExceptionFilter
for every route handler defined inside the CatsController
.
To create a global-scoped filter, you would do the following:
@@filename(main)
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
warning Warning The
useGlobalFilters()
method does not set up filters for gateways or hybrid applications.
Global-scoped filters are used across the whole application, for every controller and every route handler. In terms of dependency injection, global filters registered from outside of any module (with useGlobalFilters()
as in the example above) cannot inject dependencies since this is done outside the context of any module. In order to solve this issue, you can register a global-scoped filter directly from any module using the following construction:
@@filename(app.module)
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
info Hint When using this approach to perform dependency injection for the filter, note that regardless of the module where this construction is employed, the filter is, in fact, global. Where should this be done? Choose the module where the filter (
HttpExceptionFilter
in the example above) is defined. Also,useClass
is not the only way of dealing with custom provider registration. Learn more here.
You can add as many filters with this technique as needed; simply add each to the providers array.
In order to catch every unhandled exception (regardless of the exception type), leave the @Catch()
decorator's parameter list empty, e.g., @Catch()
.
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
In the example above the filter will catch each exception thrown, regardless of its type (class).
Typically, you'll create fully customized exception filters crafted to fulfill your application requirements. However, there might be use-cases when you would like to simply extend the built-in default global exception filter, and override the behavior based on certain factors.
In order to delegate exception processing to the base filter, you need to extend BaseExceptionFilter
and call the inherited catch()
method.
@@filename(all-exceptions.filter)
import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
super.catch(exception, host);
}
}
@@switch
import { Catch } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
catch(exception, host) {
super.catch(exception, host);
}
}
warning Warning Method-scoped and Controller-scoped filters that extend the
BaseExceptionFilter
should not be instantiated withnew
. Instead, let the framework instantiate them automatically.
The above implementation is just a shell demonstrating the approach. Your implementation of the extended exception filter would include your tailored business logic (e.g., handling various conditions).
Global filters can extend the base filter. This can be done in either of two ways.
The first method is to inject the HttpServer
reference when instantiating the custom global filter:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));
await app.listen(3000);
}
bootstrap();
The second method is to use the APP_FILTER
token as shown here.
A pipe is a class annotated with the @Injectable()
decorator. Pipes should implement the PipeTransform
interface.
Pipes have two typical use cases:
- transformation: transform input data to the desired form (e.g., from string to integer)
- validation: evaluate input data and if valid, simply pass it through unchanged; otherwise, throw an exception when the data is incorrect
In both cases, pipes operate on the arguments
being processed by a controller route handler. Nest interposes a pipe just before a method is invoked, and the pipe receives the arguments destined for the method and operates on them. Any transformation or validation operation takes place at that time, after which the route handler is invoked with any (potentially) transformed arguments.
Nest comes with a number of built-in pipes that you can use out-of-the-box. You can also build your own custom pipes. In this chapter, we'll introduce the built-in pipes and show how to bind them to route handlers. We'll then examine several custom-built pipes to show how you can build one from scratch.
info Hint Pipes run inside the exceptions zone. This means that when a Pipe throws an exception it is handled by the exceptions layer (global exceptions filter and any exceptions filters that are applied to the current context). Given the above, it should be clear that when an exception is thrown in a Pipe, no controller method is subsequently executed. This gives you a best-practice technique for validating data coming into the application from external sources at the system boundary.
Nest comes with six pipes available out-of-the-box:
ValidationPipe
ParseIntPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
DefaultValuePipe
They're exported from the @nestjs/common
package.
Let's take a quick look at using ParseIntPipe
. This is an example of the transformation use case, where the pipe ensures that a method handler parameter is converted to a JavaScript integer (or throws an exception if the conversion fails). Later in this chapter, we'll show a simple custom implementation for a ParseIntPipe
. The example techniques below also apply to the other built-in transformation pipes (ParseBoolPipe
, ParseArrayPipe
and ParseUUIDPipe
, which we'll refer to as the Parse*
pipes in this chapter).
To use a pipe, we need to bind an instance of the pipe class to the appropriate context. In our ParseIntPipe
example, we want to associate the pipe with a particular route handler method, and make sure it runs before the method is called. We do so with the following construct, which we'll refer to as binding the pipe at the method parameter level:
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
This ensures that one of the following two conditions is true: either the parameter we receive in the findOne()
method is a number (as expected in our call to this.catsService.findOne()
), or an exception is thrown before the route handler is called.
For example, assume the route is called like:
GET localhost:3000/abc
Nest will throw an exception like this:
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
The exception will prevent the body of the findOne()
method from executing.
In the example above, we pass a class (ParseIntPipe
), not an instance, leaving responsibility for instantiation to the framework and enabling dependency injection. As with pipes and guards, we can instead pass an in-place instance. Passing an in-place instance is useful if we want to customize the built-in pipe's behavior by passing options:
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
return this.catsService.findOne(id);
}
Binding the other transformation pipes (all of the Parse* pipes) works similarly. These pipes all work in the context of validating route parameters, query string parameters and request body values.
For example with a query string parameter:
@Get()
async findOne(@Query('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
Here's an example of using the ParseUUIDPipe
to parse a string parameter and validate if it is a UUID.
@@filename()
@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
return this.catsService.findOne(uuid);
}
@@switch
@Get(':uuid')
@Bind(Param('uuid', new ParseUUIDPipe()))
async findOne(uuid) {
return this.catsService.findOne(uuid);
}
info Hint When using
ParseUUIDPipe()
you are parsing UUID in version 3, 4 or 5, if you only require a specific version of UUID you can pass a version in the pipe options.
Above we've seen examples of binding the various Parse*
family of built-in pipes. Binding validation pipes is a little bit different; we'll discuss that in the following section.
info Hint Also, see Validation techniques for extensive examples of validation pipes.
As mentioned, you can build your own custom pipes. While Nest provides a robust built-in ParseIntPipe
and ValidationPipe
, let's build simple custom versions of each from scratch to see how custom pipes are constructed.
We start with a simple ValidationPipe
. Initially, we'll have it simply take an input value and immediately return the same value, behaving like an identity function.
@@filename(validation.pipe)
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
@@switch
import { Injectable } from '@nestjs/common';
@Injectable()
export class ValidationPipe {
transform(value, metadata) {
return value;
}
}
info Hint
PipeTransform<T, R>
is a generic interface that must be implemented by any pipe. The generic interface usesT
to indicate the type of the inputvalue
, andR
to indicate the return type of thetransform()
method.
Every pipe must implement the transform()
method to fulfill the PipeTransform
interface contract. This method has two parameters:
value
metadata
The value
parameter is the currently processed method argument (before it is received by the route handling method), and metadata
is the currently processed method argument's metadata. The metadata object has these properties:
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
}
These properties describe the currently processed argument.
type
|
Indicates whether the argument is a body
@Body() , query
@Query() , param
@Param() , or a custom parameter (read more
here). |
metatype
|
Provides the metatype of the argument, for example,
String . Note: the value is
undefined if you either omit a type declaration in the route handler method signature, or use vanilla JavaScript.
|
data
|
The string passed to the decorator, for example
@Body('string') . It's
undefined if you leave the decorator parenthesis empty. |
warning Warning TypeScript interfaces disappear during transpilation. Thus, if a method parameter's type is declared as an interface instead of a class, the
metatype
value will beObject
.
Let's make our validation pipe a little more useful. Take a closer look at the create()
method of the CatsController
, where we probably would like to ensure that the post body object is valid before attempting to run our service method.
@@filename()
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@@switch
@Post()
async create(@Body() createCatDto) {
this.catsService.create(createCatDto);
}
Let's focus in on the createCatDto
body parameter. Its type is CreateCatDto
:
@@filename(create-cat.dto)
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
We want to ensure that any incoming request to the create method contains a valid body. So we have to validate the three members of the createCatDto
object. We could do this inside the route handler method, but doing so is not ideal as it would break the single responsibility rule (SRP).
Another approach could be to create a validator class and delegate the task there. This has the disadvantage that we would have to remember to call this validator at the beginning of each method.
How about creating validation middleware? This could work, but unfortunately it's not possible to create generic middleware which can be used across all contexts across the whole application. This is because middleware is unaware of the execution context, including the handler that will be called and any of its parameters.
This is, of course, exactly the use case for which pipes are designed. So let's go ahead and refine our validation pipe.
There are several approaches available for doing object validation in a clean, DRY way. One common approach is to use schema-based validation. Let's go ahead and try that approach.
The Joi library allows you to create schemas in a straightforward way, with a readable API. Let's build a validation pipe that makes use of Joi-based schemas.
Start by installing the required package:
$ npm install --save @hapi/joi
$ npm install --save-dev @types/hapi__joi
In the code sample below, we create a simple class that takes a schema as a constructor
argument. We then apply the schema.validate()
method, which validates our incoming argument against the provided schema.
As noted earlier, a validation pipe either returns the value unchanged, or throws an exception.
In the next section, you'll see how we supply the appropriate schema for a given controller method using the @UsePipes()
decorator. Doing so makes our validation pipe re-usable across contexts, just as we set out to do.
@@filename()
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ObjectSchema } from '@hapi/joi';
@Injectable()
export class JoiValidationPipe implements PipeTransform {
constructor(private schema: ObjectSchema) {}
transform(value: any, metadata: ArgumentMetadata) {
const { error } = this.schema.validate(value);
if (error) {
throw new BadRequestException('Validation failed');
}
return value;
}
}
@@switch
import { Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class JoiValidationPipe {
constructor(schema) {
this.schema = schema;
}
transform(value, metadata) {
const { error } = this.schema.validate(value);
if (error) {
throw new BadRequestException('Validation failed');
}
return value;
}
}
Earlier, we saw how to bind transformation pipes (like ParseIntPipe
and the rest of the Parse*
pipes).
Binding validation pipes is also very straightforward.
In this case, we want to bind the pipe at the method call level. In our current example, we need to do the following to use the JoiValidationPipe
:
- Create an instance of the
JoiValidationPipe
- Pass the context-specific Joi schema in the class constructor of the pipe
- Bind the pipe to the method
We do that using the @UsePipes()
decorator as shown below:
@@filename()
@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@@switch
@Post()
@Bind(Body())
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(createCatDto) {
this.catsService.create(createCatDto);
}
info Hint The
@UsePipes()
decorator is imported from the@nestjs/common
package.
warning Warning The techniques in this section require TypeScript, and are not available if your app is written using vanilla JavaScript.
Let's look at an alternate implementation for our validation technique.
Nest works well with the class-validator library. This powerful library allows you to use decorator-based validation. Decorator-based validation is extremely powerful, especially when combined with Nest's Pipe capabilities since we have access to the metatype
of the processed property. Before we start, we need to install the required packages:
$ npm i --save class-validator class-transformer
Once these are installed, we can add a few decorators to the CreateCatDto
class. Here we see a significant advantage of this technique: the CreateCatDto
class remains the single source of truth for our Post body object (rather than having to create a separate validation class).
@@filename(create-cat.dto)
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
age: number;
@IsString()
breed: string;
}
info Hint Read more about the class-validator decorators here.
Now we can create a ValidationPipe
class that uses these annotations.
@@filename(validation.pipe)
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToClass(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
warning Notice Above, we have used the class-transformer library. It's made by the same author as the class-validator library, and as a result, they play very well together.
Let's go through this code. First, note that the transform()
method is marked as async
. This is possible because Nest supports both synchronous and asynchronous pipes. We make this method async
because some of the class-validator validations can be async (utilize Promises).
Next note that we are using destructuring to extract the metatype field (extracting just this member from an ArgumentMetadata
) into our metatype
parameter. This is just shorthand for getting the full ArgumentMetadata
and then having an additional statement to assign the metatype variable.
Next, note the helper function toValidate()
. It's responsible for bypassing the validation step when the current argument being processed is a native JavaScript type (these can't have validation decorators attached, so there's no reason to run them through the validation step).
Next, we use the class-transformer function plainToClass()
to transform our plain JavaScript argument object into a typed object so that we can apply validation. The reason we must do this is that the incoming post body object, when deserialized from the network request, does not have any type information (this is the way the underlying platform, such as Express, works). Class-validator needs to use the validation decorators we defined for our DTO earlier, so we need to perform this transformation to treat the incoming body as an appropriately decorated object, not just a plain vanilla object.
Finally, as noted earlier, since this is a validation pipe it either returns the value unchanged, or throws an exception.
The last step is to bind the ValidationPipe
. Pipes can be parameter-scoped, method-scoped, controller-scoped, or global-scoped. Earlier, with our Joi-based validation pipe, we saw an example of binding the pipe at the method level.
In the example below, we'll bind the pipe instance to the route handler @Body()
decorator so that our pipe is called to validate the post body.
@@filename(cats.controller)
@Post()
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
this.catsService.create(createCatDto);
}
Parameter-scoped pipes are useful when the validation logic concerns only one specified parameter.
Since the ValidationPipe
was created to be as generic as possible, we can realize it's full utility by setting it up as a global-scoped pipe so that it is applied to every route handler across the entire application.
@@filename(main)
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
warning Notice In the case of hybrid apps the
useGlobalPipes()
method doesn't set up pipes for gateways and micro services. For "standard" (non-hybrid) microservice apps,useGlobalPipes()
does mount pipes globally.
Global pipes are used across the whole application, for every controller and every route handler.
Note that in terms of dependency injection, global pipes registered from outside of any module (with useGlobalPipes()
as in the example above) cannot inject dependencies since the binding has been done outside the context of any module. In order to solve this issue, you can set up a global pipe directly from any module using the following construction:
@@filename(app.module)
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
info Hint When using this approach to perform dependency injection for the pipe, note that regardless of the module where this construction is employed, the pipe is, in fact, global. Where should this be done? Choose the module where the pipe (
ValidationPipe
in the example above) is defined. Also,useClass
is not the only way of dealing with custom provider registration. Learn more here.
Validation isn't the only use case for custom pipes. At the beginning of this chapter, we mentioned that a pipe can also transform the input data to the desired format. This is possible because the value returned from the transform
function completely overrides the previous value of the argument.
When is this useful? Consider that sometimes the data passed from the client needs to undergo some change - for example converting a string to an integer - before it can be properly handled by the route handler method. Furthermore, some required data fields may be missing, and we would like to apply default values. Transformation pipes can perform these functions by interposing a processing function between the client request and the request handler.
Here's a simple ParseIntPipe
which is responsible for parsing a string into an integer value. (As noted above, Nest has a built-in ParseIntPipe
that is more sophisticated; we include this as a simple example of a custom transformation pipe).
@@filename(parse-int.pipe)
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}
@@switch
import { Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe {
transform(value, metadata) {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}
We can then bind this pipe to the selected param as shown below:
@@filename()
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
return this.catsService.findOne(id);
}
@@switch
@Get(':id')
@Bind(Param('id', new ParseIntPipe()))
async findOne(id) {
return this.catsService.findOne(id);
}
Another useful transformation case would be to select an existing user entity from the database using an id supplied in the request:
@@filename()
@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
return userEntity;
}
@@switch
@Get(':id')
@Bind(Param('id', UserByIdPipe))
findOne(userEntity) {
return userEntity;
}
We leave the implementation of this pipe to the reader, but note that like all other transformation pipes, it receives an input value (an id
) and returns an output value (a UserEntity
object). This can make your code more declarative and DRY by abstracting boilerplate code out of your handler and into a common pipe.
Parse*
pipes expect a parameter's value to be defined. They throw an exception upon receiving null
or undefined
values. To allow an endpoint to handle missing querystring parameter values, we have to provide a default value to be injected before the Parse*
pipes operate on these values. The DefaultValuePipe
serves that purpose. Simply instantiate a DefaultValuePipe
in the @Query()
decorator before the relevant Parse*
pipe, as shown below:
@@filename()
@Get()
async findAll(
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
@Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
return this.catsService.findAll({ activeOnly, page });
}
As a reminder, you don't have to build a generic validation pipe on your own since the ValidationPipe
is provided by Nest out-of-the-box. The built-in ValidationPipe
offers more options than the sample we built in this chapter, which has been kept basic for the sake of illustrating the mechanics of a custom-built pipe. You can find full details, along with lots of examples here.
A guard is a class annotated with the @Injectable()
decorator. Guards should implement the CanActivate
interface.
Guards have a single responsibility. They determine whether a given request will be handled by the route handler or not, depending on certain conditions (like permissions, roles, ACLs, etc.) present at run-time. This is often referred to as authorization. Authorization (and its cousin, authentication, with which it usually collaborates) has typically been handled by middleware in traditional Express applications. Middleware is a fine choice for authentication, since things like token validation and attaching properties to the request
object are not strongly connected with a particular route context (and its metadata).
But middleware, by its nature, is dumb. It doesn't know which handler will be executed after calling the next()
function. On the other hand, Guards have access to the ExecutionContext
instance, and thus know exactly what's going to be executed next. They're designed, much like exception filters, pipes, and interceptors, to let you interpose processing logic at exactly the right point in the request/response cycle, and to do so declaratively. This helps keep your code DRY and declarative.
info Hint Guards are executed after each middleware, but before any interceptor or pipe.
As mentioned, authorization is a great use case for Guards because specific routes should be available only when the caller (usually a specific authenticated user) has sufficient permissions. The AuthGuard
that we'll build now assumes an authenticated user (and that, therefore, a token is attached to the request headers). It will extract and validate the token, and use the extracted information to determine whether the request can proceed or not.
@@filename(auth.guard)
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}
@@switch
import { Injectable } from '@nestjs/common';
@Injectable()
export class AuthGuard {
async canActivate(context) {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}
The logic inside the validateRequest()
function can be as simple or sophisticated as needed. The main point of this example is to show how guards fit into the request/response cycle.
Every guard must implement a canActivate()
function. This function should return a boolean, indicating whether the current request is allowed or not. It can return the response either synchronously or asynchronously (via a Promise
or Observable
). Nest uses the return value to control the next action:
- if it returns
true
, the request will be processed. - if it returns
false
, Nest will deny the request.
The canActivate()
function takes a single argument, the ExecutionContext
instance. The ExecutionContext
inherits from ArgumentsHost
. We saw ArgumentsHost
previously in the exception filters chapter. In the sample above, we are just using the same helper methods defined on ArgumentsHost
that we used earlier, to get a reference to the Request
object. You can refer back to the Arguments host section of the exception filters chapter for more on this topic.
By extending ArgumentsHost
, ExecutionContext
also adds several new helper methods that provide additional details about the current execution process. These details can be helpful in building more generic guards that can work across a broad set of controllers, methods, and execution contexts. Learn more about ExecutionContext
here.
Let's build a more functional guard that permits access only to users with a specific role. We'll start with a basic guard template, and build on it in the coming sections. For now, it allows all requests to proceed:
@@filename(roles.guard)
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}
@@switch
import { Injectable } from '@nestjs/common';
@Injectable()
export class RolesGuard {
canActivate(context) {
return true;
}
}
Like pipes and exception filters, guards can be controller-scoped, method-scoped, or global-scoped. Below, we set up a controller-scoped guard using the @UseGuards()
decorator. This decorator may take a single argument, or a comma-separated list of arguments. This lets you easily apply the appropriate set of guards with one declaration.
@@filename()
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
info Hint The
@UseGuards()
decorator is imported from the@nestjs/common
package.
Above, we passed the RolesGuard
type (instead of an instance), leaving responsibility for instantiation to the framework and enabling dependency injection. As with pipes and exception filters, we can also pass an in-place instance:
@@filename()
@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}
The construction above attaches the guard to every handler declared by this controller. If we wish the guard to apply only to a single method, we apply the @UseGuards()
decorator at the method level.
In order to set up a global guard, use the useGlobalGuards()
method of the Nest application instance:
@@filename()
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
warning Notice In the case of hybrid apps the
useGlobalGuards()
method doesn't set up guards for gateways and micro services by default (see Hybrid application for information on how to change this behavior). For "standard" (non-hybrid) microservice apps,useGlobalGuards()
does mount the guards globally.
Global guards are used across the whole application, for every controller and every route handler. In terms of dependency injection, global guards registered from outside of any module (with useGlobalGuards()
as in the example above) cannot inject dependencies since this is done outside the context of any module. In order to solve this issue, you can set up a guard directly from any module using the following construction:
@@filename(app.module)
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule {}
info Hint When using this approach to perform dependency injection for the guard, note that regardless of the module where this construction is employed, the guard is, in fact, global. Where should this be done? Choose the module where the guard (
RolesGuard
in the example above) is defined. Also,useClass
is not the only way of dealing with custom provider registration. Learn more here.
Our RolesGuard
is working, but it's not very smart yet. We're not yet taking advantage of the most important guard feature - the execution context. It doesn't yet know about roles, or which roles are allowed for each handler. The CatsController
, for example, could have different permission schemes for different routes. Some might be available only for an admin user, and others could be open for everyone. How can we match roles to routes in a flexible and reusable way?
This is where custom metadata comes into play (learn more here). Nest provides the ability to attach custom metadata to route handlers through the @SetMetadata()
decorator. This metadata supplies our missing role
data, which a smart guard needs to make decisions. Let's take a look at using @SetMetadata()
:
@@filename(cats.controller)
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@@switch
@Post()
@SetMetadata('roles', ['admin'])
@Bind(Body())
async create(createCatDto) {
this.catsService.create(createCatDto);
}
info Hint The
@SetMetadata()
decorator is imported from the@nestjs/common
package.
With the construction above, we attached the roles
metadata (roles
is a key, while ['admin']
is a particular value) to the create()
method. While this works, it's not good practice to use @SetMetadata()
directly in your routes. Instead, create your own decorators, as shown below:
@@filename(roles.decorator)
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
@@switch
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles) => SetMetadata('roles', roles);
This approach is much cleaner and more readable, and is strongly typed. Now that we have a custom @Roles()
decorator, we can use it to decorate the create()
method.
@@filename(cats.controller)
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@@switch
@Post()
@Roles('admin')
@Bind(Body())
async create(createCatDto) {
this.catsService.create(createCatDto);
}
Let's now go back and tie this together with our RolesGuard
. Currently, it simply returns true
in all cases, allowing every request to proceed. We want to make the return value conditional based on the comparing the roles assigned to the current user to the actual roles required by the current route being processed. In order to access the route's role(s) (custom metadata), we'll use the Reflector
helper class, which is provided out of the box by the framework and exposed from the @nestjs/core
package.
@@filename(roles.guard)
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return matchRoles(roles, user.roles);
}
}
@@switch
import { Injectable, Dependencies } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
@Dependencies(Reflector)
export class RolesGuard {
constructor(reflector) {
this.reflector = reflector;
}
canActivate(context) {
const roles = this.reflector.get('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return matchRoles(roles, user.roles);
}
}
info Hint In the node.js world, it's common practice to attach the authorized user to the
request
object. Thus, in our sample code above, we are assuming thatrequest.user
contains the user instance and allowed roles. In your app, you will probably make that association in your custom authentication guard (or middleware).
warning Warning The logic inside the
matchRoles()
function can be as simple or sophisticated as needed. The main point of this example is to show how guards fit into the request/response cycle.
Refer to the Reflection and metadata section of the Execution context chapter for more details on utilizing Reflector
in a context-sensitive way.
When a user with insufficient privileges requests an endpoint, Nest automatically returns the following response:
{
"statusCode": 403,
"message": "Forbidden resource",
"error": "Forbidden"
}
Note that behind the scenes, when a guard returns false
, the framework throws a ForbiddenException
. If you want to return a different error response, you should throw your own specific exception. For example:
throw new UnauthorizedException();
Any exception thrown by a guard will be handled by the exceptions layer (global exceptions filter and any exceptions filters that are applied to the current context).
An interceptor is a class annotated with the @Injectable()
decorator. Interceptors should implement the NestInterceptor
interface.
Interceptors have a set of useful capabilities which are inspired by the Aspect Oriented Programming (AOP) technique. They make it possible to:
- bind extra logic before / after method execution
- transform the result returned from a function
- transform the exception thrown from a function
- extend the basic function behavior
- completely override a function depending on specific conditions (e.g., for caching purposes)
Each interceptor implements the intercept()
method, which takes two arguments. The first one is the ExecutionContext
instance (exactly the same object as for guards). The ExecutionContext
inherits from ArgumentsHost
. We saw ArgumentsHost
before in the exception filters chapter. There, we saw that it's a wrapper around arguments that have been passed to the original handler, and contains different arguments arrays based on the type of the application. You can refer back to the exception filters for more on this topic.
By extending ArgumentsHost
, ExecutionContext
also adds several new helper methods that provide additional details about the current execution process. These details can be helpful in building more generic interceptors that can work across a broad set of controllers, methods, and execution contexts. Learn more about ExecutionContext
here.
The second argument is a CallHandler
. The CallHandler
interface implements the handle()
method, which you can use to invoke the route handler method at some point in your interceptor. If you don't call the handle()
method in your implementation of the intercept()
method, the route handler method won't be executed at all.
This approach means that the intercept()
method effectively wraps the request/response stream. As a result, you may implement custom logic both before and after the execution of the final route handler. It's clear that you can write code in your intercept()
method that executes before calling handle()
, but how do you affect what happens afterward? Because the handle()
method returns an Observable
, we can use powerful RxJS operators to further manipulate the response. Using Aspect Oriented Programming terminology, the invocation of the route handler (i.e., calling handle()
) is called a Pointcut, indicating that it's the point at which our additional logic is inserted.
Consider, for example, an incoming POST /cats
request. This request is destined for the create()
handler defined inside the CatsController
. If an interceptor which does not call the handle()
method is called anywhere along the way, the create()
method won't be executed. Once handle()
is called (and its Observable
has been returned), the create()
handler will be triggered. And once the response stream is received via the Observable
, additional operations can be performed on the stream, and a final result returned to the caller.
The first use case we'll look at is to use an interceptor to log user interaction (e.g., storing user calls, asynchronously dispatching events or calculating a timestamp). We show a simple LoggingInterceptor
below:
@@filename(logging.interceptor)
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
@@switch
import { Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor {
intercept(context, next) {
console.log('Before...');
const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
info Hint The
NestInterceptor<T, R>
is a generic interface in whichT
indicates the type of anObservable<T>
(supporting the response stream), andR
is the type of the value wrapped byObservable<R>
.
warning Notice Interceptors, like controllers, providers, guards, and so on, can inject dependencies through their
constructor
.
Since handle()
returns an RxJS Observable
, we have a wide choice of operators we can use to manipulate the stream. In the example above, we used the tap()
operator, which invokes our anonymous logging function upon graceful or exceptional termination of the observable stream, but doesn't otherwise interfere with the response cycle.
In order to set up the interceptor, we use the @UseInterceptors()
decorator imported from the @nestjs/common
package. Like pipes and guards, interceptors can be controller-scoped, method-scoped, or global-scoped.
@@filename(cats.controller)
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
info Hint The
@UseInterceptors()
decorator is imported from the@nestjs/common
package.
Using the above construction, each route handler defined in CatsController
will use LoggingInterceptor
. When someone calls the GET /cats
endpoint, you'll see the following output in your standard output:
Before...
After... 1ms
Note that we passed the LoggingInterceptor
type (instead of an instance), leaving responsibility for instantiation to the framework and enabling dependency injection. As with pipes, guards, and exception filters, we can also pass an in-place instance:
@@filename(cats.controller)
@UseInterceptors(new LoggingInterceptor())
export class CatsController {}
As mentioned, the construction above attaches the interceptor to every handler declared by this controller. If we want to restrict the interceptor's scope to a single method, we simply apply the decorator at the method level.
In order to set up a global interceptor, we use the useGlobalInterceptors()
method of the Nest application instance:
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
Global interceptors are used across the whole application, for every controller and every route handler. In terms of dependency injection, global interceptors registered from outside of any module (with useGlobalInterceptors()
, as in the example above) cannot inject dependencies since this is done outside the context of any module. In order to solve this issue, you can set up an interceptor directly from any module using the following construction:
@@filename(app.module)
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
info Hint When using this approach to perform dependency injection for the interceptor, note that regardless of the module where this construction is employed, the interceptor is, in fact, global. Where should this be done? Choose the module where the interceptor (
LoggingInterceptor
in the example above) is defined. Also,useClass
is not the only way of dealing with custom provider registration. Learn more here.
We already know that handle()
returns an Observable
. The stream contains the value returned from the route handler, and thus we can easily mutate it using RxJS's map()
operator.
warning Warning The response mapping feature doesn't work with the library-specific response strategy (using the
@Res()
object directly is forbidden).
Let's create the TransformInterceptor
, which will modify each response in a trivial way to demonstrate the process. It will use RxJS's map()
operator to assign the response object to the data
property of a newly created object, returning the new object to the client.
@@filename(transform.interceptor)
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(map(data => ({ data })));
}
}
@@switch
import { Injectable } from '@nestjs/common';
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor {
intercept(context, next) {
return next.handle().pipe(map(data => ({ data })));
}
}
info Hint Nest interceptors work with both synchronous and asynchronous
intercept()
methods. You can simply switch the method toasync
if necessary.
With the above construction, when someone calls the GET /cats
endpoint, the response would look like the following (assuming that route handler returns an empty array []
):
{
"data": []
}
Interceptors have great value in creating re-usable solutions to requirements that occur across an entire application.
For example, imagine we need to transform each occurrence of a null
value to an empty string ''
. We can do it using one line of code and bind the interceptor globally so that it will automatically be used by each registered handler.
@@filename()
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(map(value => value === null ? '' : value ));
}
}
@@switch
import { Injectable } from '@nestjs/common';
import { map } from 'rxjs/operators';
@Injectable()
export class ExcludeNullInterceptor {
intercept(context, next) {
return next
.handle()
.pipe(map(value => value === null ? '' : value ));
}
}
Another interesting use-case is to take advantage of RxJS's catchError()
operator to override thrown exceptions:
@@filename(errors.interceptor)
import {
Injectable,
NestInterceptor,
ExecutionContext,
BadGatewayException,
CallHandler,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => throwError(new BadGatewayException())),
);
}
}
@@switch
import { Injectable, BadGatewayException } from '@nestjs/common';
import { throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorsInterceptor {
intercept(context, next) {
return next
.handle()
.pipe(
catchError(err => throwError(new BadGatewayException())),
);
}
}
There are several reasons why we may sometimes want to completely prevent calling the handler and return a different value instead. An obvious example is to implement a cache to improve response time. Let's take a look at a simple cache interceptor that returns its response from a cache. In a realistic example, we'd want to consider other factors like TTL, cache invalidation, cache size, etc., but that's beyond the scope of this discussion. Here we'll provide a basic example that demonstrates the main concept.
@@filename(cache.interceptor)
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const isCached = true;
if (isCached) {
return of([]);
}
return next.handle();
}
}
@@switch
import { Injectable } from '@nestjs/common';
import { of } from 'rxjs';
@Injectable()
export class CacheInterceptor {
intercept(context, next) {
const isCached = true;
if (isCached) {
return of([]);
}
return next.handle();
}
}
Our CacheInterceptor
has a hardcoded isCached
variable and a hardcoded response []
as well. The key point to note is that we return a new stream here, created by the RxJS of()
operator, therefore the route handler won't be called at all. When someone calls an endpoint that makes use of CacheInterceptor
, the response (a hardcoded, empty array) will be returned immediately. In order to create a generic solution, you can take advantage of Reflector
and create a custom decorator. The Reflector
is well described in the guards chapter.
The possibility of manipulating the stream using RxJS operators gives us many capabilities. Let's consider another common use case. Imagine you would like to handle timeouts on route requests. When your endpoint doesn't return anything after a period of time, you want to terminate with an error response. The following construction enables this:
@@filename(timeout.interceptor)
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000),
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(new RequestTimeoutException());
}
return throwError(err);
}),
);
};
};
@@switch
import { Injectable, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor {
intercept(context, next) {
return next.handle().pipe(
timeout(5000),
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(new RequestTimeoutException());
}
return throwError(err);
}),
);
};
};
After 5 seconds, request processing will be canceled. You can also add custom logic before throwing RequestTimeoutException
(e.g. release resources).
Nest is built around a language feature called decorators. Decorators are a well-known concept in a lot of commonly used programming languages, but in the JavaScript world, they're still relatively new. In order to better understand how decorators work, we recommend reading this article. Here's a simple definition:
An ES2016 decorator is an expression which returns a function and can take a target, name and property descriptor as arguments.
You apply it by prefixing the decorator with an @
character and placing this at the very top of what
you are trying to decorate. Decorators can be defined for either a class, a method or a property.
Nest provides a set of useful param decorators that you can use together with the HTTP route handlers. Below is a list of the provided decorators and the plain Express (or Fastify) objects they represent
@Request(), @Req() |
req |
@Response(), @Res() |
res |
@Next() |
next |
@Session() |
req.session |
@Param(param?: string) |
req.params / req.params[param] |
@Body(param?: string) |
req.body / req.body[param] |
@Query(param?: string) |
req.query / req.query[param] |
@Headers(param?: string) |
req.headers / req.headers[param] |
@Ip() |
req.ip |
@HostParam() |
req.hosts |
Additionally, you can create your own custom decorators. Why is this useful?
In the node.js world, it's common practice to attach properties to the request object. Then you manually extract them in each route handler, using code like the following:
const user = req.user;
In order to make your code more readable and transparent, you can create a @User()
decorator and reuse it across all of your controllers.
@@filename(user.decorator)
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
Then, you can simply use it wherever it fits your requirements.
@@filename()
@Get()
async findOne(@User() user: UserEntity) {
console.log(user);
}
@@switch
@Get()
@Bind(User())
async findOne(user) {
console.log(user);
}
When the behavior of your decorator depends on some conditions, you can use the data
parameter to pass an argument to the decorator's factory function. One use case for this is a custom decorator that extracts properties from the request object by key. Let's assume, for example, that our authentication layer validates requests and attaches a user entity to the request object. The user entity for an authenticated request might look like:
{
"id": 101,
"firstName": "Alan",
"lastName": "Turing",
"email": "alan@email.com",
"roles": ["admin"]
}
Let's define a decorator that takes a property name as key, and returns the associated value if it exists (or undefined if it doesn't exist, or if the user
object has not been created).
@@filename(user.decorator)
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user && user[data] : user;
},
);
@@switch
import { createParamDecorator } from '@nestjs/common';
export const User = createParamDecorator((data, ctx) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user && user[data] : user;
});
Here's how you could then access a particular property via the @User()
decorator in the controller:
@@filename()
@Get()
async findOne(@User('firstName') firstName: string) {
console.log(`Hello ${firstName}`);
}
@@switch
@Get()
@Bind(User('firstName'))
async findOne(firstName) {
console.log(`Hello ${firstName}`);
}
You can use this same decorator with different keys to access different properties. If the user
object is deep or complex, this can make for easier and more readable request handler implementations.
info Hint For TypeScript users, note that
createParamDecorator<T>()
is a generic. This means you can explicitly enforce type safety, for examplecreateParamDecorator<string>((data, ctx) => ...)
. Alternatively, specify a parameter type in the factory function, for examplecreateParamDecorator((data: string, ctx) => ...)
. If you omit both, the type fordata
will beany
.
Nest treats custom param decorators in the same fashion as the built-in ones (@Body()
, @Param()
and @Query()
). This means that pipes are executed for the custom annotated parameters as well (in our examples, the user
argument). Moreover, you can apply the pipe directly to the custom decorator:
@@filename()
@Get()
async findOne(@User(new ValidationPipe()) user: UserEntity) {
console.log(user);
}
@@switch
@Get()
@Bind(User(new ValidationPipe()))
async findOne(user) {
console.log(user);
}
Nest provides a helper method to compose multiple decorators. For example, suppose you want to combine all decorators related to authentication into a single decorator. This could be done with the following construction:
@@filename(auth.decorator)
import { applyDecorators } from '@nestjs/common';
export function Auth(...roles: Role[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(AuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: 'Unauthorized' }),
);
}
@@switch
import { applyDecorators } from '@nestjs/common';
export function Auth(...roles) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(AuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: 'Unauthorized' }),
);
}
You can then use this custom @Auth()
decorator as follows:
@Get('users')
@Auth('admin')
findAllUsers() {}
This has the effect of applying all four decorators with a single declaration.
warning Warning The
@ApiHideProperty()
decorator from the@nestjs/swagger
package is not composable and won't work properly with theapplyDecorators
function.
- Custom providers
- Asynchronous providers
- Dynamic modules
- Injection scopes
- Circular dependency
- Module reference
- Execution context
- Lifecycle events
- Platform agnosticism
- Testing
In earlier chapters, we touched on various aspects of Dependency Injection (DI) and how it is used in Nest. One example of this is the constructor based dependency injection used to inject instances (often service providers) into classes. You won't be surprised to learn that Dependency Injection is built in to the Nest core in a fundamental way. So far, we've only explored one main pattern. As your application grows more complex, you may need to take advantage of the full features of the DI system, so let's explore them in more detail.
Dependency injection is an inversion of control (IoC) technique wherein you delegate instantiation of dependencies to the IoC container (in our case, the NestJS runtime system), instead of doing it in your own code imperatively. Let's examine what's happening in this example from the Providers chapter.
First, we define a provider. The @Injectable()
decorator marks the CatsService
class as a provider.
@@filename(cats.service)
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
findAll(): Cat[] {
return this.cats;
}
}
@@switch
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatsService {
constructor() {
this.cats = [];
}
findAll() {
return this.cats;
}
}
Then we request that Nest inject the provider into our controller class:
@@filename(cats.controller)
import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
@@switch
import { Controller, Get, Bind, Dependencies } from '@nestjs/common';
import { CatsService } from './cats.service';
@Controller('cats')
@Dependencies(CatsService)
export class CatsController {
constructor(catsService) {
this.catsService = catsService;
}
@Get()
async findAll() {
return this.catsService.findAll();
}
}
Finally, we register the provider with the Nest IoC container:
@@filename(app.module)
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}
What exactly is happening under the covers to make this work? There are three key steps in the process:
-
In
cats.service.ts
, the@Injectable()
decorator declares theCatsService
class as a class that can be managed by the Nest IoC container. -
In
cats.controller.ts
,CatsController
declares a dependency on theCatsService
token with constructor injection:
constructor(private catsService: CatsService)
- In
app.module.ts
, we associate the tokenCatsService
with the classCatsService
from thecats.service.ts
file. We'll see below exactly how this association (also called registration) occurs.
When the Nest IoC container instantiates a CatsController
, it first looks for any dependencies*. When it finds the CatsService
dependency, it performs a lookup on the CatsService
token, which returns the CatsService
class, per the registration step (#3 above). Assuming SINGLETON
scope (the default behavior), Nest will then either create an instance of CatsService
, cache it, and return it, or if one is already cached, return the existing instance.
*This explanation is a bit simplified to illustrate the point. One important area we glossed over is that the process of analyzing the code for dependencies is very sophisticated, and happens during application bootstrapping. One key feature is that dependency analysis (or "creating the dependency graph"), is transitive. In the above example, if the CatsService
itself had dependencies, those too would be resolved. The dependency graph ensures that dependencies are resolved in the correct order - essentially "bottom up". This mechanism relieves the developer from having to manage such complex dependency graphs.
Let's take a closer look at the @Module()
decorator. In app.module
, we declare:
@Module({
controllers: [CatsController],
providers: [CatsService],
})
The providers
property takes an array of providers
. So far, we've supplied those providers via a list of class names. In fact, the syntax providers: [CatsService]
is short-hand for the more complete syntax:
providers: [
{
provide: CatsService,
useClass: CatsService,
},
];
Now that we see this explicit construction, we can understand the registration process. Here, we are clearly associating the token CatsService
with the class CatsService
. The short-hand notation is merely a convenience to simplify the most common use-case, where the token is used to request an instance of a class by the same name.
What happens when your requirements go beyond those offered by Standard providers? Here are a few examples:
- You want to create a custom instance instead of having Nest instantiate (or return a cached instance of) a class
- You want to re-use an existing class in a second dependency
- You want to override a class with a mock version for testing
Nest allows you to define Custom providers to handle these cases. It provides several ways to define custom providers. Let's walk through them.
The useValue
syntax is useful for injecting a constant value, putting an external library into the Nest container, or replacing a real implementation with a mock object. Let's say you'd like to force Nest to use a mock CatsService
for testing purposes.
import { CatsService } from './cats.service';
const mockCatsService = {
/* mock implementation
...
*/
};
@Module({
imports: [CatsModule],
providers: [
{
provide: CatsService,
useValue: mockCatsService,
},
],
})
export class AppModule {}
In this example, the CatsService
token will resolve to the mockCatsService
mock object. useValue
requires a value - in this case a literal object that has the same interface as the CatsService
class it is replacing. Because of TypeScript's structural typing, you can use any object that has a compatible interface, including a literal object or a class instance instantiated with new
.
So far, we've used class names as our provider tokens (the value of the provide
property in a provider listed in the providers
array). This is matched by the standard pattern used with constructor based injection, where the token is also a class name. (Refer back to DI Fundamentals for a refresher on tokens if this concept isn't entirely clear). Sometimes, we may want the flexibility to use strings or symbols as the DI token. For example:
import { connection } from './connection';
@Module({
providers: [
{
provide: 'CONNECTION',
useValue: connection,
},
],
})
export class AppModule {}
In this example, we are associating a string-valued token ('CONNECTION'
) with a pre-existing connection
object we've imported from an external file.
warning Notice In addition to using strings as token values, you can also use JavaScript symbols.
We've previously seen how to inject a provider using the standard constructor based injection pattern. This pattern requires that the dependency be declared with a class name. The 'CONNECTION'
custom provider uses a string-valued token. Let's see how to inject such a provider. To do so, we use the @Inject()
decorator. This decorator takes a single argument - the token.
@@filename()
@Injectable()
export class CatsRepository {
constructor(@Inject('CONNECTION') connection: Connection) {}
}
@@switch
@Injectable()
@Dependencies('CONNECTION')
export class CatsRepository {
constructor(connection) {}
}
info Hint The
@Inject()
decorator is imported from@nestjs/common
package.
While we directly use the string 'CONNECTION'
in the above examples for illustration purposes, for clean code organization, it's best practice to define tokens in a separate file, such as constants.ts
. Treat them much as you would symbols or enums that are defined in their own file and imported where needed.
The useClass
syntax allows you to dynamically determine a class that a token should resolve to. For example, suppose we have an abstract (or default) ConfigService
class. Depending on the current environment, we want Nest to provide a different implementation of the configuration service. The following code implements such a strategy.
const configServiceProvider = {
provide: ConfigService,
useClass:
process.env.NODE_ENV === 'development'
? DevelopmentConfigService
: ProductionConfigService,
};
@Module({
providers: [configServiceProvider],
})
export class AppModule {}
Let's look at a couple of details in this code sample. You'll notice that we define configServiceProvider
with a literal object first, then pass it in the module decorator's providers
property. This is just a bit of code organization, but is functionally equivalent to the examples we've used thus far in this chapter.
Also, we have used the ConfigService
class name as our token. For any class that depends on ConfigService
, Nest will inject an instance of the provided class (DevelopmentConfigService
or ProductionConfigService
) overriding any default implementation that may have been declared elsewhere (e.g., a ConfigService
declared with an @Injectable()
decorator).
The useFactory
syntax allows for creating providers dynamically. The actual provider will be supplied by the value returned from a factory function. The factory function can be as simple or complex as needed. A simple factory may not depend on any other providers. A more complex factory can itself inject other providers it needs to compute its result. For the latter case, the factory provider syntax has a pair of related mechanisms:
- The factory function can accept (optional) arguments.
- The (optional)
inject
property accepts an array of providers that Nest will resolve and pass as arguments to the factory function during the instantiation process. The two lists should be correlated: Nest will pass instances from theinject
list as arguments to the factory function in the same order.
The example below demonstrates this.
@@filename()
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
})
export class AppModule {}
@@switch
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
})
export class AppModule {}
The useExisting
syntax allows you to create aliases for existing providers. This creates two ways to access the same provider. In the example below, the (string-based) token 'AliasedLoggerService'
is an alias for the (class-based) token LoggerService
. Assume we have two different dependencies, one for 'AliasedLoggerService'
and one for LoggerService
. If both dependencies are specified with SINGLETON
scope, they'll both resolve to the same instance.
@Injectable()
class LoggerService {
/* implementation details */
}
const loggerAliasProvider = {
provide: 'AliasedLoggerService',
useExisting: LoggerService,
};
@Module({
providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}
While providers often supply services, they are not limited to that usage. A provider can supply any value. For example, a provider may supply an array of configuration objects based on the current environment, as shown below:
const configFactory = {
provide: 'CONFIG',
useFactory: () => {
return process.env.NODE_ENV === 'development' ? devConfig : prodConfig;
},
};
@Module({
providers: [configFactory],
})
export class AppModule {}
Like any provider, a custom provider is scoped to its declaring module. To make it visible to other modules, it must be exported. To export a custom provider, we can either use its token or the full provider object.
The following example shows exporting using the token:
@@filename()
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
exports: ['CONNECTION'],
})
export class AppModule {}
@@switch
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
exports: ['CONNECTION'],
})
export class AppModule {}
Alternatively, export with the full provider object:
@@filename()
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
exports: [connectionFactory],
})
export class AppModule {}
@@switch
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
exports: [connectionFactory],
})
export class AppModule {}
At times, the application start should be delayed until one or more asynchronous tasks are completed. For example, you may not want to start accepting requests until the connection with the database has been established. You can achieve this using asynchronous providers.
The syntax for this is to use async/await
with the useFactory
syntax. The factory returns a Promise
, and the factory function can await
asynchronous tasks. Nest will await resolution of the promise before instantiating any class that depends on (injects) such a provider.
{
provide: 'ASYNC_CONNECTION',
useFactory: async () => {
const connection = await createConnection(options);
return connection;
},
}
info Hint Learn more about custom provider syntax here.
Asynchronous providers are injected to other components by their tokens, like any other provider. In the example above, you would use the construct @Inject('ASYNC_CONNECTION')
.
The TypeORM recipe has a more substantial example of an asynchronous provider.
The Modules chapter covers the basics of Nest modules, and includes a brief introduction to dynamic modules. This chapter expands on the subject of dynamic modules. Upon completion, you should have a good grasp of what they are and how and when to use them.
Most application code examples in the Overview section of the documentation make use of regular, or static, modules. Modules define groups of components like providers and controllers that fit together as a modular part of an overall application. They provide an execution context, or scope, for these components. For example, providers defined in a module are visible to other members of the module without the need to export them. When a provider needs to be visible outside of a module, it is first exported from its host module, and then imported into its consuming module.
Let's walk through a familiar example.
First, we'll define a UsersModule
to provide and export a UsersService
. UsersModule
is the host module for UsersService
.
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
Next, we'll define an AuthModule
, which imports UsersModule
, making UsersModule
's exported providers available inside AuthModule
:
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
@Module({
imports: [UsersModule],
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}
These constructs allow us to inject UsersService
in, for example, the AuthService
that is hosted in AuthModule
:
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
/*
Implementation that makes use of this.usersService
*/
}
We'll refer to this as static module binding. All the information Nest needs to wire together the modules has already been declared in the host and consuming modules. Let's unpack what's happening during this process. Nest makes UsersService
available inside AuthModule
by:
- Instantiating
UsersModule
, including transitively importing other modules thatUsersModule
itself consumes, and transitively resolving any dependencies (see Custom providers). - Instantiating
AuthModule
, and makingUsersModule
's exported providers available to components inAuthModule
(just as if they had been declared inAuthModule
). - Injecting an instance of
UsersService
inAuthService
.
With static module binding, there's no opportunity for the consuming module to influence how providers from the host module are configured. Why does this matter? Consider the case where we have a general purpose module that needs to behave differently in different use cases. This is analogous to the concept of a "plugin" in many systems, where a generic facility requires some configuration before it can be used by a consumer.
A good example with Nest is a configuration module. Many applications find it useful to externalize configuration details by using a configuration module. This makes it easy to dynamically change the application settings in different deployments: e.g., a development database for developers, a staging database for the staging/testing environment, etc. By delegating the management of configuration parameters to a configuration module, the application source code remains independent of configuration parameters.
The challenge is that the configuration module itself, since it's generic (similar to a "plugin"), needs to be customized by its consuming module. This is where dynamic modules come into play. Using dynamic module features, we can make our configuration module dynamic so that the consuming module can use an API to control how the configuration module is customized at the time it is imported.
In other words, dynamic modules provide an API for importing one module into another, and customizing the properties and behavior of that module when it is imported, as opposed to using the static bindings we've seen so far.
We'll be using the basic version of the example code from the configuration chapter for this section. The completed version as of the end of this chapter is available as a working example here.
Our requirement is to make ConfigModule
accept an options
object to customize it. Here's the feature we want to support. The basic sample hard-codes the location of the .env
file to be in the project root folder. Let's suppose we want to make that configurable, such that you can manage your .env
files in any folder of your choosing. For example, imagine you want to store your various .env
files in a folder under the project root called config
(i.e., a sibling folder to src
). You'd like to be able to choose different folders when using the ConfigModule
in different projects.
Dynamic modules give us the ability to pass parameters into the module being imported so we can change its behavior. Let's see how this works. It's helpful if we start from the end-goal of how this might look from the consuming module's perspective, and then work backwards. First, let's quickly review the example of statically importing the ConfigModule
(i.e., an approach which has no ability to influence the behavior of the imported module). Pay close attention to the imports
array in the @Module()
decorator:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';
@Module({
imports: [ConfigModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Let's consider what a dynamic module import, where we're passing in a configuration object, might look like. Compare the difference in the imports
array between these two examples:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';
@Module({
imports: [ConfigModule.register({ folder: './config' })],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Let's see what's happening in the dynamic example above. What are the moving parts?
ConfigModule
is a normal class, so we can infer that it must have a static method calledregister()
. We know it's static because we're calling it on theConfigModule
class, not on an instance of the class. Note: this method, which we will create soon, can have any arbitrary name, but by convention we should call it eitherforRoot()
orregister()
.- The
register()
method is defined by us, so we can accept any input arguments we like. In this case, we're going to accept a simpleoptions
object with suitable properties, which is the typical case. - We can infer that the
register()
method must return something like amodule
since its return value appears in the familiarimports
list, which we've seen so far includes a list of modules.
In fact, what our register()
method will return is a DynamicModule
. A dynamic module is nothing more than a module created at run-time, with the same exact properties as a static module, plus one additional property called module
. Let's quickly review a sample static module declaration, paying close attention to the module options passed in to the decorator:
@Module({
imports: [DogsService],
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService]
})
Dynamic modules must return an object with the exact same interface, plus one additional property called module
. The module
property serves as the name of the module, and should be the same as the class name of the module, as shown in the example below.
info Hint For a dynamic module, all properties of the module options object are optional except
module
.
What about the static register()
method? We can now see that its job is to return an object that has the DynamicModule
interface. When we call it, we are effectively providing a module to the imports
list, similar to the way we would do so in the static case by listing a module class name. In other words, the dynamic module API simply returns a module, but rather than fix the properties in the @Modules
decorator, we specify them programmatically.
There are still a couple of details to cover to help make the picture complete:
- We can now state that the
@Module()
decorator'simports
property can take not only a module class name (e.g.,imports: [UsersModule]
), but also a function returning a dynamic module (e.g.,imports: [ConfigModule.register(...)]
). - A dynamic module can itself import other modules. We won't do so in this example, but if the dynamic module depends on providers from other modules, you would import them using the optional
imports
property. Again, this is exactly analogous to the way you'd declare metadata for a static module using the@Module()
decorator.
Armed with this understanding, we can now look at what our dynamic ConfigModule
declaration must look like. Let's take a crack at it.
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';
@Module({})
export class ConfigModule {
static register(): DynamicModule {
return {
module: ConfigModule,
providers: [ConfigService],
exports: [ConfigService],
};
}
}
It should now be clear how the pieces tie together. Calling ConfigModule.register(...)
returns a DynamicModule
object with properties which are essentially the same as those that, until now, we've provided as metadata via the @Module()
decorator.
info Hint Import
DynamicModule
from@nestjs/common
.
Our dynamic module isn't very interesting yet, however, as we haven't introduced any capability to configure it as we said we would like to do. Let's address that next.
The obvious solution for customizing the behavior of the ConfigModule
is to pass it an options
object in the static register()
method, as we guessed above. Let's look once again at our consuming module's imports
property:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';
@Module({
imports: [ConfigModule.register({ folder: './config' })],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
That nicely handles passing an options
object to our dynamic module. How do we then use that options
object in the ConfigModule
? Let's consider that for a minute. We know that our ConfigModule
is basically a host for providing and exporting an injectable service - the ConfigService
- for use by other providers. It's actually our ConfigService
that needs to read the options
object to customize its behavior. Let's assume for the moment that we know how to somehow get the options
from the register()
method into the ConfigService
. With that assumption, we can make a few changes to the service to customize its behavior based on the properties from the options
object. (Note: for the time being, since we haven't actually determined how to pass it in, we'll just hard-code options
. We'll fix this in a minute).
import { Injectable } from '@nestjs/common';
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { EnvConfig } from './interfaces';
@Injectable()
export class ConfigService {
private readonly envConfig: EnvConfig;
constructor() {
const options = { folder: './config' };
const filePath = `${process.env.NODE_ENV || 'development'}.env`;
const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
this.envConfig = dotenv.parse(fs.readFileSync(envFile));
}
get(key: string): string {
return this.envConfig[key];
}
}
Now our ConfigService
knows how to find the .env
file in the folder we've specified in options
.
Our remaining task is to somehow inject the options
object from the register()
step into our ConfigService
. And of course, we'll use dependency injection to do it. This is a key point, so make sure you understand it. Our ConfigModule
is providing ConfigService
. ConfigService
in turn depends on the options
object that is only supplied at run-time. So, at run-time, we'll need to first bind the options
object to the Nest IoC container, and then have Nest inject it into our ConfigService
. Remember from the Custom providers chapter that providers can include any value not just services, so we're fine using dependency injection to handle a simple options
object.
Let's tackle binding the options object to the IoC container first. We do this in our static register()
method. Remember that we are dynamically constructing a module, and one of the properties of a module is its list of providers. So what we need to do is define our options object as a provider. This will make it injectable into the ConfigService
, which we'll take advantage of in the next step. In the code below, pay attention to the providers
array:
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';
@Module({})
export class ConfigModule {
static register(options): DynamicModule {
return {
module: ConfigModule,
providers: [
{
provide: 'CONFIG_OPTIONS',
useValue: options,
},
ConfigService,
],
exports: [ConfigService],
};
}
}
Now we can complete the process by injecting the 'CONFIG_OPTIONS'
provider into the ConfigService
. Recall that when we define a provider using a non-class token we need to use the @Inject()
decorator as described here.
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { Injectable, Inject } from '@nestjs/common';
import { EnvConfig } from './interfaces';
@Injectable()
export class ConfigService {
private readonly envConfig: EnvConfig;
constructor(@Inject('CONFIG_OPTIONS') private options) {
const filePath = `${process.env.NODE_ENV || 'development'}.env`;
const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
this.envConfig = dotenv.parse(fs.readFileSync(envFile));
}
get(key: string): string {
return this.envConfig[key];
}
}
One final note: for simplicity we used a string-based injection token ('CONFIG_OPTIONS'
) above, but best practice is to define it as a constant (or Symbol
) in a separate file, and import that file. For example:
export const CONFIG_OPTIONS = 'CONFIG_OPTIONS';
A full example of the code in this chapter can be found here.
For people coming from different programming language backgrounds, it might be unexpected to learn that in Nest, almost everything is shared across incoming requests. We have a connection pool to the database, singleton services with global state, etc. Remember that Node.js doesn't follow the request/response Multi-Threaded Stateless Model in which every request is processed by a separate thread. Hence, using singleton instances is fully safe for our applications.
However, there are edge-cases when request-based lifetime may be the desired behavior, for instance per-request caching in GraphQL applications, request tracking, and multi-tenancy. Injection scopes provide a mechanism to obtain the desired provider lifetime behavior.
A provider can have any of the following scopes:
DEFAULT |
A single instance of the provider is shared across the entire application. The instance lifetime is tied directly to the application lifecycle. Once the application has bootstrapped, all singleton providers have been instantiated. Singleton scope is used by default. |
REQUEST |
A new instance of the provider is created exclusively for each incoming request. The instance is garbage-collected after the request has completed processing. |
TRANSIENT |
Transient providers are not shared across consumers. Each consumer that injects a transient provider will receive a new, dedicated instance. |
info Hint Using singleton scope is recommended for most use cases. Sharing providers across consumers and across requests means that an instance can be cached and its initialization occurs only once, during application startup.
Specify injection scope by passing the scope
property to the @Injectable()
decorator options object:
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {}
Similarly, for custom providers, set the scope
property in the long-hand form for a provider registration:
{
provide: 'CACHE_MANAGER',
useClass: CacheManager,
scope: Scope.TRANSIENT,
}
info Hint Import the
Scope
enum from@nestjs/common
warning Notice Gateways should not use request-scoped providers because they must act as singletons. Each gateway encapsulates a real socket and cannot be instantiated multiple times.
Singleton scope is used by default, and need not be declared. If you do want to declare a provider as singleton scoped, use the Scope.DEFAULT
value for the scope
property.
Controllers can also have scope, which applies to all request method handlers declared in that controller. Like provider scope, the scope of a controller declares its lifetime. For a request-scoped controller, a new instance is created for each inbound request, and garbage-collected when the request has completed processing.
Declare controller scope with the scope
property of the ControllerOptions
object:
@Controller({
path: 'cats',
scope: Scope.REQUEST,
})
export class CatsController {}
Scope bubbles up the injection chain. A controller that depends on a request-scoped provider will, itself, be request-scoped.
Imagine the following dependency graph: CatsController <- CatsService <- CatsRepository
. If CatsService
is request-scoped (and the others are default singletons), the CatsController
will become request-scoped as it is dependent on the injected service. The CatsRepository
, which is not dependent, would remain singleton-scoped.
In an HTTP server-based application (e.g., using @nestjs/platform-express
or @nestjs/platform-fastify
), you may want to access a reference to the original request object when using request-scoped providers. You can do this by injecting the REQUEST
object.
import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {
constructor(@Inject(REQUEST) private request: Request) {}
}
Because of underlying platform/protocol differences, you access the inbound request slightly differently for Microservice or GraphQL applications. In GraphQL applications, you inject CONTEXT
instead of REQUEST
:
import { Injectable, Scope, Inject } from '@nestjs/common';
import { CONTEXT } from '@nestjs/graphql';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {
constructor(@Inject(CONTEXT) private context) {}
}
You then configure your context
value (in the GraphQLModule
) to contain request
as its property.
Using request-scoped providers will have an impact on application performance. While Nest tries to cache as much metadata as possible, it will still have to create an instance of your class on each request. Hence, it will slow down your average response time and overall benchmarking result. Unless a provider must be request-scoped, it is strongly recommended that you use the default singleton scope.
A circular dependency occurs when two classes depend on each other. For example, class A needs class B, and class B also needs class A. Circular dependencies can arise in Nest between modules and between providers.
While circular dependencies should be avoided where possible, you can't always do so. In such cases, Nest enables resolving circular dependencies between providers in two ways. In this chapter, we describe using forward referencing as one technique, and using the ModuleRef class to retrieve a provider instance from the DI container as another.
We also describe resolving circular dependencies between modules.
A forward reference allows Nest to reference classes which aren't yet defined using the forwardRef()
utility function. For example, if CatsService
and CommonService
depend on each other, both sides of the relationship can use @Inject()
and the forwardRef()
utility to resolve the circular dependency. Otherwise Nest won't instantiate them because all of the essential metadata won't be available. Here's an example:
@@filename(cats.service)
@Injectable()
export class CatsService {
constructor(
@Inject(forwardRef(() => CommonService))
private commonService: CommonService,
) {}
}
@@switch
@Injectable()
@Dependencies(forwardRef(() => CommonService))
export class CatsService {
constructor(commonService) {
this.commonService = commonService;
}
}
info Hint The
forwardRef()
function is imported from the@nestjs/common
package.
That covers one side of the relationship. Now let's do the same with CommonService
:
@@filename(common.service)
@Injectable()
export class CommonService {
constructor(
@Inject(forwardRef(() => CatsService))
private catsService: CatsService,
) {}
}
@@switch
@Injectable()
@Dependencies(forwardRef(() => CatsService))
export class CommonService {
constructor(catsService) {
this.catsService = catsService;
}
}
warning Warning The order of instantiation is indeterminate. Make sure your code does not depend on which constructor is called first.
An alternative to using forwardRef()
is to refactor your code and use the ModuleRef
class to retrieve a provider on one side of the (otherwise) circular relationship. Learn more about the ModuleRef
utility class here.
In order to resolve circular dependencies between modules, use the same forwardRef()
utility function on both sides of the modules association. For example:
@@filename(common.module)
@Module({
imports: [forwardRef(() => CatsModule)],
})
export class CommonModule {}
Nest provides the ModuleRef
class to navigate the internal list of providers and obtain a reference to any provider using its injection token as a lookup key. The ModuleRef
class also provides a way to dynamically instantiate both static and scoped providers. ModuleRef
can be injected into a class in the normal way:
@@filename(cats.service)
@Injectable()
export class CatsService {
constructor(private moduleRef: ModuleRef) {}
}
@@switch
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
constructor(moduleRef) {
this.moduleRef = moduleRef;
}
}
info Hint The
ModuleRef
class is imported from the@nestjs/core
package.
The ModuleRef
instance (hereafter we'll refer to it as the module reference) has a get()
method. This method retrieves a provider, controller, or injectable (e.g., guard, interceptor, etc.) that exists (has been instantiated) in the current module using its injection token/class name.
@@filename(cats.service)
@Injectable()
export class CatsService implements OnModuleInit {
private service: Service;
constructor(private moduleRef: ModuleRef) {}
onModuleInit() {
this.service = this.moduleRef.get(Service);
}
}
@@switch
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
constructor(moduleRef) {
this.moduleRef = moduleRef;
}
onModuleInit() {
this.service = this.moduleRef.get(Service);
}
}
warning Warning You can't retrieve scoped providers (transient or request-scoped) with the
get()
method. Instead, use the technique described below. Learn how to control scopes here.
To retrieve a provider from the global context (for example, if the provider has been injected in a different module), pass the {{ '{' }} strict: false {{ '}' }}
option as a second argument to get()
.
this.moduleRef.get(Service, { strict: false });
To dynamically resolve a scoped provider (transient or request-scoped), use the resolve()
method, passing the provider's injection token as an argument.
@@filename(cats.service)
@Injectable()
export class CatsService implements OnModuleInit {
private transientService: TransientService;
constructor(private moduleRef: ModuleRef) {}
async onModuleInit() {
this.transientService = await this.moduleRef.resolve(TransientService);
}
}
@@switch
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
constructor(moduleRef) {
this.moduleRef = moduleRef;
}
async onModuleInit() {
this.transientService = await this.moduleRef.resolve(TransientService);
}
}
The resolve()
method returns a unique instance of the provider, from its own DI container sub-tree. Each sub-tree has a unique context identifier. Thus, if you call this method more than once and compare instance references, you will see that they are not equal.
@@filename(cats.service)
@Injectable()
export class CatsService implements OnModuleInit {
constructor(private moduleRef: ModuleRef) {}
async onModuleInit() {
const transientServices = await Promise.all([
this.moduleRef.resolve(TransientService),
this.moduleRef.resolve(TransientService),
]);
console.log(transientServices[0] === transientServices[1]); // false
}
}
@@switch
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
constructor(moduleRef) {
this.moduleRef = moduleRef;
}
async onModuleInit() {
const transientServices = await Promise.all([
this.moduleRef.resolve(TransientService),
this.moduleRef.resolve(TransientService),
]);
console.log(transientServices[0] === transientServices[1]); // false
}
}
To generate a single instance across multiple resolve()
calls, and ensure they share the same generated DI container sub-tree, you can pass a context identifier to the resolve()
method. Use the ContextIdFactory
class to generate a context identifier. This class provides a create()
method that returns an appropriate unique identifier.
@@filename(cats.service)
@Injectable()
export class CatsService implements OnModuleInit {
constructor(private moduleRef: ModuleRef) {}
async onModuleInit() {
const contextId = ContextIdFactory.create();
const transientServices = await Promise.all([
this.moduleRef.resolve(TransientService, contextId),
this.moduleRef.resolve(TransientService, contextId),
]);
console.log(transientServices[0] === transientServices[1]); // true
}
}
@@switch
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
constructor(moduleRef) {
this.moduleRef = moduleRef;
}
async onModuleInit() {
const contextId = ContextIdFactory.create();
const transientServices = await Promise.all([
this.moduleRef.resolve(TransientService, contextId),
this.moduleRef.resolve(TransientService, contextId),
]);
console.log(transientServices[0] === transientServices[1]); // true
}
}
info Hint The
ContextIdFactory
class is imported from the@nestjs/core
package.
Occasionally, you may want to resolve an instance of a request-scoped provider within a request context. Let's say that CatsService
is request-scoped and you want to resolve the CatsRepository
instance which is also marked as a request-scoped provider. In order to share the same DI container sub-tree, you must obtain the current context identifier instead of generating a new one (e.g., with the ContextIdFactory.create()
function, as shown above). To obtain the current context identifier, start by injecting the request object using @Inject()
decorator.
@@filename(cats.service)
@Injectable()
export class CatsService {
constructor(
@Inject(REQUEST) private request: Record<string, unknown>,
) {}
}
@@switch
@Injectable()
@Dependencies(REQUEST)
export class CatsService {
constructor(request) {
this.request = request;
}
}
info Hint Learn more about the request provider here.
Now, use the getByRequest()
method of the ContextIdFactory
class to create a context id based on the request object, and pass this to the resolve()
call:
const contextId = ContextIdFactory.getByRequest(this.request);
const catsRepository = await this.moduleRef.resolve(CatsRepository, contextId);
To dynamically instantiate a class that wasn't previously registered as a provider, use the module reference's create()
method.
@@filename(cats.service)
@Injectable()
export class CatsService implements OnModuleInit {
private catsFactory: CatsFactory;
constructor(private moduleRef: ModuleRef) {}
async onModuleInit() {
this.catsFactory = await this.moduleRef.create(CatsFactory);
}
}
@@switch
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
constructor(moduleRef) {
this.moduleRef = moduleRef;
}
async onModuleInit() {
this.catsFactory = await this.moduleRef.create(CatsFactory);
}
}
This technique enables you to conditionally instantiate different classes outside of the framework container.
Nest provides several utility classes that help make it easy to write applications that function across multiple application contexts (e.g., Nest HTTP server-based, microservices and WebSockets application contexts). These utilities provide information about the current execution context which can be used to build generic guards, filters, and interceptors that can work across a broad set of controllers, methods, and execution contexts.
We cover two such classes in this chapter: ArgumentsHost
and ExecutionContext
.
The ArgumentsHost
class provides methods for retrieving the arguments being passed to a handler. It allows choosing the appropriate context (e.g., HTTP, RPC (microservice), or WebSockets) to retrieve the arguments from. The framework provides an instance of ArgumentsHost
, typically referenced as a host
parameter, in places where you may want to access it. For example, the catch()
method of an exception filter is called with an ArgumentsHost
instance.
ArgumentsHost
simply acts as an abstraction over a handler's arguments. For example, for HTTP server applications (when @nestjs/platform-express
is being used), the host
object encapsulates Express's [request, response, next]
array, where request
is the request object, response
is the response object, and next
is a function that controls the application's request-response cycle. On the other hand, for GraphQL applications, the host
object contains the [root, args, context, info]
array.
When building generic guards, filters, and interceptors which are meant to run across multiple application contexts, we need a way to determine the type of application that our method is currently running in. Do this with the getType()
method of ArgumentsHost
:
if (host.getType() === 'http') {
// do something that is only important in the context of regular HTTP requests (REST)
} else if (host.getType() === 'rpc') {
// do something that is only important in the context of Microservice requests
} else if (host.getType<GqlContextType>() === 'graphql') {
// do something that is only important in the context of GraphQL requests
}
info Hint The
GqlContextType
is imported from the@nestjs/graphql
package.
With the application type available, we can write more generic components, as shown below.
To retrieve the array of arguments being passed to the handler, one approach is to use the host object's getArgs()
method.
const [req, res, next] = host.getArgs();
You can pluck a particular argument by index using the getArgByIndex()
method:
const request = host.getArgByIndex(0);
const response = host.getArgByIndex(1);
In these examples we retrieved the request and response objects by index, which is not typically recommended as it couples the application to a particular execution context. Instead, you can make your code more robust and reusable by using one of the host
object's utility methods to switch to the appropriate application context for your application. The context switch utility methods are shown below.
/**
* Switch context to RPC.
*/
switchToRpc(): RpcArgumentsHost;
/**
* Switch context to HTTP.
*/
switchToHttp(): HttpArgumentsHost;
/**
* Switch context to WebSockets.
*/
switchToWs(): WsArgumentsHost;
Let's rewrite the previous example using the switchToHttp()
method. The host.switchToHttp()
helper call returns an HttpArgumentsHost
object that is appropriate for the HTTP application context. The HttpArgumentsHost
object has two useful methods we can use to extract the desired objects. We also use the Express type assertions in this case to return native Express typed objects:
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
Similarly WsArgumentsHost
and RpcArgumentsHost
have methods to return appropriate objects in the microservices and WebSockets contexts. Here are the methods for WsArgumentsHost
:
export interface WsArgumentsHost {
/**
* Returns the data object.
*/
getData<T>(): T;
/**
* Returns the client object.
*/
getClient<T>(): T;
}
Following are the methods for RpcArgumentsHost
:
export interface RpcArgumentsHost {
/**
* Returns the data object.
*/
getData<T>(): T;
/**
* Returns the context object.
*/
getContext<T>(): T;
}
ExecutionContext
extends ArgumentsHost
, providing additional details about the current execution process. Like ArgumentsHost
, Nest provides an instance of ExecutionContext
in places you may need it, such as in the canActivate()
method of a guard and the intercept()
method of an interceptor. It provides the following methods:
export interface ExecutionContext extends ArgumentsHost {
/**
* Returns the type of the controller class which the current handler belongs to.
*/
getClass<T>(): Type<T>;
/**
* Returns a reference to the handler (method) that will be invoked next in the
* request pipeline.
*/
getHandler(): Function;
}
The getHandler()
method returns a reference to the handler about to be invoked. The getClass()
method returns the type of the Controller
class which this particular handler belongs to. For example, in an HTTP context, if the currently processed request is a POST
request, bound to the create()
method on the CatsController
, getHandler()
returns a reference to the create()
method and getClass()
returns the CatsController
type (not instance).
const methodKey = ctx.getHandler().name; // "create"
const className = ctx.getClass().name; // "CatsController"
The ability to access references to both the current class and handler method provides great flexibility. Most importantly, it gives us the opportunity to access the metadata set through the @SetMetadata()
decorator from within guards or interceptors. We cover this use case below.
Nest provides the ability to attach custom metadata to route handlers through the @SetMetadata()
decorator. We can then access this metadata from within our class to make certain decisions.
@@filename(cats.controller)
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@@switch
@Post()
@SetMetadata('roles', ['admin'])
@Bind(Body())
async create(createCatDto) {
this.catsService.create(createCatDto);
}
info Hint The
@SetMetadata()
decorator is imported from the@nestjs/common
package.
With the construction above, we attached the roles
metadata (roles
is a metadata key and ['admin']
is the associated value) to the create()
method. While this works, it's not good practice to use @SetMetadata()
directly in your routes. Instead, create your own decorators, as shown below:
@@filename(roles.decorator)
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
@@switch
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles) => SetMetadata('roles', roles);
This approach is much cleaner and more readable, and is strongly typed. Now that we have a custom @Roles()
decorator, we can use it to decorate the create()
method.
@@filename(cats.controller)
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@@switch
@Post()
@Roles('admin')
@Bind(Body())
async create(createCatDto) {
this.catsService.create(createCatDto);
}
To access the route's role(s) (custom metadata), we'll use the Reflector
helper class, which is provided out of the box by the framework and exposed from the @nestjs/core
package. Reflector
can be injected into a class in the normal way:
@@filename(roles.guard)
@Injectable()
export class RolesGuard {
constructor(private reflector: Reflector) {}
}
@@switch
@Injectable()
@Dependencies(Reflector)
export class CatsService {
constructor(reflector) {
this.reflector = reflector;
}
}
info Hint The
Reflector
class is imported from the@nestjs/core
package.
Now, to read the handler metadata, use the get()
method.
const roles = this.reflector.get<string[]>('roles', context.getHandler());
The Reflector#get
method allows us to easily access the metadata by passing in two arguments: a metadata key and a context (decorator target) to retrieve the metadata from. In this example, the specified key is 'roles'
(refer back to the roles.decorator.ts
file above and the SetMetadata()
call made there). The context is provided by the call to context.getHandler()
, which results in extracting the metadata for the currently processed route handler. Remember, getHandler()
gives us a reference to the route handler function.
Alternatively, we may organize our controller by applying metadata at the controller level, applying to all routes in the controller class.
@@filename(cats.controller)
@Roles('admin')
@Controller('cats')
export class CatsController {}
@@switch
@Roles('admin')
@Controller('cats')
export class CatsController {}
In this case, to extract controller metadata, we pass context.getClass()
as the second argument (to provide the controller class as the context for metadata extraction) instead of context.getHandler()
:
@@filename(roles.guard)
const roles = this.reflector.get<string[]>('roles', context.getClass());
@@switch
const roles = this.reflector.get('roles', context.getClass());
Given the ability to provide metadata at multiple levels, you may need to extract and merge metadata from several contexts. The Reflector
class provides two utility methods used to help with this. These methods extract both controller and method metadata at once, and combine them in different ways.
Consider the following scenario, where you've supplied 'roles'
metadata at both levels.
@@filename(cats.controller)
@Roles('user')
@Controller('cats')
export class CatsController {
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
}
@@switch
@Roles('user')
@Controller('cats')
export class CatsController {}
@Post()
@Roles('admin')
@Bind(Body())
async create(createCatDto) {
this.catsService.create(createCatDto);
}
}
If your intent is to specify 'user'
as the default role, and override it selectively for certain methods, you would probably use the getAllAndOverride()
method.
const roles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
A guard with this code, running in the context of the create()
method, with the above metadata, would result in roles
containing ['admin']
.
To get metadata for both and merge it (this method merges both arrays and objects), use the getAllAndMerge()
method:
const roles = this.reflector.getAllAndMerge<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
This would result in roles
containing ['user', 'admin']
.
For both of these merge methods, you pass the metadata key as the first argument, and an array of metadata target contexts (i.e., calls to the getHandler()
and/or getClass())
methods) as the second argument.
A Nest application, as well as every application element, has a lifecycle managed by Nest. Nest provides lifecycle hooks that give visibility into key lifecycle events, and the ability to act (run registered code on your module
, injectable
or controller
) when they occur.
The following diagram depicts the sequence of key application lifecycle events, from the time the application is bootstrapped until the node process exits. We can divide the overall lifecycle into three phases: initializing, running and terminating. Using this lifecycle, you can plan for appropriate initialization of modules and services, manage active connections, and gracefully shutdown your application when it receives a termination signal.
Lifecycle events happen during application bootstrapping and shutdown. Nest calls registered lifecycle hook methods on modules
, injectables
and controllers
at each of the following lifecycle events (shutdown hooks need to be enabled first, as described below). As shown in the diagram above, Nest also calls the appropriate underlying methods to begin listening for connections, and to stop listening for connections.
In the following table, onModuleDestroy
, beforeApplicationShutdown
and onApplicationShutdown
are only triggered if you explicitly call app.close()
or if the process receives a special system signal (such as SIGTERM) and you have correctly called enableShutdownHooks
at application bootstrap (see below Application shutdown part).
Lifecycle hook method | Lifecycle event triggering the hook method call |
---|---|
onModuleInit() |
Called once the host module's dependencies have been resolved. |
onApplicationBootstrap() |
Called once all modules have been initialized, but before listening for connections. |
onModuleDestroy() * |
Called after a termination signal (e.g., SIGTERM ) has been received. |
beforeApplicationShutdown() * |
Called after all onModuleDestroy() handlers have completed (Promises resolved or rejected);once complete (Promises resolved or rejected), all existing connections will be closed ( app.close() called). |
onApplicationShutdown() * |
Called after connections close (app.close() resolves. |
* For these events, if you're not calling app.close()
explicitly, you must opt-in to make them work with system signals such as SIGTERM
. See Application shutdown below.
warning Warning The lifecycle hooks listed above are not triggered for request-scoped classes. Request-scoped classes are not tied to the application lifecycle and their lifespan is unpredictable. They are exclusively created for each request and automatically garbage-collected after the response is sent.
Each lifecycle hook is represented by an interface. Interfaces are technically optional because they do not exist after TypeScript compilation. Nonetheless, it's good practice to use them in order to benefit from strong typing and editor tooling. To register a lifecycle hook, implement the appropriate interface. For example, to register a method to be called during module initialization on a particular class (e.g., Controller, Provider or Module), implement the OnModuleInit
interface by supplying an onModuleInit()
method, as shown below:
@@filename()
import { Injectable, OnModuleInit } from '@nestjs/common';
@Injectable()
export class UsersService implements OnModuleInit {
onModuleInit() {
console.log(`The module has been initialized.`);
}
}
@@switch
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
onModuleInit() {
console.log(`The module has been initialized.`);
}
}
Both the OnModuleInit
and OnApplicationBootstrap
hooks allow you to defer the application initialization process (return a Promise
or mark the method as async
and await
an asynchronous method completion in the method body).
@@filename()
async onModuleInit(): Promise<void> {
await this.fetch();
}
@@switch
async onModuleInit() {
await this.fetch();
}
The onModuleDestroy()
, beforeApplicationShutdown()
and onApplicationShutdown()
hooks are called in the terminating phase (in response to an explicit call to app.close()
or upon receipt of system signals such as SIGTERM if opted-in). This feature is often used with Kubernetes to manage containers' lifecycles, by Heroku for dynos or similar services.
Shutdown hook listeners consume system resources, so they are disabled by default. To use shutdown hooks, you must enable listeners by calling enableShutdownHooks()
:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Starts listening for shutdown hooks
app.enableShutdownHooks();
await app.listen(3000);
}
bootstrap();
warning warning Due to inherent platform limitations, NestJS has limited support for application shutdown hooks on Windows. You can expect
SIGINT
to work, as well asSIGBREAK
and to some extentSIGHUP
- read more. HoweverSIGTERM
will never work on Windows because killing a process in the task manager is unconditional, "i.e., there's no way for an application to detect or prevent it". Here's some relevant documentation from libuv to learn more about howSIGINT
,SIGBREAK
and others are handled on Windows. Also, see Node.js documentation of Process Signal Events
info Info
enableShutdownHooks
consumes memory by starting listeners. In cases where you are running multiple Nest apps in a single Node process (e.g., when running parallel tests with Jest), Node may complain about excessive listener processes. For this reason,enableShutdownHooks
is not enabled by default. Be aware of this condition when you are running multiple instances in a single Node process.
When the application receives a termination signal it will call any registered onModuleDestroy()
, beforeApplicationShutdown()
, then onApplicationShutdown()
methods (in the sequence described above) with the corresponding signal as the first parameter. If a registered function awaits an asynchronous call (returns a promise), Nest will not continue in the sequence until the promise is resolved or rejected.
@@filename()
@Injectable()
class UsersService implements OnApplicationShutdown {
onApplicationShutdown(signal: string) {
console.log(signal); // e.g. "SIGINT"
}
}
@@switch
@Injectable()
class UsersService implements OnApplicationShutdown {
onApplicationShutdown(signal) {
console.log(signal); // e.g. "SIGINT"
}
}
Nest is a platform-agnostic framework. This means you can develop reusable logical parts that can be used across different types of applications. For example, most components can be re-used without change across different underlying HTTP server frameworks (e.g., Express and Fastify), and even across different types of applications (e.g., HTTP server frameworks, Microservices with different transport layers, and Web Sockets).
The Overview section of the documentation primarily shows coding techniques using HTTP server frameworks (e.g., apps providing a REST API or providing an MVC-style server-side rendered app). However, all those building blocks can be used on top of different transport layers (microservices or websockets).
Furthermore, Nest comes with a dedicated GraphQL module. You can use GraphQL as your API layer interchangeably with providing a REST API.
In addition, the application context feature helps to create any kind of Node.js application - including things like CRON jobs and CLI apps - on top of Nest.
Nest aspires to be a full-fledged platform for Node.js apps that brings a higher-level of modularity and reusability to your applications. Build once, use everywhere!
Automated testing is considered an essential part of any serious software development effort. Automation makes it easy to repeat individual tests or test suites quickly and easily during development. This helps ensure that releases meet quality and performance goals. Automation helps increase coverage and provides a faster feedback loop to developers. Automation both increases the productivity of individual developers and ensures that tests are run at critical development lifecycle junctures, such as source code control check-in, feature integration, and version release.
Such tests often span a variety of types, including unit tests, end-to-end (e2e) tests, integration tests, and so on. While the benefits are unquestionable, it can be tedious to set them up. Nest strives to promote development best practices, including effective testing, so it includes features such as the following to help developers and teams build and automate tests. Nest:
- automatically scaffolds default unit tests for components and e2e tests for applications
- provides default tooling (such as a test runner that builds an isolated module/application loader)
- provides integration with Jest and Supertest out-of-the-box, while remaining agnostic to testing tools
- makes the Nest dependency injection system available in the testing environment for easily mocking components
As mentioned, you can use any testing framework that you like, as Nest doesn't force any specific tooling. Simply replace the elements needed (such as the test runner), and you will still enjoy the benefits of Nest's ready-made testing facilities.
To get started, first install the required package:
$ npm i --save-dev @nestjs/testing
In the following example, we test two classes: CatsController
and CatsService
. As mentioned, Jest is provided as the default testing framework. It serves as a test-runner and also provides assert functions and test-double utilities that help with mocking, spying, etc. In the following basic test, we manually instantiate these classes, and ensure that the controller and service fulfill their API contract.
@@filename(cats.controller.spec)
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
describe('CatsController', () => {
let catsController: CatsController;
let catsService: CatsService;
beforeEach(() => {
catsService = new CatsService();
catsController = new CatsController(catsService);
});
describe('findAll', () => {
it('should return an array of cats', async () => {
const result = ['test'];
jest.spyOn(catsService, 'findAll').mockImplementation(() => result);
expect(await catsController.findAll()).toBe(result);
});
});
});
@@switch
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
describe('CatsController', () => {
let catsController;
let catsService;
beforeEach(() => {
catsService = new CatsService();
catsController = new CatsController(catsService);
});
describe('findAll', () => {
it('should return an array of cats', async () => {
const result = ['test'];
jest.spyOn(catsService, 'findAll').mockImplementation(() => result);
expect(await catsController.findAll()).toBe(result);
});
});
});
info Hint Keep your test files located near the classes they test. Testing files should have a
.spec
or.test
suffix.
Because the above sample is trivial, we aren't really testing anything Nest-specific. Indeed, we aren't even using dependency injection (notice that we pass an instance of CatsService
to our catsController
). This form of testing - where we manually instantiate the classes being tested - is often called isolated testing as it is independent from the framework. Let's introduce some more advanced capabilities that help you test applications that make more extensive use of Nest features.
The @nestjs/testing
package provides a set of utilities that enable a more robust testing process. Let's rewrite the previous example using the built-in Test
class:
@@filename(cats.controller.spec)
import { Test } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
describe('CatsController', () => {
let catsController: CatsController;
let catsService: CatsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
catsService = moduleRef.get<CatsService>(CatsService);
catsController = moduleRef.get<CatsController>(CatsController);
});
describe('findAll', () => {
it('should return an array of cats', async () => {
const result = ['test'];
jest.spyOn(catsService, 'findAll').mockImplementation(() => result);
expect(await catsController.findAll()).toBe(result);
});
});
});
@@switch
import { Test } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
describe('CatsController', () => {
let catsController;
let catsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
catsService = moduleRef.get(CatsService);
catsController = moduleRef.get(CatsController);
});
describe('findAll', () => {
it('should return an array of cats', async () => {
const result = ['test'];
jest.spyOn(catsService, 'findAll').mockImplementation(() => result);
expect(await catsController.findAll()).toBe(result);
});
});
});
The Test
class is useful for providing an application execution context that essentially mocks the full Nest runtime, but gives you hooks that make it easy to manage class instances, including mocking and overriding. The Test
class has a createTestingModule()
method that takes a module metadata object as its argument (the same object you pass to the @Module()
decorator). This method returns a TestingModule
instance which in turn provides a few methods. For unit tests, the important one is the compile()
method. This method bootstraps a module with its dependencies (similar to the way an application is bootstrapped in the conventional main.ts
file using NestFactory.create()
), and returns a module that is ready for testing.
info Hint The
compile()
method is asynchronous and therefore has to be awaited. Once the module is compiled you can retrieve any static instance it declares (controllers and providers) using theget()
method.
TestingModule
inherits from the module reference class, and therefore its ability to dynamically resolve scoped providers (transient or request-scoped). Do this with the resolve()
method (the get()
method can only retrieve static instances).
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
catsService = await moduleRef.resolve(CatsService);
warning Warning The
resolve()
method returns a unique instance of the provider, from its own DI container sub-tree. Each sub-tree has a unique context identifier. Thus, if you call this method more than once and compare instance references, you will see that they are not equal.
info Hint Learn more about the module reference features here.
Instead of using the production version of any provider, you can override it with a custom provider for testing purposes. For example, you can mock a database service instead of connecting to a live database. We'll cover overrides in the next section, but they're available for unit tests as well.
Unlike unit testing, which focuses on individual modules and classes, end-to-end (e2e) testing covers the interaction of classes and modules at a more aggregate level -- closer to the kind of interaction that end-users will have with the production system. As an application grows, it becomes hard to manually test the end-to-end behavior of each API endpoint. Automated end-to-end tests help us ensure that the overall behavior of the system is correct and meets project requirements. To perform e2e tests we use a similar configuration to the one we just covered in unit testing. In addition, Nest makes it easy to use the Supertest library to simulate HTTP requests.
@@filename(cats.e2e-spec)
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { CatsModule } from '../../src/cats/cats.module';
import { CatsService } from '../../src/cats/cats.service';
import { INestApplication } from '@nestjs/common';
describe('Cats', () => {
let app: INestApplication;
let catsService = { findAll: () => ['test'] };
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [CatsModule],
})
.overrideProvider(CatsService)
.useValue(catsService)
.compile();
app = moduleRef.createNestApplication();
await app.init();
});
it(`/GET cats`, () => {
return request(app.getHttpServer())
.get('/cats')
.expect(200)
.expect({
data: catsService.findAll(),
});
});
afterAll(async () => {
await app.close();
});
});
@@switch
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { CatsModule } from '../../src/cats/cats.module';
import { CatsService } from '../../src/cats/cats.service';
import { INestApplication } from '@nestjs/common';
describe('Cats', () => {
let app: INestApplication;
let catsService = { findAll: () => ['test'] };
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [CatsModule],
})
.overrideProvider(CatsService)
.useValue(catsService)
.compile();
app = moduleRef.createNestApplication();
await app.init();
});
it(`/GET cats`, () => {
return request(app.getHttpServer())
.get('/cats')
.expect(200)
.expect({
data: catsService.findAll(),
});
});
afterAll(async () => {
await app.close();
});
});
In this example, we build on some of the concepts described earlier. In addition to the compile()
method we used earlier, we now use the createNestApplication()
method to instantiate a full Nest runtime environment. We save a reference to the running app in our app
variable so we can use it to simulate HTTP requests.
We simulate HTTP tests using the request()
function from Supertest. We want these HTTP requests to route to our running Nest app, so we pass the request()
function a reference to the HTTP listener that underlies Nest (which, in turn, may be provided by the Express platform). Hence the construction request(app.getHttpServer())
. The call to request()
hands us a wrapped HTTP Server, now connected to the Nest app, which exposes methods to simulate an actual HTTP request. For example, using request(...).get('/cats')
will initiate a request to the Nest app that is identical to an actual HTTP request like get '/cats'
coming in over the network.
In this example, we also provide an alternate (test-double) implementation of the CatsService
which simply returns a hard-coded value that we can test for. Use overrideProvider()
to provide such an alternate implementation. Similarly, Nest provides methods to override guards, interceptors, filters and pipes with theoverrideGuard()
, overrideInterceptor()
, overrideFilter()
, and overridePipe()
methods respectively.
Each of the override methods returns an object with 3 different methods that mirror those described for custom providers:
useClass
: you supply a class that will be instantiated to provide the instance to override the object (provider, guard, etc.).useValue
: you supply an instance that will override the object.useFactory
: you supply a function that returns an instance that will override the object.
Each of the override method types, in turn, returns the TestingModule
instance, and can thus be chained with other methods in the fluent style. You should use compile()
at the end of such a chain to cause Nest to instantiate and initialize the module.
The compiled module has several useful methods, as described in the following table:
createNestApplication()
|
Creates and returns a Nest application (INestApplication instance) based on the given module.
Note that you must manually initialize the application using the init() method.
|
createNestMicroservice()
|
Creates and returns a Nest microservice (INestMicroservice instance) based on the given module.
|
get()
|
Retrieves a static instance of a controller or provider (including guards, filters, etc.) available in the application context. Inherited from the module reference class. |
resolve()
|
Retrieves a dynamically created scoped instance (request or transient) of a controller or provider (including guards, filters, etc.) available in the application context. Inherited from the module reference class. |
select()
|
Navigates through the module's dependency graph; can be used to retrieve a specific instance from the selected module (used along with strict mode (strict: true ) in get() method).
|
info Hint Keep your e2e test files inside the
e2e
directory. The testing files should have a.e2e-spec
or.e2e-test
suffix.
Request-scoped providers are created uniquely for each incoming request. The instance is garbage-collected after the request has completed processing. This poses a problem, because we can't access a dependency injection sub-tree generated specifically for a tested request.
We know (based on the sections above) that the resolve()
method can be used to retrieve a dynamically instantiated class. Also, as described here, we know we can pass a unique context identifier to control the lifecycle of a DI container sub-tree. How do we leverage this in a testing context?
The strategy is to generate a context identifier beforehand and force Nest to use this particular ID to create a sub-tree for all incoming requests. In this way we'll be able to retrieve instances created for a tested request.
To accomplish this, use jest.spyOn()
on the ContextIdFactory
:
const contextId = ContextIdFactory.create();
jest
.spyOn(ContextIdFactory, 'getByRequest')
.mockImplementation(() => contextId);
Now we can use the contextId
to access a single generated DI container sub-tree for any subsequent request.
catsService = await moduleRef.resolve(CatsService, contextId);
- Authentication
- Database
- Mongo
- Configuration
- Validation
- Caching
- Serialization
- Task scheduling
- Compression
- Security
- Queues
- Logger
- File upload
- HTTP module
- Model-View-Controller
- Performance (Fastify)
Authentication is an essential part of most applications. There are many different approaches and strategies to handle authentication. The approach taken for any project depends on its particular application requirements. This chapter presents several approaches to authentication that can be adapted to a variety of different requirements.
Passport is the most popular node.js authentication library, well-known by the community and successfully used in many production applications. It's straightforward to integrate this library with a Nest application using the @nestjs/passport
module. At a high level, Passport executes a series of steps to:
- Authenticate a user by verifying their "credentials" (such as username/password, JSON Web Token (JWT), or identity token from an Identity Provider)
- Manage authenticated state (by issuing a portable token, such as a JWT, or creating an Express session)
- Attach information about the authenticated user to the
Request
object for further use in route handlers
Passport has a rich ecosystem of strategies that implement various authentication mechanisms. While simple in concept, the set of Passport strategies you can choose from is large and presents a lot of variety. Passport abstracts these varied steps into a standard pattern, and the @nestjs/passport
module wraps and standardizes this pattern into familiar Nest constructs.
In this chapter, we'll implement a complete end-to-end authentication solution for a RESTful API server using these powerful and flexible modules. You can use the concepts described here to implement any Passport strategy to customize your authentication scheme. You can follow the steps in this chapter to build this complete example. You can find a repository with a completed sample app here.
Let's flesh out our requirements. For this use case, clients will start by authenticating with a username and password. Once authenticated, the server will issue a JWT that can be sent as a bearer token in an authorization header on subsequent requests to prove authentication. We'll also create a protected route that is accessible only to requests that contain a valid JWT.
We'll start with the first requirement: authenticating a user. We'll then extend that by issuing a JWT. Finally, we'll create a protected route that checks for a valid JWT on the request.
First we need to install the required packages. Passport provides a strategy called passport-local that implements a username/password authentication mechanism, which suits our needs for this portion of our use case.
$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-local
warning Notice For any Passport strategy you choose, you'll always need the
@nestjs/passport
andpassport
packages. Then, you'll need to install the strategy-specific package (e.g.,passport-jwt
orpassport-local
) that implements the particular authentication strategy you are building. In addition, you can also install the type definitions for any Passport strategy, as shown above with@types/passport-local
, which provides assistance while writing TypeScript code.
We're now ready to implement the authentication feature. We'll start with an overview of the process used for any Passport strategy. It's helpful to think of Passport as a mini framework in itself. The elegance of the framework is that it abstracts the authentication process into a few basic steps that you customize based on the strategy you're implementing. It's like a framework because you configure it by supplying customization parameters (as plain JSON objects) and custom code in the form of callback functions, which Passport calls at the appropriate time. The @nestjs/passport
module wraps this framework in a Nest style package, making it easy to integrate into a Nest application. We'll use @nestjs/passport
below, but first let's consider how vanilla Passport works.
In vanilla Passport, you configure a strategy by providing two things:
- A set of options that are specific to that strategy. For example, in a JWT strategy, you might provide a secret to sign tokens.
- A "verify callback", which is where you tell Passport how to interact with your user store (where you manage user accounts). Here, you verify whether a user exists (and/or create a new user), and whether their credentials are valid. The Passport library expects this callback to return a full user if the validation succeeds, or a null if it fails (failure is defined as either the user is not found, or, in the case of passport-local, the password does not match).
With @nestjs/passport
, you configure a Passport strategy by extending the PassportStrategy
class. You pass the strategy options (item 1 above) by calling the super()
method in your subclass, optionally passing in an options object. You provide the verify callback (item 2 above) by implementing a validate()
method in your subclass.
We'll start by generating an AuthModule
and in it, an AuthService
:
$ nest g module auth
$ nest g service auth
As we implement the AuthService
, we'll find it useful to encapsulate user operations in a UsersService
, so let's generate that module and service now:
$ nest g module users
$ nest g service users
Replace the default contents of these generated files as shown below. For our sample app, the UsersService
simply maintains a hard-coded in-memory list of users, and a find method to retrieve one by username. In a real app, this is where you'd build your user model and persistence layer, using your library of choice (e.g., TypeORM, Sequelize, Mongoose, etc.).
@@filename(users/users.service)
import { Injectable } from '@nestjs/common';
export type User = any;
@Injectable()
export class UsersService {
private readonly users: User[];
constructor() {
this.users = [
{
userId: 1,
username: 'john',
password: 'changeme',
},
{
userId: 2,
username: 'chris',
password: 'secret',
},
{
userId: 3,
username: 'maria',
password: 'guess',
},
];
}
async findOne(username: string): Promise<User | undefined> {
return this.users.find(user => user.username === username);
}
}
@@switch
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
constructor() {
this.users = [
{
userId: 1,
username: 'john',
password: 'changeme',
},
{
userId: 2,
username: 'chris',
password: 'secret',
},
{
userId: 3,
username: 'maria',
password: 'guess',
},
];
}
async findOne(username) {
return this.users.find(user => user.username === username);
}
}
In the UsersModule
, the only change needed is to add the UsersService
to the exports array of the @Module
decorator so that it is visible outside this module (we'll soon use it in our AuthService
).
@@filename(users/users.module)
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
@@switch
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
Our AuthService
has the job of retrieving a user and verifying the password. We create a validateUser()
method for this purpose. In the code below, we use a convenient ES6 spread operator to strip the password property from the user object before returning it. We'll be calling into the validateUser()
method from our Passport local strategy in a moment.
@@filename(auth/auth.service)
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
}
@@switch
import { Injectable, Dependencies } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
@Dependencies(UsersService)
export class AuthService {
constructor(usersService) {
this.usersService = usersService;
}
async validateUser(username, pass) {
const user = await this.usersService.findOne(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
}
Warning Warning Of course in a real application, you wouldn't store a password in plain text. You'd instead use a library like bcrypt, with a salted one-way hash algorithm. With that approach, you'd only store hashed passwords, and then compare the stored password to a hashed version of the incoming password, thus never storing or exposing user passwords in plain text. To keep our sample app simple, we violate that absolute mandate and use plain text. Don't do this in your real app!
Now, we update our AuthModule
to import the UsersModule
.
@@filename(auth/auth.module)
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
@Module({
imports: [UsersModule],
providers: [AuthService],
})
export class AuthModule {}
@@switch
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
@Module({
imports: [UsersModule],
providers: [AuthService],
})
export class AuthModule {}
Now we can implement our Passport local authentication strategy. Create a file called local.strategy.ts
in the auth
folder, and add the following code:
@@filename(auth/local.strategy)
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
@@switch
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException, Dependencies } from '@nestjs/common';
import { AuthService } from './auth.service';
@Injectable()
@Dependencies(AuthService)
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(authService) {
super();
this.authService = authService;
}
async validate(username, password) {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
We've followed the recipe described earlier for all Passport strategies. In our use case with passport-local, there are no configuration options, so our constructor simply calls super()
, without an options object.
info Hint We can pass an options object in the call to
super()
to customize the behavior of the passport strategy. In this example, the passport-local strategy by default expects properties calledusername
andpassword
in the request body. Pass an options object to specify different property names, for example:super({{ '{' }} usernameField: 'email' {{ '}' }})
. See the Passport documentation for more information.
We've also implemented the validate()
method. For each strategy, Passport will call the verify function (implemented with the validate()
method in @nestjs/passport
) using an appropriate strategy-specific set of parameters. For the local-strategy, Passport expects a validate()
method with the following signature: validate(username: string, password:string): any
.
Most of the validation work is done in our AuthService
(with the help of our UsersService
), so this method is quite straightforward. The validate()
method for any Passport strategy will follow a similar pattern, varying only in the details of how credentials are represented. If a user is found and the credentials are valid, the user is returned so Passport can complete its tasks (e.g., creating the user
property on the Request
object), and the request handling pipeline can continue. If it's not found, we throw an exception and let our exceptions layer handle it.
Typically, the only significant difference in the validate()
method for each strategy is how you determine if a user exists and is valid. For example, in a JWT strategy, depending on requirements, we may evaluate whether the userId
carried in the decoded token matches a record in our user database, or matches a list of revoked tokens. Hence, this pattern of sub-classing and implementing strategy-specific validation is consistent, elegant and extensible.
We need to configure our AuthModule
to use the Passport features we just defined. Update auth.module.ts
to look like this:
@@filename(auth/auth.module)
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
@Module({
imports: [UsersModule, PassportModule],
providers: [AuthService, LocalStrategy],
})
export class AuthModule {}
@@switch
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
@Module({
imports: [UsersModule, PassportModule],
providers: [AuthService, LocalStrategy],
})
export class AuthModule {}
The Guards chapter describes the primary function of Guards: to determine whether a request will be handled by the route handler or not. That remains true, and we'll use that standard capability soon. However, in the context of using the @nestjs/passport
module, we will also introduce a slight new wrinkle that may at first be confusing, so let's discuss that now. Consider that your app can exist in two states, from an authentication perspective:
- the user/client is not logged in (is not authenticated)
- the user/client is logged in (is authenticated)
In the first case (user is not logged in), we need to perform two distinct functions:
-
Restrict the routes an unauthenticated user can access (i.e., deny access to restricted routes). We'll use Guards in their familiar capacity to handle this function, by placing a Guard on the protected routes. As you may anticipate, we'll be checking for the presence of a valid JWT in this Guard, so we'll work on this Guard later, once we are successfully issuing JWTs.
-
Initiate the authentication step itself when a previously unauthenticated user attempts to login. This is the step where we'll issue a JWT to a valid user. Thinking about this for a moment, we know we'll need to
POST
username/password credentials to initiate authentication, so we'll set up aPOST /auth/login
route to handle that. This raises the question: how exactly do we invoke the passport-local strategy in that route?
The answer is straightforward: by using another, slightly different type of Guard. The @nestjs/passport
module provides us with a built-in Guard that does this for us. This Guard invokes the Passport strategy and kicks off the steps described above (retrieving credentials, running the verify function, creating the user
property, etc).
The second case enumerated above (logged in user) simply relies on the standard type of Guard we already discussed to enable access to protected routes for logged in users.
With the strategy in place, we can now implement a bare-bones /auth/login
route, and apply the built-in Guard to initiate the passport-local flow.
Open the app.controller.ts
file and replace its contents with the following:
@@filename(app.controller)
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller()
export class AppController {
@UseGuards(AuthGuard('local'))
@Post('auth/login')
async login(@Request() req) {
return req.user;
}
}
@@switch
import { Controller, Bind, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller()
export class AppController {
@UseGuards(AuthGuard('local'))
@Post('auth/login')
@Bind(Request())
async login(req) {
return req.user;
}
}
With @UseGuards(AuthGuard('local'))
we are using an AuthGuard
that @nestjs/passport
automatically provisioned for us when we extended the passport-local strategy. Let's break that down. Our Passport local strategy has a default name of 'local'
. We reference that name in the @UseGuards()
decorator to associate it with code supplied by the passport-local
package. This is used to disambiguate which strategy to invoke in case we have multiple Passport strategies in our app (each of which may provision a strategy-specific AuthGuard
). While we only have one such strategy so far, we'll shortly add a second, so this is needed for disambiguation.
In order to test our route we'll have our /auth/login
route simply return the user for now. This also lets us demonstrate another Passport feature: Passport automatically creates a user
object, based on the value we return from the validate()
method, and assigns it to the Request
object as req.user
. Later, we'll replace this with code to create and return a JWT instead.
Since these are API routes, we'll test them using the commonly available cURL library. You can test with any of the user
objects hard-coded in the UsersService
.
$ # POST to /auth/login
$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
$ # result -> {"userId":1,"username":"john"}
While this works, passing the strategy name directly to the AuthGuard()
introduces magic strings in the codebase. Instead, we recommend creating your own class, as shown below:
@@filename(auth/local-auth.guard)
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
Now, we can update the /auth/login
route handler and use the LocalAuthGuard
instead:
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(@Request() req) {
return req.user;
}
We're ready to move on to the JWT portion of our auth system. Let's review and refine our requirements:
- Allow users to authenticate with username/password, returning a JWT for use in subsequent calls to protected API endpoints. We're well on our way to meeting this requirement. To complete it, we'll need to write the code that issues a JWT.
- Create API routes which are protected based on the presence of a valid JWT as a bearer token
We'll need to install a couple more packages to support our JWT requirements:
$ npm install --save @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-jwt
The @nestjs/jwt
package (see more here) is a utility package that helps with JWT manipulation. The passport-jwt
package is the Passport package that implements the JWT strategy and @types/passport-jwt
provides the TypeScript type definitions.
Let's take a closer look at how a POST /auth/login
request is handled. We've decorated the route using the built-in AuthGuard
provided by the passport-local strategy. This means that:
- The route handler will only be invoked if the user has been validated
- The
req
parameter will contain auser
property (populated by Passport during the passport-local authentication flow)
With this in mind, we can now finally generate a real JWT, and return it in this route. To keep our services cleanly modularized, we'll handle generating the JWT in the authService
. Open the auth.service.ts
file in the auth
folder, and add the login()
method, and import the JwtService
as shown:
@@filename(auth/auth.service)
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService
) {}
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: any) {
const payload = { username: user.username, sub: user.userId };
return {
access_token: this.jwtService.sign(payload),
};
}
}
@@switch
import { Injectable, Dependencies } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
@Dependencies(UsersService, JwtService)
@Injectable()
export class AuthService {
constructor(usersService, jwtService) {
this.usersService = usersService;
this.jwtService = jwtService;
}
async validateUser(username, pass) {
const user = await this.usersService.findOne(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user) {
const payload = { username: user.username, sub: user.userId };
return {
access_token: this.jwtService.sign(payload),
};
}
}
We're using the @nestjs/jwt
library, which supplies a sign()
function to generate our JWT from a subset of the user
object properties, which we then return as a simple object with a single access_token
property. Note: we choose a property name of sub
to hold our userId
value to be consistent with JWT standards. Don't forget to inject the JwtService provider into the AuthService
.
We now need to update the AuthModule
to import the new dependencies and configure the JwtModule
.
First, create constants.ts
in the auth
folder, and add the following code:
@@filename(auth/constants)
export const jwtConstants = {
secret: 'secretKey',
};
@@switch
export const jwtConstants = {
secret: 'secretKey',
};
We'll use this to share our key between the JWT signing and verifying steps.
Warning Warning Do not expose this key publicly. We have done so here to make it clear what the code is doing, but in a production system you must protect this key using appropriate measures such as a secrets vault, environment variable, or configuration service.
Now, open auth.module.ts
in the auth
folder and update it to look like this:
@@filename(auth/auth.module)
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy],
exports: [AuthService],
})
export class AuthModule {}
@@switch
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy],
exports: [AuthService],
})
export class AuthModule {}
We configure the JwtModule
using register()
, passing in a configuration object. See here for more on the Nest JwtModule
and here for more details on the available configuration options.
Now we can update the /auth/login
route to return a JWT.
@@filename(app.controller)
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { LocalAuthGuard } from './auth/local-auth.guard';
import { AuthService } from './auth/auth.service';
@Controller()
export class AppController {
constructor(private authService: AuthService) {}
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(@Request() req) {
return this.authService.login(req.user);
}
}
@@switch
import { Controller, Bind, Request, Post, UseGuards } from '@nestjs/common';
import { LocalAuthGuard } from './auth/local-auth.guard';
import { AuthService } from './auth/auth.service';
@Controller()
export class AppController {
constructor(private authService: AuthService) {}
@UseGuards(LocalAuthGuard)
@Post('auth/login')
@Bind(Request())
async login(req) {
return this.authService.login(req.user);
}
}
Let's go ahead and test our routes using cURL again. You can test with any of the user
objects hard-coded in the UsersService
.
$ # POST to /auth/login
$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
$ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
$ # Note: above JWT truncated
We can now address our final requirement: protecting endpoints by requiring a valid JWT be present on the request. Passport can help us here too. It provides the passport-jwt strategy for securing RESTful endpoints with JSON Web Tokens. Start by creating a file called jwt.strategy.ts
in the auth
folder, and add the following code:
@@filename(auth/jwt.strategy)
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
@@switch
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload) {
return { userId: payload.sub, username: payload.username };
}
}
With our JwtStrategy
, we've followed the same recipe described earlier for all Passport strategies. This strategy requires some initialization, so we do that by passing in an options object in the super()
call. You can read more about the available options here. In our case, these options are:
jwtFromRequest
: supplies the method by which the JWT will be extracted from theRequest
. We will use the standard approach of supplying a bearer token in the Authorization header of our API requests. Other options are described here.ignoreExpiration
: just to be explicit, we choose the defaultfalse
setting, which delegates the responsibility of ensuring that a JWT has not expired to the Passport module. This means that if our route is supplied with an expired JWT, the request will be denied and a401 Unauthorized
response sent. Passport conveniently handles this automatically for us.secretOrKey
: we are using the expedient option of supplying a symmetric secret for signing the token. Other options, such as a PEM-encoded public key, may be more appropriate for production apps (see here for more information). In any case, as cautioned earlier, do not expose this secret publicly.
The validate()
method deserves some discussion. For the jwt-strategy, Passport first verifies the JWT's signature and decodes the JSON. It then invokes our validate()
method passing the decoded JSON as its single parameter. Based on the way JWT signing works, we're guaranteed that we're receiving a valid token that we have previously signed and issued to a valid user.
As a result of all this, our response to the validate()
callback is trivial: we simply return an object containing the userId
and username
properties. Recall again that Passport will build a user
object based on the return value of our validate()
method, and attach it as a property on the Request
object.
It's also worth pointing out that this approach leaves us room ('hooks' as it were) to inject other business logic into the process. For example, we could do a database lookup in our validate()
method to extract more information about the user, resulting in a more enriched user
object being available in our Request
. This is also the place we may decide to do further token validation, such as looking up the userId
in a list of revoked tokens, enabling us to perform token revocation. The model we've implemented here in our sample code is a fast, "stateless JWT" model, where each API call is immediately authorized based on the presence of a valid JWT, and a small bit of information about the requester (its userId
and username
) is available in our Request pipeline.
Add the new JwtStrategy
as a provider in the AuthModule
:
@@filename(auth/auth.module)
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
@@switch
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
By importing the same secret used when we signed the JWT, we ensure that the verify phase performed by Passport, and the sign phase performed in our AuthService, use a common secret.
Finally, we define the JwtAuthGuard
class which extends the built-in AuthGuard
:
@@filename(auth/jwt-auth.guard)
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
We can now implement our protected route and its associated Guard.
Open the app.controller.ts
file and update it as shown below:
@@filename(app.controller)
import { Controller, Get, Request, Post, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
import { LocalAuthGuard } from './auth/local-auth.guard';
import { AuthService } from './auth/auth.service';
@Controller()
export class AppController {
constructor(private authService: AuthService) {}
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(@Request() req) {
return this.authService.login(req.user);
}
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}
@@switch
import { Controller, Dependencies, Bind, Get, Request, Post, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
import { LocalAuthGuard } from './auth/local-auth.guard';
import { AuthService } from './auth/auth.service';
@Dependencies(AuthService)
@Controller()
export class AppController {
constructor(authService) {
this.authService = authService;
}
@UseGuards(LocalAuthGuard)
@Post('auth/login')
@Bind(Request())
async login(req) {
return this.authService.login(req.user);
}
@UseGuards(JwtAuthGuard)
@Get('profile')
@Bind(Request())
getProfile(req) {
return req.user;
}
}
Once again, we're applying the AuthGuard
that the @nestjs/passport
module has automatically provisioned for us when we configured the passport-jwt module. This Guard is referenced by its default name, jwt
. When our GET /profile
route is hit, the Guard will automatically invoke our passport-jwt custom configured logic, validating the JWT, and assigning the user
property to the Request
object.
Ensure the app is running, and test the routes using cURL
.
$ # GET /profile
$ curl http://localhost:3000/profile
$ # result -> {"statusCode":401,"error":"Unauthorized"}
$ # POST /auth/login
$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
$ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm... }
$ # GET /profile using access_token returned from previous step as bearer code
$ curl http://localhost:3000/profile -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."
$ # result -> {"userId":1,"username":"john"}
Note that in the AuthModule
, we configured the JWT to have an expiration of 60 seconds
. This is probably too short an expiration, and dealing with the details of token expiration and refresh is beyond the scope of this article. However, we chose that to demonstrate an important quality of JWTs and the passport-jwt strategy. If you wait 60 seconds after authenticating before attempting a GET /profile
request, you'll receive a 401 Unauthorized
response. This is because Passport automatically checks the JWT for its expiration time, saving you the trouble of doing so in your application.
We've now completed our JWT authentication implementation. JavaScript clients (such as Angular/React/Vue), and other JavaScript apps, can now authenticate and communicate securely with our API Server. You can find a complete version of the code in this chapter here.
In our AppController
, we pass the name of the strategy in the AuthGuard()
function. We need to do this because we've introduced two Passport strategies (passport-local and passport-jwt), both of which supply implementations of various Passport components. Passing the name disambiguates which implementation we're linking to. When multiple strategies are included in an application, we can declare a default strategy so that we no longer have to pass the name in the AuthGuard
function if using that default strategy. Here's how to register a default strategy when importing the PassportModule
. This code would go in the AuthModule
:
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
UsersModule,
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
The passport API is based on registering strategies to the global instance of the library. Therefore strategies are not designed to have request-dependent options or to be dynamically instantiated per request (read more about the request-scoped providers). When you configure your strategy to be request-scoped, Nest will never instantiate it since it's not tied to any specific route. There is no physical way to determine which "request-scoped" strategies should be executed per request.
However, there are ways to dynamically resolve request-scoped providers within the strategy. For this, we leverage the module reference feature.
First, open the local.strategy.ts
file and inject the ModuleRef
in the normal way:
constructor(private moduleRef: ModuleRef) {
super({
passReqToCallback: true,
});
}
info Hint The
ModuleRef
class is imported from the@nestjs/core
package.
Be sure to set the passReqToCallback
configuration property to true
, as shown above.
In the next step, the request instance will be used to obtain the current context identifier, instead of generating a new one (read more about request context here).
Now, inside the validate()
method of the LocalStrategy
class, use the getByRequest()
method of the ContextIdFactory
class to create a context id based on the request object, and pass this to the resolve()
call:
async validate(
request: Request,
username: string,
password: string,
) {
const contextId = ContextIdFactory.getByRequest(request);
// "AuthService" is a request-scoped provider
const authService = await this.moduleRef.resolve(AuthService, contextId);
...
}
In the example above, the resolve()
method will asynchronously return the request-scoped instance of the AuthService
provider (we assumed that AuthService
is marked as a request-scoped provider).
In most cases, using a provided AuthGuard
class is sufficient. However, there might be use-cases when you would like to simply extend the default error handling or authentication logic. For this, you can extend the built-in class and override methods within a sub-class.
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
// Add your custom authentication logic here
// for example, call super.logIn(request) to establish a session.
return super.canActivate(context);
}
handleRequest(err, user, info) {
// You can throw an exception based on either "info" or "err" arguments
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}
Any standard Passport customization options can be passed the same way, using the register()
method. The available options depend on the strategy being implemented. For example:
PassportModule.register({ session: true });
You can also pass strategies an options object in their constructors to configure them. For the local strategy you can pass e.g.:
constructor(private authService: AuthService) {
super({
usernameField: 'email',
passwordField: 'password',
});
}
Take a look at the official Passport Website for property names.
When implementing a strategy, you can provide a name for it by passing a second argument to the PassportStrategy
function. If you don't do this, each strategy will have a default name (e.g., 'jwt' for jwt-strategy):
export class JwtStrategy extends PassportStrategy(Strategy, 'myjwt')
Then, you refer to this via a decorator like @UseGuards(AuthGuard('myjwt'))
.
In order to use an AuthGuard with GraphQL, extend the built-in AuthGuard class and override the getRequest() method.
@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
To use the above construct, be sure to pass the request (req
) object as part of the context value in the GraphQL Module settings:
GraphQLModule.forRoot({
context: ({ req }) => ({ req }),
});
To get the current authenticated user in your graphql resolver, you can define a @CurrentUser()
decorator:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
export const CurrentUser = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req.user;
},
);
To use above decorator in your resolver, be sure to include it as a parameter of your query or mutation:
@Query(returns => User)
@UseGuards(GqlAuthGuard)
whoAmI(@CurrentUser() user: User) {
return this.usersService.findById(user.id);
}
Nest is database agnostic, allowing you to easily integrate with any SQL or NoSQL database. You have a number of options available to you, depending on your preferences. At the most general level, connecting Nest to a database is simply a matter of loading an appropriate Node.js driver for the database, just as you would with Express or Fastify.
You can also directly use any general purpose Node.js database integration library or ORM, such as Sequelize (navigate to the Sequelize integration section), Knex.js (tutorial) TypeORM, and Prisma (recipe) , to operate at a higher level of abstraction.
For convenience, Nest provides tight integration with TypeORM and Sequelize out-of-the-box with the @nestjs/typeorm
and @nestjs/sequelize
packages respectively, which we'll cover in the current chapter, and Mongoose with @nestjs/mongoose
, which is covered in this chapter. These integrations provide additional NestJS-specific features, such as model/repository injection, testability, and asynchronous configuration to make accessing your chosen database even easier.
For integrating with SQL and NoSQL databases, Nest provides the @nestjs/typeorm
package. Nest uses TypeORM because it's the most mature Object Relational Mapper (ORM) available for TypeScript. Since it's written in TypeScript, it integrates well with the Nest framework.
To begin using it, we first install the required dependencies. In this chapter, we'll demonstrate using the popular MySQL Relational DBMS, but TypeORM provides support for many relational databases, such as PostgreSQL, Oracle, Microsoft SQL Server, SQLite, and even NoSQL databases like MongoDB. The procedure we walk through in this chapter will be the same for any database supported by TypeORM. You'll simply need to install the associated client API libraries for your selected database.
$ npm install --save @nestjs/typeorm typeorm mysql
Once the installation process is complete, we can import the TypeOrmModule
into the root AppModule
.
@@filename(app.module)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [],
synchronize: true,
}),
],
})
export class AppModule {}
The forRoot()
method supports all the configuration properties exposed by the createConnection()
function from the TypeORM package. In addition, there are several extra configuration properties described below.
retryAttempts |
Number of attempts to connect to the database (default: 10 ) |
retryDelay |
Delay between connection retry attempts (ms) (default: 3000 ) |
autoLoadEntities |
If true , entities will be loaded automatically (default: false ) |
keepConnectionAlive |
If true , connection will not be closed on application shutdown (default: false ) |
info Hint Learn more about the connection options here.
Alternatively, rather than passing a configuration object to forRoot()
, we can create an ormconfig.json
file in the project root directory.
{
"type": "mysql",
"host": "localhost",
"port": 3306,
"username": "root",
"password": "root",
"database": "test",
"entities": ["dist/**/*.entity{.ts,.js}"],
"synchronize": true
}
Then, we can call forRoot()
without any options:
@@filename(app.module)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [TypeOrmModule.forRoot()],
})
export class AppModule {}
info Warning Static glob paths (e.g.,
dist/**/*.entity{{ '{' }} .ts,.js{{ '}' }}
) won't work properly with webpack.
warning Warning Note that the
ormconfig.json
file is loaded by thetypeorm
library. Thus, any of the extra properties described above (which are supported internally by way of theforRoot()
method - for example,autoLoadEntities
andretryDelay
) won't be applied.
Once this is done, the TypeORM Connection
and EntityManager
objects will be available to inject across the entire project (without needing to import any modules), for example:
@@filename(app.module)
import { Connection } from 'typeorm';
@Module({
imports: [TypeOrmModule.forRoot(), UsersModule],
})
export class AppModule {
constructor(private connection: Connection) {}
}
@@switch
import { Connection } from 'typeorm';
@Dependencies(Connection)
@Module({
imports: [TypeOrmModule.forRoot(), UsersModule],
})
export class AppModule {
constructor(connection) {
this.connection = connection;
}
}
TypeORM supports the repository design pattern, so each entity has its own repository. These repositories can be obtained from the database connection.
To continue the example, we need at least one entity. Let's define the User
entity.
@@filename(user.entity)
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column({ default: true })
isActive: boolean;
}
info Hint Learn more about entities in the TypeORM documentation.
The User
entity file sits in the users
directory. This directory contains all files related to the UsersModule
. You can decide where to keep your model files, however, we recommend creating them near their domain, in the corresponding module directory.
To begin using the User
entity, we need to let TypeORM know about it by inserting it into the entities
array in the module forRoot()
method options (unless you use a static glob path):
@@filename(app.module)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/user.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [User],
synchronize: true,
}),
],
})
export class AppModule {}
Next, let's look at the UsersModule
:
@@filename(users.module)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
This module uses the forFeature()
method to define which repositories are registered in the current scope. With that in place, we can inject the UsersRepository
into the UsersService
using the @InjectRepository()
decorator:
@@filename(users.service)
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
findAll(): Promise<User[]> {
return this.usersRepository.find();
}
findOne(id: string): Promise<User> {
return this.usersRepository.findOne(id);
}
async remove(id: string): Promise<void> {
await this.usersRepository.delete(id);
}
}
@@switch
import { Injectable, Dependencies } from '@nestjs/common';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './user.entity';
@Injectable()
@Dependencies(getRepositoryToken(User))
export class UsersService {
constructor(usersRepository) {
this.usersRepository = usersRepository;
}
findAll() {
return this.usersRepository.find();
}
findOne(id) {
return this.usersRepository.findOne(id);
}
async remove(id) {
await this.usersRepository.delete(id);
}
}
warning Notice Don't forget to import the
UsersModule
into the rootAppModule
.
If you want to use the repository outside of the module which imports TypeOrmModule.forFeature
, you'll need to re-export the providers generated by it.
You can do this by exporting the whole module, like this:
@@filename(users.module)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
exports: [TypeOrmModule]
})
export class UsersModule {}
Now if we import UsersModule
in UserHttpModule
, we can use @InjectRepository(User)
in the providers of the latter module.
@@filename(users-http.module)
import { Module } from '@nestjs/common';
import { UsersModule } from './user.module';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
@Module({
imports: [UsersModule],
providers: [UsersService],
controllers: [UsersController]
})
export class UserHttpModule {}
Relations are associations established between two or more tables. Relations are based on common fields from each table, often involving primary and foreign keys.
There are three types of relations:
One-to-one |
Every row in the primary table has one and only one associated row in the foreign table. Use the @OneToOne() decorator to define this type of relation. |
One-to-many / Many-to-one |
Every row in the primary table has one or more related rows in the foreign table. Use the @OneToMany() and @ManyToOne() decorators to define this type of relation. |
Many-to-many |
Every row in the primary table has many related rows in the foreign table, and every record in the foreign table has many related rows in the primary table. Use the @ManyToMany() decorator to define this type of relation. |
To define relations in entities, use the corresponding decorators. For example, to define that each User
can have multiple photos, use the @OneToMany()
decorator.
@@filename(user.entity)
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { Photo } from '../photos/photo.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column({ default: true })
isActive: boolean;
@OneToMany(type => Photo, photo => photo.user)
photos: Photo[];
}
info Hint To learn more about relations in TypeORM, visit the TypeORM documentation.
Manually adding entities to the entities
array of the connection options can be tedious. In addition, referencing entities from the root module breaks application domain boundaries and causes leaking implementation details to other parts of the application. To solve this issue, static glob paths can be used (e.g., dist/**/*.entity{{ '{' }} .ts,.js{{ '}' }}
).
Note, however, that glob paths are not supported by webpack, so if you are building your application within a monorepo, you won't be able to use them. To address this issue, an alternative solution is provided. To automatically load entities, set the autoLoadEntities
property of the configuration object (passed into the forRoot()
method) to true
, as shown below:
@@filename(app.module)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
...
autoLoadEntities: true,
}),
],
})
export class AppModule {}
With that option specified, every entity registered through the forFeature()
method will be automatically added to the entities
array of the configuration object.
warning Warning Note that entities that aren't registered through the
forFeature()
method, but are only referenced from the entity (via a relationship), won't be included by way of theautoLoadEntities
setting.
You can define an entity and its columns right in the model, using decorators. But some people prefer to define entities and their columns inside separate files using the "entity schemas".
import { EntitySchema } from 'typeorm';
import { User } from './user.entity';
export const UserSchema = new EntitySchema<User>({
name: 'User',
target: User,
columns: {
id: {
type: Number,
primary: true,
generated: true,
},
firstName: {
type: String,
},
lastName: {
type: String,
},
isActive: {
type: Boolean,
default: true,
},
},
relations: {
photos: {
type: 'one-to-many',
target: 'Photo', // the name of the PhotoSchema
},
},
});
warning error Warning If you provide the
target
option, thename
option value has to be the same as the name of the target class. If you do not provide thetarget
you can use any name.
Nest allows you to use an EntitySchema
instance wherever an Entity
is expected, for example:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserSchema } from './user.schema';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports: [TypeOrmModule.forFeature([UserSchema])],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
A database transaction symbolizes a unit of work performed within a database management system against a database, and treated in a coherent and reliable way independent of other transactions. A transaction generally represents any change in a database (learn more).
There are many different strategies to handle TypeORM transactions. We recommend using the QueryRunner
class because it gives full control over the transaction.
First, we need to inject the Connection
object into a class in the normal way:
@Injectable()
export class UsersService {
constructor(private connection: Connection) {}
}
info Hint The
Connection
class is imported from thetypeorm
package.
Now, we can use this object to create a transaction.
async createMany(users: User[]) {
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.manager.save(users[0]);
await queryRunner.manager.save(users[1]);
await queryRunner.commitTransaction();
} catch (err) {
// since we have errors lets rollback the changes we made
await queryRunner.rollbackTransaction();
} finally {
// you need to release a queryRunner which was manually instantiated
await queryRunner.release();
}
}
info Hint Note that the
connection
is used only to create theQueryRunner
. However, to test this class would require mocking the entireConnection
object (which exposes several methods). Thus, we recommend using a helper factory class (e.g.,QueryRunnerFactory
) and defining an interface with a limited set of methods required to maintain transactions. This technique makes mocking these methods pretty straightforward.
Alternatively, you can use the callback-style approach with the transaction
method of the Connection
object (read more).
async createMany(users: User[]) {
await this.connection.transaction(async manager => {
await manager.save(users[0]);
await manager.save(users[1]);
});
}
Using decorators to control the transaction (@Transaction()
and @TransactionManager()
) is not recommended.
With TypeORM subscribers, you can listen to specific entity events.
import {
Connection,
EntitySubscriberInterface,
EventSubscriber,
InsertEvent,
} from 'typeorm';
import { User } from './user.entity';
@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
constructor(connection: Connection) {
connection.subscribers.push(this);
}
listenTo() {
return User;
}
beforeInsert(event: InsertEvent<User>) {
console.log(`BEFORE USER INSERTED: `, event.entity);
}
}
error Warning Event subscribers can not be request-scoped.
Now, add the UserSubscriber
class to the providers
array:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UserSubscriber } from './user.subscriber';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService, UserSubscriber],
controllers: [UsersController],
})
export class UsersModule {}
info Hint Learn more about entity subscribers here.
Migrations provide a way to incrementally update the database schema to keep it in sync with the application's data model while preserving existing data in the database. To generate, run, and revert migrations, TypeORM provides a dedicated CLI.
Migration classes are separate from the Nest application source code. Their lifecycle is maintained by the TypeORM CLI. Therefore, you are not able to leverage dependency injection and other Nest specific features with migrations. To learn more about migrations, follow the guide in the TypeORM documentation.
Some projects require multiple database connections. This can also be achieved with this module. To work with multiple connections, first create the connections. In this case, connection naming becomes mandatory.
Suppose you have an Album
entity stored in its own database.
const defaultOptions = {
type: 'postgres',
port: 5432,
username: 'user',
password: 'password',
database: 'db',
synchronize: true,
};
@Module({
imports: [
TypeOrmModule.forRoot({
...defaultOptions,
host: 'user_db_host',
entities: [User],
}),
TypeOrmModule.forRoot({
...defaultOptions,
name: 'albumsConnection',
host: 'album_db_host',
entities: [Album],
}),
],
})
export class AppModule {}
warning Notice If you don't set the
name
for a connection, its name is set todefault
. Please note that you shouldn't have multiple connections without a name, or with the same name, otherwise they will get overridden.
At this point, you have User
and Album
entities registered with their own connection. With this setup, you have to tell the TypeOrmModule.forFeature()
method and the @InjectRepository()
decorator which connection should be used. If you do not pass any connection name, the default
connection is used.
@Module({
imports: [
TypeOrmModule.forFeature([User]),
TypeOrmModule.forFeature([Album], 'albumsConnection'),
],
})
export class AppModule {}
You can also inject the Connection
or EntityManager
for a given connection:
@Injectable()
export class AlbumsService {
constructor(
@InjectConnection('albumsConnection')
private connection: Connection,
@InjectEntityManager('albumsConnection')
private entityManager: EntityManager,
) {}
}
When it comes to unit testing an application, we usually want to avoid making a database connection, keeping our test suites independent and their execution process as fast as possible. But our classes might depend on repositories that are pulled from the connection instance. How do we handle that? The solution is to create mock repositories. In order to achieve that, we set up custom providers. Each registered repository is automatically represented by an <EntityName>Repository
token, where EntityName
is the name of your entity class.
The @nestjs/typeorm
package exposes the getRepositoryToken()
function which returns a prepared token based on a given entity.
@Module({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: mockRepository,
},
],
})
export class UsersModule {}
Now a substitute mockRepository
will be used as the UsersRepository
. Whenever any class asks for UsersRepository
using an @InjectRepository()
decorator, Nest will use the registered mockRepository
object.
TypeORM provides a feature called custom repositories. Custom repositories allow you to extend a base repository class, and enrich it with several special methods. To learn more about this feature, visit this page.
In order to create your custom repository, use the @EntityRepository()
decorator and extend the Repository
class.
@EntityRepository(Author)
export class AuthorRepository extends Repository<Author> {}
info Hint Both
@EntityRepository()
andRepository
are imported from thetypeorm
package.
Once the class is created, the next step is to delegate instantiation responsibility to Nest. For this, we have to pass theAuthorRepository
class to the TypeOrm.forFeature()
method.
@Module({
imports: [TypeOrmModule.forFeature([AuthorRepository])],
controller: [AuthorController],
providers: [AuthorService],
})
export class AuthorModule {}
Afterward, simply inject the repository using the following construction:
@Injectable()
export class AuthorService {
constructor(private authorRepository: AuthorRepository) {}
}
You may want to pass your repository module options asynchronously instead of statically. In this case, use the forRootAsync()
method, which provides several ways to deal with async configuration.
One approach is to use a factory function:
TypeOrmModule.forRootAsync({
useFactory: () => ({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
});
Our factory behaves like any other asynchronous provider (e.g., it can be async
and it's able to inject dependencies through inject
).
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get('HOST'),
port: +configService.get<number>('PORT'),
username: configService.get('USERNAME'),
password: configService.get('PASSWORD'),
database: configService.get('DATABASE'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
inject: [ConfigService],
});
Alternatively, you can use the useClass
syntax:
TypeOrmModule.forRootAsync({
useClass: TypeOrmConfigService,
});
The construction above will instantiate TypeOrmConfigService
inside TypeOrmModule
and use it to provide an options object by calling createTypeOrmOptions()
. Note that this means that the TypeOrmConfigService
has to implement the TypeOrmOptionsFactory
interface, as shown below:
@Injectable()
class TypeOrmConfigService implements TypeOrmOptionsFactory {
createTypeOrmOptions(): TypeOrmModuleOptions {
return {
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
};
}
}
In order to prevent the creation of TypeOrmConfigService
inside TypeOrmModule
and use a provider imported from a different module, you can use the useExisting
syntax.
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});
This construction works the same as useClass
with one critical difference - TypeOrmModule
will lookup imported modules to reuse an existing ConfigService
instead of instantiating a new one.
A working example is available here.
An alternative to using TypeORM is to use the Sequelize ORM with the @nestjs/sequelize
package. In addition, we leverage the sequelize-typescript package which provides a set of additional decorators to declaratively define entities.
To begin using it, we first install the required dependencies. In this chapter, we'll demonstrate using the popular MySQL Relational DBMS, but Sequelize provides support for many relational databases, such as PostgreSQL, MySQL, Microsoft SQL Server, SQLite, and MariaDB. The procedure we walk through in this chapter will be the same for any database supported by Sequelize. You'll simply need to install the associated client API libraries for your selected database.
$ npm install --save @nestjs/sequelize sequelize sequelize-typescript mysql2
$ npm install --save-dev @types/sequelize
Once the installation process is complete, we can import the SequelizeModule
into the root AppModule
.
@@filename(app.module)
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
@Module({
imports: [
SequelizeModule.forRoot({
dialect: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
models: [],
}),
],
})
export class AppModule {}
The forRoot()
method supports all the configuration properties exposed by the Sequelize constructor (read more). In addition, there are several extra configuration properties described below.
retryAttempts |
Number of attempts to connect to the database (default: 10 ) |
retryDelay |
Delay between connection retry attempts (ms) (default: 3000 ) |
autoLoadModels |
If true , models will be loaded automatically (default: false ) |
keepConnectionAlive |
If true , connection will not be closed on the application shutdown (default: false ) |
synchronize |
If true , automatically loaded models will be synchronized (default: false ) |
Once this is done, the Sequelize
object will be available to inject across the entire project (without needing to import any modules), for example:
@@filename(app.service)
import { Injectable } from '@nestjs/common';
import { Sequelize } from 'sequelize-typescript';
@Injectable()
export class AppService {
constructor(private sequelize: Sequelize) {}
}
@@switch
import { Injectable } from '@nestjs/common';
import { Sequelize } from 'sequelize-typescript';
@Dependencies(Sequelize)
@Injectable()
export class AppService {
constructor(sequelize) {
this.sequelize = sequelize;
}
}
Sequelize implements the Active Record pattern. With this pattern, you use model classes directly to interact with the database. To continue the example, we need at least one model. Let's define the User
model.
@@filename(user.model)
import { Column, Model, Table } from 'sequelize-typescript';
@Table
export class User extends Model<User> {
@Column
firstName: string;
@Column
lastName: string;
@Column({ defaultValue: true })
isActive: boolean;
}
info Hint Learn more about the available decorators here.
The User
model file sits in the users
directory. This directory contains all files related to the UsersModule
. You can decide where to keep your model files, however, we recommend creating them near their domain, in the corresponding module directory.
To begin using the User
model, we need to let Sequelize know about it by inserting it into the models
array in the module forRoot()
method options:
@@filename(app.module)
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './users/user.model';
@Module({
imports: [
SequelizeModule.forRoot({
dialect: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
models: [User],
}),
],
})
export class AppModule {}
Next, let's look at the UsersModule
:
@@filename(users.module)
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './user.model';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports: [SequelizeModule.forFeature([User])],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
This module uses the forFeature()
method to define which models are registered in the current scope. With that in place, we can inject the UserModel
into the UsersService
using the @InjectModel()
decorator:
@@filename(users.service)
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { User } from './user.model';
@Injectable()
export class UsersService {
constructor(
@InjectModel(User)
private userModel: typeof User,
) {}
async findAll(): Promise<User[]> {
return this.userModel.findAll();
}
findOne(id: string): Promise<User> {
return this.userModel.findOne({
where: {
id,
},
});
}
async remove(id: string): Promise<void> {
const user = await this.findOne(id);
await user.destroy();
}
}
@@switch
import { Injectable, Dependencies } from '@nestjs/common';
import { getModelToken } from '@nestjs/sequelize';
import { User } from './user.model';
@Injectable()
@Dependencies(getModelToken(User))
export class UsersService {
constructor(usersRepository) {
this.usersRepository = usersRepository;
}
async findAll() {
return this.userModel.findAll();
}
findOne(id) {
return this.userModel.findOne({
where: {
id,
},
});
}
async remove(id) {
const user = await this.findOne(id);
await user.destroy();
}
}
warning Notice Don't forget to import the
UsersModule
into the rootAppModule
.
If you want to use the repository outside of the module which imports SequelizeModule.forFeature
, you'll need to re-export the providers generated by it.
You can do this by exporting the whole module, like this:
@@filename(users.module)
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './user.entity';
@Module({
imports: [SequelizeModule.forFeature([User])],
exports: [SequelizeModule]
})
export class UsersModule {}
Now if we import UsersModule
in UserHttpModule
, we can use @InjectModel(User)
in the providers of the latter module.
@@filename(users-http.module)
import { Module } from '@nestjs/common';
import { UsersModule } from './user.module';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
@Module({
imports: [UsersModule],
providers: [UsersService],
controllers: [UsersController]
})
export class UserHttpModule {}
Relations are associations established between two or more tables. Relations are based on common fields from each table, often involving primary and foreign keys.
There are three types of relations:
One-to-one |
Every row in the primary table has one and only one associated row in the foreign table |
One-to-many / Many-to-one |
Every row in the primary table has one or more related rows in the foreign table |
Many-to-many |
Every row in the primary table has many related rows in the foreign table, and every record in the foreign table has many related rows in the primary table |
To define relations in entities, use the corresponding decorators. For example, to define that each User
can have multiple photos, use the @HasMany()
decorator.
@@filename(user.entity)
import { Column, Model, Table, HasMany } from 'sequelize-typescript';
import { Photo } from '../photos/photo.model';
@Table
export class User extends Model<User> {
@Column
firstName: string;
@Column
lastName: string;
@Column({ defaultValue: true })
isActive: boolean;
@HasMany(() => Photo)
photos: Photo[];
}
info Hint To learn more about associations in Sequelize, read this chapter.
Manually adding models to the models
array of the connection options can be tedious. In addition, referencing models from the root module breaks application domain boundaries and causes leaking implementation details to other parts of the application. To solve this issue, automatically load models by setting both autoLoadModels
and synchronize
properties of the configuration object (passed into the forRoot()
method) to true
, as shown below:
@@filename(app.module)
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
@Module({
imports: [
SequelizeModule.forRoot({
...
autoLoadModels: true,
synchronize: true,
}),
],
})
export class AppModule {}
With that option specified, every model registered through the forFeature()
method will be automatically added to the models
array of the configuration object.
warning Warning Note that models that aren't registered through the
forFeature()
method, but are only referenced from the model (via an association), won't be included.
A database transaction symbolizes a unit of work performed within a database management system against a database, and treated in a coherent and reliable way independent of other transactions. A transaction generally represents any change in a database (learn more).
There are many different strategies to handle Sequelize transactions. Below is a sample implementation of a managed transaction (auto-callback).
First, we need to inject the Sequelize
object into a class in the normal way:
@Injectable()
export class UsersService {
constructor(private sequelize: Sequelize) {}
}
info Hint The
Sequelize
class is imported from thesequelize-typescript
package.
Now, we can use this object to create a transaction.
async createMany() {
try {
await this.sequelize.transaction(async t => {
const transactionHost = { transaction: t };
await this.userModel.create(
{ firstName: 'Abraham', lastName: 'Lincoln' },
transactionHost,
);
await this.userModel.create(
{ firstName: 'John', lastName: 'Boothe' },
transactionHost,
);
});
} catch (err) {
// Transaction has been rolled back
// err is whatever rejected the promise chain returned to the transaction callback
}
}
info Hint Note that the
Sequelize
instance is used only to start the transaction. However, to test this class would require mocking the entireSequelize
object (which exposes several methods). Thus, we recommend using a helper factory class (e.g.,TransactionRunner
) and defining an interface with a limited set of methods required to maintain transactions. This technique makes mocking these methods pretty straightforward.
Migrations provide a way to incrementally update the database schema to keep it in sync with the application's data model while preserving existing data in the database. To generate, run, and revert migrations, Sequelize provides a dedicated CLI.
Migration classes are separate from the Nest application source code. Their lifecycle is maintained by the Sequelize CLI. Therefore, you are not able to leverage dependency injection and other Nest specific features with migrations. To learn more about migrations, follow the guide in the Sequelize documentation.
Some projects require multiple database connections. This can also be achieved with this module. To work with multiple connections, first create the connections. In this case, connection naming becomes mandatory.
Suppose you have an Album
entity stored in its own database.
const defaultOptions = {
dialect: 'postgres',
port: 5432,
username: 'user',
password: 'password',
database: 'db',
synchronize: true,
};
@Module({
imports: [
SequelizeModule.forRoot({
...defaultOptions,
host: 'user_db_host',
models: [User],
}),
SequelizeModule.forRoot({
...defaultOptions,
name: 'albumsConnection',
host: 'album_db_host',
models: [Album],
}),
],
})
export class AppModule {}
warning Notice If you don't set the
name
for a connection, its name is set todefault
. Please note that you shouldn't have multiple connections without a name, or with the same name, otherwise they will get overridden.
At this point, you have User
and Album
models registered with their own connection. With this setup, you have to tell the SequelizeModule.forFeature()
method and the @InjectModel()
decorator which connection should be used. If you do not pass any connection name, the default
connection is used.
@Module({
imports: [
SequelizeModule.forFeature([User]),
SequelizeModule.forFeature([Album], 'albumsConnection'),
],
})
export class AppModule {}
You can also inject the Sequelize
instance for a given connection:
@Injectable()
export class AlbumsService {
constructor(
@InjectConnection('albumsConnection')
private sequelize: Sequelize,
) {}
}
When it comes to unit testing an application, we usually want to avoid making a database connection, keeping our test suites independent and their execution process as fast as possible. But our classes might depend on models that are pulled from the connection instance. How do we handle that? The solution is to create mock models. In order to achieve that, we set up custom providers. Each registered model is automatically represented by a <ModelName>Model
token, where ModelName
is the name of your model class.
The @nestjs/sequelize
package exposes the getModelToken()
function which returns a prepared token based on a given model.
@Module({
providers: [
UsersService,
{
provide: getModelToken(User),
useValue: mockModel,
},
],
})
export class UsersModule {}
Now a substitute mockModel
will be used as the UserModel
. Whenever any class asks for UserModel
using an @InjectModel()
decorator, Nest will use the registered mockModel
object.
You may want to pass your SequelizeModule
options asynchronously instead of statically. In this case, use the forRootAsync()
method, which provides several ways to deal with async configuration.
One approach is to use a factory function:
SequelizeModule.forRootAsync({
useFactory: () => ({
dialect: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
models: [],
}),
});
Our factory behaves like any other asynchronous provider (e.g., it can be async
and it's able to inject dependencies through inject
).
SequelizeModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
dialect: 'mysql',
host: configService.get('HOST'),
port: +configService.get('PORT'),
username: configService.get('USERNAME'),
password: configService.get('PASSWORD'),
database: configService.get('DATABASE'),
models: [],
}),
inject: [ConfigService],
});
Alternatively, you can use the useClass
syntax:
SequelizeModule.forRootAsync({
useClass: SequelizeConfigService,
});
The construction above will instantiate SequelizeConfigService
inside SequelizeModule
and use it to provide an options object by calling createSequelizeOptions()
. Note that this means that the SequelizeConfigService
has to implement the SequelizeOptionsFactory
interface, as shown below:
@Injectable()
class SequelizeConfigService implements SequelizeOptionsFactory {
createSequelizeOptions(): SequelizeModuleOptions {
return {
dialect: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
models: [],
};
}
}
In order to prevent the creation of SequelizeConfigService
inside SequelizeModule
and use a provider imported from a different module, you can use the useExisting
syntax.
SequelizeModule.forRootAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});
This construction works the same as useClass
with one critical difference - SequelizeModule
will lookup imported modules to reuse an existing ConfigService
instead of instantiating a new one.
A working example is available here.
Nest supports two methods for integrating with the MongoDB database. You can either use the built-in TypeORM module described here, which has a connector for MongoDB, or use Mongoose, the most popular MongoDB object modeling tool. In this chapter we'll describe the latter, using the dedicated @nestjs/mongoose
package.
Start by installing the required dependencies:
$ npm install --save @nestjs/mongoose mongoose
$ npm install --save-dev @types/mongoose
Once the installation process is complete, we can import the MongooseModule
into the root AppModule
.
@@filename(app.module)
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [MongooseModule.forRoot('mongodb://localhost/nest')],
})
export class AppModule {}
The forRoot()
method accepts the same configuration object as mongoose.connect()
from the Mongoose package, as described here.
With Mongoose, everything is derived from a Schema. Each schema maps to a MongoDB collection and defines the shape of the documents within that collection. Schemas are used to define Models. Models are responsible for creating and reading documents from the underlying MongoDB database.
Schemas can be created with NestJS decorators, or with Mongoose itself manually. Using decorators to create schemas greatly reduces boilerplate and improves overall code readability.
Let's define the CatSchema
:
@@filename(schemas/cat.schema)
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type CatDocument = Cat & Document;
@Schema()
export class Cat {
@Prop()
name: string;
@Prop()
age: number;
@Prop()
breed: string;
}
export const CatSchema = SchemaFactory.createForClass(Cat);
info Hint Note you can also generate a raw schema definition using the
DefinitionsFactory
class (from thenestjs/mongoose
). This allows you to manually modify the schema definition generated based on the metadata you provided. This is useful for certain edge-cases where it may be hard to represent everything with decorators.
The @Schema()
decorator marks a class as a schema definition. It maps our Cat
class to a MongoDB collection of the same name, but with an additional “s” at the end - so the final mongo collection name will be cats
. This decorator accepts a single optional argument which is a schema options object. Think of it as the object you would normally pass as a second argument of the mongoose.Schema
class' constructor (e.g., new mongoose.Schema(_, options)
)). To learn more about available schema options, see this chapter.
The @Prop()
decorator defines a property in the document. For example, in the schema definition above, we defined three properties: name
, age
, and breed
. The schema types for these properties are automatically inferred thanks to TypeScript metadata (and reflection) capabilities. However, in more complex scenarios in which types cannot be implicitly reflected (for example, arrays or nested object structures), types must be indicated explicitly, as follows:
@Prop([String])
tags: string[];
Alternatively, the @Prop()
decorator accepts an options object argument (read more about the available options). With this, you can indicate whether a property is required or not, specify a default value, or mark it as immutable. For example:
@Prop({ required: true })
name: string;
Finally, the raw schema definition can also be passed to the decorator. This is useful when, for example, a property represents a nested object which is not defined as a class. For this, use the raw()
function from the @nestjs/mongoose
package, as follows:
@Prop(raw({
firstName: { type: String },
lastName: { type: String }
}))
details: Record<string, any>;
Alternatively, if you prefer not using decorators, you can define a schema manually. For example:
export const CatSchema = new mongoose.Schema({
name: String,
age: Number,
breed: String,
});
The cat.schema
file resides in a folder in the cats
directory, where we also define the CatsModule
. While you can store schema files wherever you prefer, we recommend storing them near their related domain objects, in the appropriate module directory.
Let's look at the CatsModule
:
@@filename(cats.module)
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { Cat, CatSchema } from './schemas/cat.schema';
@Module({
imports: [MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }])],
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
The MongooseModule
provides the forFeature()
method to configure the module, including defining which models should be registered in the current scope. If you also want to use the models in another module, add MongooseModule to the exports
section of CatsModule
and import CatsModule
in the other module.
Once you've registered the schema, you can inject a Cat
model into the CatsService
using the @InjectModel()
decorator:
@@filename(cats.service)
import { Model } from 'mongoose';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Cat, CatDocument } from './schemas/cat.schema';
import { CreateCatDto } from './dto/create-cat.dto';
@Injectable()
export class CatsService {
constructor(@InjectModel(Cat.name) private catModel: Model<CatDocument>) {}
async create(createCatDto: CreateCatDto): Promise<Cat> {
const createdCat = new this.catModel(createCatDto);
return createdCat.save();
}
async findAll(): Promise<Cat[]> {
return this.catModel.find().exec();
}
}
@@switch
import { Model } from 'mongoose';
import { Injectable, Dependencies } from '@nestjs/common';
import { getModelToken } from '@nestjs/mongoose';
import { Cat } from './schemas/cat.schema';
@Injectable()
@Dependencies(getModelToken(Cat.name))
export class CatsService {
constructor(catModel) {
this.catModel = catModel;
}
async create(createCatDto) {
const createdCat = new this.catModel(createCatDto);
return createdCat.save();
}
async findAll() {
return this.catModel.find().exec();
}
}
At times you may need to access the native Mongoose Connection object. For example, you may want to make native API calls on the connection object. You can inject the Mongoose Connection by using the @InjectConnection()
decorator as follows:
import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@nestjs/mongoose';
import { Connection } from 'mongoose';
@Injectable()
export class CatsService {
constructor(@InjectConnection() private connection: Connection) {}
}
Some projects require multiple database connections. This can also be achieved with this module. To work with multiple connections, first create the connections. In this case, connection naming becomes mandatory.
@@filename(app.module)
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/test', {
connectionName: 'cats',
}),
MongooseModule.forRoot('mongodb://localhost/users', {
connectionName: 'users',
}),
],
})
export class AppModule {}
warning Notice Please note that you shouldn't have multiple connections without a name, or with the same name, otherwise they will get overridden.
With this setup, you have to tell the MongooseModule.forFeature()
function which connection should be used.
@Module({
imports: [
MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }], 'cats'),
],
})
export class AppModule {}
You can also inject the Connection
for a given connection:
import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@nestjs/mongoose';
import { Connection } from 'mongoose';
@Injectable()
export class CatsService {
constructor(@InjectConnection('cats') private connection: Connection) {}
}
Middleware (also called pre and post hooks) are functions which are passed control during execution of asynchronous functions. Middleware is specified on the schema level and is useful for writing plugins (source). Calling pre()
or post()
after compiling a model does not work in Mongoose. To register a hook before model registration, use the forFeatureAsync()
method of the MongooseModule
along with a factory provider (i.e., useFactory
). With this technique, you can access a schema object, then use the pre()
or post()
method to register a hook on that schema. See example below:
@Module({
imports: [
MongooseModule.forFeatureAsync([
{
name: Cat.name,
useFactory: () => {
const schema = CatsSchema;
schema.pre('save', () => console.log('Hello from pre save'));
return schema;
},
},
]),
],
})
export class AppModule {}
Like other factory providers, our factory function can be async
and can inject dependencies through inject
.
@Module({
imports: [
MongooseModule.forFeatureAsync([
{
name: Cat.name,
imports: [ConfigModule],
useFactory: (configService: ConfigService) => {
const schema = CatsSchema;
schema.pre('save', () =>
console.log(
`${configService.get('APP_NAME')}: Hello from pre save`,
),
);
return schema;
},
inject: [ConfigService],
},
]),
],
})
export class AppModule {}
To register a plugin for a given schema, use the forFeatureAsync()
method.
@Module({
imports: [
MongooseModule.forFeatureAsync([
{
name: Cat.name,
useFactory: () => {
const schema = CatsSchema;
schema.plugin(require('mongoose-autopopulate'));
return schema;
},
},
]),
],
})
export class AppModule {}
To register a plugin for all schemas at once, call the .plugin()
method of the Connection
object. You should access the connection before models are created; to do this, use the connectionFactory
:
@@filename(app.module)
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/test', {
connectionFactory: (connection) => {
connection.plugin(require('mongoose-autopopulate'));
return connection;
}
}),
],
})
export class AppModule {}
When unit testing an application, we usually want to avoid any database connection, making our test suites simpler to set up and faster to execute. But our classes might depend on models that are pulled from the connection instance. How do we resolve these classes? The solution is to create mock models.
To make this easier, the @nestjs/mongoose
package exposes a getModelToken()
function that returns a prepared injection token based on a token name. Using this token, you can easily provide a mock implementation using any of the standard custom provider techniques, including useClass
, useValue
, and useFactory
. For example:
@Module({
providers: [
CatsService,
{
provide: getModelToken(Cat.name),
useValue: catModel,
},
],
})
export class CatsModule {}
In this example, a hardcoded catModel
(object instance) will be provided whenever any consumer injects a Model<Cat>
using an @InjectModel()
decorator.
When you need to pass module options asynchronously instead of statically, use the forRootAsync()
method. As with most dynamic modules, Nest provides several techniques to deal with async configuration.
One technique is to use a factory function:
MongooseModule.forRootAsync({
useFactory: () => ({
uri: 'mongodb://localhost/nest',
}),
});
Like other factory providers, our factory function can be async
and can inject dependencies through inject
.
MongooseModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
uri: configService.get<string>('MONGODB_URI'),
}),
inject: [ConfigService],
});
Alternatively, you can configure the MongooseModule
using a class instead of a factory, as shown below:
MongooseModule.forRootAsync({
useClass: MongooseConfigService,
});
The construction above instantiates MongooseConfigService
inside MongooseModule
, using it to create the required options object. Note that in this example, the MongooseConfigService
has to implement the MongooseOptionsFactory
interface, as shown below. The MongooseModule
will call the createMongooseOptions()
method on the instantiated object of the supplied class.
@Injectable()
class MongooseConfigService implements MongooseOptionsFactory {
createMongooseOptions(): MongooseModuleOptions {
return {
uri: 'mongodb://localhost/nest',
};
}
}
If you want to reuse an existing options provider instead of creating a private copy inside the MongooseModule
, use the useExisting
syntax.
MongooseModule.forRootAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});
A working example is available here.
Applications often run in different environments. Depending on the environment, different configuration settings should be used. For example, usually the local environment relies on specific database credentials, valid only for the local DB instance. The production environment would use a separate set of DB credentials. Since configuration variables change, best practice is to store configuration variables in the environment.
Externally defined environment variables are visible inside Node.js through the process.env
global. We could try to solve the problem of multiple environments by setting the environment variables separately in each environment. This can quickly get unwieldy, especially in the development and testing environments where these values need to be easily mocked and/or changed.
In Node.js applications, it's common to use .env
files, holding key-value pairs where each key represents a particular value, to represent each environment. Running an app in different environments is then just a matter of swapping in the correct .env
file.
A good approach for using this technique in Nest is to create a ConfigModule
that exposes a ConfigService
which loads the appropriate .env
file. While you may choose to write such a module yourself, for convenience Nest provides the @nestjs/config
package out-of-the box. We'll cover this package in the current chapter.
To begin using it, we first install the required dependency.
$ npm i --save @nestjs/config
info Hint The
@nestjs/config
package internally uses dotenv.
Once the installation process is complete, we can import the ConfigModule
. Typically, we'll import it into the root AppModule
and control its behavior using the .forRoot()
static method. During this step, environment variable key/value pairs are parsed and resolved. Later, we'll see several options for accessing the ConfigService
class of the ConfigModule
in our other feature modules.
@@filename(app.module)
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot()],
})
export class AppModule {}
The above code will load and parse a .env
file from the default location (the project root directory), merge key/value pairs from the .env
file with environment variables assigned to process.env
, and store the result in a private structure that you can access through the ConfigService
. The forRoot()
method registers the ConfigService
provider, which provides a get()
method for reading these parsed/merged configuration variables. Since @nestjs/config
relies on dotenv, it uses that package's rules for resolving conflicts in environment variable names. When a key exists both in the runtime environment as an environment variable (e.g., via OS shell exports like export DATABASE_USER=test
) and in a .env
file, the runtime environment variable takes precedence.
A sample .env
file looks something like this:
DATABASE_USER=test
DATABASE_PASSWORD=test
By default, the package looks for a .env
file in the root directory of the application. To specify another path for the .env
file, set the envFilePath
property of an (optional) options object you pass to forRoot()
, as follows:
ConfigModule.forRoot({
envFilePath: '.development.env',
});
You can also specify multiple paths for .env
files like this:
ConfigModule.forRoot({
envFilePath: ['.env.development.local', '.env.development'],
});
If a variable is found in multiple files, the first one takes precedence.
If you don't want to load the .env
file, but instead would like to simply access environment variables from the runtime environment (as with OS shell exports like export DATABASE_USER=test
), set the options object's ignoreEnvFile
property to true
, as follows:
ConfigModule.forRoot({
ignoreEnvFile: true,
});
When you want to use ConfigModule
in other modules, you'll need to import it (as is standard with any Nest module). Alternatively, declare it as a global module by setting the options object's isGlobal
property to true
, as shown below. In that case, you will not need to import ConfigModule
in other modules once it's been loaded in the root module (e.g., AppModule
).
ConfigModule.forRoot({
isGlobal: true,
});
For more complex projects, you may utilize custom configuration files to return nested configuration objects. This allows you to group related configuration settings by function (e.g., database-related settings), and to store related settings in individual files to help manage them independently.
A custom configuration file exports a factory function that returns a configuration object. The configuration object can be any arbitrarily nested plain JavaScript object. The process.env
object will contain the fully resolved environment variable key/value pairs (with .env
file and externally defined variables resolved and merged as described above). Since you control the returned configuration object, you can add any required logic to cast values to an appropriate type, set default values, etc. For example:
@@filename(config/configuration)
export default () => ({
port: parseInt(process.env.PORT, 10) || 3000,
database: {
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT, 10) || 5432
}
});
We load this file using the load
property of the options object we pass to the ConfigModule.forRoot()
method:
import configuration from './config/configuration';
@Module({
imports: [
ConfigModule.forRoot({
load: [configuration],
}),
],
})
export class AppModule {}
info Notice The value assigned to the
load
property is an array, allowing you to load multiple configuration files (e.g.load: [databaseConfig, authConfig]
)
To access configuration values from our ConfigService
, we first need to inject ConfigService
. As with any provider, we need to import its containing module - the ConfigModule
- into the module that will use it (unless you set the isGlobal
property in the options object passed to the ConfigModule.forRoot()
method to true
). Import it into a feature module as shown below.
@@filename(feature.module)
@Module({
imports: [ConfigModule],
// ...
})
Then we can inject it using standard constructor injection:
constructor(private configService: ConfigService) {}
And use it in our class:
// get an environment variable
const dbUser = this.configService.get<string>('DATABASE_USER');
// get a custom configuration value
const dbHost = this.configService.get<string>('database.host');
As shown above, use the configService.get()
method to get a simple environment variable by passing the variable name. You can do TypeScript type hinting by passing the type, as shown above (e.g., get<string>(...)
). The get()
method can also traverse a nested custom configuration object (created via a Custom configuration file), as shown in the second example above.
You can also get the whole nested custom configuration object using an interface as the type hint:
interface DatabaseConfig {
host: string;
port: number;
}
const dbConfig = this.configService.get<DatabaseConfig>('database');
// you can now use `dbConfig.port` and `dbConfig.host`
const port = dbConfig.port;
The get()
method also takes an optional second argument defining a default value, which will be returned when the key doesn't exist, as shown below:
// use "localhost" when "database.host" is not defined
const dbHost = this.configService.get<string>('database.host', 'localhost');
ConfigService
has an optional generic (type argument) to help prevent accessing a config property that does not exist. Use it as shown below:
interface EnvironmentVariables {
PORT: number;
TIMEOUT: string;
}
// somewhere in the code
constructor(private configService: ConfigService<EnvironmentVariables>) {
// this is valid
const port = this.configService.get<number>('PORT');
// this is invalid as URL is not a property on the EnvironmentVariables interface
const url = this.configService.get<string>('URL');
}
warning Notice If you have nested properties in your config, like in the
database.host
example above, the interface must have a matching'database.host': string;
property. Otherwise a TypeScript error will be thrown.
The ConfigModule
allows you to define and load multiple custom configuration files, as shown in Custom configuration files above. You can manage complex configuration object hierarchies with nested configuration objects as shown in that section. Alternatively, you can return a "namespaced" configuration object with the registerAs()
function as follows:
@@filename(config/database.config)
export default registerAs('database', () => ({
host: process.env.DATABASE_HOST,
port: process.env.DATABASE_PORT || 5432
}));
As with custom configuration files, inside your registerAs()
factory function, the process.env
object will contain the fully resolved environment variable key/value pairs (with .env
file and externally defined variables resolved and merged as described above).
info Hint The
registerAs
function is exported from the@nestjs/config
package.
Load a namespaced configuration with the load
property of the forRoot()
method's options object, in the same way you load a custom configuration file:
import databaseConfig from './config/database.config';
@Module({
imports: [
ConfigModule.forRoot({
load: [databaseConfig],
}),
],
})
export class AppModule {}
Now, to get the host
value from the database
namespace, use dot notation. Use 'database'
as the prefix to the property name, corresponding to the name of the namespace (passed as the first argument to the registerAs()
function):
const dbHost = this.configService.get<string>('database.host');
A reasonable alternative is to inject the database
namespace directly. This allows us to benefit from strong typing:
constructor(
@Inject(databaseConfig.KEY)
private dbConfig: ConfigType<typeof databaseConfig>,
) {}
info Hint The
ConfigType
is exported from the@nestjs/config
package.
Thus far, we've processed configuration files in our root module (e.g., AppModule
), with the forRoot()
method. Perhaps you have a more complex project structure, with feature-specific configuration files located in multiple different directories. Rather than load all these files in the root module, the @nestjs/config
package provides a feature called partial registration, which references only the configuration files associated with each feature module. Use the forFeature()
static method within a feature module to perform this partial registration, as follows:
import databaseConfig from './config/database.config';
@Module({
imports: [ConfigModule.forFeature(databaseConfig)],
})
export class DatabaseModule {}
info Warning In some circumstances, you may need to access properties loaded via partial registration using the
onModuleInit()
hook, rather than in a constructor. This is because theforFeature()
method is run during module initialization, and the order of module initialization is indeterminate. If you access values loaded this way by another module, in a constructor, the module that the configuration depends upon may not yet have initialized. TheonModuleInit()
method runs only after all modules it depends upon have been initialized, so this technique is safe.
It is standard practice to throw an exception during application startup if required environment variables haven't been provided or if they don't meet certain validation rules. The @nestjs/config
package enables use of the Joi npm package to support this type of validation. With Joi, you define an object schema and validate JavaScript objects against it.
Install Joi (and its types, for TypeScript users):
$ npm install --save @hapi/joi
$ npm install --save-dev @types/hapi__joi
warning Notice The latest version of
@hapi/joi
requires you to be running Node v12 or later. For older versions of node, please installv16.1.8
. This is mainly after the release ofv17.0.2
which causes errors during build time. For more information, please refer to their documentation & this github issue.
Now we can define a Joi validation schema and pass it via the validationSchema
property of the forRoot()
method's options object, as shown below:
@@filename(app.module)
import * as Joi from '@hapi/joi';
@Module({
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test', 'provision')
.default('development'),
PORT: Joi.number().default(3000),
}),
}),
],
})
export class AppModule {}
By default, all schema keys are considered optional. Here, we set default values for NODE_ENV
and PORT
which will be used if we don't provide these variables in the environment (.env
file or process environment). Alternatively, we can use the required()
validation method to require that a value must be defined in the environment (.env
file or process environment). In this case, the validation step will throw an exception if we don't provide the variable in the environment. See Joi validation methods for more on how to construct validation schemas.
By default, unknown environment variables (environment variables whose keys are not present in the schema) are allowed and do not trigger a validation exception. By default, all validation errors are reported. You can alter these behaviors by passing an options object via the validationOptions
key of the forRoot()
options object. This options object can contain any of the standard validation options properties provided by Joi validation options. For example, to reverse the two settings above, pass options like this:
@@filename(app.module)
import * as Joi from '@hapi/joi';
@Module({
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test', 'provision')
.default('development'),
PORT: Joi.number().default(3000),
}),
validationOptions: {
allowUnknown: false,
abortEarly: true,
},
}),
],
})
export class AppModule {}
The @nestjs/config
package uses default settings of:
allowUnknown
: controls whether or not to allow unknown keys in the environment variables. Default istrue
abortEarly
: if true, stops validation on the first error; if false, returns all errors. Defaults tofalse
.
Note that once you decide to pass a validationOptions
object, any settings you do not explicitly pass will default to Joi
standard defaults (not the @nestjs/config
defaults). For example, if you leave allowUnknowns
unspecified in your custom validationOptions
object, it will have the Joi
default value of false
. Hence, it is probably safest to specify both of these settings in your custom object.
ConfigService
defines a generic get()
method to retrieve a configuration value by key. We may also add getter
functions to enable a little more natural coding style:
@@filename()
@Injectable()
export class ApiConfigService {
constructor(private configService: ConfigService) {}
get isAuthEnabled(): boolean {
return this.configService.get('AUTH_ENABLED') === 'true';
}
}
@@switch
@Dependencies(ConfigService)
@Injectable()
export class ApiConfigService {
constructor(configService) {
this.configService = configService;
}
get isAuthEnabled() {
return this.configService.get('AUTH_ENABLED') === 'true';
}
}
Now we can use the getter function as follows:
@@filename(app.service)
@Injectable()
export class AppService {
constructor(apiConfigService: ApiConfigService) {
if (apiConfigService.isAuthEnabled) {
// Authentication is enabled
}
}
}
@@switch
@Dependencies(ApiConfigService)
@Injectable()
export class AppService {
constructor(apiConfigService) {
if (apiConfigService.isAuthEnabled) {
// Authentication is enabled
}
}
}
The @nestjs/config
package supports environment variable expansion. With this technique, you can create nested environment variables, where one variable is referred to within the definition of another. For example:
APP_URL=mywebsite.com
SUPPORT_EMAIL=support@${APP_URL}
With this construction, the variable SUPPORT_EMAIL
resolves to 'support@mywebsite.com'
. Note the use of the ${{ '{' }}...{{ '}' }}
syntax to trigger resolving the value of the variable APP_URL
inside the definition of SUPPORT_EMAIL
.
info Hint For this feature,
@nestjs/config
package internally uses dotenv-expand.
Enable environment variable expansion using the expandVariables
property in the options object passed to the forRoot()
method of the ConfigModule
, as shown below:
@@filename(app.module)
@Module({
imports: [
ConfigModule.forRoot({
// ...
expandVariables: true,
}),
],
})
export class AppModule {}
While our config is a stored in a service, it can still be used in the main.ts
file. This way, you can use it to store variables such as the application port or the CORS host.
To access it, you must use the app.get()
method, followed by the service reference:
const configService = app.get(ConfigService);
You can then use it as usual, by calling the get
method with the configuration key:
const port = configService.get('PORT');
It is best practice to validate the correctness of any data sent into a web application. To automatically validate incoming requests, Nest provides several pipes available right out-of-the-box:
ValidationPipe
ParseIntPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
The ValidationPipe
makes use of the powerful class-validator package and its declarative validation decorators. The ValidationPipe
provides a convenient approach to enforce validation rules for all incoming client payloads, where the specific rules are declared with simple annotations in local class/DTO declarations in each module.
In the Pipes chapter, we went through the process of building simple pipes and binding them to controllers, methods or to the global app to demonstrate how the process works. Be sure to review that chapter to best understand the topics of this chapter. Here, we'll focus on various real world use cases of the ValidationPipe
, and show how to use some of its advanced customization features.
info Hint The
ValidationPipe
is imported from the@nestjs/common
package.
Because this pipe uses the class-validator
and class-transformer
libraries, there are many options available. You configure these settings via a configuration object passed to the pipe. Following are the built-in options:
export interface ValidationPipeOptions extends ValidatorOptions {
transform?: boolean;
disableErrorMessages?: boolean;
exceptionFactory?: (errors: ValidationError[]) => any;
}
In addition to these, all class-validator
options (inherited from the ValidatorOptions
interface) are available:
Option | Type | Description |
---|---|---|
skipMissingProperties |
boolean |
If set to true, validator will skip validation of all properties that are missing in the validating object. |
whitelist |
boolean |
If set to true, validator will strip validated (returned) object of any properties that do not use any validation decorators. |
forbidNonWhitelisted |
boolean |
If set to true, instead of stripping non-whitelisted properties validator will throw an exception. |
forbidUnknownValues |
boolean |
If set to true, attempts to validate unknown objects fail immediately. |
disableErrorMessages |
boolean |
If set to true, validation errors will not be returned to the client. |
errorHttpStatusCode |
number |
This setting allows you to specify which exception type will be used in case of an error. By default it throws BadRequestException . |
exceptionFactory |
Function |
Takes an array of the validation errors and returns an exception object to be thrown. |
groups |
string[] |
Groups to be used during validation of the object. |
dismissDefaultMessages |
boolean |
If set to true, the validation will not use default messages. Error message always will be undefined if
its not explicitly set. |
validationError.target |
boolean |
Indicates if target should be exposed in ValidationError |
validationError.value |
boolean |
Indicates if validated value should be exposed in ValidationError . |
info Notice Find more information about the
class-validator
package in its repository.
We'll start by binding ValidationPipe
at the application level, thus ensuring all endpoints are protected from receiving incorrect data.
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
To test our pipe, let's create a basic endpoint.
@Post()
create(@Body() createUserDto: CreateUserDto) {
return 'This action adds a new user';
}
info Hint Since TypeScript does not store metadata about generics or interfaces, when you use them in your DTOs,
ValidationPipe
may not be able to properly validate incoming data. For this reason, consider using concrete classes in your DTOs.
Now we can add a few validation rules in our CreateUserDto
. We do this using decorators provided by the class-validator
package, described in detail here. In this fashion, any route that uses the CreateUserDto
will automatically enforce these validation rules.
import { IsEmail, IsNotEmpty } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsNotEmpty()
password: string;
}
With these rules in place, if a request hits our endpoint with an invalid email
property in the request body, the application will automatically respond with a 400 Bad Request
code, along with the following response body:
{
"statusCode": 400,
"error": "Bad Request",
"message": ["email must be an email"]
}
In addition to validating request bodies, the ValidationPipe
can be used with other request object properties as well. Imagine that we would like to accept :id
in the endpoint path. To ensure that only numbers are accepted for this request parameter, we can use the following construct:
@Get(':id')
findOne(@Param() params: FindOneParams) {
return 'This action returns a user';
}
FindOneParams
, like a DTO, is simply a class that defines validation rules using class-validator
. It would look like this:
import { IsNumberString } from 'class-validator';
export class FindOneParams {
@IsNumberString()
id: number;
}
Error messages can be helpful to explain what was incorrect in a request. However, some production environments prefer to disable detailed errors. Do this by passing an options object to the ValidationPipe
:
app.useGlobalPipes(
new ValidationPipe({
disableErrorMessages: true,
}),
);
As a result, detailed error messages won't be displayed in the response body.
Our ValidationPipe
can also filter out properties that should not be received by the method handler. In this case, we can whitelist the acceptable properties, and any property not included in the whitelist is automatically stripped from the resulting object. For example, if our handler expects email
and password
properties, but a request also includes an age
property, this property can be automatically removed from the resulting DTO. To enable such behavior, set whitelist
to true
.
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
}),
);
When set to true, this will automatically remove non-whitelisted properties (those without any decorator in the validation class).
Alternatively, you can stop the request from processing when non-whitelisted properties are present, and return an error response to the user. To enable this, set the forbidNonWhitelisted
option property to true
, in combination with setting whitelist
to true
.
Payloads coming in over the network are plain JavaScript objects. The ValidationPipe
can automatically transform payloads to be objects typed according to their DTO classes. To enable auto-transformation, set transform
to true
. This can be done at a method level:
@@filename(cats.controller)
@Post()
@UsePipes(new ValidationPipe({ transform: true }))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
To enable this behavior globally, set the option on a global pipe:
app.useGlobalPipes(
new ValidationPipe({
transform: true,
}),
);
With the auto-transformation option enabled, the ValidationPipe
will also perform conversion of primitive types. In the following example, the findOne()
method takes one argument which represents an extracted id
path parameter:
@Get(':id')
findOne(@Param('id') id: number) {
console.log(typeof id === 'number'); // true
return 'This action returns a user';
}
By default, every path parameter and query parameter comes over the network as a string
. In the above example, we specified the id
type as a number
(in the method signature). Therefore, the ValidationPipe
will try to automatically convert a string identifier to a number.
In the above section, we showed how the ValidationPipe
can implicitly transform query and path parameters based on the expected type. However, this feature requires having auto-transformation enabled.
Alternatively (with auto-transformation disabled), you can explicitly cast values using the ParseIntPipe
or ParseBoolPipe
(note that ParseStringPipe
is not needed because, as mentioned earlier, every path parameter and query parameter comes over the network as a string
by default).
@Get(':id')
findOne(
@Param('id', ParseIntPipe) id: number,
@Query('sort', ParseBoolPipe) sort: boolean,
) {
console.log(typeof id === 'number'); // true
console.log(typeof sort === 'boolean'); // true
return 'This action returns a user';
}
info Hint The
ParseIntPipe
andParseBoolPipe
are exported from the@nestjs/common
package.
TypeScript does not store metadata about generics or interfaces, so when you use them in your DTOs, ValidationPipe
may not be able to properly validate incoming data. For instance, in the following code, createUserDtos
won't be correctly validated:
@Post()
createBulk(@Body() createUserDtos: CreateUserDto[]) {
return 'This action adds new users';
}
To validate the array, create a dedicated class which contains a property that wraps the array, or use the ParseArrayPipe
.
@Post()
createBulk(
@Body(new ParseArrayPipe({ items: CreateUserDto }))
createUserDtos: CreateUserDto[],
) {
return 'This action adds new users';
}
In addition, the ParseArrayPipe
may come in handy when parsing query parameters. Let's consider a findByIds()
method that returns users based on identifiers passed as query parameters.
@Get()
findByIds(
@Query('id', new ParseArrayPipe({ items: Number, separator: ',' }))
ids: number[],
) {
return 'This action returns users by ids';
}
This construction validates the incoming query parameters from an HTTP GET
request like the following:
GET /?ids=1,2,3
While this chapter shows examples using HTTP style applications (e.g., Express or Fastify), the ValidationPipe
works the same for WebSockets and microservices, regardless of the transport method that is used.
Read more about custom validators, error messages, and available decorators as provided by the class-validator
package here.
Caching is a great and simple technique that helps improve your app's performance. It acts as a temporary data store providing high performance data access.
First install the required package:
$ npm install --save cache-manager
Nest provides a unified API for various cache storage providers. The built-in one is an in-memory data store. However, you can easily switch to a more comprehensive solution, like Redis. In order to enable caching, first import the CacheModule
and call its register()
method.
import { CacheModule, Module } from '@nestjs/common';
import { AppController } from './app.controller';
@Module({
imports: [CacheModule.register()],
controllers: [AppController],
})
export class ApplicationModule {}
warning Warning In GraphQL applications, interceptors are executed separately for each field resolver. Thus,
CacheModule
(which uses interceptors to cache responses) will not work properly.
Then just tie the CacheInterceptor
where you want to cache data.
@Controller()
@UseInterceptors(CacheInterceptor)
export class AppController {
@Get()
findAll(): string[] {
return [];
}
}
warningWarning Only
GET
endpoints are cached. Also, HTTP server routes that inject the native response object (@Res()
) cannot use the Cache Interceptor. See response mapping for more details.
To reduce the amount of required boilerplate, you can bind CacheInterceptor
to all endpoints globally:
import { CacheModule, Module, CacheInterceptor } from '@nestjs/common';
import { AppController } from './app.controller';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
imports: [CacheModule.register()],
controllers: [AppController],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: CacheInterceptor,
},
],
})
export class ApplicationModule {}
All cached data has its own expiration time (TTL). To customize default values, pass the options object to the register()
method.
CacheModule.register({
ttl: 5, // seconds
max: 10, // maximum number of items in cache
});
While global cache is enabled, cache entries are stored under a CacheKey
that is auto-generated based on the route path. You may override certain cache settings (@CacheKey()
and @CacheTTL()
) on a per-method basis, allowing customized caching strategies for individual controller methods. This may be most relevant while using different cache stores.
@Controller()
export class AppController {
@CacheKey('custom_key')
@CacheTTL(20)
findAll(): string[] {
return [];
}
}
info Hint The
@CacheKey()
and@CacheTTL()
decorators are imported from the@nestjs/common
package.
The @CacheKey()
decorator may be used with or without a corresponding @CacheTTL()
decorator and vice versa. One may choose to override only the @CacheKey()
or only the @CacheTTL()
. Settings that are not overridden with a decorator will use the default values as registered globally (see Customize caching).
You can also apply the CacheInterceptor
to WebSocket subscribers as well as Microservice's patterns (regardless of the transport method that is being used).
@@filename()
@CacheKey('events')
@UseInterceptors(CacheInterceptor)
@SubscribeMessage('events')
handleEvent(client: Client, data: string[]): Observable<string[]> {
return [];
}
@@switch
@CacheKey('events')
@UseInterceptors(CacheInterceptor)
@SubscribeMessage('events')
handleEvent(client, data) {
return [];
}
However, the additional @CacheKey()
decorator is required in order to specify a key used to subsequently store and retrieve cached data. Also, please note that you shouldn't cache everything. Actions which perform some business operations rather than simply querying the data should never be cached.
Additionally, you may specify a cache expiration time (TTL) by using the @CacheTTL()
decorator, which will override the global default TTL value.
@@filename()
@CacheTTL(10)
@UseInterceptors(CacheInterceptor)
@SubscribeMessage('events')
handleEvent(client: Client, data: string[]): Observable<string[]> {
return [];
}
@@switch
@CacheTTL(10)
@UseInterceptors(CacheInterceptor)
@SubscribeMessage('events')
handleEvent(client, data) {
return [];
}
info Hint The
@CacheTTL()
decorator may be used with or without a corresponding@CacheKey()
decorator.
This service takes advantage of cache-manager under the hood. The cache-manager
package supports a wide-range of useful stores, for example, Redis store. A full list of supported stores is available here. To set up the Redis store, simply pass the package together with corresponding options to the register()
method.
import * as redisStore from 'cache-manager-redis-store';
import { CacheModule, Module } from '@nestjs/common';
import { AppController } from './app.controller';
@Module({
imports: [
CacheModule.register({
store: redisStore,
host: 'localhost',
port: 6379,
}),
],
controllers: [AppController],
})
export class ApplicationModule {}
By default, Nest uses the request URL (in an HTTP app) or cache key (in websockets and microservices apps, set through the @CacheKey()
decorator) to associate cache records with your endpoints. Nevertheless, sometimes you might want to set up tracking based on different factors, for example, using HTTP headers (e.g. Authorization
to properly identify profile
endpoints).
In order to accomplish that, create a subclass of CacheInterceptor
and override the trackBy()
method.
@Injectable()
class HttpCacheInterceptor extends CacheInterceptor {
trackBy(context: ExecutionContext): string | undefined {
return 'key';
}
}
You may want to asynchronously pass in module options instead of passing them statically at compile time. In this case, use the registerAsync()
method, which provides several ways to deal with async configuration.
One approach is to use a factory function:
CacheModule.registerAsync({
useFactory: () => ({
ttl: 5,
}),
});
Our factory behaves like all other asynchronous module factories (it can be async
and is able to inject dependencies through inject
).
CacheModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
ttl: configService.getString('CACHE_TTL'),
}),
inject: [ConfigService],
});
Alternatively, you can use the useClass
method:
CacheModule.registerAsync({
useClass: CacheConfigService,
});
The above construction will instantiate CacheConfigService
inside CacheModule
and will use it to get the options object. The CacheConfigService
has to implement the CacheOptionsFactory
interface in order to provide the configuration options:
@Injectable()
class CacheConfigService implements CacheOptionsFactory {
createCacheOptions(): CacheModuleOptions {
return {
ttl: 5,
};
}
}
If you wish to use an existing configuration provider imported from a different module, use the useExisting
syntax:
CacheModule.registerAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});
This works the same as useClass
with one critical difference - CacheModule
will lookup imported modules to reuse any already-created ConfigService
, instead of instantiating its own.
Serialization is a process that happens before objects are returned in a network response. This is an appropriate place to provide rules for transforming and sanitizing the data to be returned to the client. For example, sensitive data like passwords should always be excluded from the response. Or, certain properties might require additional transformation, such as sending only a subset of properties of an entity. Performing these transformations manually can be tedious and error prone, and can leave you uncertain that all cases have been covered.
Nest provides a built-in capability to help ensure that these operations can be performed in a straightforward way. The ClassSerializerInterceptor
interceptor uses the powerful class-transformer package to provide a declarative and extensible way of transforming objects. The basic operation it performs is to take the value returned by a method handler and apply the classToPlain()
function from class-transformer. In doing so, it can apply rules expressed by class-transformer
decorators on an entity/DTO class, as described below.
Let's assume that we want to automatically exclude a password
property from a user entity. We annotate the entity as follows:
import { Exclude } from 'class-transformer';
export class UserEntity {
id: number;
firstName: string;
lastName: string;
@Exclude()
password: string;
constructor(partial: Partial<UserEntity>) {
Object.assign(this, partial);
}
}
Now consider a controller with a method handler that returns an instance of this class.
@UseInterceptors(ClassSerializerInterceptor)
@Get()
findOne(): UserEntity {
return new UserEntity({
id: 1,
firstName: 'Kamil',
lastName: 'Mysliwiec',
password: 'password',
});
}
Warning Note that we must return an instance of the class. If you return a plain JavaScript object, for example,
{{ '{' }} user: new UserEntity() {{ '}' }}
, the object won't be properly serialized.
info Hint The
ClassSerializerInterceptor
is imported from@nestjs/common
.
When this endpoint is requested, the client receives the following response:
{
"id": 1,
"firstName": "Kamil",
"lastName": "Mysliwiec"
}
Note that the interceptor can be applied application-wide (as covered here). The combination of the interceptor and the entity class declaration ensures that any method that returns a UserEntity
will be sure to remove the password
property. This gives you a measure of centralized enforcement of this business rule.
You can use the @Expose()
decorator to provide alias names for properties, or to execute a function to calculate a property value (analogous to getter functions), as shown below.
@Expose()
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
You can perform additional data transformation using the @Transform()
decorator. For example, the following construct returns the name property of the RoleEntity
instead of returning the whole object.
@Transform(role => role.name)
role: RoleEntity;
You may want to modify the default behavior of the transformation functions. To override default settings, pass them in an options
object with the @SerializeOptions()
decorator.
@SerializeOptions({
excludePrefixes: ['_'],
})
@Get()
findOne(): UserEntity {
return new UserEntity();
}
info Hint The
@SerializeOptions()
decorator is imported from@nestjs/common
.
Options passed via @SerializeOptions()
are passed as the second argument of the underlying classToPlain()
function. In this example, we are automatically excluding all properties that begin with the _
prefix.
While this chapter shows examples using HTTP style applications (e.g., Express or Fastify), the ClassSerializerInterceptor
works the same for WebSockets and Microservices, regardless of the transport method that is used.
Read more about available decorators and options as provided by the class-transformer
package here.
Task scheduling allows you to schedule arbitrary code (methods/functions) to execute at a fixed date/time, at recurring intervals, or once after a specified interval. In the Linux world, this is often handled by packages like cron at the OS level. For Node.js apps, there are several packages that emulate cron-like functionality. Nest provides the @nestjs/schedule
package, which integrates with the popular Node.js node-cron package. We'll cover this package in the current chapter.
To begin using it, we first install the required dependencies.
$ npm install --save @nestjs/schedule
To activate job scheduling, import the ScheduleModule
into the root AppModule
and run the forRoot()
static method as shown below:
@@filename(app.module)
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [
ScheduleModule.forRoot()
],
})
export class AppModule {}
The .forRoot()
call initializes the scheduler and registers any declarative cron jobs, timeouts and intervals that exist within your app. Registration occurs when the onApplicationBootstrap
lifecycle hook occurs, ensuring that all modules have loaded and declared any scheduled jobs.
A cron job schedules an arbitrary function (method call) to run automatically. Cron jobs can run:
- Once, at a specified date/time.
- On a recurring basis; recurring jobs can run at a specified instant within a specified interval (for example, once per hour, once per week, once every 5 minutes)
Declare a cron job with the @Cron()
decorator preceding the method definition containing the code to be executed, as follows:
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name);
@Cron('45 * * * * *')
handleCron() {
this.logger.debug('Called when the current second is 45');
}
}
In this example, the handleCron()
method will be called each time the current second is 45
. In other words, the method will be run once per minute, at the 45 second mark.
The @Cron()
decorator supports all standard cron patterns:
- Asterisk (e.g.
*
) - Ranges (e.g.
1-3,5
) - Steps (e.g.
*/2
)
In the example above, we passed 45 * * * * *
to the decorator. The following key shows how each position in the cron pattern string is interpreted:
* * * * * *
| | | | | |
| | | | | day of week
| | | | month
| | | day of month
| | hour
| minute
second (optional)
Some sample cron patterns are:
* * * * * * |
every second |
45 * * * * * |
every minute, on the 45th second |
* 10 * * * * |
every hour, at the start of the 10th minute |
0 */30 9-17 * * * |
every 30 minutes between 9am and 5pm |
0 30 11 * * 1-5 |
Monday to Friday at 11:30am |
The @nestjs/schedule
package provides a convenience enum with commonly used cron patterns. You can use this enum as follows:
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name);
@Cron(CronExpression.EVERY_45_SECONDS)
handleCron() {
this.logger.debug('Called every 45 seconds');
}
}
In this example, the handleCron()
method will be called every 45
seconds.
Alternatively, you can supply a JavaScript Date
object to the @Cron()
decorator. Doing so causes the job to execute exactly once, at the specified date.
info Hint Use JavaScript date arithmetic to schedule jobs relative to the current date. For example,
@Cron(new Date(Date.now() + 10 * 1000))
to schedule a job to run 10 seconds after the app starts.
You can access and control a cron job after it's been declared, or dynamically create a cron job (where its cron pattern is defined at runtime) with the Dynamic API. To access a declarative cron job via the API, you must associate the job with a name by passing the name
property in an optional options object as the second argument of the decorator, as shown below:
@Cron('* * 8 * * *', {
name: 'notifications',
})
triggerNotifications() {}
To declare that a method should run at a (recurring) specified interval, prefix the method definition with the @Interval()
decorator. Pass the interval value, as a number in milliseconds, to the decorator as shown below:
@Interval(10000)
handleInterval() {
this.logger.debug('Called every 10 seconds');
}
info Hint This mechanism uses the JavaScript
setInterval()
function under the hood. You can also utilize a cron job to schedule recurring jobs.
If you want to control your declarative interval from outside the declaring class via the Dynamic API, associate the interval with a name using the following construction:
@Interval('notifications', 2500)
handleInterval() {}
The Dynamic API also enables creating dynamic intervals, where the interval's properties are defined at runtime, and listing and deleting them.
To declare that a method should run (once) at a specified timeout, prefix the method definition with the @Timeout()
decorator. Pass the relative time offset (in milliseconds), from application startup, to the decorator as shown below:
@Timeout(5000)
handleTimeout() {
this.logger.debug('Called once after 5 seconds');
}
info Hint This mechanism uses the JavaScript
setTimeout()
function under the hood.
If you want to control your declarative timeout from outside the declaring class via the Dynamic API, associate the timeout with a name using the following construction:
@Timeout('notifications', 2500)
handleTimeout() {}
The Dynamic API also enables creating dynamic timeouts, where the timeout's properties are defined at runtime, and listing and deleting them.
The @nestjs/schedule
module provides a dynamic API that enables managing declarative cron jobs, timeouts and intervals. The API also enables creating and managing dynamic cron jobs, timeouts and intervals, where the properties are defined at runtime.
Obtain a reference to a CronJob
instance by name from anywhere in your code using the SchedulerRegistry
API. First, inject SchedulerRegistry
using standard constructor injection:
constructor(private schedulerRegistry: SchedulerRegistry) {}
info Hint Import the
SchedulerRegistry
from the@nestjs/schedule
package.
Then use it in a class as follows. Assume a cron job was created with the following declaration:
@Cron('* * 8 * * *', {
name: 'notifications',
})
triggerNotifications() {}
Access this job using the following:
const job = this.schedulerRegistry.getCronJob('notifications');
job.stop();
console.log(job.lastDate());
The getCronJob()
method returns the named cron job. The returned CronJob
object has the following methods:
stop()
- stops a job that is scheduled to run.start()
- restarts a job that has been stopped.setTime(time: CronTime)
- stops a job, sets a new time for it, and then starts itlastDate()
- returns a string representation of the last date a job executednextDates(count: number)
- returns an array (sizecount
) ofmoment
objects representing upcoming job execution dates.
info Hint Use
toDate()
onmoment
objects to render them in human readable form.
Create a new cron job dynamically using the SchedulerRegistry.addCronJob()
method, as follows:
addCronJob(name: string, seconds: string) {
const job = new CronJob(`${seconds} * * * * *`, () => {
this.logger.warn(`time (${seconds}) for job ${name} to run!`);
});
this.scheduler.addCronJob(name, job);
job.start();
this.logger.warn(
`job ${name} added for each minute at ${seconds} seconds!`,
);
}
In this code, we use the CronJob
object from the cron
package to create the cron job. The CronJob
constructor takes a cron pattern (just like the @Cron()
decorator) as its first argument, and a callback to be executed when the cron timer fires as its second argument. The SchedulerRegistry.addCronJob()
method takes two arguments: a name for the CronJob
, and the CronJob
object itself.
warning Warning Remember to inject the
SchedulerRegistry
before accessing it. ImportCronJob
from thecron
package.
Delete a named cron job using the SchedulerRegistry.deleteCronJob()
method, as follows:
deleteCron(name: string) {
this.scheduler.deleteCronJob(name);
this.logger.warn(`job ${name} deleted!`);
}
List all cron jobs using the SchedulerRegistry.getCronJobs()
method as follows:
getCrons() {
const jobs = this.scheduler.getCronJobs();
jobs.forEach((value, key, map) => {
let next;
try {
next = value.nextDates().toDate();
} catch (e) {
next = 'error: next fire date is in the past!';
}
this.logger.log(`job: ${key} -> next: ${next}`);
});
}
The getCronJobs()
method returns a map
. In this code, we iterate over the map and attempt to access the nextDates()
method of each CronJob
. In the CronJob
API, if a job has already fired and has no future firing dates, it throws an exception.
Obtain a reference to an interval with the SchedulerRegistry.getInterval()
method. As above, inject SchedulerRegistry
using standard constructor injection:
constructor(private schedulerRegistry: SchedulerRegistry) {}
And use it as follows:
const interval = this.schedulerRegistry.getInterval('notifications');
clearInterval(interval);
Create a new interval dynamically using the SchedulerRegistry.addInterval()
method, as follows:
addInterval(name: string, seconds: string) {
const callback = () => {
this.logger.warn(`Interval ${name} executing at time (${seconds})!`);
};
const interval = setInterval(callback, seconds);
this.scheduler.addInterval(name, interval);
}
In this code, we create a standard JavaScript interval, then pass it to the ScheduleRegistry.addInterval()
method.
That method takes two arguments: a name for the interval, and the interval itself.
Delete a named interval using the SchedulerRegistry.deleteInterval()
method, as follows:
deleteInterval(name: string) {
this.scheduler.deleteInterval(name);
this.logger.warn(`Interval ${name} deleted!`);
}
List all intervals using the SchedulerRegistry.getIntervals()
method as follows:
getIntervals() {
const intervals = this.scheduler.getIntervals();
intervals.forEach(key => this.logger.log(`Interval: ${key}`));
}
Obtain a reference to a timeout with the SchedulerRegistry.getTimeout()
method. As above, inject SchedulerRegistry
using standard constructor injection:
constructor(private schedulerRegistry: SchedulerRegistry) {}
And use it as follows:
const timeout = this.schedulerRegistry.getTimeout('notifications');
clearTimeout(timeout);
Create a new timeout dynamically using the SchedulerRegistry.addTimeout()
method, as follows:
addTimeout(name: string, seconds: string) {
const callback = () => {
this.logger.warn(`Timeout ${name} executing after (${seconds})!`);
};
const timeout = setTimeout(callback, seconds);
this.scheduler.addTimeout(name, timeout);
}
In this code, we create a standard JavaScript timeout, then pass it to the ScheduleRegistry.addTimeout()
method.
That method takes two arguments: a name for the timeout, and the timeout itself.
Delete a named timeout using the SchedulerRegistry.deleteTimeout()
method, as follows:
deleteTimeout(name: string) {
this.scheduler.deleteTimeout(name);
this.logger.warn(`Timeout ${name} deleted!`);
}
List all timeouts using the SchedulerRegistry.getTimeouts()
method as follows:
getTimeouts() {
const timeouts = this.scheduler.getTimeouts();
timeouts.forEach(key => this.logger.log(`Timeout: ${key}`));
}
A working example is available here.
Compression can greatly decrease the size of the response body, thereby increasing the speed of a web app.
For high-traffic websites in production, it is strongly recommended to offload compression from the application server - typically in a reverse proxy (e.g., Nginx). In that case, you should not use compression middleware.
Use the compression middleware package to enable gzip compression.
First install the required package:
$ npm i --save compression
Once the installation is complete, apply the compression middleware as global middleware.
import * as compression from 'compression';
// somewhere in your initialization file
app.use(compression());
If using the FastifyAdapter
, you'll want to use fastify-compress:
$ npm i --save fastify-compress
Once the installation is complete, apply the fastify-compress middleware as global middleware.
import * as compression from 'fastify-compress';
// somewhere in your initialization file
app.register(compression);
By default, fastify-compress will use Brotli compression (on Node >= 11.7.0) when browsers indicate support for the encoding. While Brotli is quite efficient in terms of compression ratio, it's also quite slow. Due to this, you may want to tell fastify-compress to only use deflate and gzip to compress responses; you'll end up with larger responses but they'll be delivered much more quickly.
To specify encodings, provide a second argument to app.register
:
app.register(compression, { encodings: ['gzip', 'deflate'] });
The above tells fastify-compress
to only use gzip and deflate encodings, preferring gzip if the client supports both.
In this chapter we cover various techniques that help you to increase the security of your applications.
Helmet can help protect your app from some well-known web vulnerabilities by setting HTTP headers appropriately. Generally, Helmet is just a collection of 14 smaller middleware functions that set security-related HTTP headers (read more).
Start by installing the required package. If you are using Express (default in Nest):
$ npm i --save helmet
Once the installation is complete, apply it as a global middleware.
import * as helmet from 'helmet';
// somewhere in your initialization file
app.use(helmet());
If you are using the FastifyAdapter
, you'll need fastify-helmet instead:
$ npm i --save fastify-helmet
fastify-helmet should not be used as a middleware, but as a Fastify plugin, i.e., by using app.register()
:
import * as helmet from 'fastify-helmet';
// somewhere in your initialization file
app.register(helmet);
// or the following, but note that it's not type safe
// app.getHttpAdapter().register(helmet);
info Hint Note that applying
helmet
as global or registering it must come before other calls toapp.use()
or setup functions that may callapp.use()
). This is due to the way the underlying platform (i.e., Express or Fastify) works, where the order that middleware/routes are defined matters. If you use middleware likehelmet
orcors
after you define a route, then that middleware will not apply to that route, it will only apply to middleware defined after the route.
Cross-origin resource sharing (CORS) is a mechanism that allows resources to be requested from another domain. Under the hood, Nest makes use of the Express cors package. This package provides various options that you can customize based on your requirements. To enable CORS, call the enableCors()
method on the Nest application object.
const app = await NestFactory.create(AppModule);
app.enableCors();
await app.listen(3000);
The enableCors()
method takes an optional configuration object argument. The available properties of this object are described in the official CORS documentation.
Alternatively, enable CORS via the create()
method's options object. Set the cors
property to true
to enable CORS with default settings. Alternatively, pass a CORS configuration object as the cors
property value to customize its behavior.
const app = await NestFactory.create(AppModule, { cors: true });
await app.listen(3000);
Cross-site request forgery (also known as CSRF or XSRF) is a type of malicious exploit of a website where unauthorized commands are transmitted from a user that the web application trusts. To mitigate this kind of attack you can use the csurf package.
Start by installing the required package:
$ npm i --save csurf
warning Warning As explained on the csurf middleware page, the csurf module requires either session middleware or a cookie-parser to be initialized first. Please see that documentation for further instructions.
Once the installation is complete, apply the csurf middleware as global middleware.
import * as csurf from 'csurf';
// somewhere in your initialization file
app.use(csurf());
A common technique to protect applications from brute-force attacks is rate-limiting. Many Express packages exist to provide a rate-limiting feature. A popular one is express-rate-limit.
Start by installing the required package:
$ npm i --save express-rate-limit
Once the installation is complete, apply the rate-limiter as global middleware.
import * as rateLimit from 'express-rate-limit';
// somewhere in your initialization file
app.use(
rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
}),
);
When there is a load balancer or reverse proxy between the server and the internet, Express may need to be configured to trust the headers set by the proxy in order to get the correct IP for the end user. To do so, first use the NestExpressApplication
platform interface when creating your app
instance, then enable the trust proxy setting:
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// see https://expressjs.com/en/guide/behind-proxies.html
app.set('trust proxy', 1);
info Hint If you use the
FastifyAdapter
, consider using fastify-rate-limit instead.
Queues are a powerful design pattern that help you deal with common application scaling and performance challenges. Some examples of problems that Queues can help you solve are:
- Smooth out processing peaks. For example, if users can initiate resource-intensive tasks at arbitrary times, you can add these tasks to a queue instead of performing them synchronously. Then you can have worker processes pull tasks from the queue in a controlled manner. You can easily add new Queue consumers to scale up the back-end task handling as the application scales up.
- Break up monolithic tasks that may otherwise block the Node.js event loop. For example, if a user request requires CPU intensive work like audio transcoding, you can delegate this task to other processes, freeing up user-facing processes to remain responsive.
- Provide a reliable communication channel across various services. For example, you can queue tasks (jobs) in one process or service, and consume them in another. You can be notified (by listening for status events) upon completion, error or other state changes in the job life cycle from any process or service. When Queue producers or consumers fail, their state is preserved and task handling can restart automatically when nodes are restarted.
Nest provides the @nestjs/bull
package as an abstraction/wrapper on top of Bull, a popular, well supported, high performance Node.js based Queue system implementation. The package makes it easy to integrate Bull Queues in a Nest-friendly way to your application.
Bull uses Redis to persist job data, so you'll need to have Redis installed on your system. Because it is Redis-backed, your Queue architecture can be completely distributed and platform-independent. For example, you can have some Queue producers and consumers and listeners running in Nest on one (or several) nodes, and other producers, consumers and listeners running on other Node.js platforms on other network nodes.
This chapter covers the @nestjs/bull
package. We also recommend reading the Bull documentation for more background and specific implementation details.
To begin using it, we first install the required dependencies.
$ npm install --save @nestjs/bull bull
$ npm install --save-dev @types/bull
Once the installation process is complete, we can import the BullModule
into the root AppModule
.
@@filename(app.module)
import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bull';
@Module({
imports: [
BullModule.registerQueue({
name: 'audio',
redis: {
host: 'localhost',
port: 6379,
},
}),
],
})
export class AppModule {}
The registerQueue()
method is used to instantiate and/or register queues. Queues are shared across modules and processes that connect to the same underlying Redis database with the same credentials. Each queue is unique by its name property (see below). When sharing queues (across modules/processes), the first registerQueue()
method to run both instantiates the queue and registers it for that module. Other modules (in the same or separate processes) simply register the queue. Queue registration creates an injection token that can be used to access the queue in a given Nest module.
For each queue, pass a configuration object containing the following properties:
name: string
- A queue name, which will be used as both an injection token (for injecting the queue into controllers/providers), and as an argument to decorators to associate consumer classes and listeners with queues. Required.limiter: RateLimiter
- Options to control the rate at which the queue's jobs are processed. See RateLimiter for more information. Optional.redis: RedisOpts
- Options to configure the Redis connection. See RedisOpts for more information. Optional.prefix: string
- Prefix for all queue keys. Optional.defaultJobOptions: JobOpts
- Options to control the default settings for new jobs. See JobOpts for more information. Optional.settings: AdvancedSettings
- Advanced Queue configuration settings. These should usually not be changed. See AdvancedSettings for more information. Optional.
As noted, the name
property is required. The rest of the options are optional, providing detailed control over queue behavior. These are passed directly to the Bull Queue
constructor. Read more about these options here. When registering a queue in a second or subsequent module, it is best practice to omit all options but the name
property from the configuration object. These options should be specified only in the module that instantiates the queue.
info Hint Create multiple queues by passing multiple comma-separated configuration objects to the
registerQueue()
method.
Since jobs are persisted in Redis, each time a specific named queue is instantiated (e.g., when an app is started/restarted), it attempts to process any old jobs that may exist from a previous unfinished session.
Each queue can have one or many producers, consumers, and listeners. Consumers retrieve jobs from the queue in a specific order: FIFO (the default), LIFO, or according to priorities. Controlling queue processing order is discussed here.
Job producers add jobs to queues. Producers are typically application services (Nest providers). To add jobs to a queue, first inject the queue into the service as follows:
import { Injectable } from '@nestjs/common';
import { Queue } from 'bull';
import { InjectQueue } from '@nestjs/bull';
@Injectable()
export class AudioService {
constructor(@InjectQueue('audio') private audioQueue: Queue) {}
}
info Hint The
@InjectQueue()
decorator identifies the queue by its name, as provided in theregisterQueue()
method call (e.g.,'audio'
).
Now, add a job by calling the queue's add()
method, passing a user-defined job object. Jobs are represented as serializable JavaScript objects (since that is how they are stored in the Redis database). The shape of the job you pass is arbitrary; use it to represent the semantics of your job object.
const job = await this.audioQueue.add({
foo: 'bar',
});
Jobs may have unique names. This allows you to create specialized consumers that will only process jobs with a given name.
const job = await this.audioQueue.add('transcode', {
foo: 'bar',
});
Warning Warning When using named jobs, you must create processors for each unique name added to a queue, or the queue will complain that you are missing a processor for the given job. See here for more information on consuming named jobs.
Jobs can have additional options associated with them. Pass an options object after the job
argument in the Queue.add()
method. Job options properties are:
priority
:number
- Optional priority value. Ranges from 1 (highest priority) to MAX_INT (lowest priority). Note that using priorities has a slight impact on performance, so use them with caution.delay
:number
- An amount of time (milliseconds) to wait until this job can be processed. Note that for accurate delays, both server and clients should have their clocks synchronized.attempts
:number
- The total number of attempts to try the job until it completes.repeat
:RepeatOpts
- Repeat job according to a cron specification. See RepeatOpts.backoff
:number | BackoffOpts
- Backoff setting for automatic retries if the job fails. See BackoffOpts.lifo
:boolean
- If true, adds the job to the right end of the queue instead of the left (default false).timeout
:number
- The number of milliseconds after which the job should fail with a timeout error.jobId
:number
|string
- Override the job ID - by default, the job ID is a unique integer, but you can use this setting to override it. If you use this option, it is up to you to ensure the jobId is unique. If you attempt to add a job with an id that already exists, it will not be added.removeOnComplete
:boolean | number
- If true, removes the job when it successfully completes. A number specifies the amount of jobs to keep. Default behavior is to keep the job in the completed set.removeOnFail
:boolean | number
- If true, removes the job when it fails after all attempts. A number specifies the amount of jobs to keep. Default behavior is to keep the job in the failed set.stackTraceLimit
:number
- Limits the amount of stack trace lines that will be recorded in the stacktrace.
Here are a few examples of customizing jobs with job options.
To delay the start of a job, use the delay
configuration property.
const job = await this.audioQueue.add(
{
foo: 'bar',
},
{ delay: 3000 }, // 3 seconds delayed
);
To add a job to the right end of the queue (process the job as LIFO (Last In First Out)), set the lifo
property of the configuration object to true
.
const job = await this.audioQueue.add(
{
foo: 'bar',
},
{ lifo: true },
);
To prioritize a job, use the priority
property.
const job = await this.audioQueue.add(
{
foo: 'bar',
},
{ priority: 2 },
);
A consumer is a class defining methods that either process jobs added into the queue, or listen for events on the queue, or both. Declare a consumer class using the @Processor()
decorator as follows:
import { Processor } from '@nestjs/bull';
@Processor('audio')
export class AudioConsumer {}
Where the decorator's string argument (e.g., 'audio'
) is the name of the queue to be associated with the class methods.
Within a consumer class, declare job handlers by decorating handler methods with the @Process()
decorator.
import { Processor, Process } from '@nestjs/bull';
import { Job } from 'bull';
@Processor('audio')
export class AudioConsumer {
@Process()
async transcode(job: Job<unknown>) {
let progress = 0;
for (i = 0; i < 100; i++) {
await doSomething(job.data);
progress += 10;
job.progress(progress);
}
return {};
}
}
The decorated method (e.g., transcode()
) is called whenever the worker is idle and there are jobs to process in the queue. This handler method receives the job
object as its only argument. The value returned by the handler method is stored in the job object and can be accessed later on, for example in a listener for the completed event.
Job
objects have multiple methods that allow you to interact with their state. For example, the above code uses the progress()
method to update the job's progress. See here for the complete Job
object API reference.
You can designate that a job handler method will handle only jobs of a certain type (jobs with a specific name
) by passing that name
to the @Process()
decorator as shown below. You can have multiple @Process()
handlers in a given consumer class, corresponding to each job type (name
). When you use named jobs, be sure to have a handler corresponding to each name.
@Process('transcode')
async transcode(job: Job<unknown>) { ... }
Bull generates a set of useful events when queue and/or job state changes occur. Nest provides a set of decorators that allow subscribing to a core set of standard events. These are exported from the @nestjs/bull
package.
Event listeners must be declared within a consumer class (i.e., within a class decorated with the @Processor()
decorator). To listen for an event, use one of the decorators in the table below to declare a handler for the event. For example, to listen to the event emitted when a job enters the active state in the audio
queue, use the following construct:
import { Processor, Process } from '@nestjs/bull';
import { Job } from 'bull';
@Processor('audio')
export class AudioConsumer {
@OnQueueActive()
onActive(job: Job) {
console.log(
`Processing job ${job.id} of type ${job.name} with data ${job.data}...`,
);
}
...
Since Bull operates in a distributed (multi-node) environment, it defines the concept of event locality. This concept recognizes that events may be triggered either entirely within a single process, or on shared queues from different processes. A local event is one that is produced when an action or state change is triggered on a queue in the local process. In other words, when your event producers and consumers are local to a single process, all events happening on queues are local.
When a queue is shared across multiple processes, we encounter the possibility of global events. For a listener in one process to receive an event notification triggered by another process, it must register for a global event.
Event handlers are invoked whenever their corresponding event is emitted. The handler is called with the signature shown in the table below, providing access to information relevant to the event. We discuss one key difference between local and global event handler signatures below.
Local event listeners | Global event listeners | Handler method signature / When fired |
---|---|---|
@OnQueueError() | @OnGlobalQueueError() | handler(error: Error) - An error occurred. error contains the triggering error. |
@OnQueueWaiting() | @OnGlobalQueueWaiting() | handler(jobId: number | string) - A Job is waiting to be processed as soon as a worker is idling. jobId contains the id for the job that has entered this state. |
@OnQueueActive() | @OnGlobalQueueActive() | handler(job: Job) - Job job has started. |
@OnQueueStalled() | @OnGlobalQueueStalled() | handler(job: Job) - Job job has been marked as stalled. This is useful for debugging job workers that crash or pause the event loop. |
@OnQueueProgress() | @OnGlobalQueueProgress() | handler(job: Job, progress: number) - Job job 's progress was updated to value progress . |
@OnQueueCompleted() | @OnGlobalQueueCompleted() | handler(job: Job, result: any) Job job successfully completed with a result result . |
@OnQueueFailed() | @OnGlobalQueueFailed() | handler(job: Job, err: Error) Job job failed with reason err . |
@OnQueuePaused() | @OnGlobalQueuePaused() | handler() The queue has been paused. |
@OnQueueResumed() | @OnGlobalQueueResumed() | handler(job: Job) The queue has been resumed. |
@OnQueueCleaned() | @OnGlobalQueueCleaned() | handler(jobs: Job[], type: string) Old jobs have been cleaned from the queue. jobs is an array of cleaned jobs, and type is the type of jobs cleaned. |
@OnQueueDrained() | @OnGlobalQueueDrained() | handler() Emitted whenever the queue has processed all the waiting jobs (even if there can be some delayed jobs not yet processed). |
@OnQueueRemoved() | @OnGlobalQueueRemoved() | handler(job: Job) Job job was successfully removed. |
When listening for global events, the method signatures can be slightly different from their local counterpart. Specifically, any method signature that receives job
objects in the local version, instead receives a jobId
(number
) in the global version. To get a reference to the actual job
object in such a case, use the Queue#getJob
method. This call should be awaited, and therefore the handler should be declared async
. For example:
@OnGlobalQueueCompleted()
async onGlobalCompleted(jobId: number, result: any) {
const job = await this.immediateQueue.getJob(jobId);
console.log('(Global) on completed: job ', job.id, ' -> result: ', result);
}
info Hint To access the
Queue
object (to make agetJob()
call), you must of course inject it. Also, the Queue must be registered in the module where you are injecting it.
In addition to the specific event listener decorators, you can also use the generic @OnQueueEvent()
decorator in combination with either BullQueueEvents
or BullQueueGlobalEvents
enums. Read more about events here.
Queue's have an API that allows you to perform management functions like pausing and resuming, retrieving the count of jobs in various states, and several more. You can find the full queue API here. Invoke any of these methods directly on the Queue
object, as shown below with the pause/resume examples.
Pause a queue with the pause()
method call. A paused queue will not process new jobs until resumed, but current jobs being processed will continue until they are finalized.
await audioQueue.pause();
To resume a paused queue, use the resume()
method, as follows:
await audioQueue.resume();
You may want to pass your queue options asynchronously instead of statically. In this case, use the registerQueueAsync()
method, which provides several ways to deal with async configuration.
One approach is to use a factory function:
BullModule.registerQueueAsync({
name: 'audio',
useFactory: () => ({
redis: {
host: 'localhost',
port: 6379,
},
}),
});
Our factory behaves like any other asynchronous provider (e.g., it can be async
and it's able to inject dependencies through inject
).
BullModule.registerQueueAsync({
name: 'audio',
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
redis: {
host: configService.get('QUEUE_HOST'),
port: +configService.get('QUEUE_PORT'),
},
}),
inject: [ConfigService],
});
Alternatively, you can use the useClass
syntax:
BullModule.registerQueueAsync({
name: 'audio',
useClass: BullConfigService,
});
The construction above will instantiate BullConfigService
inside BullModule
and use it to provide an options object by calling createBullOptions()
. Note that this means that the BullConfigService
has to implement the BullOptionsFactory
interface, as shown below:
@Injectable()
class BullConfigService implements BullOptionsFactory {
createBullOptions(): BullModuleOptions {
return {
redis: {
host: 'localhost',
port: 6379,
},
};
}
}
In order to prevent the creation of BullConfigService
inside BullModule
and use a provider imported from a different module, you can use the useExisting
syntax.
BullModule.registerQueueAsync({
name: 'audio',
imports: [ConfigModule],
useExisting: ConfigService,
});
This construction works the same as useClass
with one critical difference - BullModule
will lookup imported modules to reuse an existing ConfigService
instead of instantiating a new one.
A working example is available here.
Nest comes with a built-in text-based logger which is used during application bootstrapping and several other circumstances such as displaying caught exceptions (i.e., system logging). This functionality is provided via the Logger
class in the @nestjs/common
package. You can fully control the behavior of the logging system, including any of the following:
- disable logging entirely
- specify the log level of detail (e.g., display errors, warnings, debug information, etc.)
- completely override the default logger
- customize the default logger by extending it
- make use of dependency injection to simplify composing and testing your application
You can also make use of the built-in logger, or create your own custom implementation, to log your own application-level events and messages.
For more advanced logging functionality, you can make use of any Node.js logging package, such as Winston, to implement a completely custom, production grade logging system.
To disable logging, set the logger
property to false
in the (optional) Nest application options object passed as the second argument to the NestFactory.create()
method.
const app = await NestFactory.create(ApplicationModule, {
logger: false,
});
await app.listen(3000);
To enable specific logging levels, set the logger
property to an array of strings specifying the log levels to display, as follows:
const app = await NestFactory.create(ApplicationModule, {
logger: ['error', 'warn'],
});
await app.listen(3000);
Values in the array can be any combination of 'log'
, 'error'
, 'warn'
, 'debug'
, and 'verbose'
.
You can provide a custom logger implementation to be used by Nest for system logging by setting the value of the logger
property to an object that fulfills the LoggerService
interface. For example, you can tell Nest to use the built-in global JavaScript console
object (which implements the LoggerService
interface), as follows:
const app = await NestFactory.create(ApplicationModule, {
logger: console,
});
await app.listen(3000);
Implementing your own custom logger is straightforward. Simply implement each of the methods of the LoggerService
interface as shown below.
import { LoggerService } from '@nestjs/common';
export class MyLogger implements LoggerService {
log(message: string) {
/* your implementation */
}
error(message: string, trace: string) {
/* your implementation */
}
warn(message: string) {
/* your implementation */
}
debug(message: string) {
/* your implementation */
}
verbose(message: string) {
/* your implementation */
}
}
You can then supply an instance of MyLogger
via the logger
property of the Nest application options object.
const app = await NestFactory.create(ApplicationModule, {
logger: new MyLogger(),
});
await app.listen(3000);
This technique, while simple, doesn't utilize dependency injection for the MyLogger
class. This can pose some challenges, particularly for testing, and limit the reusability of MyLogger
. For a better solution, see the Dependency Injection section below.
Rather than writing a logger from scratch, you may be able to meet your needs by extending the built-in Logger
class and overriding selected behavior of the default implementation.
import { Logger } from '@nestjs/common';
export class MyLogger extends Logger {
error(message: string, trace: string) {
// add your tailored logic here
super.error(message, trace);
}
}
You can use such an extended logger in your feature modules as described in the Using the logger for application logging section below.
You can tell Nest to use your extended logger for system logging by passing an instance of it via the logger
property of the application options object (as shown in the Custom implementation section above), or by using the technique shown in the Dependency Injection section below. If you do so, you should take care to call super
, as shown in the sample code above, to delegate the specific log method call to the parent (built-in) class so that Nest can rely on the built-in features it expects.
For more advanced logging functionality, you'll want to take advantage of dependency injection. For example, you may want to inject a ConfigService
into your logger to customize it, and in turn inject your custom logger into other controllers and/or providers. To enable dependency injection for your custom logger, create a class that implements LoggerService
and register that class as a provider in some module. For example, you can
- Define a
MyLogger
class that either extends the built-inLogger
or completely overrides it, as shown in previous sections. - Create a
LoggerModule
as shown below, and provideMyLogger
from that module.
import { Module } from '@nestjs/common';
import { MyLogger } from './my-logger.service';
@Module({
providers: [MyLogger],
exports: [MyLogger],
})
export class LoggerModule {}
With this construct, you are now providing your custom logger for use by any other module. Because your MyLogger
class is part of a module, it can use dependency injection (for example, to inject a ConfigService
). There's one more technique needed to provide this custom logger for use by Nest for system logging (e.g., for bootstrapping and error handling).
Because application instantiation (NestFactory.create()
) happens outside the context of any module, it doesn't participate in the normal Dependency Injection phase of initialization. So we must ensure that at least one application module imports the LoggerModule
to trigger Nest to instantiate a singleton instance of our MyLogger
class. We can then instruct Nest to use the same singleton instance of MyLogger
with the following construction:
const app = await NestFactory.create(ApplicationModule, {
logger: false,
});
app.useLogger(app.get(MyLogger));
await app.listen(3000);
Here we use the get()
method on the NestApplication
instance to retrieve the singleton instance of the MyLogger
object. This technique is essentially a way to "inject" an instance of a logger for use by Nest. The app.get()
call retrieves the singleton instance of MyLogger
, and depends on that instance being first injected in another module, as described above.
You can also inject this MyLogger
provider in your feature classes, thus ensuring consistent logging behavior across both Nest system logging and application logging. See Using the logger for application logging below for more information.
The only downside of this solution is that your first initialization messages won't be handled by your logger instance, though, it shouldn't really matter at this point.
We can combine several of the techniques above to provide consistent behavior and formatting across both Nest system logging and our own application event/message logging. In this section, we'll achieve this with the following steps:
- We extend the built-in logger and customize the
context
portion of the log message (e.g., the phraseNestFactory
in square brackets in the log line shown below).
[Nest] 19096 - 12/08/2019, 7:12:59 AM [NestFactory] Starting Nest application...
- We inject a transient instance of the
Logger
into our feature modules so that each one has its own custom context. - We supply this extended logger for Nest to use for system logging.
To start, extend the built-in logger with code like the following. We supply the scope
option as configuration metadata for the Logger
class, specifying a transient scope, to ensure that we'll have a unique instance of the Logger
in each feature module. In this example, we do not extend the individual Logger
methods (like log()
, warn()
, etc.), though you may choose to do so.
import { Injectable, Scope, Logger } from '@nestjs/common';
@Injectable({ scope: Scope.TRANSIENT })
export class MyLogger extends Logger {}
Next, create a LoggerModule
with a construction like this:
import { Module } from '@nestjs/common';
import { MyLogger } from './my-logger.service';
@Module({
providers: [MyLogger],
exports: [MyLogger],
})
export class LoggerModule {}
Next, import the LoggerModule
into your feature module. Then set the logger context, and start using the context-aware custom logger, like this:
import { Injectable } from '@nestjs/common';
import { MyLogger } from './my-logger.service';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
constructor(private myLogger: MyLogger) {
this.myLogger.setContext('CatsService');
}
findAll(): Cat[] {
this.myLogger.warn('About to return cats!');
return this.cats;
}
}
Finally, instruct Nest to use an instance of the custom logger in your main.ts
file as shown below. Of course in this example, we haven't actually customized the logger behavior (by extending the Logger
methods like log()
, warn()
, etc.), so this step isn't actually needed. But it would be needed if you added custom logic to those methods and wanted Nest to use the same implementation.
const app = await NestFactory.create(ApplicationModule, {
logger: false,
});
app.useLogger(new MyLogger());
await app.listen(3000);
Production applications often have specific logging requirements, including advanced filtering, formatting and centralized logging. Nest's built-in logger is used for monitoring Nest system behavior, and can also be useful for basic formatted text logging in your feature modules while in development, but production applications often take advantage of dedicated logging modules like Winston. As with any standard Node.js application, you can take full advantage of such modules in Nest.
To handle file uploading, Nest provides a built-in module based on the multer middleware package for Express. Multer handles data posted in the multipart/form-data
format, which is primarily used for uploading files via an HTTP POST
request. This module is fully configurable and you can adjust its behavior to your application requirements.
warning Warning Multer cannot process data which is not in the supported multipart format (
multipart/form-data
). Also, note that this package is not compatible with theFastifyAdapter
.
To upload a single file, simply tie the FileInterceptor()
interceptor to the route handler and extract file
from the request
using the @UploadedFile()
decorator.
@@filename()
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file) {
console.log(file);
}
@@switch
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
@Bind(UploadedFile())
uploadFile(file) {
console.log(file);
}
info Hint The
FileInterceptor()
decorator is exported from the@nestjs/platform-express
package. The@UploadedFile()
decorator is exported from@nestjs/common
.
The FileInterceptor()
decorator takes two arguments:
fieldName
: string that supplies the name of the field from the HTML form that holds a fileoptions
: optional object of typeMulterOptions
. This is the same object used by the multer constructor (more details here).
To upload an array of files (identified with a single field name), use the FilesInterceptor()
decorator (note the plural Files in the decorator name). This decorator takes three arguments:
fieldName
: as described abovemaxCount
: optional number defining the maximum number of files to acceptoptions
: optionalMulterOptions
object, as described above
When using FilesInterceptor()
, extract files from the request
with the @UploadedFiles()
decorator.
@@filename()
@Post('upload')
@UseInterceptors(FilesInterceptor('files'))
uploadFile(@UploadedFiles() files) {
console.log(files);
}
@@switch
@Post('upload')
@UseInterceptors(FilesInterceptor('files'))
@Bind(UploadedFiles())
uploadFile(files) {
console.log(files);
}
info Hint The
FilesInterceptor()
decorator is exported from the@nestjs/platform-express
package. The@UploadedFiles()
decorator is exported from@nestjs/common
.
To upload multiple fields (all with different field name keys), use the FileFieldsInterceptor()
decorator. This decorator takes two arguments:
uploadedFields
: an array of objects, where each object specifies a requiredname
property with a string value specifying a field name, as described above, and an optionalmaxCount
property, as described aboveoptions
: optionalMulterOptions
object, as described above
When using FileFieldsInterceptor()
, extract files from the request
with the @UploadedFiles()
decorator.
@@filename()
@Post('upload')
@UseInterceptors(FileFieldsInterceptor([
{ name: 'avatar', maxCount: 1 },
{ name: 'background', maxCount: 1 },
]))
uploadFile(@UploadedFiles() files) {
console.log(files);
}
@@switch
@Post('upload')
@Bind(UploadedFiles())
@UseInterceptors(FileFieldsInterceptor([
{ name: 'avatar', maxCount: 1 },
{ name: 'background', maxCount: 1 },
]))
uploadFile(files) {
console.log(files);
}
To upload all fields with arbitrary field name keys, use the AnyFilesInterceptor()
decorator. This decorator can accept an optional options
object as described above.
When using AnyFilesInterceptor()
, extract files from the request
with the @UploadedFiles()
decorator.
@@filename()
@Post('upload')
@UseInterceptors(AnyFilesInterceptor())
uploadFile(@UploadedFiles() files) {
console.log(files);
}
@@switch
@Post('upload')
@Bind(UploadedFiles())
@UseInterceptors(AnyFilesInterceptor())
uploadFile(files) {
console.log(files);
}
You can specify multer options in the file interceptors as described above. To set default options, you can call the static register()
method when you import the MulterModule
, passing in supported options. You can use all options listed here.
MulterModule.register({
dest: '/upload',
});
info Hint The
MulterModule
class is exported from the@nestjs/platform-express
package.
When you need to set MulterModule
options asynchronously instead of statically, use the registerAsync()
method. As with most dynamic modules, Nest provides several techniques to deal with async configuration.
One technique is to use a factory function:
MulterModule.registerAsync({
useFactory: () => ({
dest: '/upload',
}),
});
Like other factory providers, our factory function can be async
and can inject dependencies through inject
.
MulterModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
dest: configService.getString('MULTER_DEST'),
}),
inject: [ConfigService],
});
Alternatively, you can configure the MulterModule
using a class instead of a factory, as shown below:
MulterModule.registerAsync({
useClass: MulterConfigService,
});
The construction above instantiates MulterConfigService
inside MulterModule
, using it to create the required options object. Note that in this example, the MulterConfigService
has to implement the MulterOptionsFactory
interface, as shown below. The MulterModule
will call the createMulterOptions()
method on the instantiated object of the supplied class.
@Injectable()
class MulterConfigService implements MulterOptionsFactory {
createMulterOptions(): MulterModuleOptions {
return {
dest: '/upload',
};
}
}
If you want to reuse an existing options provider instead of creating a private copy inside the MulterModule
, use the useExisting
syntax.
MulterModule.registerAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});
Axios is richly featured HTTP client package that is widely used. Nest wraps Axios and exposes it via the built-in HttpModule
. The HttpModule
exports the HttpService
class, which exposes Axios-based methods to perform HTTP requests. The library also transforms the resulting HTTP responses into Observables
.
To use the HttpService
, first import HttpModule
.
@Module({
imports: [HttpModule],
providers: [CatsService],
})
export class CatsModule {}
Next, inject HttpService
using normal constructor injection.
info Hint
HttpModule
andHttpService
are imported from@nestjs/common
package.
@@filename()
@Injectable()
export class CatsService {
constructor(private httpService: HttpService) {}
findAll(): Observable<AxiosResponse<Cat[]>> {
return this.httpService.get('http://localhost:3000/cats');
}
}
@@switch
@Injectable()
@Dependencies(HttpService)
export class CatsService {
constructor(httpService) {
this.httpService = httpService;
}
findAll() {
return this.httpService.get('http://localhost:3000/cats');
}
}
All HttpService
methods return an AxiosResponse
wrapped in an Observable
object.
Axios can be configured with a variety of options to customize the behavior of the HttpService
. Read more about them here. To configure the underlying Axios instance, pass an optional options object to the register()
method of HttpModule
when importing it. This options object will be passed directly to the underlying Axios constructor.
@Module({
imports: [
HttpModule.register({
timeout: 5000,
maxRedirects: 5,
}),
],
providers: [CatsService],
})
export class CatsModule {}
When you need to pass module options asynchronously instead of statically, use the registerAsync()
method. As with most dynamic modules, Nest provides several techniques to deal with async configuration.
One technique is to use a factory function:
HttpModule.registerAsync({
useFactory: () => ({
timeout: 5000,
maxRedirects: 5,
}),
});
Like other factory providers, our factory function can be async and can inject dependencies through inject
.
HttpModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
timeout: configService.getString('HTTP_TIMEOUT'),
maxRedirects: configService.getString('HTTP_MAX_REDIRECTS'),
}),
inject: [ConfigService],
});
Alternatively, you can configure the HttpModule
using a class instead of a factory, as shown below.
HttpModule.registerAsync({
useClass: HttpConfigService,
});
The construction above instantiates HttpConfigService
inside HttpModule
, using it to create an options object. Note that in this example, the HttpConfigService
has to implement HttpModuleOptionsFactory
interface as shown below. The HttpModule
will call the createHttpOptions()
method on the instantiated object of the supplied class.
@Injectable()
class HttpConfigService implements HttpModuleOptionsFactory {
createHttpOptions(): HttpModuleOptions {
return {
timeout: 5000,
maxRedirects: 5,
};
}
}
If you want to reuse an existing options provider instead of creating a private copy inside the HttpModule
, use the useExisting
syntax.
HttpModule.registerAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});
Nest, by default, makes use of the Express library under the hood. Hence, every technique for using the MVC (Model-View-Controller) pattern in Express applies to Nest as well.
First, let's scaffold a simple Nest application using the CLI tool:
$ npm i -g @nestjs/cli
$ nest new project
In order to create an MVC app, we also need a template engine to render our HTML views:
$ npm install --save hbs
We've used the hbs
(Handlebars) engine, though you can use whatever fits your requirements. Once the installation process is complete, we need to configure the express instance using the following code:
@@filename(main)
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(
AppModule,
);
app.useStaticAssets(join(__dirname, '..', 'public'));
app.setBaseViewsDir(join(__dirname, '..', 'views'));
app.setViewEngine('hbs');
await app.listen(3000);
}
bootstrap();
@@switch
import { NestFactory } from '@nestjs/core';
import { join } from 'path';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(
AppModule,
);
app.useStaticAssets(join(__dirname, '..', 'public'));
app.setBaseViewsDir(join(__dirname, '..', 'views'));
app.setViewEngine('hbs');
await app.listen(3000);
}
bootstrap();
We told Express that the public
directory will be used for storing static assets, views
will contain templates, and the hbs
template engine should be used to render HTML output.
Now, let's create a views
directory and index.hbs
template inside it. In the template, we'll print a message
passed from the controller:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>App</title>
</head>
<body>
{{ "{{ message }\}" }}
</body>
</html>
Next, open the app.controller
file and replace the root()
method with the following code:
@@filename(app.controller)
import { Get, Controller, Render } from '@nestjs/common';
@Controller()
export class AppController {
@Get()
@Render('index')
root() {
return { message: 'Hello world!' };
}
}
In this code, we are specifying the template to use in the @Render()
decorator, and the return value of the route handler method is passed to the template for rendering. Notice that the return value is an object with a property message
, matching the message
placeholder we created in the template.
While the application is running, open your browser and navigate to http://localhost:3000
. You should see the Hello world!
message.
If the application logic must dynamically decide which template to render, then we should use the @Res()
decorator, and supply the view name in our route handler, rather than in the @Render()
decorator:
info Hint When Nest detects the
@Res()
decorator, it injects the library-specificresponse
object. We can use this object to dynamically render the template. Learn more about theresponse
object API here.
@@filename(app.controller)
import { Get, Controller, Res, Render } from '@nestjs/common';
import { Response } from 'express';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private appService: AppService) {}
@Get()
root(@Res() res: Response) {
return res.render(
this.appService.getViewName(),
{ message: 'Hello world!' },
);
}
}
A working example is available here.
As mentioned in this chapter, we are able to use any compatible HTTP provider together with Nest. One such library is Fastify. In order to create an MVC application with Fastify, we have to install the following packages:
$ npm i --save fastify point-of-view handlebars
The next steps cover almost the same process used with Express, with minor differences specific to the platform. Once the installation process is complete, open the main.ts
file and update its contents:
@@filename(main)
import { NestFactory } from '@nestjs/core';
import { NestFastifyApplication, FastifyAdapter } from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
import { join } from 'path';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
app.useStaticAssets({
root: join(__dirname, '..', 'public'),
prefix: '/public/',
});
app.setViewEngine({
engine: {
handlebars: require('handlebars'),
},
templates: join(__dirname, '..', 'views'),
});
await app.listen(3000);
}
bootstrap();
@@switch
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter } from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
import { join } from 'path';
async function bootstrap() {
const app = await NestFactory.create(AppModule, new FastifyAdapter());
app.useStaticAssets({
root: join(__dirname, '..', 'public'),
prefix: '/public/',
});
app.setViewEngine({
engine: {
handlebars: require('handlebars'),
},
templates: join(__dirname, '..', 'views'),
});
await app.listen(3000);
}
bootstrap();
The Fastify API is slightly different but the end result of those methods calls remains the same. One difference to notice with Fastify is that the template name passed into the @Render()
decorator must include a file extension.
@@filename(app.controller)
import { Get, Controller, Render } from '@nestjs/common';
@Controller()
export class AppController {
@Get()
@Render('index.hbs')
root() {
return { message: 'Hello world!' };
}
}
While the application is running, open your browser and navigate to http://localhost:3000
. You should see the Hello world!
message.
A working example is available here.
By default, Nest makes use of the Express framework. As mentioned earlier, Nest also provides compatibility with other libraries such as, for example, Fastify. Nest achieves this framework independence by implementing a framework adapter whose primary function is to proxy middleware and handlers to appropriate library-specific implementations.
info Hint Note that in order for a framework adapter to be implemented, the target library has to provide similar request/response pipeline processing as found in Express.
Fastify provides a good alternative framework for Nest because it solves design issues in a similar manner to Express. However, fastify is much faster than Express, achieving almost two times better benchmarks results. A fair question is why does Nest use Express as the default HTTP provider? The reason is that Express is widely-used, well-known, and has an enormous set of compatible middleware, which is available to Nest users out-of-the-box.
But since Nest provides framework-independence, you can easily migrate between them. Fastify can be a better choice when you place high value on very fast performance. To utilize Fastify, simply choose the built-in FastifyAdapter
as shown in this chapter.
First, we need to install the required package:
$ npm i --save @nestjs/platform-fastify
Once the Fastify platform is installed, we can use the FastifyAdapter
.
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter()
);
await app.listen(3000);
}
bootstrap();
By default, Fastify listens only on the localhost 127.0.0.1
interface (read more). If you want to accept connections on other hosts, you should specify '0.0.0.0'
in the listen()
call:
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter()
);
await app.listen(3000, '0.0.0.0');
}
Keep in mind that when you use the FastifyAdapter
, Nest uses Fastify as the HTTP provider. This means that each recipe that relies on Express may no longer work. You should, instead, use Fastify equivalent packages.
Fastify handles redirect responses slightly differently than Express. To do a proper redirect with Fastify, return both the status code and the URL, as follows:
@Get()
index(@Res() res) {
res.status(302).redirect('/login');
}
You can pass options into the Fastify constructor through the FastifyAdapter
constructor. For example:
new FastifyAdapter({ logger: true });
A working example is available here.
- Quick start
- Resolvers
- Mutations
- Subscriptions
- Scalars
- Directives
- Plugins
- Interfaces
- Unions
- Enums
- Mapped types
- Complexity
- Extensions
- CLI Plugin
- Generating SDL
- Other features
- Federation
GraphQL is a powerful query language for APIs and a runtime for fulfilling those queries with your existing data. It's an elegant approach that solves many problems typically found with REST APIs. For background, we suggest reading this comparison between GraphQL and REST. GraphQL combined with TypeScript helps you develop better type safety with your GraphQL queries, giving you end-to-end typing.
In this chapter, we assume a basic understanding of GraphQL, and focus on how to work with the built-in @nestjs/graphql
module. The GraphQLModule
is a wrapper around the Apollo server. We use this proven GraphQL package to provide a way to use GraphQL with Nest.
Start by installing the required packages:
$ npm i @nestjs/graphql graphql-tools graphql apollo-server-express
info Hint If using Fastify, instead of installing
apollo-server-express
, you should installapollo-server-fastify
.
Nest offers two ways of building GraphQL applications, the code first and the schema first methods. You should choose the one that works best for you. Most of the chapters in this GraphQL section are divided into two main parts: one you should follow if you adopt code first, and the other to be used if you adopt schema first.
In the code first approach, you use decorators and TypeScript classes to generate the corresponding GraphQL schema. This approach is useful if you prefer to work exclusively with TypeScript and avoid context switching between language syntaxes.
In the schema first approach, the source of truth is GraphQL SDL (Schema Definition Language) files. SDL is a language-agnostic way to share schema files between different platforms. Nest automatically generates your TypeScript definitions (using either classes or interfaces) based on the GraphQL schemas to reduce the need to write redundant boilerplate code.
Once the packages are installed, we can import the GraphQLModule
and configure it with the forRoot()
static method.
@@filename()
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
@Module({
imports: [
GraphQLModule.forRoot({}),
],
})
export class AppModule {}
The forRoot()
method takes an options object as an argument. These options are passed through to the underlying Apollo instance (read more about available settings here). For example, if you want to disable the playground
and turn off debug
mode, pass the following options:
@@filename()
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
@Module({
imports: [
GraphQLModule.forRoot({
debug: false,
playground: false,
}),
],
})
export class AppModule {}
As mentioned, these options will be forwarded to the ApolloServer
constructor.
The playground is a graphical, interactive, in-browser GraphQL IDE, available by default on the same URL as the GraphQL server itself. To access the playground, you need a basic GraphQL server configured and running. To see it now, you can install and build the working example here. Alternatively, if you're following along with these code samples, once you've complete the steps in the Resolvers chapter, you can access the playground.
With that in place, and with your application running in the background, you can then open your web browser and navigate to http://localhost:3000/graphql
(host and port may vary depending on your configuration). You will then see the GraphQL playground, as shown below.
Another useful feature of the @nestjs/graphql
module is the ability to serve multiple endpoints at once. This lets you decide which modules should be included in which endpoint. By default, GraphQL
searches for resolvers throughout the whole app. To limit this scan to only a subset of modules, use the include
property.
GraphQLModule.forRoot({
include: [CatsModule],
}),
In the code first approach, you use decorators and TypeScript classes to generate the corresponding GraphQL schema.
To use the code first approach, start by adding the autoSchemaFile
property to the options object:
GraphQLModule.forRoot({
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
}),
The autoSchemaFile
property value is the path where your automatically generated schema will be created. Alternatively, the schema can be generated on-the-fly in memory. To enable this, set the autoSchemaFile
property to true
:
GraphQLModule.forRoot({
autoSchemaFile: true,
}),
By default, the types in the generated schema will be in the order they are defined in the included modules. To sort the schema lexicographically, set the sortSchema
property to true
:
GraphQLModule.forRoot({
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
sortSchema: true,
}),
A fully working code first sample is available here.
To use the schema first approach, start by adding a typePaths
property to the options object. The typePaths
property indicates where the GraphQLModule
should look for GraphQL SDL schema definition files you'll be writing. These files will be combined in memory; this allows you to split your schemas into several files and locate them near their resolvers.
GraphQLModule.forRoot({
typePaths: ['./**/*.graphql'],
}),
You will typically also need to have TypeScript definitions (classes and interfaces) that correspond to the GraphQL SDL types. Creating the corresponding TypeScript definitions by hand is redundant and tedious. It leaves us without a single source of truth -- each change made within SDL forces us to adjust TypeScript definitions as well. To address this, the @nestjs/graphql
package can automatically generate TypeScript definitions from the abstract syntax tree (AST). To enable this feature, add the definitions
options property when configuring the GraphQLModule
.
GraphQLModule.forRoot({
typePaths: ['./**/*.graphql'],
definitions: {
path: join(process.cwd(), 'src/graphql.ts'),
},
}),
The path property of the definitions
object indicates where to save generated TypeScript output. By default, all generated TypeScript types are created as interfaces. To generate classes instead, specify the outputAs
property with a value of 'class'
.
GraphQLModule.forRoot({
typePaths: ['./**/*.graphql'],
definitions: {
path: join(process.cwd(), 'src/graphql.ts'),
outputAs: 'class',
},
}),
The above approach dynamically generates TypeScript definitions each time the application starts. Alternatively, it may be preferable to build a simple script to generate these on demand. For example, assume we create the following script as generate-typings.ts
:
import { GraphQLDefinitionsFactory } from '@nestjs/graphql';
import { join } from 'path';
const definitionsFactory = new GraphQLDefinitionsFactory();
definitionsFactory.generate({
typePaths: ['./src/**/*.graphql'],
path: join(process.cwd(), 'src/graphql.ts'),
outputAs: 'class',
});
Now you can run this script on demand:
$ ts-node generate-typings
info Hint You can compile the script beforehand (e.g., with
tsc
) and usenode
to execute it.
To enable watch mode for the script (to automatically generate typings whenever any .graphql
file changes), pass the watch
option to the generate()
method.
definitionsFactory.generate({
typePaths: ['./src/**/*.graphql'],
path: join(process.cwd(), 'src/graphql.ts'),
outputAs: 'class',
watch: true,
});
To automatically generate the additional __typename
field for every object type, enable the emitTypenameField
option.
definitionsFactory.generate({
// ...,
emitTypenameField: true,
});
To generate resolvers (queries, mutations, subscriptions) as plain fields without arguments, enable the skipResolverArgs
option.
definitionsFactory.generate({
// ...,
skipResolverArgs: true,
});
A fully working schema first sample is available here.
In some circumstances (for example end-to-end tests), you may want to get a reference to the generated schema object. In end-to-end tests, you can then run queries using the graphql
object without using any HTTP listeners.
You can access the generated schema (in either the code first or schema first approach), using the GraphQLSchemaHost
class:
const { schema } = app.get(GraphQLSchemaHost);
info Hint You must call the
GraphQLSchemaHost#schema
getter after the application has been initialized (after theonModuleInit
hook has been triggered by either theapp.listen()
orapp.init()
method).
When you need to pass module options asynchronously instead of statically, use the forRootAsync()
method. As with most dynamic modules, Nest provides several techniques to deal with async configuration.
One technique is to use a factory function:
GraphQLModule.forRootAsync({
useFactory: () => ({
typePaths: ['./**/*.graphql'],
}),
}),
Like other factory providers, our factory function can be async and can inject dependencies through inject
.
GraphQLModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
typePaths: configService.getString('GRAPHQL_TYPE_PATHS'),
}),
inject: [ConfigService],
}),
Alternatively, you can configure the GraphQLModule
using a class instead of a factory, as shown below:
GraphQLModule.forRootAsync({
useClass: GqlConfigService,
}),
The construction above instantiates GqlConfigService
inside GraphQLModule
, using it to create options object. Note that in this example, the GqlConfigService
has to implement the GqlOptionsFactory
interface, as shown below. The GraphQLModule
will call the createGqlOptions()
method on the instantiated object of the supplied class.
@Injectable()
class GqlConfigService implements GqlOptionsFactory {
createGqlOptions(): GqlModuleOptions {
return {
typePaths: ['./**/*.graphql'],
};
}
}
If you want to reuse an existing options provider instead of creating a private copy inside the GraphQLModule
, use the useExisting
syntax.
GraphQLModule.forRootAsync({
imports: [ConfigModule],
useExisting: ConfigService,
}),
Resolvers provide the instructions for turning a GraphQL operation (a query, mutation, or subscription) into data. They return the same shape of data we specify in our schema -- either synchronously or as a promise that resolves to a result of that shape. Typically, you create a resolver map manually. The @nestjs/graphql
package, on the other hand, generates a resolver map automatically using the metadata provided by decorators you use to annotate classes. To demonstrate the process of using the package features to create a GraphQL API, we'll create a simple authors API.
In the code first approach, we don't follow the typical process of creating our GraphQL schema by writing GraphQL SDL by hand. Instead, we use TypeScript decorators to generate the SDL from TypeScript class definitions. The @nestjs/graphql
package reads the metadata defined through the decorators and automatically generates the schema for you.
Most of the definitions in a GraphQL schema are object types. Each object type you define should represent a domain object that an application client might need to interact with. For example, our sample API needs to be able to fetch a list of authors and their posts, so we should define the Author
type and Post
type to support this functionality.
If we were using the schema first approach, we'd define such a schema with SDL like this:
type Author {
id: Int!
firstName: String
lastName: String
posts: [Post]
}
In this case, using the code first approach, we define schemas using TypeScript classes and using TypeScript decorators to annotate the fields of those classes. The equivalent of the above SDL in the code first approach is:
@@filename(authors/models/author.model)
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Post } from './post';
@ObjectType()
export class Author {
@Field(type => Int)
id: number;
@Field({ nullable: true })
firstName?: string;
@Field({ nullable: true })
lastName?: string;
@Field(type => [Post])
posts: Post[];
}
info Hint TypeScript's metadata reflection system has several limitations which make it impossible, for instance, to determine what properties a class consists of or recognize whether a given property is optional or required. Because of these limitations, we must either explicitly use the
@Field()
decorator in our schema definition classes to provide metadata about each field's GraphQL type and optionality, or use a CLI plugin to generate these for us.
The Author
object type, like any class, is made of a collection of fields, with each field declaring a type. A field's type corresponds to a GraphQL type. A field's GraphQL type can be either another object type or a scalar type. A GraphQL scalar type is a primitive (like ID
, String
, Boolean
, or Int
) that resolves to a single value.
info Hint In addition to GraphQL's built-in scalar types, you can define custom scalar types (read more).
The above Author
object type definition will cause Nest to generate the SDL we showed above:
type Author {
id: Int!
firstName: String
lastName: String
posts: [Post]
}
The @Field()
decorator accepts an optional type function (e.g., type => Int
), and optionally an options object.
The type function is required when there's the potential for ambiguity between the TypeScript type system and the GraphQL type system. Specifically: it is not required for string
and boolean
types; it is required for arrays, numbers (which must be mapped to either a GraphQL Int
or Float
) and object types. The type function should simply return the desired GraphQL type (as shown in various examples in these chapters).
The options object can have any of the following key/value pairs:
nullable
: for specifying whether a field is nullable (in SDL, each field is non-nullable by default);boolean
description
: for setting a field description;string
deprecationReason
: for marking a field as deprecated;string
For example:
@Field({ description: `Book title`, deprecationReason: 'Not useful in v2 schema' })
title: string;
info Hint You can also add a description to, or deprecate, the whole object type:
@ObjectType({{ '{' }} description: 'Author model' {{ '}' }})
.
When the field is an array, we must manually indicate the array type in the Field()
decorator's type function, as shown below:
@Field(type => [Post])
posts: Post[];
info Hint Using array bracket notation (
[ ]
), we can indicate the depth of the array. For example, using[[Int]]
would represent an integer matrix.
To declare that an array's items (not the array itself) are nullable, set the nullable
property to 'items'
as shown below:
@Field(type => [Post], { nullable: 'items' })
posts: Post[];
info Hint If both the array and its items are nullable, set
nullable
to'itemsAndList'
instead.
Now that the Author
object type is created, let's define the Post
object type.
@@filename(posts/models/post.model)
import { Field, Int, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class Post {
@Field(type => Int)
id: number;
@Field()
title: string;
@Field(type => Int, { nullable: true })
votes?: number;
}
The Post
object type will result in generating the following part of the GraphQL schema in SDL:
@@filename(schema.gql)
type Post {
id: Int!
title: String!
votes: Int
}
At this point, we've defined the objects (type definitions) that can exist in our data graph, but clients don't yet have a way to interact with those objects. To address that, we need to create a resolver class. In the code first method, a resolver class both defines resolver functions and generates the Query type. This will be clear as we work through the example below:
@@filename(authors/authors.resolver)
@Resolver(of => Author)
export class AuthorsResolver {
constructor(
private authorsService: AuthorsService,
private postsService: PostsService,
) {}
@Query(returns => Author)
async author(@Args('id', { type: () => Int }) id: number) {
return this.authorsService.findOneById(id);
}
@ResolveField()
async posts(@Parent() author: Author) {
const { id } = author;
return this.postsService.findAll({ authorId: id });
}
}
info Hint All decorators (e.g.,
@Resolver
,@ResolveField
,@Args
, etc.) are exported from the@nestjs/graphql
package.
You can define multiple resolver classes. Nest will combine these at run time. See the module section below for more on code organization.
warning Note The logic inside the
AuthorsService
andPostsService
classes can be as simple or sophisticated as needed. The main point of this example is to show how to construct resolvers and how they can interact with other providers.
In the example above, we created the AuthorsResolver
which defines one query resolver function and one field resolver function. To create a resolver, we create a class with resolver functions as methods, and annotate the class with the @Resolver()
decorator.
In this example, we defined a query handler to get the author object based on the id
sent in the request. To specify that the method is a query handler, use the @Query()
decorator.
The argument passed to the @Resolver()
decorator is optional, but comes into play when our graph becomes non-trivial. It's used to supply a parent object used by field resolver functions as they traverse down through an object graph.
In our example, since the class includes a field resolver function (for the posts
property of the Author
object type), we must supply the @Resolver()
decorator with a value to indicate which class is the parent type (i.e., the corresponding ObjectType
class name) for all field resolvers defined within this class. As should be clear from the example, when writing a field resolver function, it's necessary to access the parent object (the object the field being resolved is a member of). In this example, we populate an author's posts array with a field resolver that calls a service which takes the author's id
as an argument. Hence the need to identify the parent object in the @Resolver()
decorator. Note the corresponding use of the @Parent()
method parameter decorator to then extract a reference to that parent object in the field resolver.
We can define multiple @Query()
resolver functions (both within this class, and in any other resolver class), and they will be aggregated into a single Query type definition in the generated SDL along with the appropriate entries in the resolver map. This allows you to define queries close to the models and services that they use, and to keep them well organized in modules.
In the above examples, the @Query()
decorator generates a GraphQL schema query type name based on the method name. For example, consider the following construction from the example above:
@Query(returns => Author)
async author(@Args('id', { type: () => Int }) id: number) {
return this.authorsService.findOneById(id);
}
This generates the following entry for the author query in our schema (the query type uses the same name as the method name):
type Query {
author(id: Int!): Author
}
info Hint Learn more about GraphQL queries here.
Conventionally, we prefer to decouple these names; for example, we prefer to use a name like getAuthor()
for our query handler method, but still use author
for our query type name. The same applies to our field resolvers. We can easily do this by passing the mapping names as arguments of the @Query()
and @ResolveField()
decorators, as shown below:
@@filename(authors/authors.resolver)
@Resolver(of => Author)
export class AuthorsResolver {
constructor(
private authorsService: AuthorsService,
private postsService: PostsService,
) {}
@Query(returns => Author, { name: 'author' })
async getAuthor(@Args('id', { type: () => Int }) id: number) {
return this.authorsService.findOneById(id);
}
@ResolveField('posts', returns => [Post])
async getPosts(@Parent() author: Author) {
const { id } = author;
return this.postsService.findAll({ authorId: id });
}
}
The getAuthor
handler method above will result in generating the following part of the GraphQL schema in SDL:
type Query {
author(id: Int!): Author
}
The @Query()
decorator's options object (where we pass {{ '{' }}name: 'author'{{ '}' }}
above) accepts a number of key/value pairs:
name
: name of the query; astring
description
: a description that will be used to generate GraphQL schema documentation (e.g., in GraphQL playground); astring
deprecationReason
: sets query metadata to show the query as deprecated (e.g., in GraphQL playground); astring
nullable
: whether the query can return a null data response;boolean
or'items'
or'itemsAndList'
(see above for details of'items'
and'itemsAndList'
)
Use the @Args()
decorator to extract arguments from a request for use in the method handler. This works in a very similar fashion to REST route parameter argument extraction.
Usually your @Args()
decorator will be simple, and not require an object argument as seen with the getAuthor()
method above. For example, if the type of an identifier is string, the following construction is sufficient, and simply plucks the named field from the inbound GraphQL request for use as a method argument.
@Args('id') id: string
In the getAuthor()
case, the number
type is used, which presents a challenge. The number
TypeScript type doesn't give us enough information about the expected GraphQL representation (e.g., Int
vs. Float
). Thus we have to explicitly pass the type reference. We do that by passing a second argument to the Args()
decorator, containing argument options, as shown below:
@Query(returns => Author, { name: 'author' })
async getAuthor(@Args('id', { type: () => Int }) id: number) {
return this.authorsService.findOneById(id);
}
The options object allows us to specify the following optional key value pairs:
type
: a function returning the GraphQL typedefaultValue
: a default value;any
description
: description metadata;string
deprecationReason
: to deprecate a field and provide meta data describing why;string
nullable
: whether the field is nullable
Query handler methods can take multiple arguments. Let's imagine that we want to fetch an author based on its firstName
and lastName
. In this case, we can call @Args
twice:
getAuthor(
@Args('firstName', { nullable: true }) firstName?: string,
@Args('lastName', { defaultValue: '' }) lastName?: string,
) {}
With inline @Args()
calls, code like the example above becomes bloated. Instead, you can create a dedicated GetAuthorArgs
arguments class and access it in the handler method as follows:
@Args() args: GetAuthorArgs
Create the GetAuthorArgs
class using @ArgsType()
as shown below:
@@filename(authors/dto/get-author.args)
import { MinLength } from 'class-validator';
import { Field, ArgsType } from '@nestjs/graphql';
@ArgsType()
class GetAuthorArgs {
@Field({ nullable: true })
firstName?: string;
@Field({ defaultValue: '' })
@MinLength(3)
lastName: string;
}
info Hint Again, due to TypeScript's metadata reflection system limitations, it's required to either use the
@Field
decorator to manually indicate type and optionality, or use a CLI plugin.
This will result in generating the following part of the GraphQL schema in SDL:
type Query {
author(firstName: String, lastName: String = ''): Author
}
info Hint Note that arguments classes like
GetAuthorArgs
play very well with theValidationPipe
(read more).
You can use standard TypeScript class inheritance to create base classes with generic utility type features (fields and field properties, validations, etc.) that can be extended. For example, you may have a set of pagination related arguments that always include the standard offset
and limit
fields, but also other index fields that are type-specific. You can set up a class hierarchy as shown below.
Base @ArgsType()
class:
@ArgsType()
class PaginationArgs {
@Field((type) => Int)
offset: number = 0;
@Field((type) => Int)
limit: number = 10;
}
Type specific sub-class of the base @ArgsType()
class:
@ArgsType()
class GetAuthorArgs extends PaginationArgs {
@Field({ nullable: true })
firstName?: string;
@Field({ defaultValue: '' })
@MinLength(3)
lastName: string;
}
The same approach can be taken with @ObjectType()
objects. Define generic properties on the base class:
@ObjectType()
class Character {
@Field((type) => Int)
id: number;
@Field()
name: string;
}
Add type-specific properties on sub-classes:
@ObjectType()
class Warrior extends Character {
@Field()
level: number;
}
You can use inheritance with a resolver as well. You can ensure type safety by combining inheritance and TypeScript generics. For example, to create a base class with a generic findAll
query, use a construction like this:
function BaseResolver<T extends Type<unknown>>(classRef: T): any {
@Resolver({ isAbstract: true })
abstract class BaseResolverHost {
@Query((type) => [classRef], { name: `findAll${classRef.name}` })
async findAll(): Promise<T[]> {
return [];
}
}
return BaseResolverHost;
}
Note the following:
- an explicit return type (
any
above) is required: otherwise TypeScript complains about the usage of a private class definition. Recommended: define an interface instead of usingany
. Type
is imported from the@nestjs/common
package- The
isAbstract: true
property indicates that SDL (Schema Definition Language statements) shouldn't be generated for this class. Note, you can set this property for other types as well to suppress SDL generation.
Here's how you could generate a concrete sub-class of the BaseResolver
:
@Resolver((of) => Recipe)
export class RecipesResolver extends BaseResolver(Recipe) {
constructor(private recipesService: RecipesService) {
super();
}
}
This construct would generated the following SDL:
type Query {
findAllRecipe: [Recipe!]!
}
We saw one use of generics above. This powerful TypeScript feature can be used to create useful abstractions. For example, here's a sample cursor-based pagination implementation based on this documentation:
import { Field, ObjectType, Int } from '@nestjs/graphql';
import { Type } from '@nestjs/common';
export function Paginated<T>(classRef: Type<T>): any {
@ObjectType(`${classRef.name}Edge`)
abstract class EdgeType {
@Field((type) => String)
cursor: string;
@Field((type) => classRef)
node: T;
}
@ObjectType({ isAbstract: true })
abstract class PaginatedType {
@Field((type) => [EdgeType], { nullable: true })
edges: EdgeType[];
@Field((type) => [classRef], { nullable: true })
nodes: T[];
@Field((type) => Int)
totalCount: number;
@Field()
hasNextPage: boolean;
}
return PaginatedType;
}
With the above base class defined, we can now easily create specialized types that inherit this behavior. For example:
@ObjectType()
class PaginatedAuthor extends Paginated(Author) {}
As mentioned in the previous chapter, in the schema first approach we start by manually defining schema types in SDL (read more). Consider the following SDL type definitions.
info Hint For convenience in this chapter, we've aggregated all of the SDL in one location (e.g., one
.graphql
file, as shown below). In practice, you may find it appropriate to organize your code in a modular fashion. For example, it can be helpful to create individual SDL files with type definitions representing each domain entity, along with related services, resolver code, and the Nest module definition class, in a dedicated directory for that entity. Nest will aggregate all the individual schema type definitions at run time.
type Author {
id: Int!
firstName: String
lastName: String
posts: [Post]
}
type Post {
id: Int!
title: String!
votes: Int
}
type Query {
author(id: Int!): Author
}
The schema above exposes a single query - author(id: Int!): Author
.
info Hint Learn more about GraphQL queries here.
Let's now create an AuthorsResolver
class that resolves author queries:
@@filename(authors/authors.resolver)
@Resolver('Author')
export class AuthorsResolver {
constructor(
private authorsService: AuthorsService,
private postsService: PostsService,
) {}
@Query()
async author(@Args('id') id: number) {
return this.authorsService.findOneById(id);
}
@ResolveField()
async posts(@Parent() author) {
const { id } = author;
return this.postsService.findAll({ authorId: id });
}
}
info Hint All decorators (e.g.,
@Resolver
,@ResolveField
,@Args
, etc.) are exported from the@nestjs/graphql
package.
warning Note The logic inside the
AuthorsService
andPostsService
classes can be as simple or sophisticated as needed. The main point of this example is to show how to construct resolvers and how they can interact with other providers.
The @Resolver()
decorator is required. It takes an optional string argument with the name of a class. This class name is required whenever the class includes @ResolveField()
decorators to inform Nest that the decorated method is associated with a parent type (the Author
type in our current example). Alternatively, instead of setting @Resolver()
at the top of the class, this can be done for each method:
@Resolver('Author')
@ResolveField()
async posts(@Parent() author) {
const { id } = author;
return this.postsService.findAll({ authorId: id });
}
In this case (@Resolver()
decorator at the method level), if you have multiple @ResolveField()
decorators inside a class, you must add @Resolver()
to all of them. This is not considered the best practice (as it creates extra overhead).
info Hint Any class name argument passed to
@Resolver()
does not affect queries (@Query()
decorator) or mutations (@Mutation()
decorator).
warning Warning Using the
@Resolver
decorator at the method level is not supported with the code first approach.
In the above examples, the @Query()
and @ResolveField()
decorators are associated with GraphQL schema types based on the method name. For example, consider the following construction from the example above:
@Query()
async author(@Args('id') id: number) {
return this.authorsService.findOneById(id);
}
This generates the following entry for the author query in our schema (the query type uses the same name as the method name):
type Query {
author(id: Int!): Author
}
Conventionally, we would prefer to decouple these, using names like getAuthor()
or getPosts()
for our resolver methods. We can easily do this by passing the mapping name as an argument to the decorator, as shown below:
@@filename(authors/authors.resolver)
@Resolver('Author')
export class AuthorsResolver {
constructor(
private authorsService: AuthorsService,
private postsService: PostsService,
) {}
@Query('author')
async getAuthor(@Args('id') id: number) {
return this.authorsService.findOneById(id);
}
@ResolveField('posts')
async getPosts(@Parent() author) {
const { id } = author;
return this.postsService.findAll({ authorId: id });
}
}
Assuming that we use the schema first approach and have enabled the typings generation feature (with outputAs: 'class'
as shown in the previous chapter), once you run the application it will generate the following file (in the location you specified in the GraphQLModule.forRoot()
method. For example, in src/graphql.ts
)
@@filename(graphql)
export class Author {
id: number;
firstName?: string;
lastName?: string;
posts?: Post[];
}
export class Post {
id: number;
title: string;
votes?: number;
}
export abstract class IQuery {
abstract author(id: number): Author | Promise<Author>;
}
By generating classes (instead of the default technique of generating interfaces), you can use declarative validation decorators in combination with the schema first approach, which is an extremely useful technique (read more). For example, you could add class-validator
decorators to the generated CreatePostInput
class as shown below to enforce minimum and maximum string lengths on the title
field:
import { MinLength, MaxLength } from 'class-validator';
export class CreatePostInput {
@MinLength(3)
@MaxLength(50)
title: string;
}
warning Notice To enable auto-validation of your inputs (and parameters), use
ValidationPipe
. Read more about validation here and more specifically about pipes here.
However, if you add decorators directly to the automatically generated file, they will be overwritten each time the file is generated. Instead, create a separate file and simply extend the generated class.
import { MinLength, MaxLength } from 'class-validator';
import { Post } from '../../graphql.ts';
export class CreatePostInput extends Post {
@MinLength(3)
@MaxLength(50)
title: string;
}
We can access the standard GraphQL resolver arguments using dedicated decorators. Below is a comparison of the Nest decorators and the plain Apollo parameters they represent.
@Root() and @Parent() |
root /parent |
@Context(param?: string) |
context / context[param] |
@Info(param?: string) |
info / info[param] |
@Args(param?: string) |
args / args[param] |
These arguments have the following meanings:
root
: an object that contains the result returned from the resolver on the parent field, or, in the case of a top-levelQuery
field, therootValue
passed from the server configuration.context
: an object shared by all resolvers in a particular query; typically used to contain per-request state.info
: an object that contains information about the execution state of the query.args
: an object with the arguments passed into the field in the query.
Once we're done with the above steps, we have declaratively specified all the information needed by the GraphQLModule
to generate a resolver map. The GraphQLModule
uses reflection to introspect the meta data provided via the decorators, and transforms classes into the correct resolver map automatically.
The only other thing you need to take care of is to provide (i.e., list as a provider
in some module) the resolver class(es) (AuthorsResolver
), and importing the module (AuthorsModule
) somewhere, so Nest will be able to utilize it.
For example, we can do this in an AuthorsModule
, which can also provide other services needed in this context. Be sure to import AuthorsModule
somewhere (e.g., in the root module, or some other module imported by the root module).
@@filename(authors/authors.module)
@Module({
imports: [PostsModule],
providers: [AuthorsService, AuthorsResolver],
})
export class AuthorsModule {}
info Hint It is helpful to organize your code by your so-called domain model (similar to the way you would organize entry points in a REST API). In this approach, keep your models (
ObjectType
classes), resolvers and services together within a Nest module representing the domain model. Keep all of these components in a single folder per module. When you do this, and use the Nest CLI to generate each element, Nest will wire all of these parts together (locating files in appropriate folders, generating entries inprovider
andimports
arrays, etc.) automatically for you.
Most discussions of GraphQL focus on data fetching, but any complete data platform needs a way to modify server-side data as well. In REST, any request could end up causing side-effects on the server, but best practice suggests we should not modify data in GET requests. GraphQL is similar - technically any query could be implemented to cause a data write. However, like REST, it's recommended to observe the convention that any operations that cause writes should be sent explicitly via a mutation (read more here).
The official Apollo documentation uses an upvotePost()
mutation example. This mutation implements a method to increase a post's votes
property value. To create an equivalent mutation in Nest, we'll make use of the @Mutation()
decorator.
Let's add another method to the AuthorResolver
used in the previous section (see resolvers).
@Mutation(returns => Post)
async upvotePost(@Args({ name: 'postId', type: () => Int }) postId: number) {
return this.postsService.upvoteById({ id: postId });
}
info Hint All decorators (e.g.,
@Resolver
,@ResolveField
,@Args
, etc.) are exported from the@nestjs/graphql
package.
This will result in generating the following part of the GraphQL schema in SDL:
type Mutation {
upvotePost(postId: Int!): Post
}
The upvotePost()
method takes postId
(Int
) as an argument and returns an updated Post
entity. For the reasons explained in the resolvers section, we have to explicitly set the expected type.
If the mutation needs to take an object as an argument, we can create an input type. The input type is a special kind of object type that can be passed in as an argument (read more here). To declare an input type, use the @InputType()
decorator.
import { InputType, Field } from '@nestjs/graphql';
@InputType()
export class UpvotePostInput {
@Field()
postId: number;
}
info Hint The
@InputType()
decorator takes an options object as an argument, so you can, for example, specify the input type's description. Note that, due to TypeScript's metadata reflection system limitations, you must either use the@Field
decorator to manually indicate a type, or use a CLI plugin.
We can then use this type in the resolver class:
@Mutation(returns => Post)
async upvotePost(
@Args('upvotePostData') upvotePostData: UpvotePostInput,
) {}
Let's extend our AuthorResolver
used in the previous section (see resolvers).
@Mutation()
async upvotePost(@Args('postId') postId: number) {
return this.postsService.upvoteById({ id: postId });
}
Note that we assumed above that the business logic has been moved to the PostsService
(querying the post and incrementing its votes
property). The logic inside the PostsService
class can be as simple or sophisticated as needed. The main point of this example is to show how resolvers can interact with other providers.
The last step is to add our mutation to the existing types definition.
type Author {
id: Int!
firstName: String
lastName: String
posts: [Post]
}
type Post {
id: Int!
title: String
votes: Int
}
type Query {
author(id: Int!): Author
}
type Mutation {
upvotePost(postId: Int!): Post
}
The upvotePost(postId: Int!): Post
mutation is now available to be called as part of our application's GraphQL API.
In addition to fetching data using queries and modifying data using mutations, the GraphQL spec supports a third operation type, called subscription
. GraphQL subscriptions are a way to push data from the server to the clients that choose to listen to real time messages from the server. Subscriptions are similar to queries in that they specify a set of fields to be delivered to the client, but instead of immediately returning a single answer, a channel is opened and a result is sent to the client every time a particular event happens on the server.
A common use case for subscriptions is notifying the client side about particular events, for example the creation of a new object, updated fields and so on (read more here).
To enable subscriptions, set the installSubscriptionHandlers
property to true
.
GraphQLModule.forRoot({
installSubscriptionHandlers: true,
}),
To create a subscription using the code first approach, we use the @Subscription()
decorator and the PubSub
class from the graphql-subscriptions
package, which provides a simple publish/subscribe API.
The following subscription handler takes care of subscribing to an event by calling PubSub#asyncIterator
. This method takes a single argument, the triggerName
, which corresponds to an event topic name.
const pubSub = new PubSub();
@Resolver(of => Author)
export class AuthorResolver {
// ...
@Subscription(returns => Comment)
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
}
info Hint All decorators are exported from the
@nestjs/graphql
package, while thePubSub
class is exported from thegraphql-subscriptions
package.
warning Note
PubSub
is a class that exposes a simplepublish
andsubscribe API
. Read more about it here. Note that the Apollo docs warn that the default implementation is not suitable for production (read more here). Production apps should use aPubSub
implementation backed by an external store (read more here).
This will result in generating the following part of the GraphQL schema in SDL:
type Subscription {
commentAdded(): Comment!
}
Note that subscriptions, by definition, return an object with a single top level property whose key is the name of the subscription. This name is either inherited from the name of the subscription handler method (i.e., commentAdded
above), or is provided explicitly by passing an option with the key name
as the second argument to the @Subscription()
decorator, as shown below.
@Subscription(returns => Comment, {
name: 'commentAdded',
})
addCommentHandler() {
return pubSub.asyncIterator('commentAdded');
}
This construct produces the same SDL as the previous code sample, but allows us to decouple the method name from the subscription.
Now, to publish the event, we use the PubSub#publish
method. This is often used within a mutation to trigger a client-side update when a part of the object graph has changed. For example:
@@filename(posts/posts.resolver)
@Mutation(returns => Post)
async addComment(
@Args('postId', { type: () => Int }) postId: number,
@Args('comment', { type: () => Comment }) comment: CommentInput,
) {
const newComment = this.commentsService.addComment({ id: postId, comment });
pubSub.publish('commentAdded', { commentAdded: newComment });
return newComment;
}
The PubSub#publish
method takes a triggerName
(again, think of this as an event topic name) as the first parameter, and an event payload as the second parameter. As mentioned, the subscription, by definition, returns a value and that value has a shape. Look again at the generated SDL for our commentAdded
subscription:
type Subscription {
commentAdded(): Comment!
}
This tells us that the subscription must return an object with a top-level property name of commentAdded
that has a value which is a Comment
object. The important point to note is that the shape of the event payload emitted by the PubSub#publish
method must correspond to the shape of the value expected to return from the subscription. So, in our example above, the pubSub.publish('commentAdded', {{ '{' }} commentAdded: newComment {{ '}' }})
statement publishes a commentAdded
event with the appropriately shaped payload. If these shapes don't match, your subscription will fail during the GraphQL validation phase.
To filter out specific events, set the filter
property to a filter function. This function acts similar to the function passed to an array filter
. It takes two arguments: payload
containing the event payload (as sent by the event publisher), and variables
taking any arguments passed in during the subscription request. It returns a boolean determining whether this event should be published to client listeners.
@Subscription(returns => Comment, {
filter: (payload, variables) =>
payload.commentAdded.title === variables.title,
})
commentAdded(@Args('title') title: string) {
return pubSub.asyncIterator('commentAdded');
}
To mutate the published event payload, set the resolve
property to a function. The function receives the event payload (as sent by the event publisher) and returns the appropriate value.
@Subscription(returns => Comment, {
resolve: value => value,
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
warning Note If you use the
resolve
option, you should return the unwrapped payload (e.g., with our example, return anewComment
object directly, not a{{ '{' }} commentAdded: newComment {{ '}' }}
object).
If you need to access injected providers (e.g., use an external service to validate the data), use the following construction.
@Subscription(returns => Comment, {
resolve(this: AuthorResolver, value) {
// "this" refers to an instance of "AuthorResolver"
return value;
}
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
The same construction works with filters:
@Subscription(returns => Comment, {
filter(this: AuthorResolver, payload, variables) {
// "this" refers to an instance of "AuthorResolver"
return payload.commentAdded.title === variables.title;
}
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
To create an equivalent subscription in Nest, we'll make use of the @Subscription()
decorator.
const pubSub = new PubSub();
@Resolver('Author')
export class AuthorResolver {
// ...
@Subscription()
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
}
To filter out specific events based on context and arguments, set the filter
property.
@Subscription('commentAdded', {
filter: (payload, variables) =>
payload.commentAdded.title === variables.title,
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
To mutate the published payload, we can use a resolve
function.
@Subscription('commentAdded', {
resolve: value => value,
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
If you need to access injected providers (e.g., use an external service to validate the data), use the following construction:
@Subscription('commentAdded', {
resolve(this: AuthorResolver, value) {
// "this" refers to an instance of "AuthorResolver"
return value;
}
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
The same construction works with filters:
@Subscription('commentAdded', {
filter(this: AuthorResolver, payload, variables) {
// "this" refers to an instance of "AuthorResolver"
return payload.commentAdded.title === variables.title;
}
})
commentAdded() {
return pubSub.asyncIterator('commentAdded');
}
The last step is to update the type definitions file.
type Author {
id: Int!
firstName: String
lastName: String
posts: [Post]
}
type Post {
id: Int!
title: String
votes: Int
}
type Query {
author(id: Int!): Author
}
type Comment {
id: String
content: String
}
type Subscription {
commentAdded(title: String!): Comment
}
With this, we've created a single commentAdded(title: String!): Comment
subscription. You can find a full sample implementation here.
We instantiated a local PubSub
instance above. The preferred approach is to define PubSub
as a provider and inject it through the constructor (using the @Inject()
decorator). This allows us to re-use the instance across the whole application. For example, define a provider as follows, then inject 'PUB_SUB'
where needed.
{
provide: 'PUB_SUB',
useValue: new PubSub(),
}
To customize the subscriptions server (e.g., change the listener port), use the subscriptions
options property (read [more](https://www.apollographql.com