preface

Hi, everyone, I’m Koala, an interesting and sharing person. Currently, I focus on sharing the complete Node.js technology stack, and I’m responsible for building the mid-platform of the department and some capabilities of the low-code platform. If you are interested in node.js learning (the follow-up plan can also be), you can follow me, add my wechat [ikoala520], pull you into the exchange group to communicate, learn, build, or follow my public account programmer growth point north. Github blog open Source project github.com/koala-codin…

Recently, I have been busy, and the things I do in my work are not suitable for writing articles, so I have not been more.

Recently, I received a small demand that I need to do all of my own (front-end + back-end). Seeing that everyone in the group is very enthusiastic about Nest. Js, I feel itchy, so I went on the road of no return to Nest

I will record the process of doing this small project by myself, and also share some experience of stepping pits for the reference of friends who want to learn. This article is a step by step, not at the beginning is deep into the difficulties of Nest. Js, but each article has some development attention points and their own thinking, welcome to guide one or two.

Why nest.js

The front also said, everyone said sweet ~

Second, I’ve used egg.js before. In 19, I felt that Egg was a bit restrictive, but good for internal conventions. Now in 2021, I’m used to TS, but egg.js doesn’t have native TypeScript support. Use egg-ts-Helper to help generate d.ts files automatically during development, so that third-party library support is completely out of control and the risk is still too high, so all options are abandoned

With that said, here we go! The article mainly contains the following contents:

Meet Nest. Js

Nest. Js

Nest (NestJS) is a development framework for building efficient, scalable Node.js server-side applications. It takes advantage of the incremental enhancements of JavaScript, uses and fully supports TypeScript (which still allows developers to develop in pure JavaScript), and combines 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 (the default), and can be configured to use Fastify!

Nest raises the level of abstraction above these common Node.js frameworks (Express/Fastify), but still exposes the underlying framework’s APIS directly to developers. This gives developers the freedom to use numerous third-party modules suitable for the underlying platform.

Nest.js is a new Nest. Js is a new Nest.

  • A framework that supports TypeScript natively
  • Can be based onExpressOr you can choosefastifyIf you are rightExpressIt’s very familiar, and you can use the API directly

As for the other do not understand, temporarily put aside, because it does not affect our entry, after in-depth study will be analyzed.

Project creation

First make sure you have Node.js installed. Node.js installation comes with NPX and an NPM package to run the program. To create a new Nest.js application, run the following command on your terminal:

NPM i-g@nestjs /cli // Install Nest Nest new project-name // Create projectCopy the code

After executing the create project, it initializes the following files and asks you how to manage dependencies:

If you have yarn installed, you can choose YARN, which can be faster. NPM is slower to install in China. Here I use NPM to download. After a long wait, I saw it succeed (then I deleted it again, using YARN, it was much faster).

Next, run the project as prompted:

Some of the apis will be different depending on the version of Nest.js that I installed

package version
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 version of Node.js does not meet the requirements, you can use the NVM package management tool to install the correct version of Node.js

The project structure

When you enter your project, you should see a directory structure that looks like this:

Here is a brief description of these core files:

SRC ├─ app.Controll.spec. School Exercises ─ app.Class.School Exercises ─ app.Class.School Exercises ─ App.Class.School Exercises ─ App.School ExercisesCopy the code
app.controller.ts Base Controller for a single route
app.controller.spec.ts Unit tests for controllers
app.module.ts The root Module of the application
app.service.ts Basic Services with a single method
main.ts An entry file for an application that uses core functionsNestFactoryTo create an instance of the Nest application.

First interface

We have already started the service, so how do we 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 the code

The content is simple, using the NestFactory function of Nest.js to create an AppModule instance that starts an HTTP listener to listen for the port defined in main.ts.

The listening port number can be customized. If port 3000 is used by other projects, you can change it to another port number

Because my 3000 port is in use for another project, so I change it to 9080 and restart the project

We open a browser to visit http://localhost:9080 address:

