write at the beginning
Each article is written by the author 心
, and it takes a lot of time to proofread and adjust, aiming to bring you the best reading experience.
Your 点赞、收藏、转发
is the greatest encouragement to the author, and also allows more people to see this article, thank you very much!
If you think this article is helpful to you, please help to light up on github star
encourage it!
text
Nest
is a framework for building efficient, scalable Node.js
server-side applications. It uses progressive JavaScript
, built and fully supported TypeScript
combined with OOP
(object oriented programming), FP
(Functional Programming ) and FRP
(Functional Reactive Programming).
Under the hood, Nest
uses the powerful HTTP Server
framework, such as Express
(default) and Fastify
Nest
provides a certain level of abstraction on top of these frameworks, while also exposing it API
directly to developers. This makes it easy to use countless third-party modules for each platform.
It can also be seen from the above picture that Nest
is currently the second most popular framework after the old Express
, and the second Nodejs
framework.
Today, we use this article Nest
quick customs clearance guide, use Nest
to create a travel guide, will use the following functions including but not limited to Nest
- middleware
- pipeline
- class validator
- guard
- interceptor
- custom decorator
- database
- File Upload
- MVC
- permission
- ...
This project has a goal of finding best practices for simple cases in the Nest
documents, and putting them into actual scenarios.
Well, without further ado, let's get started!
Initialize the project
The source code repository of this case can be downloaded from the source code address .
First, use the npm i -g @nestjs/cli
command to install nest-cli
and then use the scaffold command to create a new nest
project. (as follows)
nest new webapi-travel
After the project is initialized, we enter the project and run the npm run start:dev
command to start the project! (Open ( http://localhost:3000/ )[ http://localhost:3000/ ] after the project is started to view the effect)
As shown in the figure above, after entering the page and seeing Hello World
, it means that the project has been successfully started!
configuration database
data table design
Next, we need to design our data table structure, we are going to make a 吃喝玩乐
store collection, the store needs to display this information:
Store Name
- Store Profile (slogan)
- Store Type (Eat, Drink and Play)
Cover (single image)
- Carousel (multiple images)
- label(s)
- Per capita consumption
- Rating (0 - 5)
- Address
- longitude
- latitude
As can be seen from the above, we should have at least two tables: the shop table and the shop carousel chart.
So next, let's define the DDL of the two tables. (as follows)
CREATE TABLE IF NOT EXISTS `shop` (
`id` int PRIMARY KEY AUTO_INCREMENT,
`name` varchar(16) NOT NULL DEFAULT '',
`description` varchar(64) NOT NULL DEFAULT '',
`type` tinyint unsigned NOT NULL DEFAULT 0,
`poster` varchar(200) NOT NULL DEFAULT '',
`average_cost` smallint NOT NULL DEFAULT 0 COMMENT '人均消费',
`score` float NOT NULL DEFAULT 0,
`tags` varchar(200) NOT NULL DEFAULT '',
`evaluation` varchar(500) NOT NULL DEFAULT '',
`address` varchar(200) NOT NULL DEFAULT '',
`longitude` float NOT NULL DEFAULT 0,
`latitude` float NOT NULL DEFAULT 0,
index `type`(`type`)
) engine=InnoDB charset=utf8;
CREATE TABLE IF NOT EXISTS `shop_banner` (
`id` int PRIMARY KEY AUTO_INCREMENT,
`shop_id` int NOT NULL DEFAULT 0,
`url` varchar(255) NOT NULL DEFAULT '',
`sort` smallint NOT NULL DEFAULT 0 COMMENT '排序',
index `shop_id`(`shop_id`, `sort`, `url`)
) engine=InnoDB charset=utf8;
Among them, shop_banner
uses a joint index, which can effectively reduce the number of returns to the table, and can sort the pictures. Once created, you can check it out. (As shown below)
Configure database connection
After the data table initialization is complete, we need to configure our database connection in nest
. In this tutorial, we use the typeorm
library for database operations. Let's install the relevant dependencies in the project first.
npm install --save @nestjs/typeorm typeorm mysql2
Then, let's configure the database connection configuration and create a ormconfig.json
configuration file in the project root directory.
{
"type": "mysql",
"host": "localhost", // 数据库主机地址
"port": 3306, // 数据库连接端口
"username": "root", // 数据库用户名
"password": "root", // 数据库密码
"database": "test", // 数据库名
"entities": ["dist/**/*.entity{.ts,.js}"],
"synchronize": false // 同步设置,这个建议设置为 false,数据库结构统一用 sql 来调整
}
The database configuration is different according to each person's server settings. I have not uploaded this file to the warehouse. If you want to experience Demo
, you need to create this file yourself.
After the configuration is complete, we complete the database connection configuration in the app.module.ts
file.
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [TypeOrmModule.forRoot()],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
typeorm
will automatically load theormconfig.json
configuration file in the project, so there is no need to display the import.
Project initialization
This article will serve as a practical guide, and will advance some of the content, because I think these are the core parts of nest
.
validator
In nest
, we use pipeline for function verification. We first define a ValidationPipe
for verification. The content of the file is as follows:
// src/pipes/validate.pipe.ts
import {
ArgumentMetadata,
BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || this.toValidate(metatype)) {
return value;
}
const object = plainToClass(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
const error = errors[0];
const firstKey = Object.keys(error.constraints)[0];
throw new BadRequestException(error.constraints[firstKey]);
}
return value;
}
// eslint-disable-next-line @typescript-eslint/ban-types
private toValidate(metatype: Function): boolean {
// eslint-disable-next-line @typescript-eslint/ban-types
const types: Function[] = [String, Boolean, Number, Array, Object];
return types.includes(metatype);
}
}
Then, we can register the pipeline as a global pipeline in main.ts
. The code is implemented as follows:
// src/main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(7788);
}
bootstrap();
In this way, we can define a validation class in the entity to quickly complete the validation of a single field.
Response result formatting
I want all response results to be returned in a unified format, which is code
(status code) + message
(response information) + data
(data) The format is returned, in this case, we can define an interceptor ResponseFormatInterceptor
, which is used to serially format all the response results.
The code is implemented as follows:
// src/interceptors/responseFormat.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class ResponseFormatInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> | Promise<Observable<any>> {
return next.handle().pipe(
// 将原有的 `data` 转化为统一的格式后返回
map((data) => ({
code: 1,
message: 'ok',
data,
})),
);
}
}
Then, also register the interceptor in main.ts
in bootstrap
(below)
app.useGlobalInterceptors(new ResponseFormatInterceptor());
Unified error handling
Here, I want our error to not return a bad status code (as that could cause the frontend to throw a cross-origin error). So, I want to return the status code 200 for all errors, and then return the actual error code in the response body code
, we need to write an interceptor to implement this function--- ResponseErrorInterceptor
.
// src/interceptors/responseError.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ResponseErrorInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> | Promise<Observable<any>> {
return next.handle().pipe(
catchError(async (err) => ({
code: err.status || -1,
message: err.message,
data: null,
})),
);
}
}
Then, also register the interceptor in main.ts
in bootstrap
(below)
app.useGlobalInterceptors(new ResponseErrorInterceptor());
Parse the header token
In our subsequent authentication operations, we are going to use the token
parameter passed in the header. I want to parse token
when the actual request of each interface occurs, parse out the user information corresponding to the token
, and then continue to pass the user information down.
Here, we need to implement a middleware that can parse token
information. The code is implemented as follows:
// src/middlewares/auth.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction } from 'express';
import { UserService } from '../user/user/user.service';
@Injectable()
export class AuthMiddleware implements NestMiddleware {
constructor(private readonly userService: UserService) {}
async use(req: Request, res: Response, next: NextFunction) {
const token = req.headers['token'];
req['context'] = req['context'] || {};
if (!token) return next();
try {
// 使用 token 查询相关的用户信息,如果该函数抛出错误,说明 token 无效,则用户信息不会被写入 req.context 中
const user = await this.userService.queryUserByToken(token);
req['context']['token'] = token;
req['context']['user_id'] = user.id;
req['context']['user_role'] = user.role;
} finally {
next();
}
}
}
Then, we need to register this middleware globally in src/app.module.ts
(below)
export class AppModule {
configure(consumer: MiddlewareConsumer) {
// * 代表该中间件在所有路由均生效
consumer.apply(AuthMiddleware).forRoutes('*');
}
}
route guard
We need to set guards in some routes, only users with specified permissions can access, here we need to implement a routing guard AuthGuard
for guarding the route, and a custom decorator Roles
for Set routing permissions.
The code is implemented as follows:
// src/guards/auth.guard.ts
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const req = context.switchToHttp().getRequest();
const token = req.headers['token'];
const user_id = req['context']['user_id'];
const user_role = req['context']['user_role'];
// 没有 token,或者 token 不包含用户信息时,认为 token 失效
if (!token || !user_id) {
throw new ForbiddenException('token 已失效');
}
const roles = this.reflector.get<string[]>('roles', context.getHandler());
// 没有角色权限限制时,直接放行
if (!roles) {
return true;
}
// 角色权限为 `admin` 时,需要用户 role 为 99 才能访问
if (roles[0] === 'admin' && user_role !== 99) {
throw new ForbiddenException('角色权限不足');
}
return true;
}
}
Here is the implementation of the custom decorator:
// src/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
Since the guard only takes effect for some routes, we only need to use it in the specified route.
shop operation
Next, we go back to the project and prepare to complete our store operations. We need to implement the following functions:
- Check all store information
- Query individual store information
- add store
- delete store
- Modify store information
Register ShopModule
We create shop/shop.controller.ts
, shop/shop.service.ts
, shop/shop.module.ts
in order. (as follows)
// shop/shop.controller.ts
import { Controller, Get } from '@nestjs/common';
@Controller('shop')
export class ShopController {
@Get('list')
async findAll() {
return 'Test Shops List';
}
}
// shop/shop.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class ShopService {}
import { Module } from '@nestjs/common';
import { ShopController } from './shop.controller';
import { ShopService } from './shop.service';
@Module({
controllers: [ShopController],
providers: [ShopService],
})
export class ShopModule {}
After initialization, don't forget to register ---d2f6619b816276179736d03cf9eb9e94 app.module.ts
in ShopModule
.
// app.module.ts
// ...
@Module({
imports: [TypeOrmModule.forRoot(), ShopModule], // 注册 ShopModule
controllers: [AppController],
providers: [AppService],
})
After registration, we can use postman
to verify our service. (As shown below)
As can be seen from the above figure, our route registration is successful, then let's define our data entities.
define data entities
Data entity typeorm
Use @Entity
decorator decoration model can be used to create database tables (Open synchronize
time), can also be used typeorm
Data sheet CURD operation.
We created two new data tables earlier, now let's create the corresponding data entities.
// src/shop/models/shop.entity.ts
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { ShopBanner } from './shop_banner.entity';
export enum ShopType {
EAT = 1,
DRINK,
PLAY,
HAPPY,
}
// 数据表 —— shops
@Entity()
export class Shop {
// 自增主键
@PrimaryGeneratedColumn()
id: number;
@Column({ default: '' })
name: string;
@Column({ default: '' })
description: string;
@Column({ default: 0 })
type: ShopType;
@Column({ default: '' })
poster: string;
// 一对多关系,单个店铺对应多张店铺图片
@OneToMany(() => ShopBanner, (banner) => banner.shop)
banners: ShopBanner[];
@Column({ default: '' })
tags: string;
@Column({ default: 0 })
score: number;
@Column({ default: '' })
evaluation: string;
@Column({ default: '' })
address: string;
@Column({ default: 0 })
longitude: number;
@Column({ default: 0 })
latitude: number;
@Column({ default: 0 })
average_cost: number;
@Column({ default: '' })
geo_code: string;
}
// src/shop/models/shop_banner.entity.ts
import {
Column,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Shop } from './shop.entity';
@Entity()
export class ShopBanner {
@PrimaryGeneratedColumn()
id: number;
// 多对一关系,多张店铺图片对应一家店铺
// 在使用 left join 时,使用 shop_id 字段查询驱动表
@ManyToOne(() => Shop, (shop) => shop.banners)
@JoinColumn({ name: 'shop_id' })
shop: Shop;
@Column()
url: string;
@Column()
sort: number;
}
As can be seen from the above, our three data entities have corresponding decorator descriptions. You can refer to the TypeORM documentation for the usefulness of decorators.
Add shop interface
After the entity is defined, let's write 新增店铺
interface.
This interface needs to receive a 店铺
object as input parameter. We will also use this object for parameter verification later. Let's define this class first. (as follows)
// src/shop/dto/create-shop.dto.ts
import { IsNotEmpty } from 'class-validator';
import { ShopType } from '../models/shop.entity';
export class CreateShopDto {
// 使用了 ValidationPipe 进行校验
@IsNotEmpty({ message: '店铺名称不能为空' })
name: string;
description: string;
@IsNotEmpty({ message: '店铺类型不能为空' })
type: ShopType;
poster: string;
banners: string[];
tags: string[];
@IsNotEmpty({ message: '店铺评分不能为空' })
score: number;
evaluation: string;
@IsNotEmpty({ message: '店铺地址不能为空' })
address: string;
@IsNotEmpty({ message: '店铺经度不能为空' })
longitude: number;
@IsNotEmpty({ message: '店铺纬度不能为空' })
latitude: number;
average_cost: number;
}
Then, we add a method in ShopController
to register the 新增店铺
interface. (as follows)
// src/shop/shop.controller.ts
@Controller('shop')
export class ShopController {
constructor(private readonly shopService: ShopService) {}
// add 接口
@Post('add')
// 返回状态码 200
@HttpCode(200)
// 使用鉴权路由守卫
@UseGuards(AuthGuard)
// 定义只有 admin 身份可访问
@Roles('admin')
// 接收入参,类型为 CreateShopDto
async addShop(@Body() createShopDto: CreateShopDto) {
// 调用 service 的 addShop 方法,新增店铺
await this.shopService.addShop(createShopDto);
// 成功后返回 null
return null;
}
}
After the interface request is initiated, we call the new store method of service
, and then return a success prompt.
Next, let's edit ShopService
新增店铺
define a method of addShop
(below)
export class ShopService {
constructor(private readonly connection: Connection) {}
async addShop(createShopDto: CreateShopDto) {
const shop = this.getShop(new Shop(), createShopDto);
// 处理 banner
if (createShopDto.banners?.length) {
shop.banners = this.getBanners(createShopDto);
await this.connection.manager.save(shop.banners);
}
// 存储店铺信息
return this.connection.manager.save(shop);
}
getShop(shop: Shop, createShopDto: CreateShopDto) {
shop.name = createShopDto.name;
shop.description = createShopDto.description;
shop.poster = createShopDto.poster;
shop.score = createShopDto.score;
shop.type = createShopDto.type;
shop.tags = createShopDto.tags.join(',');
shop.evaluation = createShopDto.evaluation;
shop.address = createShopDto.address;
shop.longitude = createShopDto.longitude;
shop.latitude = createShopDto.latitude;
shop.average_cost = createShopDto.average_cost;
shop.geo_code = geohash.encode(
createShopDto.longitude,
createShopDto.latitude,
);
return shop;
}
getBanners(createShopDto: CreateShopDto) {
return createShopDto.banners.map((item, index) => {
const banner = new ShopBanner();
banner.url = item;
banner.sort = index;
return banner;
});
}
}
It can be seen that ShopService
is responsible for interacting with the database. Here, the store information is stored first, and then 店铺 banner
and 店铺标签
are stored.
After the interface is completed, we use postman
to verify our new interface. The following is the test data we prepared.
{
"name": "蚝满园",
"description": "固戍的宝藏店铺!生蚝馆!还有超大蟹钳!",
"type": 1,
"poster": "https://img1.baidu.com/it/u=2401989050,2062596849&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",
"banners": [
"https://img1.baidu.com/it/u=2401989050,2062596849&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500",
"https://img1.baidu.com/it/u=2043954707,1889077177&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400",
"https://img1.baidu.com/it/u=1340805476,3006236737&fm=253&fmt=auto&app=120&f=JPEG?w=360&h=360"
],
"tags": [
"绝绝子好吃",
"宝藏店铺",
"价格实惠"
],
"score": 4.5,
"evaluation": "吃过两次了,他们家的高压锅生蚝、蒜蓉生蚝、粥、蟹钳、虾都是首推,风味十足,特别好吃!",
"address": "宝安区上围园新村十二巷 5-6 号 101",
"longitude": 113.151415,
"latitude": 22.622297,
"average_cost": 80
}
Query store list/update store information/delete store
After the new store is completed, let's improve the interface for querying the store list, updating the store information and deleting the store.
First, define the corresponding controller
entry.
// src/shop/shop.controller.ts
@Controller('shop')
export class ShopController {
constructor(private readonly shopService: ShopService) {}
// 获取店铺列表接口
@Get('list')
async getShopList(@Query() queryShopListDto: QueryShopListDto) {
const list = await this.shopService.getShopList(queryShopListDto);
return {
pageIndex: queryShopListDto.pageIndex,
pageSize: queryShopListDto.pageSize,
list,
};
}
// update 接口
@Post('update')
@HttpCode(200)
@UseGuards(AuthGuard)
@Roles('admin')
// 接收入参,类型为 UpdateShopDto
async updateShop(@Body() updateShopDto: UpdateShopDto) {
// 调用 service 的 addShop 方法,新增店铺
await this.shopService.updateShop(updateShopDto);
// 返回成功提示
return null;
}
// delete 接口
@Post('delete')
@HttpCode(200)
@UseGuards(AuthGuard)
@Roles('admin')
async deleteShop(@Body() deleteShopDto: QueryShopDto) {
await this.shopService.deleteShop(deleteShopDto);
return null;
}
}
Then, we will complete the corresponding service
.
When the store information is updated, another situation needs to be handled, that is, the store update is successful, but the store image update fails. In this case, the update is only partially effective.
Therefore, store information and store image information should either be stored successfully together or fail to be stored together (through transaction rollback). According to this feature, we will need to enable transactions here.
Use the TypeORM
getManager().transaction()
to explicitly start a transaction. The code is implemented as follows:
export class ShopService {
constructor(private readonly connection: Connection) {}
async getShopList(queryShopListDto: QueryShopListDto) {
const shopRepository = this.connection.getRepository(Shop);
const { pageIndex = 1, pageSize = 10 } = queryShopListDto;
const data = await shopRepository
.createQueryBuilder('shop')
.leftJoinAndSelect('shop.banners', 'shop_banner')
.take(pageSize)
.skip((pageIndex - 1) * pageSize)
.getMany();
return data
.map((item) => {
// 计算用户传入的位置信息与当前店铺的距离信息
const distance = computeInstance(
+queryShopListDto.longitude,
+queryShopListDto.latitude,
item.longitude,
item.latitude,
);
return {
...item,
tags: item.tags.split(','),
distanceKm: distance,
distance: convertKMToKmStr(distance),
};
})
.sort((a, b) => a.distanceKm - b.distanceKm);
}
async updateShop(updateShopDto: UpdateShopDto) {
return getManager().transaction(async (transactionalEntityManager) => {
await transactionalEntityManager
.createQueryBuilder()
.delete()
.from(ShopBanner)
.where('shop_id = :shop_id', { shop_id: updateShopDto.id })
.execute();
const originalShop: Shop = await transactionalEntityManager.findOne(
Shop,
updateShopDto.id,
);
const shop = this.getShop(originalShop, updateShopDto);
if (updateShopDto.banners?.length) {
shop.banners = this.getBanners(updateShopDto);
await transactionalEntityManager.save(shop.banners);
}
await transactionalEntityManager.save(shop);
});
}
async deleteShop(deleteShopDto: QueryShopDto) {
return getManager().transaction(async (transactionalEntityManager) => {
await transactionalEntityManager
.createQueryBuilder()
.delete()
.from(Shop)
.where('id = :id', { id: deleteShopDto.id })
.execute();
await transactionalEntityManager
.createQueryBuilder()
.delete()
.from(ShopBanner)
.where('shop_id = :shop_id', { shop_id: deleteShopDto.id })
.execute();
});
}
}
When querying the list interface, it also involves a distance calculation point. Since this part is not the core content of nest
, it will not be introduced here. Interested children's shoes can find the source code address for reading. .
Let's see the effect. (As shown below)
So far, list query, update store information, and delete store have all been completed.
File Upload
Finally, I will introduce some derived knowledge points, such as how to upload files using nest
.
Here you can use the nest
provided FileInterceptor
interceptor and UploadedFile
file receiver to receive the file stream, and then use your own graph bed tool, For example oss
transfer to your own server, the following is a code example for reference.
// common.controller.ts
import {
Controller,
HttpCode,
Post,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import '../../utils/oss';
import { CommonService } from './common.service';
@Controller('common')
export class CommonController {
constructor(private readonly commonService: CommonService) {}
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
@HttpCode(200)
uploadFile(@UploadedFile() file: Express.Multer.File) {
return upload(file);
}
}
// oss.ts
const { createHmac } = require('crypto');
const OSS = require('ali-oss');
const Duplex = require('stream').Duplex;
const path = require('path');
export const hash = (str: string): string => {
return createHmac('sha256', 'jacklove' + new Date().getTime())
.update(str)
.digest('hex');
};
const ossConfig = {
region: process.env.OSS_REGION,
accessKeyId: process.env.OSS_ACCESS_KEY_ID,
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
bucket: process.env.OSS_BUCKET,
};
const client = new OSS(ossConfig);
export const upload = async (file: Express.Multer.File): Promise<string> => {
const stream = new Duplex();
stream.push(file.buffer);
stream.push(null);
// 文件名 hash 化处理
const fileName = hash(file.originalname) + path.extname(file.originalname);
await client.putStream(fileName, stream);
const url = `http://${ossConfig.bucket}.${ossConfig.region}.aliyuncs.com/${fileName}`;
return url;
};
Except for the uploading part of the image bed, other codes are basically the same, and the focus is on the reception and processing of file information.
Deploy the application
We can use pm2
to deploy the application. First, we need to use npm run build
to build the production product.
After the production product is built, execute the following command in the current project directory to run the project.
pm2 start npm --name "webapi-travel" -- run start:prod
Then, you can see that our project has been successfully started (as shown below).
You can access it through https://webapi-travel.jt-gmall.com , which is the site address after my deployment.
summary
This article does not discuss the implementation of each line of code too carefully, but simply and rudely puts the simple cases in the nest
document into the actual scene to see the corresponding processing method.
Of course, there will be better processing methods, such as authentication, which can have better processing methods.
There is no introduction here. Interested children's shoes can study it by themselves.
This article aims to provide a quick reference to nest
scenario actual combat cases.
You can also list any scenes you are interested in. I will choose some typical scenes and continue to add them in the article.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。