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
- 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~
- 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:
- At that time, I wrote a Category entity, and then wanted to add a Tag entity
- Copy category.entity.ts, put it in the tag folder, and rename it tag.entity.ts
- The internal attributes (deleted, deleted and modified) are modified to become a Tag entity, which is saved happily
- 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:
- Why not use interface instead of class to declare CreatePostDto
- 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!