Postman: Hello World: http://localhost:9080

By default, the Nest. Js project creates an interface example, so we can use this interface example to see how to implement an interface.

SRC /app.module.ts: SRC /app.module.ts: 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 the code

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

Mudule files need to use a class with an @Module() decorator, which can be thought of as a wrapped function, but is actually a syntactic sugar (see MidwayJS: TS decorators and IoC mechanisms for a start). The @Module() decorator receives four properties: providers, controllers, imports, exports.

  • Will:Nest.jsProviders instantiated by the injector (service providers) that handle specific business logic that can be shared between modules (The concept of an injector is explained later in the dependency injection section);
  • Controllers: Handle HTTP requests, including route control, and return responses to clients. Delegate business logic to providers.
  • Imports: List of modules to import if you want to use services from other modules.
  • Exports: A list of exported services that are imported 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’re a Vue or React tech stack, you may feel unfamiliar with Nest.js, which is perfectly normal. The way of thinking in Nest.js is not easy to understand at first, but it will be familiar if you’ve been exposed to AngularJS. If you’ve used Java or Spring, You may think, this is not copied Spring Boot!

It’s true that AngularJS, Spring, and Nest.js are all designed with inversion of control in mind and use dependency injection to solve decoupling problems. If you feel confused, don’t worry, these questions will be covered in further study. Here’s a look at how Nest works.

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

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

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

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

Use the @Controller decorator to define the Controller. @get is the decorator for the request method and decorates the getHello method to indicate that the method will be called by the Get request.

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

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

AppService(@Injectable) can be used in app.controller.ts without new AppService(). It can be used in app.controller.ts.

Now that the Hello World logic returned by http://localhost:9080/ is clear, let’s take a look at routing in Nest.js in detail.

Route decorator

There is no place in Nest.js to configure routes separately, instead decorators are used. Several decorators are defined in Nest.js to handle routing.

@Controller

For example, each class that becomes a Controller needs to be decorated with the @Controller decorator, which can pass in a path argument 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 the code

Then restart the service and visit http://localhost:9080/ again to find 404.

Is due to @ Controller (” app “) routing prefix for app, modify the Controller can be access by http://localhost:9080/app at this time.

HTTP methods handle decorators

@GET, @post, @PUT, and many other decorators are used to decorate methods that can respond to HTTP requests. They can also take a string as an argument or an array of strings, which can be fixed paths or wildcards.

To continue modifying app.Controller.ts, look at the following example:

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

Due to the modification of the file, we need to restart to see the route, which is a nightmare to restart every time. Originally, WE planned to configure a real-time monitoring of file changes, but found that Nest. Js is very thoughtful configuration, we just run the command:

npm run start:dev
Copy the code

In this way, any modification will automatically restart the service after saving.

When we have a put request with a path of /app/list/user, we add a method to the app.controller.ts controller file:

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

Do you think this route will be matched? Let’s test it out:

/app/list/user matches the update method instead of the updateUser method. That’s what I’m talking about.

If the @put (“list/:id”) method is not matched because it is already satisfied during the matching process, the @put (“list/user”) method should be written before it.

Global route prefix

In addition to the above decorators that can set routes, we can also set global route prefixes, such as/API prefixes for all routes. You need to modify main.ts

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

All routes before this point should be changed to:

http://localhost/api/xxxx
Copy the code

Now that we know about Controllers, services, Modules, routing, and some common decorators, let’s get into the game. We’ll use the Post Module as an example to implement simple CRUD for articles.

Write the code

Nest-cli provides several useful commands before writing the code:

Nest g [file type] [file name] [file directory]Copy the code
  • Create a module

nest g mo posts

Create a “posts.module.ts” file in the “posts” directory

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

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

After executing the command, we can also see that the PostsModule module has been introduced in the root module app.module.ts and the PostsModule module in the @Model decorator inports

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 the code
  • Creating a Controller

nest g co posts

At this point, you create a Posts controller named Posts.Controller.ts and a unit test file for that controller.

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

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

