DEV Community

Cover image for Building A Robust Backend Server With NestJS 🚧
Damien Chazoule
Damien Chazoule

Posted on • Updated on

Building A Robust Backend Server With NestJS 🚧

It's time to talk about it... Not Next, nor Nuxt, but Nest!

P r o l o g u e

Some time ago, I challenged myself to build a robust backend solution in JavaScript. Many developers believe that JavaScript isn't secure enough, efficient or simply organized to manage data through APIs. I'll convince you otherwise....

For this new project, I chose technologies that are on the rise, in particular PostgreSQL but especially NestJS. So, I gave up my good old database that is MongoDB, in favor of a more traditional database.

NB: I was a little bit rusty at first, but going back to relational database was a real good idea; if only to retrieve data from several tables in one call, unlike NoSQL which requires as many calls as there are documents to query... Finally, SQL is like riding a bike, it's unforgettable πŸ˜‰

NestJS is a framework for creating high-scalability server applications (powered by NodeJS). This one is based on Typescript and combines three development paradigms:

  • The Object Oriented Programming #OOP
  • The Functional Programming #FP
  • The Functional Reactive Programming #FRP

NestJS starts from the observation that Web frameworks (especially those component-oriented), aren't structured enough... Except one: Angular! So, it's inspired by this last one to provide a complete library for backend developments.

NB: Indeed, Angular has a similar organization, but unlike NestJS, the structure of this one is (in my opinion) more restrictive than anything else... I prefer its counterparts like React and Vue (and Svelte) which offer more flexibility in frontend application design. However, I'm convinced that this "heavy" architecture can be a substantial help to those who are new to frontend development.

Finally, NestJS embeds the ExpressJS engine or that of Fastify (at choice), to deliver a robust and powerful HTTP server. So, you can talk about a framework of frameworks (meta-framework!? πŸ€”) For this project, I chose to use Fastify which seems more than promising...

I n i t i a l i z a t i o n

It's time to code! πŸ§‘β€πŸ’» Let's start by installing the main NestJS dependency, then initialize a new project: hello-community

npm i -g @nestjs/cli
nest new hello-community
Enter fullscreen mode Exit fullscreen mode

So, do you see the similarities with Angular? πŸ™ƒ Using the NestJS command line interface, you get an "modules - controllers - services" architecture:

  • Controllers are responsible for exposing application routes
  • Services handle communication between data sources
  • Modules link services with controllers

NB: In addition to initializing the project, the NestJS CLI will allow you to individually scaffold these same files (nest generate <module|controller|service>). So useful!

