After learning this Nest.js actual combat, those who haven't started to hammer me! (long text warning)

After learning this Nest.js actual combat, those who haven't started to hammer me! (long text warning)

preface

Hello, I'm koala , an interesting and willing to share, currently focuses on sharing the complete node.js technology stack, and is responsible for building the middle office of the Department and some capabilities of low code platform. If you are interested in Node.js learning, and you can follow me, and WeChat ikoala520, I will pull you into the exchange group to exchange, study, build together, or pay attention to my official account. Programmer growth points north Github blog open source project github.com/koala-codin...

I've been busy recently, and what I do in my work is not suitable for writing articles, so I haven't been more polite

Recently, I received a small demand and needed to do it all by myself (front end + back end). Seeing that everyone in the group was very enthusiastic about Nest.js, I was itchy, so I embarked on the road of Nest.js~

I will record the process of doing this small project, and also share some experience of stepping on the pit, so as to give some reference to the little partners who want to learn. The article is step-by-step, not to go deep into the difficulties of Nest.js, but each article has some development notes and their own thinking. You are welcome to give some advice.

Why Nest.js

As I said earlier, everyone said incense~

Secondly, I have also used Egg.js before. When I used it in 19 years, I felt that egg was quite restrictive, but it was good for internal unified specification. But now I am used to TS in 2021, but Egg.js does not have native TypeScript support. During development, I can use egg TS helper to help automatically generate d.ts files, so the support of third-party libraries is completely uncontrolled, The risk is still great. All choices have been abandoned

Having said so much, let's start! The article mainly includes the following contents:

First met Nest.js

Introduction to Nest.js official website:

Nest (nest JS) is a platform for building efficient and extensible   Node.js   The development framework of server-side application. It uses and fully supports the progressive enhancement of JavaScript   TypeScript   (developers are still allowed to develop with pure JavaScript), and combine OOP (object-oriented programming), FP (functional programming) and FRP (functional responsive programming). At the bottom, Nest is built on a powerful HTTP server framework, such as   Express   (default), and can also be configured to use   Fastify ! Nest raises a level of abstraction above these common Node.js frameworks (express / fast), but still directly exposes the API of the underlying framework to developers, which allows developers to freely use countless third-party modules suitable for the underlying platform.

The above paragraph can not be fully understood at first, but several features of Nest.js can be simply interpreted:

  • Native TypeScript supported framework
  • It can be based on Express or fast. If you are very proficient in Express, you can use its API directly

As for others who don't understand, just put them aside for the time being, because it doesn't affect our introduction. We will analyze them after in-depth study.

Project creation

First, make sure that you have installed node.js. Node.js installation will be accompanied by npx and an npm package to run the program. To create a new Nest.js application, run the following command on the terminal:

npm i -g @nestjs/cli  // Global install Nest
nest new project-name  // Create project
 Copy code

After the project is created, the following files will be initialized, and you will be asked if there is any way to manage dependent packages:

If you have installed yarn, you can choose yarn. It can be faster, and the installation speed of npm in China will be slower. I will use npm to download here. Here, I omit a long waiting process ~, and finally see that it is successful (then I deleted it and used yarn, which is really much faster)

Next, run the project as prompted:

Let's talk about the environment I installed. Nest.js has different versions, and some API s will be different

package

edition

Node.js

v12.16.1

npm

6.13.4

nest.js

8.1.4

typescript

4.3.5

Note: Nest.js requires Node.js (> = 10.13.0, except v13). If your Node.js version does not meet the requirements, you can install the qualified Node.js version through the nvm package management tool

Project structure

After entering the project, the directory structure should look like this:

Here is a brief description of these core files:

src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── main.ts
 Copy code

app.controller.ts

Basic controller for a single route

app.controller.spec.ts

Unit test for controller

app.module.ts

Application root module

app.service.ts

Basic service with single method

main.ts

The entry file of the application, which uses the core functions   NestFactory   To create an instance of the Nest application.

First interface

We have started the service earlier. How can we check it? First, find the entry file main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();
Copy code

The content is relatively simple. An AppModule instance is created by using NestFactory, the factory function of Nest.js, and the HTTP listener is started to listen to the port defined in main.ts.

The listening port number can be customized. If port 3000 is used by other projects, it can be changed to other port numbers

Because there are other projects in use on my 3000 port, it is modified to: 9080, restart the project