After executing the command, PostsController is automatically imported into the posts.module.ts file and injected into the @Module decorator controllers.

  • Creating a Service Class

nest g service posts

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

@Injectable(a)export class PostsService {}
Copy the code

Create the app.service.ts file and inject the @Module decorator providers in the app.module.ts file.

In fact, nest-CLI provides many other commands, such as creating filters, interceptors, middleware, etc. Since they are not used here, they will be introduced in the following chapters.

Module, Controller, and Service will be automatically registered in the Module. Otherwise, Module, Controller, and Service will be registered in app.module.ts

Take a look at the current directory structure:

Connect the Mysql

Routing works, since it’s a back-end project, you have to use the database, otherwise it’s no different than writing static pages to play by yourself.

The database I chose was Mysql, after all, it’s the one most projects use. Since the article is a zero-based tutorial, it will cover the installation, connection, and use of the database, as well as the pits encountered during the use of the database. If you are experienced, you can skip this section.

Database Installation

If you don’t have a mysql database on your computer, or a database in the cloud, install mysql locally and download it from the official website

Choose what you needMySQL Community ServerVersion and corresponding platform:Installing MySQL on Windows is relatively simple, similar to installing an applicationMySQL > Install MySQL on WindowsStep by step, I don’t need to describe it here.

Navicat Premium, SQLyog, Navicat Premium, Navicat Premium, SQLyog, Navicat Premium

First connect to database:

Then create a new database blog:

Click on the blog you created and there’s nothing in it. We can either create the table manually here or we can create it in code later. I’ll choose the latter.

TypeORM connects to the database

Front knowledge

First, what is ORM?

If we use node.js directly to manipulate the interface provided by mysql, we will write low-level code, such as a code to insert data:

// 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 the code

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

mysql> select * from posts; +, + + -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + | | id title | content | + - + -- -- -- -- -- -- -- -- -- -- -- -- - + -- -- -- -- -- -- -- -- -- -- -- -- -- -- + | 1 | Nest. Introduction to js | | content description +----+--------+------------+Copy the code

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

{
    id: 1.title:"Nest. Introduction to js".content:"Article Content Description"
}
Copy the code

This is the fabled technique of Object-Relational Mapping, which maps the variable structures of Relational databases onto objects.

So there are Sequelize, typeORM, Prisma ORM frameworks to do this transformation, and we chose typeORM to operate on the database. In this way, we can read and write JavaScript objects, such as the insert statement above:

await connection.getRepository(Posts).save({title:"Nest. Introduction to js".content:"Article Content Description"});
Copy the code

The next step is to actually use typeORM to manipulate the database. First we install the following dependencies:

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

There are two ways to connect to the database, which are introduced here:

Method 1

Env and.env.prod, which store different environment variables for the development environment and online environment respectively:

// Database address DB_HOST=localhost // Database port DB_PORT=3306 // Database login name DB_USER=root // database login password DB_PASSWD=root // database name DB_DATABASE=blogCopy the code

Env.prod is the database information for launching. If your project is going to be managed online, it is recommended to add this file to.gitignore for security reasons.

Next, create a folder config(the same as SRC) under the root directory, and then create an env.ts to read the appropriate configuration file for your environment.

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 the 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: [].// Table entities
        host: configService.get('DB_HOST'.'localhost'), // host, default localhost
        port: configService.get<number> ('DB_PORT'.3306), / / the 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 the server
        synchronize: true.// Database tables are automatically created based on entities. You are advised to disable this function in production environments
      }),
    }),
    PostsModule,
  ],
 ...
})
export class AppModule {}
Copy the code

To use environment variables, it is recommended to use the official @nestjs/config, out of the box. Just a quick explanation

@nestjs/config relies on dotenv to configure environment variables with key=value. The project will load the. Env file in the root directory by default. Use the configModule.forroot () method, and ConfigService reads the relevant configuration variables.