Now, let's take a closer look at the main.ts file, entry point of the application. By default, NestJS will mount an HTTP server based on ExpressJS. But, in my case, I'm gonna need Fastify, which (let's remember) is supposed to be faster and lighter than its counterpart.

To do this, it's necessary to add the @nestjs/platform-fastify dependency to the project, then adapt the main.ts file code.

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();
Enter fullscreen mode Exit fullscreen mode
main.ts

Et voilΓ  ! Just go to http://localhost:3000, to see your first Hello World! (or Hello Community! if you've been curious) πŸ‘

NB: By specifying the 0.0.0.0 string as a second parameter of the listen() function, you'll be able to access your endpoints within the same local network.

It's hard to see a real difference between ExpressJS and Fastify on a simple call. Although... I noticed a small difference when loading. According to Google Chrome, this first Hello World! loads 239 bytes with ExpressJS, versus 176 bytes via Fastify (confirmed by Mozilla Firefox). It's clear that this framework is (at least) lighter than its predecessor.

C R U D

Let's make things more interesting / more concrete by implementing your first REST(ful) API. For that, you can use the CLI tool to create files one by one (module / controller / service), or generate the whole set directly (i.e. a new CRUD resource).

The nest g resource command does this for you. By typing the resource name (users) and specifying that it's a REST API, you quickly get a ready-to-use controller and service.

One of benefits of this mode of operation is that NestJS also generates some development patterns, including the well-known DTO (Data Transfer Object) of developers. #OOP

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './interfaces/user.interface';
import { generateUuid } from '../utils';

@Injectable()
export class UsersService {
  users: User[] = [];

  findAll(): User[] {
    return this.users;
  }

  findOne(id: string): User {
    const user = this.users.find(user => user.id === id);
    if (user) return user;
    throw new HttpException('No User Found', HttpStatus.NOT_FOUND);
  }

  findOneByEmail(email: string): User {
    const user = this.users.find(user => user.email === email);
    if (user) return user;
    throw new HttpException('No User Found', HttpStatus.NOT_FOUND);
  }

  create(createUserDto: CreateUserDto): { createdId: string } {
    const uuid = generateUuid();
    this.users = [
      ...this.users,
      {
        id: uuid,
        ...createUserDto
      }
    ];
    return { createdId: uuid };
  }

  update(id: string, updateUserDto: UpdateUserDto): { updatedId: string } {
    let founded = false;
    this.users = this.users.map(user => {
      if (user.id === id) {
        founded = true;
        return { ...user, ...updateUserDto };
      }
      return user;
    });
    if (founded) return { updatedId: id };
    throw new HttpException('No User Found', HttpStatus.NOT_FOUND);
  }

  remove(id: string): { removedId: string } {
    const length = this.users.length;
    this.users = this.users.filter(user => user.id !== id);
    if (length !== this.users.length) return { removedId: id };
    throw new HttpException('No User Found', HttpStatus.NOT_FOUND);
  }
}
Enter fullscreen mode Exit fullscreen mode
users.controller.ts

For this first resource, I suggest you to keep the controller as it is (users.controller.ts), but adapt the service code (users.service.ts), in order to simulate a data source.

NB: For practical reasons (prevent the size of this post from growing fast), I chose to show the type of my data. In a real case, I advise you to put it in the right places, namely in interfaces and enums (or simply models) folders.

Make sure you have registered your users.module.ts module at the main module level (app.module.ts), then launch the application (npm run start). You should be able to start playing with your API via the REST protocol (and Postman of course 😎).

S e c u r i t y

Serious things start here... If you go back quickly, you notice that your endpoints are all reachable with the same level of security (that is to say none). Furthermore, the user's password is directly saved in the database... So, it's necessary to secure all of this!

First step: the password obfuscation. NestJS supports data encryption (with the NodeJS crypto module), but also data hashing. Here, I chose to use the bcrypt library to hash my strings, to guarantee unidirectional (and optimal) security! πŸ‘Œ

Let's install dependencies (npm i --save bcrypt && npm i --save-dev @types/bcrypt) and modify the user creation service to secure its password.

import * as bcrypt from 'bcrypt';

@Injectable()
export class UsersService {
  // ...

  async create(createUserDto: CreateUserDto): Promise<{ createdId: string }> {
    const uuid = generateUuid();

    const salt = await bcrypt.genSalt();
    const hash = await bcrypt.hash(password, salt);

    this.users = [
      ...this.users,
      {
        id: uuid,
        password: hash,
        ...createUserDto
      }
    ];
    return { createdId: uuid };
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode
users.service.ts

Second step: securing calls using an authentication protocol. This second task is more complex since it requires the implementation of one or more authentication strategies. Fortunately, NestJS already has a "guard" concept allowing to "validate / invalidate" routes of the application. Let's go!

npm install --save @nestjs/passport passport passport-local passport-jwt
npm install --save-dev @types/passport-local @types/passport-jwt
Enter fullscreen mode Exit fullscreen mode

The best way to establish this kind of operation (with NodeJS) is to use Passport. On the design side, I'll have to create a new authentication route that will provide me a token to validate /users/:id endpoints (GET, PATCH and DELETE). That's why, I recover (above) two strategies:

  • The LOCAL strategy, allows simple authentication (based on the username / password pair)
  • The JWT strategy, is based on the validity of a token over time

Again, you'll have to use the command line interface to instantiate a module, a controller and a service (nest g <module|controller|service> auth). Furthermore, you will have to create two new files corresponding to the Passport strategies respectively.

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { User } from '../users/interfaces/user.interface';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super({
      usernameField: 'email'
    });
  }

  async validate(email: string, password: string): Promise<User> {
    const user = await this.authService.validateUser(email, password);
    if (user) return user;
    throw new UnauthorizedException();
  }
}
Enter fullscreen mode Exit fullscreen mode
local.strategy.ts

NB: Here, you'll notice that I replaced the default behavior of the LOCAL strategy, to use the email / password pair.

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

interface Payload {
  id: string;
  email: string;
  iat: string;
  exp: string;
}

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_SECRET
    });
  }

  async validate(payload: Payload) {
    return { userId: payload.id, userEmail: payload.email };
  }
}
Enter fullscreen mode Exit fullscreen mode
jwt.strategy.ts