We open the browser to access http://localhost:9080 Address:

The Hello World seen here is the interface address http://localhost:9080 If you don't believe the returned content, we can also use the common Postman to see:

Explain that Nest.js creates an interface example for the project by default. Let's see how we should implement an interface through this interface example.

You can see that there are no other files in mian.ts, only AppModule. Open src/app.module.ts:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
Copy code

AppModule is the root module of an application. The root module provides a boot mechanism for starting an application and can contain many functional modules.

The. mudule file needs to use a class of @ Module() decorator. The decorator can be understood as an encapsulated function, but it is actually a syntax sugar (for those who do not know about the decorator, you can see Approaching MidwayJS: getting to know TS decorator and IoC mechanism )The @ Module() decorator receives four attributes: providers, controllers, imports, and exports.

  • providers: the provider (service provider) instantiated by Nest.js injector, which handles specific business logic and can be shared among various modules (the concept of injector will be explained in the dependency injection section later);
  • controllers: handle http requests, including routing control, return responses to clients, and delegate specific business logic to providers for processing;
  • imports: list of imported modules. If you need to use the services of other modules, you need to import them here;
  • exports: export the list of services for import by other modules. If you want the services under the current module to be shared by other modules, you need to configure export here;

If you are a Vue or React technology stack, you may feel very unfamiliar when you first contact Nest.js. In fact, it is normal. The way of thinking of Nest.js is really not easy to understand at the beginning, but if you contact AngularJS, you will feel familiar. If you have used Java and Spring, you may think that this is not a copied Spring boot!

Indeed, AngularJS, Spring and Nest.js are all designed based on the principle of control inversion, and they all use dependency injection to solve the decoupling problem. If you feel confused, don't worry. These problems will be explained one by one after in-depth study. Here, let's draw the ladle according to the gourd and learn how Nest uses it.

In app.module.ts, you can see that it introduces app.controller.ts and app.service.ts. take a look at these two files respectively:

// app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

Copy code

Use the @ Controller decorator to define the Controller, @ GET is the decorator of the request method. Modify the getHello method to indicate that this method will be called by the GET request.

// app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService { 
  getHello(): string {
    return 'Hello World!';
  }
}
Copy code

From the above, we can see that the AppService decorated with @ Injectable is registered in the AppModule and used in app.controller.ts. we don't need to use new AppService() to instantiate it. We can use it directly.

So far, for http://localhost:9080/ Even if the Hello World logic returned by the interface is clear, on this basis, we will learn more about the use of routing in Nest.js.

Route decorator

Nest.js does not have a separate place to configure routing, but uses decorators. Nest.js defines several decorators to handle routing.

@Controller

For example, each class to become a Controller needs to be decorated with the help of the @ Controller decorator, which can pass in a path parameter as the main path to access the Controller:

Modify the app.controller.ts file

// The main path is app
@Controller("app")
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}
Copy code

Then restart the service and visit again at this time http://localhost:9080/ You'll find 404.

Because the routing prefix of this controller is modified to app through @ Controller("app"), you can http://localhost:9080/app To access.

HTTP method handler decorator

@Get, @ Post, @ Put and many other decorators for HTTP method processing can respond to corresponding HTTP requests through their decorated methods. At the same time, they can accept a string or an array of strings as parameters. The string here can be a fixed path or a wildcard.

Continue to modify app.controller.ts to see the following example:

// The main path is app
@Controller("app")
export class AppController {
  constructor(private readonly appService: AppService) {}
  
  // 1. Fixed path:
  // You can match the get request, http://localhost:9080/app/list
  @Get("list")
  getHello(): string {...}
  
  // Can match the post request, http://localhost:9080/app/list
  @Post("list")
  create():string{...}
  
  // 2. Wildcard path (? + * three wildcards)
  // You can match the get request, http://localhost:9080/app/user_xxx
  @Get("user_*")
  getUser(){return "getUser"}
  
  // 3. Path with parameters
  // Can match the put request, http://localhost:9080/app/list/xxxx
  @Put("list/:id")
  update(){ return "update"}
}
  
Copy code

Because the file is modified, you need to restart to see the route. Restarting every time is a nightmare. I originally planned to configure a real-time monitoring file change. I found Nest.js has been configured very carefully. We just need to run the command:

npm run start:dev
 Copy code

In this way, the service will be automatically restarted after saving any content modified.

