Dmitrii Snytkin

Quick and Easy CRUD with NestJS, @nestjsx/crud and TestMace

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.

Why NestJS

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:
  1. TypeScript as the main programming language. Although NestJS supports JavaScript, it may not work properly, especially in third-party packages;
  2. DI container that allows to create loosely coupled components;
  3. The framework functionality is divided into independent interchangeable components. For instance, both express and fastify can be used as frameworks under the hood, and for databases Nest provides typeorm, mongoose, sequelize bindings out of the box;
  4. NestJS is platform independent and supports REST, GraphQL, Websockets, gRPC, etc.
The framework was inspired by Angular (frontend framework), and conceptually they have much in common.

NestJS Installation and Project Deployment

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

Setting Up the Database

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:
  1. typeorm — comes with ORM;
  2. @nestjs/typeorm — a TypeORM package for NestJS. It adds helper decorators and modules ready for import into project modules;
  3. pg — a PostgreSQL driver.
  4. 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:
    1. db — a container with the database. We’re using PostgreSQL version 11.2;
    2. adminer — a database manager. It provides a web interface to view and manage the database.
    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.
    1. 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.
    2. 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.
    3. entities
      . It specifies where to find models description. Note that mask search is supported here.
    4. 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:
    1. “migration:generate”
       — generate migrations based on changes in your models.
    2. “migration:create”
       — create an empty migration FILE.
    3. “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.

    Installing CRUD Packages

    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:
    • DBMS independence;
    • easy installation and setup;
    • powerful query language with filtering, pagination, sorting, relations, nested relations, cache, etc.;
    • a package with query builder for frontend usage;
    • easy controller methods overriding;
    • tiny config;
    • swagger documentation support.
    It consisnts of several packages:
    • @nestjsx/crud — core package which provides @Crud() decorator for endpoints generation, configuration, and validation;
    • @nestjsx/crud-request — request builder/parser package for frontend usage;
    • @nestjsx/crud-typeorm — TypeORM package which provides base TypeOrmCrudService with methods for CRUD database operations.
    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.

    CRUD Implementation

    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!

    Creating a Request Scenario in TestMace

    To test our service, we’ll be using TestMace — an IDE for API design. Why TestMace? It has some advantages compared to its counterparts:
    • powerful variables mechanism. There are several variable types, each of which serves its own purpose: default variables, dynamic variables, environment variables. Each variable belongs to a specific node, inheritance is supported;
    • creating scenarios without programming. We’ll discuss it a bit later;
    • a human-readable format, which allows to store your projects in a version control system;
    • autocomplete feature, syntax highlighting, variables values highlighting;
    • API description support + import from Swagger.
    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.
    After running TestMace, you can see the following interface:
    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:
    1. create a user;
    2. request the user by its id;
    3. delete the user by its id.
    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:
    • Request type — POST;
    • URL — 
      localhost:3000/users
      ;
    • Body — JSON with the value
      {“email": “user@user.com", “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:
    • Node specifies the parent node for the variable. Choose check-create;
    • Variable name. Let’s name it
      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:
    1. Request type — DELETE;
    2. URL — 
      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.

    Conclusion

    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:
    1. custom model validation and transformation;
    2. powerful query language and its convenient frontend usage;
    3. adding and overriding methods in crud-controllers;
    4. swagger support;
    5. cache management.
    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.

Tags

Comments

More by Dmitrii Snytkin

Topics of interest