These days REST API has become a web applications development standard, allowing to divide web development into two separate parts. There are several mainstream frameworks like Angular, React, Vue, that are used for UI. Backend developers are free to choose from large variety of languages and frameworks. Today I’d like to discuss NestJS framework. We’re going to create a simple CRUD application using Nest and the @nestjsx/crud package.
Quite a number of backend frameworks have appeared in JavaScript community lately. They all offer the same features as Nest, but Nest architecture makes it stand out from the others. The following NestJS features allow you to create production-ready applications and scale development for larger teams:
The framework was inspired by Angular (frontend framework), and conceptually they have much in common.
@nest/cli is a Nest package which allows to quickly deploy a basic application framework. Install this package globally:
npm install --global @nest/cli
After that generate your application framework named nest-rest running the command
nest new nest-rest
.dmitrii@dmitrii-HP-ZBook-17-G3:~/projects $ nest new nest-rest
⚡ We will scaffold your app in a few seconds..
CREATE /nest-rest/.prettierrc (51 bytes)
CREATE /nest-rest/README.md (3370 bytes)
CREATE /nest-rest/nest-cli.json (84 bytes)
CREATE /nest-rest/nodemon-debug.json (163 bytes)
CREATE /nest-rest/nodemon.json (67 bytes)
CREATE /nest-rest/package.json (1805 bytes)
CREATE /nest-rest/tsconfig.build.json (97 bytes)
CREATE /nest-rest/tsconfig.json (325 bytes)
CREATE /nest-rest/tslint.json (426 bytes)
CREATE /nest-rest/src/app.controller.spec.ts (617 bytes)
CREATE /nest-rest/src/app.controller.ts (274 bytes)
CREATE /nest-rest/src/app.module.ts (249 bytes)
CREATE /nest-rest/src/app.service.ts (142 bytes)
CREATE /nest-rest/src/main.ts (208 bytes)
CREATE /nest-rest/test/app.e2e-spec.ts (561 bytes)
CREATE /nest-rest/test/jest-e2e.json (183 bytes)
? Which package manager would you ❤️ to use? yarn
✔ Installation in progress... ☕
🚀 Successfully created project nest-rest
👉 Get started with the following commands:
$ cd nest-rest
$ yarn run start
Thanks for installing Nest 🙏
Please consider donating to our open collective
to help us maintain this package.
🍷 Donate: https://opencollective.com/nest
Choose yarn as a package manager.
You can now start the server running the
npm start
command. Follow the link http://localhost:3000 to see the main page. Nice, but that’s not what we’re here for.Thinking of a database management system for this project, I decided to go with PostrgreSQL. Tastes differ, and I believe this is the most mature database management system with all possible features you may need. As I already mentioned, Nest provides integration with different packages to work with databases. Since I’ve chosen PostgreSQL, it’s only logical to choose TypeORM as an ORM. So let’s install all necessary packages for database integration:
yarn add typeorm @nestjs/typeorm pg
Now let’s see what we need each package for:
version: '3.1'
services:
db:
image: postgres:11.2
restart: always
environment:
POSTGRES_PASSWORD: example
volumes:
- ../db:/var/lib/postgresql/data
- ./postgresql.conf:/etc/postgresql/postgresql.conf
ports:
- 5432:5432
adminer:
image: adminer
restart: always
ports:
- 8080:8080
As you can see, this file configures running 2 containers:
To interact with TCP connections, I added the following config.
That’s it, now you can run the containers with the docker-compose up -d command or the docker-compose up command to run them in a separate console window. Viewing logs is simpler this way.
Well then, packages are installed, the database is run, now we have to bring them together. To do that, add ormconfig.js file to the project root directory:
const process = require('process');
const username = process.env.POSTGRES_USER || "postgres";
const password = process.env.POSTGRES_PASSWORD || "example";
module.exports = {
"type": "postgres",
"host": "localhost",
"port": 5432,
username,
password,
"database": "postgres",
"synchronize": true,
"dropSchema": false,
"logging": true,
"entities": [__dirname + "/src/**/*.entity.ts", __dirname + "/dist/**/*.entity.js"],
"migrations": ["migrations/**/*.ts"],
"subscribers": ["subscriber/**/*.ts", "dist/subscriber/**/.js"],
"cli": {
"entitiesDir": "src",
"migrationsDir": "migrations",
"subscribersDir": "subscriber"
}
}
This configuration will be used for the TypeORM CLI.
Let’s look at it a little closer. In the lines 3 and 4 we get username and password from environmental variables. That is quite handy when you have several environments (dev, stage, prod, etc). The default username is postgres, password — example. The rest of the config is quite trivial, so let’s consider only the most important parameters.
synchronize
. It specifies if the database schema should be automatically created when launching the application. Mind that if you use this parameter in production, you’re going to lose some data. This parameter is useful for debug and development though. You can run the schema:sync command from the TypeORM CLI instead.dropSchema
. It drops the schema whenever the connection is established. This parameter should only be used for debug and development just like the previous one.entities
. It specifies where to find models description. Note that mask search is supported here.cli.entitiesDir
. It is the directory where models created from the TypeORM CLI are stored by default.To take full advantage of TypeORM features in our Nest application, we need to import
TypeOrmModule
into AppModule
. Your AppModule
will look like this:import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import * as process from "process";
const username = process.env.POSTGRES_USER || 'postgres';
const password = process.env.POSTGRES_PASSWORD || 'example';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username,
password,
database: 'postgres',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
As you see, we pass into
forRoot
method the same database configuration as in ormconfig.ts file.We’ve got one more thing to do — add a few tasks into package.json to work with TypeORM. The thing is that CLI is written in JavaScript and therefore runs in NodeJS, but all our models and migrations are written in Typescript. For that reason, we need to transpile them before using CLI. To do that, we need the
ts-node
package to be installed globally:npm install -g ts-node
Add the following commands to package.json:
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
"migration:generate": "yarn run typeorm migration:generate -n",
"migration:create": "yarn run typeorm migration:create -n",
"migration:run": "yarn run typeorm migration:run"
The first command is typeorm which adds ts-node as a wrap to run the TypeORM CLI. Other commands are just useful contractions that you are going to use every day as a developer:
“migration:generate”
— generate migrations based on changes in your models.“migration:create”
— create an empty migration FILE.“migration:run”
— run a migration.That’s finally it! We’ve added all required packages, configured the app to interact with the database from both the CLI and the app itself, and started up the database management system. Now it’s time to add some logic to our app.
You can design an API to create, read, update, and delete an entity using Nest only. This approach is the most flexible one, but in some cases it may be excessive. For example, if you need to quickly create a prototype, you can sacrifice flexibility for high development speed. A lot of frameworks allow you to generate CRUD API from an entity data model description. Nest is not an exception. The @nestjsx/crud package makes things happen. Its features are quite exciting:
It consisnts of several packages:
For our application we’ll need the @nestjsx/crud and @nestjsx/crud-typeorm packages. Let’s install them:
yarn add @nestjsx/crud class-transformer class-validator
The class-transformer and class-validator packages are required for declarative description of model instance transformation rules and incoming requests validation respectively. These packages were created by the same author and therefore have similar implementations.
Let’s take a list of users as a model. Users have the following fields:
id
, username
, displayName
, email
. Id
is an auto-increment field, email
and username
— unique fields. Simple as that! Now let’s bring the idea to life in the form of a Nest application.First, let’s create a users module, that will be responsible for working with users. Run the nest g module users command in the NestJS CLI in the project root directory.
dmitrii@dmitrii-HP-ZBook-17-G3:~/projects/nest-rest git:(master*)$ nest g module users
CREATE /src/users/users.module.ts (82 bytes)
UPDATE /src/app.module.ts (312 bytes)
Add the entities folder where you’ll be storing this module models. Add here the
user.entity.ts
file with users model description:import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: string;
@Column({unique: true})
email: string;
@Column({unique: true})
username: string;
@Column({nullable: true})
displayName: string;
}
For our app to find this model we need to import
TypeOrmModule
with the following content into UsersModule
:import { Module } from '@nestjs/common';
import { UsersController } from './controllers/users/users.controller';
import { UsersService } from './services/users/users.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
@Module({
controllers: [UsersController],
providers: [UsersService],
imports: [
TypeOrmModule.forFeature([User])
]
})
export class UsersModule {}
We import
TypeOrmModule
and pass this module models list as the forFeature
parameter.Now we need to create a corresponding database entity. That’s what we need our migration mechanism for. To create a migration based on model changes run the following command:
$ npm run migration:generate -- CreateUserTable
Migration /home/dmitrii/projects/nest-rest/migrations/1563346135367-CreateUserTable.ts has been generated successfully.
Done in 1.96s.
See, you don’t have to manually write the migration, it is created automatically. It’s magic! But this is just a beginning. Let’s take a look at our migration file:
import {MigrationInterface, QueryRunner} from "typeorm";
export class CreateUserTable1563346816726 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`CREATE TABLE "user" ("id" SERIAL NOT NULL, "email" character varying NOT NULL, "username" character varying NOT NULL, "displayName" character varying, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "UQ_78a916df40e02a9deb1c4b75edb" UNIQUE ("username"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`DROP TABLE "user"`);
}
}
As you can see, not only a run but also a rollback method was generated automatically. Incredible!
Now we only have to run this migration. Use the following command:
npm run migration:run.
Schema changes have just been transferred to the database.
Now let’s create a service that will be responsible for working with users and inherit it from
TypeOrmCrudService
. We need to pass a relevant entity repository (User repository in our case) to the parent constructor parameter.import { Injectable } from '@nestjs/common';
import { TypeOrmCrudService } from '@nestjsx/crud-typeorm';
import { User } from '../../entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@Injectable()
export class UsersService extends TypeOrmCrudService<User>{
constructor(@InjectRepository(User) usersRepository: Repository<User>){
super(usersRepository);
}
}
We’ll need this service in users controller. To create the controller run the
nest g controller users/controllers/users
command in the CLI.dmitrii@dmitrii-HP-ZBook-17-G3:~/projects/nest-rest git:(master*)$ nest g controller users/controllers/users
CREATE /src/users/controllers/users/users.controller.spec.ts (486 bytes)
CREATE /src/users/controllers/users/users.controller.ts (99 bytes)
UPDATE /src/users/users.module.ts (188 bytes)
Open the controller and add some @nestjsx/crud magic to it. Add the following decorator for the
UsersController
class:@Crud({
model: {
type: User
}
})
The
@Crud
decorator adds to the controller some useful methods to work with models. Model type is specified in model.type field in the decorator configuration.The next step is implementing
CrudController<User>
interface. The entire code of the controller will look like this:import { Controller } from '@nestjs/common';
import { Crud, CrudController } from '@nestjsx/crud';
import { User } from '../../entities/user.entity';
import { UsersService } from '../../services/users/users.service';
@Crud({
model: {
type: User
}
})
@Controller('users')
export class UsersController implements CrudController<User>{
constructor(public service: UsersService){}
}
That’s all! The controller now supports the whole set of operations with the model. It can’t be true. Let’s see our application in action!
To test our service, we’ll be using TestMace — an IDE for API design. Why TestMace? It has some advantages compared to its counterparts:
Let’s start our server running the
npm start
command and access the user list. According to the controller configuration, the list can be found at localhost:3000/users
. Let’s send a request to this url.At the top left of the screen there is the project tree with Project being a root node. Let’s create the first request to get the user list. Right-click on the Project node and choose Add node -> RequestStep.
Insert
localhost:3000/users
in the URL field and send the request. You’ll get a 200 code and an empty array in the response body. It makes sense, we haven’t yet added anything.Let’s create a scenario with the following steps:
Let’s go. Create a Folder node for convenience. In fact, it’s just a folder where the whole scenario will be stored. To create a Folder node right-click in the Project node and choose Add node -> Folder. Name it check-create. Add your first request within this node to create a user. Name the new node create-user. At the moment we have the following node hierarchy:
Now go to the create-user node tab. Enter the following request parameters:
localhost:3000/users
;{“email": “[email protected]", “displayName": “New user", “username": “user"}
.Send the request. The application tells us the record has been created.
Let’s check then. We need to save the id of the added user to be able to use it later. That’s what our dynamic variables mechanism can help us with. Let’s see how it works using the previous example. Right-click on the id node and choose Assign to variable in on the parsed tab. Set the parameters in the dialog window:
userId
.That’s how creating a dynamic variable looks like:
With that done, the dynamic variable value will be updated every time you send this request. Since dynamic variables support hierarchical inheritance, the
userId
variable will always be available in check-create node children of any nesting level.We’ll need this variable in the next request. Let’s get the newly added user. Create the check-if-exists request as a child of the check-create node with
localhost:3000/users/${$dynamicVar.userId}
as a url parameter. The ${variable_name}
construct is used to get the variable value. As we’ve got a dynamic variable , we have to access $dynamicVar, so the construct will look like this: ${dynamicVar.userId}
. Send the request and make sure it does what it was made for.And the last thing to do is to delete the user. We need to send the final request not only to check if it works properly, but also to clean up the database, as email and username fields are unique. Create the delete-user request in the check-create node with the following parameters:
localhost:3000/users/${$dynamicVar.userId}
.Run it. Wait. Enjoy :)
Now you can run the full scenario any time you want. To do that, just right-click on the check-create node and choose Run.
Nodes will be run consequently.
You can save this scenario to use it in your own projects by File -> Save project.
I haven’t covered all possible features of the tools used in this article. As for the @nestjsx/crud package — the main subject of this article, the following features are worth discussing:
However, the information given in the article is enough to realize that even such enterprise framework as NestJS has it all to quickly prototype an application. And our awesome TestMace IDE helps to keep up the pace.
You can find the source code and the text of this article in the repository: https://github.com/TestMace/nest-rest. Choose File -> Open project to open a TestMace project in the app.