Here is a note about route matching. When we have a put request with the path of / app/list/user, we add a method in the app.controller.ts controller file:

 @Put("list/user")
 updateUser(){
      return {userId:1}
  }
Copy code

Do you think this route will be matched? Let's test:

It is found that / app/list/user does not match the updateUser method, but the update method. That's what I want to say.

If it is found that @ Put("list/:id") has been satisfied during the matching process, the matching will not continue. Therefore, the method decorated by @ Put("list/user") should be written before it.

Global Routing Prefix

In addition to the above decorators, we can also set the global route prefix, such as adding / api prefix to all routes. At this point, you need to modify main.ts

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.setGlobalPrefix('api'); // Set global routing prefix
  await app.listen(9080);
}
bootstrap();
Copy code

The previous routes should be changed to:

http://localhost/api/xxxx
 Copy code

Now we know about Controller, Service, Module, routing and some common decorators. Next, let's take the Post Module as an example to realize the simple CRUD of the article and familiarize you with this process.

Write code

Before writing code, first introduce some useful commands provided by nest cli:

//grammar
nest g [file type] [file name] [File directory]
Copy code
  • Create module

nest g mo posts

Create a Posts module. The file directory is not written. By default, create a Posts directory with the same file name. Create a posts.module.ts under the posts directory

// src/posts/posts.module.ts
import { Module } from '@nestjs/common';

@Module({})
export class PostsModule {}
Copy code

After executing the command, we can also find that PostsModule is also introduced into the root module app.module.ts, and PostsModule is also introduced into the inputs of the @ Model decorator

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PostsModule } from './posts/posts.module';

@Module({
  controllers: [AppController],
  providers: [AppService],
  imports: [PostsModule],
})
export class AppModule {}
Copy code
  • Create controller

nest g co posts

A Posts controller named posts.controller.ts and a unit test file for the controller are created

// src/posts/posts.controller.ts
import { Controller } from '@nestjs/common';

@Controller('posts')
export class PostsController {}
Copy code

After executing the command, the PostsController will be automatically introduced into the file posts.module.ts and injected into the controllers of the @ Module decorator.

  • Create service class

nest g service posts

// src/posts/posts.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class PostsService {}
Copy code

Create the app.service.ts file and inject it into the providers of the @ Module decorator under the app.module.ts file.

In fact, nest cli provides many creation commands, such as creating filters, interceptors and middleware. Since it is not available here for the time being, there is only a lot of introduction, which will be introduced in the following chapters.

Note the creation sequence: first create the Module, then create the Controller and Service. In this way, the created files are automatically registered in the Module. On the contrary, after creating the Module, Controller and Service, they will be registered in the outer app.module.ts

Take a look at the current directory structure:

Connect to Mysql

The route takes effect. Since it is a back-end project, you must use the database, otherwise it is no different from writing static pages and playing by yourself.

I chose Mysql for the database. After all, most of the actual projects still chose it. Because the article is a zero tutorial, it will include the installation, connection, use and pit encountered in the process of using the database. If you are an experienced veteran, you can skip this part.

Database installation

If there is no mysql database or cloud database in your computer, first install a mysql locally through Download from the official website

Select the MySQL Community Server you need   Version and corresponding platform:

Installing MySQL on Windows is relatively simple, just like installing an application. You can follow the steps below #Detailed installation tutorial of MySQL under Windows Step by step, I won't repeat it here.

Next, use visual tools to manage the database. The commonly used ones are SQLyog or Navicat Premium. I'm used to Navicat for MySQL. Here's a demonstration:

Connect to the database first:

Then create a new database blog:

Click the created blog. There is nothing in it. We can manually create the table here or create it later with code. Here I choose the latter.

TypeORM connection database

Pre knowledge

First, what is ORM?

If we directly use Node.js to operate the interface provided by mysql, the code written is relatively low-level, such as an insert data code:

// Insert data into the database 
connection.query(`INSERT INTO posts (title, content) VALUES ('${title}', '${content}')`,
    (err, data) => {
    if (err) { 
    console.error(err) 
    } else {
    console.log(data) 
    }
})
Copy code

Considering that the database table is a two-dimensional table with multiple rows and columns, such as a table of posts:

mysql> select * from posts;
+----+--------+------------+
| id | title       | content      |
+----+-------------+--------------+
|  1 | Nest.js introduction | Article content description |
+----+--------+------------+

Copy code