NB: In a real case, I'd advise against exposing the JWT secret! It's better to fetch it in another way, through the file .env (the @nestjs/config module can then be useful) or another process...

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { User } from '../users/interfaces/user.interface';
import { UsersService } from '../users/users.service';

type UserOrNull = User | null;

@Injectable()
export class AuthService {
  constructor(private usersService: UsersService, private jwtService: JwtService) {}

  async validateUser(email: string, password: string): Promise<UserOrNull> {
    const user = await this.usersService.findOneByEmail(email);
    const isMatch = await bcrypt.compare(password, user.password);

    if (user && isMatch) {
      return user;
    }
    return null;
  }

  async login(user: { id: string; email: string; }) {
    const payload = { userId: user.id, userEmail: user.email };

    return {
      access_token: this.jwtService.sign(payload)
    };
  }
}
Enter fullscreen mode Exit fullscreen mode
auth.service.ts
import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @UseGuards(AuthGuard('local'))
  @Post('login')
  async login(@Request() req) {
    return this.authService.login(req.user);
  }
}
Enter fullscreen mode Exit fullscreen mode
auth.controller.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from '../users/users.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { LocalStrategy } from './local.strategy';

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: 'Hello_Community',
      signOptions: { expiresIn: '300s' }
    }),
    UsersModule
  ],
  controllers: [AuthController],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService]
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode
auth.module.ts

After all this, some explanations are mandatory. Let’s start with the controller...

When calling the /login endpoint, the user will "POST" his credentials in email / password format.

Then, NestJS will protect the route by the LOCAL "guard" which will take care of checking the user presence into the database. If the user with the unique email address is found, the password will then be checked by bcrypt (auth.service.ts).

If things go well, the login() function will be played with the information relating to the user (especially the unique identifier and the email) which will be necessary to generate a token with 5 minutes (300s) validity.

Finally, at the endpoint return, you should retrieve a JWT token (access_token) which will authenticate the other calls of your REST API. To do this, two final things:

  • Add the JWT "guard" on the /users/:id endpoints (users.controller.ts)
  • Add the Bearer <token> in the header of the call(s)

P R I S M A

Now let's talk about the data part. From now on, you will have to connect to your database (previously installed on your machine: sudo apt install postgresql), then query it in a REST(ful) way, namely with GET, POST, PATCH, DELETE, etc...

One last time, a difficult choice is required, between:

  • The database driver (low level query)
  • The "Query Builder" (such as KnexJS)
  • The ORM - Object Relational Mapping (high level query)

NB: As an example, with MongoDB, I used to develop with Mongoose (which is an ODM - Object Document Mapper) to query my NoSQL data source. Having made the choice of SQL with Postgres, it will be essential to use a new library...

Don't panic! NestJS is here πŸŽ‰ And it already has "connectors" for some libraries, including: Sequelize, TypeORM or even Prisma. I preferred to exclude Sequelize from the challengers list (despite its popularity), since I want to be able to re-use the "chosen one" for future development with NoSQL (I can't live without Mongo πŸ˜…). In fact, the only possible choice is none other than Prisma. By the way, I was pleasantly surprised by its concept and its intuitive API! It's time to install this new dependency, and then initialize the database schema.

npm install prisma --save-dev
npx prisma init
Enter fullscreen mode Exit fullscreen mode

By executing Prisma's init command, you should see a new folder appear at the root of your project, with a first draft of the schema. I invite you to complete it as follows:

datasource db {
  provider = "postgresql"
  url      = "postgresql://<username>:<password>@<host>:<post>/hello-community?schema=public"
}

generator client {
  provider = "prisma-client-js"
}

enum Gender {
  X
  Y
}

