NestJS


So, I tried NestJS several times to make something simple and usable and the documentation was very daunting to me. I wish I had a tutorial that is more straightforward and easy to understand. This is intended for people that already understand some fundamental knowledge of backend development and want to try NestJS.

ℹ️ Information

I write all of this in 2.5 hours. Just following it should take you less than 1 hour.

Continue reading if you already understand dependency injection, mvc, rest api, orm, active-record pattern, decorators, mysql, database migration. Or just google them along the way.

Why not just use Express?

“Just use express” they said. Yes, I believe it is more easier for me (who have been working with Express for several year) to just use Express, I just want to try it as I saw it in enough job postings. Not that many, but enough to spark my curiosity. I used Spring Boot and Ruby on Rails, they are both opinionated and I kinda like it, especially RoR, it was kinda magical. So, let’s try NestJS.

The Goal

  • Simple CRUD API for a simple entity, let’s say User
  • The API should be able to:
    • Create a user
    • Read a user
    • Update a user
    • Delete a user
  • Read environment variables for database connection
  • Really save a User to the database

This should be simple enough right? Can done in no time.

Hehe, I was wrong, the amopunt documentation just to do these simple things is outrageous. I hope this tutorial can help you to kick started your journey with NestJS.

Setup

First, let’s instasll nestjs cli globally.

$ npm i -g @nestjs/cli

Then create a new project.

$ nest new project-name
$ cd project-name

It will ask you several questions, just answer it as you like. I choose to use pnpm as the package manager. It is fast. Try it.

After installation, you will get these directory structure. You just need to put your attention to src directory for now.

$ tree --gitignore
.
├── README.md
├── nest-cli.json
├── package.json
├── pnpm-lock.yaml
├── src
   ├── app.controller.spec.ts
   ├── app.controller.ts
   ├── app.module.ts
   ├── app.service.ts
   └── main.ts
├── test
   ├── app.e2e-spec.ts
   └── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json

4 directories, 14 files

Now lets try to run it.

$ pnpm run start:dev

Now if you try to open http://localhost:3000 you should see a welcome message.

$ curl localhost:3000/
Hello World!%

Congratulation! You have created a NestJS project. Simple enough until now isn’t it?

You can keep the server running or stop it, doesnt matter, but if you stop it, just remember to start it everytime you want to hit any endpoint.

Create User Module

NestJS cli tool is very helpful. It can generate a module, controller, service, and even a test file for you. Let’s try to generate things for our User.

$ nest g resource user
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes

It will generate a module, controller, service, entities, and a test file for you. You can see the generated files in src/user directory.

$ git status --untracked-files -s
 M package.json
 M pnpm-lock.yaml
 M src/app.module.ts
?? src/user/dto/create-user.dto.ts
?? src/user/dto/update-user.dto.ts
?? src/user/entities/user.entity.ts
?? src/user/user.controller.spec.ts
?? src/user/user.controller.ts
?? src/user/user.module.ts
?? src/user/user.service.spec.ts
?? src/user/user.service.ts

Now we have a functional CRUD API for User. You can try to run the server and hit the endpoint.

 curl localhost:3000/user
This action returns all user%

Env Variables

Reference

Now I want to read the database connection from environment variables.

$ pnpm i --save @nestjs/config

Then change add the ConfigModule to the AppModule in src/app.module.ts.

# src/app.module.ts

import { AppController } from './app.controller';
 import { AppService } from './app.service';
 import { UserModule } from './user/user.module';
+import { ConfigModule } from '@nestjs/config';

 @Module({
-  imports: [UserModule],
+  imports: [
+    UserModule,
+    ConfigModule.forRoot(),
+  ],
   controllers: [AppController],
   providers: [AppService],
 })

now lets andd .env file to the root of the project.

# .env
DB_DIALECT=mysql
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=<fill with yours>
DB_DATABASE=nest

Database Connection

Reference

I will use sequelize as the ORM. First, install the packages.