Each line can be represented by a JavaScript object, such as the first line:

{
    id: 1,
    title:"Nest.js introduction",
    content:"Article content description"
}
Copy code

This is the legendary ORM Technology (object relational mapping), which maps the variable structure of relational database to objects.

Therefore, ORM frameworks such as serialize, typeORM and prism appear to do this transformation. (ps: prism has a high voice, and those who like to explore can try it.) here we choose typeORM to operate the database. In this way, both reading and writing are JavaScript objects. For example, the insertion statement above can be implemented in this way:

await connection.getRepository(Posts).save({title:"Nest.js introduction", content:"Article content description"});
Copy code

The next step is to use typeORM to operate the database. First, we need to install the following dependency packages:

npm install @nestjs/typeorm typeorm mysql2 -S
 Copy code

The official provides two methods to connect to the database, which are introduced here:

Method 1

First, create two files. env and. env.prod in the project root directory, which store different environment variables of development environment and online environment respectively:

// Database address
DB_HOST=localhost  
// Database port
DB_PORT=3306
// Database login
DB_USER=root
// Database login password
DB_PASSWD=root
// Database name
DB_DATABASE=blog
 Copy code

. env.prod is the database information to be used online. If your project needs to be uploaded to online management, it is recommended to add this file to. gitignore for security reasons.

Next, create a config folder (at the same level as src) in the root directory, and then create an env.ts to read the corresponding configuration files according to different environments.

import * as fs from 'fs';
import * as path from 'path';
const isProd = process.env.NODE_ENV === 'production';

function parseEnv() {
  const localEnv = path.resolve('.env');
  const prodEnv = path.resolve('.env.prod');

  if (!fs.existsSync(localEnv) && !fs.existsSync(prodEnv)) {
    throw new Error('Missing environment profile');
  }

  const filePath = isProd && fs.existsSync(prodEnv) ? prodEnv : localEnv;
  return { path:filePath };
}
export default parseEnv();

Copy code

Then connect to the database in app.module.ts:

import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService, ConfigModule } from '@nestjs/config';
import envConfig from '../config/env';

@Module({
  imports: [
    ConfigModule.forRoot({ 
    isGlobal: true,  // Set to global
    envFilePath: [envConfig.path] 
   }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        type: 'mysql', // Database type
        entities: [],  // Data table entity
        host: configService.get('DB_HOST', 'localhost'), // Host, localhost by default
        port: configService.get<number>('DB_PORT', 3306), // Port number
        username: configService.get('DB_USER', 'root'),   // user name
        password: configService.get('DB_PASSWORD', 'root'), // password
        database: configService.get('DB_DATABASE', 'blog'), //Database name
        timezone: '+08:00', //Time zone configured on server
        synchronize: true, //The database table is automatically created according to the entity. It is recommended to close the production environment
      }),
    }),
    PostsModule,
  ],
 ...
})
export class AppModule {}
Copy code

When using environment variables, it is recommended to use the official @ nestjs/config, which can be used out of the box. For a brief explanation

@nestjs/config depends on dotenv. You can configure environment variables in the form of key=value. The project will load the. env file in the root directory by default. We just need to introduce ConfigModule in app.module.ts, use ConfigModule.forRoot() method, and then ConfigService reads the relevant configuration variables.

TypeORM provides a variety of connection methods. Here we will introduce how to use ormconfig.json

Method 2

Create an ormconfig.json file (at the same level as src) in the root directory instead of passing the configuration object to forRoot().

{ 
    "type": "mysql",
    "host": "localhost", 
    "port": 3306, 
    "username": "root", 
    "password": "root", 
    "database": "blog", 
    "entities": ["dist/**/*.entity{.ts,.js}"], 
    "synchronize": true  // Automatically loaded models are synchronized
}
Copy code

Then call forRoot() without any options in app.module.ts, which is OK. If you want to know more ways to connect to the database, you can go to TypeORM official website see

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({ 
    imports: [TypeOrmModule.forRoot()],
})
export class AppModule {}
Copy code

OK, the database connection is successful. If the connection fails, there will be such an error message:

Check whether your database configuration is correct.

CRUD

OK, let's move on to the data operation. We said that the table is created through code. TypeORM is mapped to the database table through entities, so we first create an article entity postentity and create posts.entity.ts in the posts directory

//    posts/posts.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";