TypeORM provides a variety of connection methods. Here, we will use the ormconfig.json method

Method 2

Instead of passing configuration objects to forRoot(), create an ormconfig.json file (the same as SRC) in the root directory.

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

Then call forRoot() without any options in app.module.ts, and that’s it. For more information about how to connect to the database, visit TypeORM’s official website

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

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

If you fail to connect to the database, you will get this error message:

Check that your database is configured correctly.

CRUD

Ok, then for data operation, the front we said through the code to build tables, TypeORM is through the entity mapped to database table, so we set up a first article entity PostsEntity, create posts on the posts directory. The entity. The ts

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

@Entity("posts")
export class PostsEntity {
    @PrimaryGeneratedColumn(a)id:number; // Mark 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 the code

The next step is to implement the business logic for CRUD operations in the posts.service.ts file. The table here is not the final article table, but a simple add, delete, change and review interface, followed by 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(a)export class PostsService {
  constructor(
    @InjectRepository(PostsEntity)
    private readonly postsRepository: Repository<PostsEntity>,
  ) {}

  // Create the 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 the list of articles
  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 the article
  async updateById(id, post): Promise<PostsEntity> {
    const existPost = await this.postsRepository.findOne(id);
    if(! existPost) {throw new HttpException(` id for${id}The article does not exist.401);
    }
    const updatePost = this.postsRepository.merge(existPost, post);
    return this.postsRepository.save(updatePost);
  }

  // Delete the article
  async remove(id) {
    const existPost = await this.postsRepository.findOne(id);
    if(! existPost) {throw new HttpException(` id for${id}The article does not exist.401);
    }
    return await this.postsRepository.remove(existPost); }}Copy the code

Error message PostsEntity did not import file:

Now import PostsEntity in posts.module.ts:

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

If you are following the article and using the first way to connect to the database, there is still a little pit here to find the PostsEntity entity:

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, so we need to add the following in app.module.ts:

To implement the interface in the REST style, we can set the route in posts.controller.ts, handle 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(a)async create(@Body() post){
        return await this.postsService.create(post)
    }

    /**
     * 获取所有文章
     */
    @Get(a)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 the 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 the code

Potholes that operate databases

  1. Strong substitutions of entities, inexplicable deletion of tables, emptying data

Take the entity we set up above:

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

    @Column(a)title: string;
}
Copy the code

When I first designed the title field in the table, I directly set the field type to string, which corresponds to the database type vARCHar (255). Later, I felt that it was not appropriate, so I limited the length and changed it to varchar(50).

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

After saving the code, the result! All the titles in my database have been emptied, this pit is really who steps who knows ~

  1. entitiesThe three Settings of the

Every time we create an entity, we have to import it in the link to the database. Officially, there are 3 ways to do this, and here are the pits of each way:

Method 1: Define it separately

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

Is to use which entities, one by one in connection with the database to import, the disadvantage is trouble, it is easy to forget ~

Mode 2: Automatic loading

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

Automatically load our entities. Each entity registered with forFeature() is automatically added to the entities array of the configuration object. ForFeature () is introduced in one of the imports in the service, which I personally recommend. This is how I actually develop.

** Mode 3: Configure paths to automatically import **

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

Import entities automatically through the configured path.

This method is used in the second method of connecting to the database, But~ is not recommended. I’ll show you the pit I stepped in:

  1. I wrote one at the timeCategoryEntity, and then want to add oneTagentity
  2. Copy thecategory.entity.tsAnd put ittagFolder, and renamed totag.entiry.ts
  3. Modified internal attributes (delete delete, change change) to become oneTagEntity, happy to save
  4. However, I forgot to change the class name, so mycategoryThe table has been emptied and all data in it is gone

For example, if your database is empty, start synchronize:false when you connect to your database

At this point we have implemented a simple database add, delete, change query operation, is not very simple, we try to use Postman to test the interface.