$ pnpm install --save @nestjs/sequelize sequelize sequelize-typescript mysql2
$ pnpm install --save-dev @types/sequelize

Now we need to configure the database connection. Using the ConfigModule we prepared before. In simple terms, we inject ConfigService from ConfigModule into SequelizeModule to read the environment variables to connect to DB.

# src/app.module.ts

import { AppController } from './app.controller';
 import { AppService } from './app.service';
 import { UserModule } from './user/user.module';
-import { ConfigModule } from '@nestjs/config';
+import { ConfigModule, ConfigService } from '@nestjs/config';
+import { SequelizeModule } from '@nestjs/sequelize';

 @Module({
   imports: [
     UserModule,
     ConfigModule.forRoot(),
+    SequelizeModule.forRootAsync({
+      imports: [ConfigModule],
+      useFactory: (configService: ConfigService) => ({
+        dialect: configService.get('DB_DIALECT'),
+        host: configService.get('DB_HOST'),
+        port: +configService.get('DB_PORT'),
+        username: configService.get('DB_USERNAME'),
+        password: configService.get('DB_PASSWORD'),
+        database: configService.get('DB_DATABASE'),
+        autoLoadModels: true,
+        // MUST be false in production
+        synchronize: MUST_BE_FALSE_IN_PRODUCTION,
+      }),
+      inject: [ConfigService],
+    })
   ],
   controllers: [AppController],
   providers: [AppService],

User Entity

Now lets complete the User entity. We need to define the User entity and its attributes.

// src/user/entities/user.entity.ts

import {
  Column,
  Model,
  PrimaryKey,
  Table,
  DataType,
  CreatedAt,
  UpdatedAt,
  DeletedAt,
} from 'sequelize-typescript';


@Table({
  tableName: 'users',
  timestamps: true,
  paranoid: true,
  underscored: true,
})
export class User extends Model {
  @PrimaryKey
  @Column({
    type: DataType.BIGINT.UNSIGNED,
  })
  id: number;

  @Column
  firstname: string;

  @Column
  lastname: string;

  @Column
  email: string;

  @Column
  timezone: string;

  @Column
  birthdate: Date;

  @CreatedAt
  created_at: Date;

  @UpdatedAt
  updated_at: Date;

  @DeletedAt
  deleted_at: Date;
}


// src/user/user.module.ts

import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';

import { UserService } from './user.service';
import { UserController } from './user.controller';
import { User } from './entities/user.entity';

@Module({
  // 👇🏼 import User model, allow this module to use the entity
  imports: [SequelizeModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

Setup Database

We will leverage sequelize-cli to setup the database. We will have a directory called db that contains everything about database migration. This directory is basically isolated from the rest of the project.

First, install the package.

$ pnpm install --save-dev sequelize-cli

Then create a sequelize configuration file. Here we define db folder is everything about migration.

touch .sequelizerc
// .sequelizerc

const path = require('path');

module.exports = {
  'config': path.resolve('db', 'configs', 'config.js'),
  'models-path': path.resolve('db', 'models'),
  'seeders-path': path.resolve('db', 'seeders'),
  'migrations-path': path.resolve('db', 'migrations')
};

Now lets initialize the sequelize configuration.

$ npx sequelize-cli init

It will generate several directories and files for you.

$ tree db
db
├── configs
   └── config.js
├── migrations
├── models
   └── index.js
└── seeders

We need to update the config.js so it can read the .env file.

// db/configs/config.js

require('dotenv').config();

module.exports = {
  production: {
    host: process.env.DB_HOST,
    port: process.env.DB_PORT,
    password: process.env.DB_PASSWORD,
    username: process.env.DB_USERNAME,
    database: process.env.DB_DATABASE,
    dialect: process.env.DB_DIALECT,
  },
  development: {
    host: process.env.DB_HOST,
    port: process.env.DB_PORT,
    password: process.env.DB_PASSWORD,
    username: process.env.DB_USERNAME,
    database: process.env.DB_DATABASE,
    dialect: process.env.DB_DIALECT,
  },
  test: {
    host: process.env.DB_HOST,
    port: process.env.DB_PORT,
    password: process.env.DB_PASSWORD,
    username: process.env.DB_USERNAME,
    database: process.env.DB_DATABASE,
    dialect: process.env.DB_DIALECT,
  }
}

If you are lucky you will see this error like this, saying that database nest is unknown. Ofcourse, we havent created the db yet

 npx sequelize db:migrate:status

Sequelize CLI [Node: 20.8.0, CLI: 6.6.2, ORM: 6.36.0]

Loaded configuration file "db/configs/config.js".
Using environment "development".

ERROR: Unknown database 'nest'

Now lets create the DB

$ npx sequelize db:create

Now we create a migration file to create the users table.

$ npx sequelize migration:create --name add_user

It will generate a file in db/migrations directory. Fill like below.

// db/migrations/<some timestamp>-add_user.js

'use strict';

const table_name = 'users';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
  async up (queryInterface, Sequelize) {
    await queryInterface.createTable(table_name, {
      id: {
        type: Sequelize.INTEGER(10).UNSIGNED,
        primaryKey: true,
        autoIncrement: true,
      },
      firstname: {
        type: Sequelize.STRING,
        allowNull: false,
      },
      lastname: {
        type: Sequelize.STRING,
        allowNull: false,
      },
      email: {
        type: Sequelize.STRING,
        allowNull: false,
      },
      timezone: {
        type: Sequelize.STRING,
        allowNull: false,
      },
      birthdate: {
        type: Sequelize.DATE,
        allowNull: false,
      },
      created_at: {
        type: Sequelize.DATE,
        allowNull: false,
      },
      updated_at: {
        type: Sequelize.DATE,
        allowNull: false,
      },
      deleted_at: {
        type: Sequelize.DATE,
        allowNull: true,
      },
    });
  },

  async down (queryInterface, Sequelize) {
    await queryInterface.dropTable(table_name);
  }
};

Now lets run the migration.

$ npx sequelize db:migrate

# check the status, you should see the migration is applied
$ npx sequelize db:migrate:status

Change the service to use the model

Replace the existing service

// src/user/user.service.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { User } from './entities/user.entity';

import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Injectable()
export class UserService {
  constructor(
    @InjectModel(User)
    private user_model: typeof User,
  ) {}

  async create(createUserDto: CreateUserDto) {
    return this.user_model.create({
      ...createUserDto,
    });
  }

  async findAll() {
    return this.user_model.findAll();
  }

  async findOne(id: number) {
    return await this.user_model.findOne({
      where: {
        id,
      },
    });
  }

  async update(id: number, updateUserDto: UpdateUserDto) {
    const user = await this.findOne(id);
    const updated = await user.update(updateUserDto);

    return updated;
  }

  async remove(id: number) {
    const user = await this.findOne(id);
    await user.destroy();

    return user;
  }
}

YEAY!

Now (hopefully) you have a simple CRUD API for User entity (if nothing wrong). You can try to run the server and hit the endpoint.

$ pnpm run start:dev

# create a user
$ curl -XPOST localhost:3000/user -H 'content-type: application/json' -d '{ "firstname": "marchell", "lastname": "imanuel", "email": "imanuel.marchell@gmail.com", "timezone": "Asia/Jakarta", "birthdate":"2000-12-12"}'

# get all
$ curl localhost:3000/user
[{"id":1,"firstname":"marchell","lastname":"imanuel","email":"imanuel.marchell@gmail.com","timezone":"Asia/Jakarta","birthdate":"2000-12-12T00:00:00.000Z","created_at":"2024-02-09T19:52:02.000Z","updated_at":"2024-02-09T19:52:02.000Z","deleted_at":null}]%

The END

I hope this tutorial can help you to kick started your journey with NestJS. I will try to write more about NestJS in the future. I hope you can find it useful. If you have any question, feel free to ask me. I will try to help you as much as I can.

I will add part 2 for this tutorial, where I will add cron and queue, patterns that is very common in backend development.