@Entity("posts")
export class PostsEntity {
    @PrimaryGeneratedColumn()
    id:number; // Mark as the main column, and the value is automatically generated

    @Column({ length:50 })
    title: string;

    @Column({ length: 20})
    author: string;

    @Column("text")
    content:string;

    @Column({default:''})
    thumb_url: string;

    @Column('tinyint')
    type:number

    @Column({type: 'timestamp', default: () => "CURRENT_TIMESTAMP"})
    create_time: Date

    @Column({type: 'timestamp', default: () => "CURRENT_TIMESTAMP"})
    update_time: Date
}
Copy code

Next, implement the business logic of CRUD operation in posts.service.ts file. The table here is not the final article table, but to realize the simple addition, deletion, modification and query interface first, and then realize the complex multi table Association.

import { HttpException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { getRepository, Repository } from 'typeorm';
import { PostsEntity } from './posts.entity';

export interface PostsRo {
  list: PostsEntity[];
  count: number;
}
@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(PostsEntity)
    private readonly postsRepository: Repository<PostsEntity>,
  ) {}

  // Create article
  async create(post: Partial<PostsEntity>): Promise<PostsEntity> {
    const { title } = post;
    if (!title) {
      throw new HttpException('Missing article title', 401);
    }
    const doc = await this.postsRepository.findOne({ where: { title } });
    if (doc) {
      throw new HttpException('Article already exists', 401);
    }
    return await this.postsRepository.save(post);
  }
  
  // Get article list
  async findAll(query): Promise<PostsRo> {
    const qb = await getRepository(PostsEntity).createQueryBuilder('post');
    qb.where('1 = 1');
    qb.orderBy('post.create_time', 'DESC');

    const count = await qb.getCount();
    const { pageNum = 1, pageSize = 10, ...params } = query;
    qb.limit(pageSize);
    qb.offset(pageSize * (pageNum - 1));

    const posts = await qb.getMany();
    return { list: posts, count: count };
  }

  // Get the specified article
  async findById(id): Promise<PostsEntity> {
    return await this.postsRepository.findOne(id);
  }

  // Update article
  async updateById(id, post): Promise<PostsEntity> {
    const existPost = await this.postsRepository.findOne(id);
    if (!existPost) {
      throw new HttpException(`id by ${id}Your article does not exist`, 401);
    }
    const updatePost = this.postsRepository.merge(existPost, post);
    return this.postsRepository.save(updatePost);
  }

  // Delete article
  async remove(id) {
    const existPost = await this.postsRepository.findOne(id);
    if (!existPost) {
      throw new HttpException(`id by ${id}Your article does not exist`, 401);
    }
    return await this.postsRepository.remove(existPost);
  }
}

Copy code

After saving the file, the error message prompts that postentity has not been imported:

At this time, import PostsEntity into posts.module.ts:

import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
  imports: [TypeOrmModule.forFeature([PostsEntity])],
  ...
})
Copy code

If you follow the article and use the first method to connect to the database, there is also a small pit where the postentity entity cannot be found:

No repository for "PostsEntity" was found. Looks like this entity is not registered in current "default" connection?

Because we did not register the database when we connected to it, we need to add the following in app.module.ts:

Then the REST style is adopted to implement the interface. We can set the route in posts.controller.ts, process the interface request, and call the corresponding service to complete the business logic:

import { PostsService, PostsRo } from './posts.service';
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';

@Controller('post')
export class PostsController {
    constructor(private readonly postsService:PostsService){}

    /**
     * Create article
     * @param post
     */
    @Post()
    async create(@Body() post){
        return await this.postsService.create(post)
    }

    /**
     * Get all articles
     */
    @Get()
    async findAll(@Query() query):Promise<PostsRo>{
        return await this.postsService.findAll(query)
    }

    /**
     * Get the specified article
     * @param id 
     */
    @Get(':id')
    async findById(@Param('id') id) {
        return await this.postsService.findById(id)
    }

    /**
     * Update article
     * @param id 
     * @param post 
     */
    @Put(":id")
    async update(@Param("id") id, @Body() post){
        return await this.postsService.updateById(id, post)
    }

    /**
     * delete
     * @param id 
     */
    @Delete("id")
    async remove(@Param("id") id){
        return await this.postsService.remove(id)
    }
}
Copy code

Operation database stepped on the pit

  1. Strong replacement of entities, inexplicably delete tables and empty data

Take the entity set above as an example:

