By default, NestJS uses Express under the hood. Moreover, since Express is very popular, NestJS can choose from a broad set of compatible third-party solutions.
A significant advantage of NestJS is that it is framework-independent. Instead of using Express, we can use an adapter using another library with a similar request/response pipeline. The most popular implementation besides Express is the Fastify adapter.
Check out this repository to see the full code from this article.
Introducing Fastify
The main selling point of Fastify is performance. We can find various comparisons on the web that prove that Fastify handles HTTP requests faster than Express.
However, there is a big chance that Express is not the bottleneck in your application. Instead, optimizing how we use our database and caching can yield great results.
If you want to know more about caching with NestJS, check out the following articles: API with NestJS #23. Implementing in-memory cache to increase the performance API with NestJS #24. Cache with Redis. Running the app in a Node.js cluster API with NestJS #58. Using ETag to implement cache and save bandwidth
Using Fastify with NestJS
At first glance, it’s very straightforward to start using Fastify with NestJS. To do that, we only need to modify our main.ts file.
1npm install @nestjs/platform-fastify1import { NestFactory } from '@nestjs/core';
2import { AppModule } from './app.module';
3import {
4 FastifyAdapter,
5 NestFastifyApplication,
6} from '@nestjs/platform-fastify';
7import { ConfigService } from '@nestjs/config';
8
9async function bootstrap() {
10 const app = await NestFactory.create<NestFastifyApplication>(
11 AppModule,
12 new FastifyAdapter(),
13 );
14
15 const configService = app.get(ConfigService);
16
17 await app.listen(configService.get('PORT'));
18}
19bootstrap();When we do the above, NestJS starts using Fastify as its HTTP provider, and we don’t need to provide any additional configuration.
However, one of the biggest strengths of Express is its wide selection of compatible libraries. When using Fastify, we must ensure that the packages we use are compatible or use alternatives developed with Fastify in mind.
We also need to remember that since Express is the default solution for NestJS, all of the libraries maintained by the NestJS team are usually developed with Express in mind. Therefore, if we decide to go with Fastify, we must brace ourselves to deal with some incompatibilities.
Things to watch out for when switching to Fastify
Modifying our main.ts file is enough to start using Fastify. Although that’s the case, let’s go deeper and investigate some real-life scenarios to see what it’s like using Fastify.
In the third part of this series, we’ve implemented authentication using bcrypt, Passport, JWT, and cookies. Since many projects include some authentication, it is a good starting point to learn how to use Fastify with NestJS.
Accessing the request and response objects
When using Express, we can easily access the request and response object using the correct decorators.
1import {
2 Controller,
3 Get,
4 UseInterceptors,
5 ClassSerializerInterceptor,
6 Req,
7 Res,
8} from '@nestjs/common';
9import CategoriesService from './categories.service';
10import express from 'express';
11
12@Controller('categories')
13@UseInterceptors(ClassSerializerInterceptor)
14export default class CategoriesController {
15 constructor(private readonly categoriesService: CategoriesService) {}
16
17 @Get()
18 async getAllCategories(
19 @Req() request: express.Request,
20 @Res() response: express.Response,
21 ) {
22 console.log(`${request.method} ${request.url}`); // GET /categories
23
24 const categories = await this.categoriesService.getAllCategories();
25 response.send(categories);
26 }
27
28 // ...
29}When we use Fastify, we need to use different interfaces for the above objects.
1import {
2 Controller,
3 Get,
4 UseInterceptors,
5 ClassSerializerInterceptor,
6 Req,
7 Res,
8} from '@nestjs/common';
9import CategoriesService from './categories.service';
10import { FastifyReply, FastifyRequest } from 'fastify';
11
12@Controller('categories')
13@UseInterceptors(ClassSerializerInterceptor)
14export default class CategoriesController {
15 constructor(private readonly categoriesService: CategoriesService) {}
16
17 @Get()
18 async getAllCategories(
19 @Req() request: FastifyRequest,
20 @Res() response: FastifyReply,
21 ) {
22 console.log(`${request.method} ${request.url}`); // GET /categories
23
24 const categories = await this.categoriesService.getAllCategories();
25 response.send(categories);
26 }
27
28 // ...
29}While in the above example, Express and Fastify work the same, this is not always the case. For example, to set a header in the response, we need to use the response.header() function instead of response.setHeader().
1import {
2 Req,
3 Controller,
4 HttpCode,
5 Post,
6 UseGuards,
7 ClassSerializerInterceptor,
8 UseInterceptors,
9 Res,
10} from '@nestjs/common';
11import { AuthenticationService } from './authentication.service';
12import RequestWithUser from './requestWithUser.interface';
13import { LocalAuthenticationGuard } from './localAuthentication.guard';
14import { FastifyReply } from 'fastify';
15
16@Controller('authentication')
17@UseInterceptors(ClassSerializerInterceptor)
18export class AuthenticationController {
19 constructor(private readonly authenticationService: AuthenticationService) {}
20
21 @HttpCode(200)
22 @UseGuards(LocalAuthenticationGuard)
23 @Post('log-in')
24 async logIn(
25 @Req() request: RequestWithUser,
26 @Res({ passthrough: true }) response: FastifyReply,
27 ) {
28 const { user } = request;
29 const cookie = this.authenticationService.getCookieWithJwtToken(user.id);
30 response.header('Set-Cookie', cookie);
31 return user;
32 }
33
34 // ...
35}Thanks to using passthrough: true we can return the data from the above method and let NestJS send the data. Without that, we would need to call the response.send() method instead.
Working with cookies
The cookie-parser library is a very popular middleware ready to use with Express. However, when using Fastify, we need to find an alternative.
1npm install @fastify/cookieFortunately, the @fastify/cookie library is straightforward. For our application to support cookies, we need to modify our main.ts file and call the app.register method.
1import { NestFactory } from '@nestjs/core';
2import { AppModule } from './app.module';
3import {
4 FastifyAdapter,
5 NestFastifyApplication,
6} from '@nestjs/platform-fastify';
7import { ConfigService } from '@nestjs/config';
8import cookie from '@fastify/cookie';
9
10async function bootstrap() {
11 const app = await NestFactory.create<NestFastifyApplication>(
12 AppModule,
13 new FastifyAdapter(),
14 );
15
16 await app.register(cookie);
17
18 const configService = app.get(ConfigService);
19
20 await app.listen(configService.get('PORT'));
21}
22bootstrap();Passport
In this series, we’ve used the Passport library to avoid implementing all aspects of authentication manually. Sadly, the @nestjs/passport library does not support Fastify officially.
There is the @fastify/passport package, but it’s not very popular. Unfortunately, it integrates with Passport differently, and guards built into NestJS might not work out of the box with it.
Thankfully, @nestjs/passport works fine with Passport as long as we use simple JWT-based authentication.
1import { ExtractJwt, Strategy } from 'passport-jwt';
2import { PassportStrategy } from '@nestjs/passport';
3import { Injectable } from '@nestjs/common';
4import { ConfigService } from '@nestjs/config';
5import { UsersService } from '../users/users.service';
6import TokenPayload from './tokenPayload.interface';
7
8@Injectable()
9export class JwtStrategy extends PassportStrategy(Strategy) {
10 constructor(
11 private readonly configService: ConfigService,
12 private readonly userService: UsersService,
13 ) {
14 super({
15 jwtFromRequest: ExtractJwt.fromExtractors([
16 (request: { cookies: Record<string, string> }) => {
17 return request?.cookies?.Authentication;
18 },
19 ]),
20 secretOrKey: configService.get('JWT_SECRET'),
21 });
22 }
23
24 async validate(payload: TokenPayload) {
25 return this.userService.getById(payload.userId);
26 }
27}We can access request?.cookies?.Authentication thanks to using the @fastify/cookie library.
Please notice that above we use request: { cookies: Record<string, string> } instead of request: FastifyRequest. This is because using the latter would cause TypeScript to complain that FastifyRequest is incompatible with express.Request.
While JWT-based authentication works fine, we might encounter issues when implementing OAuth. Thankfully, the official Discord channel of NestJS is a great place to get help with such problems. For example, Jay McDoniel, who is a part of the core NestJS team, suggests adding the following snippet to our main.ts file if we want to make @nestjs/passport work with OAuth and Fastify:
1const fastifyInstance: FastifyInstance = app.getHttpAdapter().getInstance()
2 fastifyInstance
3 .addHook('onRequest', async (req, res) => {
4 req.socket['encrypted'] = process.env.NODE_ENV === 'production'
5 })
6 .decorateReply('setHeader', function (name: string, value: unknown) {
7 this.header(name, value)
8 })
9 .decorateReply('end', function () {
10 this.send('')
11 })In the above code we try to make Fastify more compatible with how the request and response objects work in Express.
Summary
In this article, we’ve replaced Express with Fastify and achieved a fully-working application that includes authentication. While configuring NestJS to use Fastify is very simple, working with Fastify might not be that convenient. When switching to Fastify, we might increase the performance of our application, but we need to be aware of the disadvantages.
There is a big community behind Express, and it shows. If your application requires top-notch performance, it’s worth giving Fastify a try.