model User {
  id        String  @id @default(uuid())
  email     String  @unique
  password  String
  firstName String?
  lastName  String?
  gender    Gender
}
Enter fullscreen mode Exit fullscreen mode
schema.prisma

As explicit as it is, the database schema will allow you to initialize your PostgreSQL database. Clearly, by executing this last one (with the migrate command), Prisma will transcribe the information above in SQL script, to create your tables (with the right fields and the right types) and add your relations (primary and foreign keys). It will eventually run the query directly into the database.

npx prisma migrate dev --name init
Enter fullscreen mode Exit fullscreen mode

NB: From there, your base is created. You can connect to it and see the result; or take a look at the SQL query present in the migration.sql file (in the prisma folder).

npm i @prisma/client
nest g module prisma
nest g service prisma
Enter fullscreen mode Exit fullscreen mode

The database is properly created, but you still have to make the link between PostgreSQL (via Prisma) and NestJS. Using the NestJS CLI tool (above), I generated two new files:

  • prisma.module.ts
  • prisma.service.ts
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Module({
  providers: [PrismaService],
  exports: [PrismaService]
})
export class PrismaModule {}
Enter fullscreen mode Exit fullscreen mode
prisma.module.ts
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}
Enter fullscreen mode Exit fullscreen mode
prisma.service.ts

One last step: usage! Do you remember your CRUD above? Let's make it evolved! Now, there is no longer any question of simulating your database, let's request it directly. For that, the Prisma API is simply magical! It generates (thanks to the database schema) the types corresponding to models, and provides you with intuitive functions to query your SQL database. See for yourself the result with functions for creating and retrieving a user.

NB: Don't forget to register your new module (prisma.module.ts) in the users module imports, otherwise it's impossible to request your data source...

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { User, Prisma } from '@prisma/client';
import * as bcrypt from 'bcrypt';
import { PrismaService } from '../prisma/prisma.service';

@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}

  // ...

  async findOne(id: string): Promise<User> {
    const user = await this.prisma.user.findUnique({ where: { id } });
    if (user) return user;
    throw new HttpException('No User Found', HttpStatus.NOT_FOUND);
  }

  // ...

  async create({ password, ...userCreateInput }: Prisma.UserCreateInput): Promise<{ createdId: string }> {
    const salt = await bcrypt.genSalt();
    const hash = await bcrypt.hash(password, salt);

    const user = await this.prisma.user.create({
      data: {
        password: hash,
        ...userCreateInput
      }
    });

    return { createdId: user.id };
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode
users.service.ts

NB: For practical reasons (prevent the size of this post from growing fast), I present here only two functions present in the service, but you can consult the rest of the code on GitHub.

E p i l o g u e

NestJS is a little nugget of Web development! I had a lot of fun developing this new backend solution in TypeScript. Everything is very well structured. By the way, the CLI is a great help when it comes to scaffold all or part of a REST resource.

Overall, I was pleasantly surprised by its organization, which makes me think of the frameworks such as SpringBoot (for Java) or Django (for Python). The files are stored in their place and you find yourself there. It just goes to show that the MVC pattern is always a safe bet...

NB: In a more objective way, I asked the opinion of another developer specialized in Java, and I assure you, he had no problem to immerse himself in the code (thanks to TypeScript).

Speaking of security, NestJS respects its part of the contract without reinventing things, since it relies mostly on the Passport dependency to consolidate its endpoints. It can also define the HTTP headers security by simply integrating middlewares provided by the Helmet library. The same for encryption and data hashing, these concepts aren't new and exist in other backend frameworks as well.

I didn't talk about unit tests previously, but they are present in this project. NestJS also makes a good choice by "dropping" the Karma / Jasmine pair in favor of Jest. It's very easy to mock Prisma data sources, to focus on controller calls, or on the function of a service.

By having powerful concepts and well-done documentation NestJS stands out as a reference in backend JavaScript development. It's a customizable framework (by embracing the ExpressJS engine, or switching to that of Fastify), and yet very complete with its lots of plugins, such as the possibility to add OpenAPI documentation via Swagger, or add GraphQL abstraction to your REST(ful) calls, etc...

In the future, I'll not longer hesitate in my choice of technical base for NodeJS, I'll simply opt for NestJS πŸ‘

S o u r c e C o d e

Discussion (0)