export class PostsEntity {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    title: string;
}
Copy code

At first, when I designed the title field in the table, the field type was directly set to string, that is, the corresponding database type was varchar(255). Later, I found it inappropriate to limit the length and change it to varchar(50), that is, modify the code as follows:

 @Column({length: 50})
    title: string;
Copy code

After saving the code, the result! All the title s in my database have been cleared. Who knows who stepped on this pit~

  1. Three settings of entities

We actually stepped on the front of this pit, that is, every time we create an entity, we must import it at the place where the database is linked. It's very chicken to think about it. The official has given three ways. Here are the pit points of various ways:

Method 1: defined separately

TypeOrmModule.forRoot({
  //...
  entities: [PostsEntity, UserEntity],
}),]
Copy code

It is to import the entities used one by one when connecting to the database. The disadvantage is that it is troublesome and easy to forget~

Mode 2: automatic loading

 TypeOrmModule.forRoot({
  //...
  autoLoadEntities: true,
}),]
Copy code

Automatically load our entities, and each entity registered through forFeature() will be automatically added to the entities array of the configuration object,   Forfeature () is introduced in the imports of a service. I personally recommend it. I also use this method for actual development.

**Method 3: configure automatic path import**

 TypeOrmModule.forRoot({
      //...
      entities: ['dist/**/*.entity{.ts,.js}'],
    }),]
Copy code

Automatically import entities through the configured path.

This method is used in the second method of connecting to the database, But ~ super is not recommended. Show you the pit I stepped on:

  1. At that time, I wrote a Category entity, and then wanted to add a Tag entity
  2. Copy category.entity.ts, put it in the tag folder, and rename it tag.entity.ts
  3. The internal attributes (deleted, deleted and modified) are modified to become a Tag entity, which is saved happily
  4. However, I forgot to change the class name, so my category table was cleared and the data in it was gone~

For the above two pits, if you are an empty database, you can toss around freely, but for children's shoes with data in your database, it is recommended to be cautious. When connecting to the database, set synchronize:false first. It is important to protect your life

Here we have implemented a simple database addition, deletion, modification and query operation. Is it very simple? Let's try to test the interface with Postman.

As a front-end development, you are given such an interface in the actual development. Do you have Kaisen ~, I guess you despise the back-end thousands of times! (os: what broken interface, nonstandard request status code, nonstandard return data format...). Don't do to others what you don't want. Optimize it quickly

Uniform interface format

In general, the success or failure of the interface will not be judged according to the HTTP status code, but the code field will be added according to the data returned by the request

First define the returned json format:

{
    "code": 0,
    "message": "OK",
    "data": {
    }
}
Copy code

Return on request failure:

{
    "code": -1,
    "message": "error reason",
    "data": {}
}
Copy code

Intercept error requests

First create a filter using the command:

nest g filter core/filter/http-exception
 Copy code

Filter code implementation:

import {ArgumentsHost,Catch, ExceptionFilter, HttpException} from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp(); // Get request context
    const response = ctx.getResponse(); // Gets the response object in the request context
    const status = exception.getStatus(); // Get exception status code

    // Set error message
    const message = exception.message
      ? exception.message
      : `${status >= 500 ? 'Service Error' : 'Client Error'}`;
    const errorResponse = {
      data: {},
      message: message,
      code: -1,
    };

    // Set the returned status code, request header and send error message
    response.status(status);
    response.header('Content-Type', 'application/json; charset=utf-8');
    response.send(errorResponse);
  }
}
Copy code

Finally, you need to register globally in main.ts

...
import { TransformInterceptor } from './core/interceptor/transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  ...
   // Register filters for global errors
  app.useGlobalInterceptors(new TransformInterceptor());
  await app.listen(9080);
}
bootstrap();
Copy code

In this way, the request error can be returned uniformly. To return the request error, you only need to throw an exception, such as the previous:

 throw new HttpException('Article already exists', 401);
Copy code

Next, the format of the successful return of the request is processed uniformly, which can be realized by the interceptor of Nest.js.

Intercepting the returned data successfully

First create an interceptor using the command:

nest g interceptor core/interceptor/transform
 Copy code

Interceptor code implementation:

import {CallHandler, ExecutionContext, Injectable,NestInterceptor,} from '@nestjs/common';
import { map, Observable } from 'rxjs';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map((data) => {
        return {
          data,
          code: 0,
          msg: 'Request succeeded',
        };
      }),
    );
  }
}

