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
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
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.