As a front-end development, the actual development to give you such an interface, you open sen ~, estimated that the heart despise the backend thousands of times! (OS: what broken interface, request status code is not standard, return data format is not standard….) Do unto others as you would have them do unto you. Optimize it

Unified Interface Format

In general, HTTP status codes are not used to determine the success or failure of the interface. Instead, the code field is added to the data returned by the request

First define the returned JSON format:

{
    "code": 0."message": "OK"."data": {}}Copy the code

Return on request failure:

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

Intercepting bad requests

Start by creating a filter with the command:

nest g filter core/filter/http-exception
Copy the 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 the request context
    const response = ctx.getResponse(); // Get the Response object in the request context
    const status = exception.getStatus(); // Get the exception status code

    // Set the error message
    const message = exception.message
      ? exception.message
      : `${status >= 500 ? 'Service Error' : 'Client Error'}`;
    const errorResponse = {
      data: {},
      message: message,
      code: -1};// Set the status code returned, request header, send error message
    response.status(status);
    response.header('Content-Type'.'application/json; charset=utf-8'); response.send(errorResponse); }}Copy the code

Finally, you need to register globally in main.ts

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

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

The request error can be returned uniformly by throwing an exception, such as the previous one:

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

The next unified processing of the successful return format of the request can be implemented using the Nest.js interceptor.

Intercepted successfully returned data

Start by creating an interceptor with the command:

nest g interceptor core/interceptor/transform
Copy the code

Interceptor code implementation:

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

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

Finally, as with filters, register globally in main.ts:

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

async function bootstrap() {
  const app = awaitNestFactory.create<NestExpressApplication>(AppModule); .// Globally register interceptors
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(9080);
}
bootstrap();
Copy the code

Filter and interceptor implementations are a trilogy: Create > Implement > Register.

Now let’s try the interface again and see if the returned data format is normal.

A qualified front-end, you said to me: “this is the interface address XXX, with postman to execute it can see the return result”, this is completely in provocation, ghost know you each field what meaning, each interface needs to pass what parameters, which parameters must pass, which optional….

Anyway, if I got a socket like this, I would definitely spray

Configure interface file Swagger

So let’s talk about how to write an interface document that is both efficient and practical. I use swagger here, on the one hand, because nest.js provides a dedicated module to use it, and on the other hand, it can accurately display the meaning of each field, as long as the annotation is written in place!

To tell you the truth, the experience is just as good as it gets

First install:

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

The version I have installed here is 5.1.4 and there are some API changes compared to 4.x.x.

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

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

async function bootstrap() {
  const app = awaitNestFactory.create<NestExpressApplication>(AppModule); .// Set swagger document
  const config = new DocumentBuilder()
    .setTitle('Admin background')   
    .setDescription('Manage Backend Interface Document')
    .setVersion('1.0')
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, document);

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

Configuration is complete, we can visit: http://localhost:9080/docs, now can see the Swagger generated documentation:

All the routes we wrote are displayed, but it’s too hard to find the interfaces we need, and they still don’t have any comments

The interface TAB

We can sort by 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 the code

Posts.controller. ts and app.controller.ts;

Interface specification

Further optimize the document, add caption to each interface, let users see the meaning of each interface intuitively, do not let users to guess. Also in Controller, use the @APIOperation decorator before each route:

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

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

Now that we have a description for each interface, let’s look at the interface documentation:

Interface and the cords

The last thing we need to deal with is the interface parameter description. One of Swagger’s strengths is that as long as the annotations are in place to show exactly what each field means, we want to say something about each incoming parameter.

Here we need to insert an explanation about DTO, because the following parameter description will be used:

Data Transfer Object (DTO) is a software application system that transfers Data between design modes. The data transfer target is usually a data access object that retrieves data from a database. The difference between a data transfer object and a data interaction object or a data access object is one that has no behavior other than storing and retrieving data (accessors and accessors).

The DTO itself is more of a guide to what data types are expected and what data objects are returned when using the API. Let’s just use it a little bit, maybe it’s easier to understand.

Create a dTO folder in the posts directory and 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 the code