Copy code

Finally, like the filter, register globally in main.ts:

...
import { TransformInterceptor } from './core/interceptor/transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  ...
  // Global registration interceptor
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(9080);
}
bootstrap();
Copy code

Both filter and interceptor implementations are Trilogy: create > implement > register, which is still very simple.

Now let's try the interface again to see if the returned data format is standardized?

As a qualified front-end, you said to me, "this is the interface address xxx. You can see the return result by executing it with postman". This is completely provocative. I don't know what you mean by each field, what parameters need to be passed for each interface, which parameters must be passed, and which are optional

Anyway, if I get such an interface, I will spray it~

Configure interface documentation Swagger

So let's talk about how to write interface documents, which is efficient and practical. I use swagger here. On the one hand, Nest.js provides a special module to use it. Secondly, it can accurately show the meaning of each field, as long as the annotation is written in place!

Speaking from the bottom of my heart, the use experience is general, and I can only say it's OK

First install:

npm install @nestjs/swagger swagger-ui-express -S
 Copy code

The version I installed here is 5.1.4. Compared with version 4.x.x, there are some API changes.

Next, you need to set Swagger document information in main.ts:

...
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  ...
  // Set swagger document
  const config = new DocumentBuilder()
    .setTitle('Management background')   
    .setDescription('Manage background interface documents')
    .setVersion('1.0')
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, document);

  await app.listen(9080);
}
bootstrap();
Copy code

Once configured, we can access: http://localhost:9080/docs , you can see the document generated by Swagger:

The routes we wrote are shown, but we think so. It's too difficult to find the required interfaces, and these interfaces still have no comments, so we still can't understand them~

Interface label

We can classify according to the Controller. Just add @ ApiTags

...
import { ApiTags } from '@nestjs/swagger';
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';

@ApiTags("article")
@Controller('post')
export class PostsController {...}
Copy code

Add category labels to posts.controller.ts and app.controller.ts respectively and refresh the Swagger document. The results are as follows:

Interface description

Further optimize the document and add descriptive text to each interface, so that users can intuitively see the meaning of each interface, and don't let users guess. Similarly, in the Controller, use the @ ApiOperation decorator in front of each route:

//  posts.controller.ts
...
import { ApiTags,ApiOperation } from '@nestjs/swagger';
export class PostsController {

  @ApiOperation({ summary: 'Create article' })
  @Post()
  async create(@Body() post) {....}
  
  @ApiOperation({ summary: 'Get article list' })
  @Get()
  async findAll(@Query() query): Promise<PostsRo> {...}
  ....
}
Copy code

Now that we have written a description of each interface, let's take a look at the interface document:

Interface parameters

Finally, we need to deal with the interface parameter description. One of the advantages of Swagger is that as long as the annotation is in place, the meaning of each field can be accurately displayed. We want to describe each incoming parameter.

Here, you need to insert an explanation about DTO first, because the following parameter descriptions will use:

Data transfer object (dto) is a software application system that transfers data between design patterns. The target of data transfer is often the data access object to retrieve data from the database. The difference between data transfer object and data interaction object or data access object is the data (access and accessor) that has no behavior except storage and retrieval .

This paragraph is an official explanation. It doesn't matter if you don't understand it. It can be understood that DTO itself is more like a guide. When using the API, it is convenient for us to understand the expected data type of the request and the returned data object. It may be more convenient to use it first.

Create a dto folder in the posts directory, and then create a create-post.dot.ts file:

// dto/create-post.dot.ts
export class CreatePostDto {
  readonly title: string;
  readonly author: string;
  readonly content: string;
  readonly cover_url: string;
  readonly type: number;
}
Copy code

Then, in the Controller, specify the type of the parameters passed in to create the article:

//  posts.controller.ts
...
import { CreatePostDto } from './dto/create-post.dto';

@ApiOperation({ summary: 'Create article' })
@Post()
async create(@Body() post:CreatePostDto) {...}
Copy code

Here are two questions:

  1. Why not use interface instead of class to declare CreatePostDto
  2. Why not directly use the previously defined entity type postsentity, but define another CreatePostDto

If you think of this, it's good. It shows that you've been thinking. Let's continue to improve the Swagger interface document and generally explain these two points.

For the first problem, we all know that the Typescript interface is deleted during compilation. Secondly, we will explain the parameters later. Using the Swagger decorator, the interface cannot be implemented, such as:

import { ApiProperty } from '@nestjs/swagger';

export class CreatePostDto {
  @ApiProperty({ description: 'Article title' })
  readonly title: string;

  @ApiProperty({ description: 'author' })
  readonly author: string;

  @ApiPropertyOptional({ description: 'content' })
  readonly content: string;

  @ApiPropertyOptional({ description: 'Article cover' })
  readonly cover_url: string;

  @ApiProperty({ description: 'Article type' })
  readonly type: number;
}
Copy code

@ApiPropertyOptional decoration optional parameters. Continue to look at the UI of the API document:

For the second question mentioned above, why not directly use the entity type postsentity, but define a CreatePostDto? Because the content returned by the HTTP request can adopt a different format from the content saved in the database, separating them can bring greater flexibility over time and business changes, This involves the principle of single design, because each class should handle one thing, preferably only one thing.

Now you can intuitively see the meaning, type and whether to pass each parameter from the API document. This step is not over. Although you can't tell others how to pass it, you accidentally pass it wrong. For example, if the author field above is not passed, what will happen?

The interface directly reports 500, because the author field defined by our entity cannot be empty, and all errors are reported when writing data. This experience is very bad. The front end may suspect that our interface is written incorrectly, so we should deal with exceptions to some extent.

data validation

How? The first thing I think of is to write a pile of if elese to judge the user's transmission parameters in the business. It's definitely not wise to think of a pile of judgments. I checked the data verification in Nest.js and found that the pipeline in Nest.js is specially used for data conversion. Let's take a look at its definition:

The pipe has  @ Injectable()   Class of decorator. The pipeline shall realize   PipeTransform   Interface. There are two types of pipes:

  • Conversion: the pipeline converts the input data to the desired output data
  • Verify: verify the input data. If the verification is successful, continue to transfer; If the verification fails, an exception is thrown;

The pipeline runs in abnormal areas. This means that when exceptions are thrown, they are handled by the core exception handler and the   Exception filter   handle. When an exception occurs in the Pipe, the controller will not continue to execute any method.

What does it mean? Generally speaking, it is the pre operation of verifying and converting the input parameters of the request interface. After verification, I will give the content to the method corresponding to the route. If it fails, I will enter the exception filter.

Nest.js comes with three out of the box pipes: ValidationPipe, ParseIntPipe and ParseUUIDPipe. The ValidationPipe and class validator can perfectly achieve the desired effect (verify the parameter type, and throw an exception if the verification fails).

The pipeline verification operation is usually used in dto, a transport layer file, as a verification operation. First, we install two required dependency packages: class transformer and class validator

npm install class-validator class-transformer -S
 Copy code

Then add verification in the create-post.dto.ts file to improve the error message prompt:

import { IsNotEmpty, IsNumber, IsString } from 'class-validator';

export class CreatePostDto {
  @ApiProperty({ description: 'Article title' })
  @IsNotEmpty({ message: 'Article title is required' })
  readonly title: string;

  @IsNotEmpty({ message: 'Missing author information' })
  @ApiProperty({ description: 'author' })
  readonly author: string;

  @ApiPropertyOptional({ description: 'content' })
  readonly content: string;

  @ApiPropertyOptional({ description: 'Article cover' })
  readonly cover_url: string;

  @IsNumber()
  @ApiProperty({ description: 'Article type' })
  readonly type: number;
}
Copy code

In the entry stage, the data we use is relatively simple. Only some commonly used validation methods are written above. The class validator also provides many validation methods. If you are interested, you can see them for yourself Official documents.

Finally, we have another important step, which is to globally register the ValidationPipe in main.ts:

app.useGlobalPipes(new ValidationPipe());
Copy code

At this time, we are sending a request to create an article without the author parameter, and the returned data is very clear:

Through the above learning, we can know that DTO itself does not have any verification function, but we can use class validator to enable DTO to verify data

summary

So far, we have come to an end on the quick start of Nest.js. The article starts from how to build the project, to implement a simple CRUD, to unify the interface format and complete the interface parameter verification. Finally, users can see a clear interface document and get started step by step. Next, we will implement the user module first, and then continue to improve the article module, involving user login registration, implementation, multi table association operation and unit test of the interface!

Posted by Mactek on Sun, 21 Nov 2021 15:13:13 -0800