Then type the parameters that were passed in to create the article in Controller:

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

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

Here are two questions:

  1. Why not useinterfaceAnd you want to useclassTo declareCreatePostDto
  2. Why not just use the entity types defined earlierPostsEntiryBut define another oneCreatePostDto

If you’re thinking about this, good, you’ve been thinking about it. Let’s continue to work on the Swagger interface documentation to explain these two points in general.

For the first question, we all know that Typescript interfaces are removed during compilation. For the second, we need to specify that interfaces cannot be implemented with Swagger decorators, such as:

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

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

  @ApiProperty({ description: 'the writer' })
  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 the code

@apiPropertyOptional Decorates the optional parameter, and continues to look at the API document UI:

As for the second question above, why not just use the entity type PostsEntiry, but define a CreatePostDto instead, because HTTP requests can pass parameters and return content in a different format than the content stored in the database? So separating them allows for greater flexibility over time and business changes, and there’s a single design principle involved here, because each class should handle one thing, preferably only one thing.

You can now visually see the meaning, type, and whether each parameter is required from the API documentation. To this step is not over, although and tell others how to pass, but accidentally pass the wrong, such as the above author field did not pass, what happens?

The interface directly reported 500, because the author field defined by our entity cannot be empty, so we reported an error when writing data. This experience is very bad, it is very likely that the front end suspects that we wrote the wrong interface, so we should do something to handle the exception.

Data validation

How do you do that? The first thing THAT came to my mind was to write a bunch of if-Elese judgments about the user’s pass-through. It was definitely not wise to think of a lot of judgments, so I checked the data validation 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:

Pipes are classes with @Injectable() decorators. Pipes should implement the PipeTransform interface.

There are two types of pipes:

  • Transformation: The pipe converts the input data into the desired data output
  • Validation: Validates the input data and continues to pass if validation succeeds; An exception is thrown if validation fails;

The pipeline runs in the abnormal area. This means that when exceptions are thrown, they are handled by the core exception handler and the exception filter applied to the current context. When an exception occurs in Pipe, the Controller does not continue to execute any methods.

The input parameter of the request interface is verified and converted before I pass the content to the corresponding method of the route. If it fails, it goes to the exception filter.

Nex.js comes with three pipes out of the box: ValidationPipe, ParseIntPipe, and ParseUUIDPipe. The ValidationPipe, combined with a class-Validator, does exactly what we want (validates the parameter type and throws an exception if it fails).

Pipeline validation operations are typically used in transport layer files such as Dtos for validation operations. We’ll start by installing the two dependency packages required: class-Transformer and class-Validator

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

Add validation to create-post.to. ts file to complete error message:

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

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

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

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

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

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

This class validator provides a wide range of validation methods for the class validator. This class validator provides a wide range of validation methods for the class validator.

The last important step is to register the ValidationPipe globally in main.ts:

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

We are sending a request to create an article without the author parameter.

Dtos do not have any validation functions, but we can use a class-validator to validate data

conclusion

From how to build the project, to implement simple CRUD, to unify the interface format, complete the interface parameter verification, and finally let users can see a clear interface document, step by step to get started. Next will first implement the user module, and then continue to improve the article module, involving user login registration, implementation, multi-table association operation and interface unit test, welcome to pay attention to!

Get the source of the article:

Get the full source code of the article, and the updated code will continue to be uploaded.

About me & Node communication group

Hi, everyone, I’m Koala, an interesting and sharing person. Currently, I focus on sharing the complete Node.js technology stack, and I’m responsible for building the mid-platform of the department and some capabilities of the low-code platform. If you are interested in node.js learning (the follow-up plan can also be), you can follow me, add my wechat [ikoala520], pull you into the exchange group to communicate, learn, build, or follow my public account programmer growth point north. Github blog open Source project github.com/koala-codin…

  • Welcome to add my wechat [ikoala520], pull you into node.js advanced advanced group, learn Node together, long-term exchange learning…