完整版源码:github.com/jiayinya/ne…
一、项目简介
这是一个 Nest.js 初探实战的 todo-demo (CRUD) 项目,使用的技术栈为
Nest.js
+TypeScript
+TypeORM
+Mysql
+Swagger
。
这篇文章比较适合完全不了解
Nest.js
,并且没接触过服务端的同学哦,可以对如何开发一个 api 有大概的认识。
二、效果预览
接下来,废话不多说,让我们直接进入实战吧!(这里概念就不多讲了,因为涉及到的概念实在太多,概念这东西,讲了一次也记不住,还是要从实战中去慢慢理解)
三、创建项目
npm i -g @nestjs/cli // 全局安装Nest
nest new nest-first-demo // 创建项目
执行完,如下图,会问你要用什么方式来安装依赖包,我选择的是 yarn
此时项目目录是这样的
四、启动项目
yarn start:dev
此时,我们打开浏览器访问:http://localhost:3000 ,页面长这个样子:
接下来,解释几个问题:
- 为什么打开的是 3000 端口?
src/main.ts
是项目的入口文件, 里面配置了await app.listen(3000);
这个端口号也是可以修改的
- 为什么访问页面会这样展示?
src/app.service.ts
中写的就是业务逻辑,这里返回的 'Hello World!' 就是页面上展示的啦
src/app.controller.ts
中呢负责写接口,@Get()
就是一个装饰器,表示一个 Get 接口,括号里可以写请求路径,比如写 /todo, 那么请求路径就是 http://localhost:3000/todo 不写路径默认 / 。
在这个文件里我们发现,调用了 appService.getHello(),这就是访问 http://localhost:3000 的时候,返回的东西啦!
五、TODO(CRUD)
1、创建文件
这个跟我们平时写 vue / react 等不一样的是,这个一般通过命令行创建
nest g mo todos // 创建模块
nest g co todos // 创建控制器
nest g service todos // 创建服务类
注意创建顺序:先创建Module, 再创建Controller和Service, 这样创建出来的文件在Module中自动注册,反之,后创建Module, Controller和Service,会被注册到外层的app.module.ts
到这里,肯定很多人跟我的想法一样,为什么要分三步创建,有没有办法可以一步到位?
当然有 —— nest g resource todos
但是我不建议大家一步到位,因为这个一步到位比较适合熟悉之后再用,它太到位了,可以直接帮我们写好CRUD, 我们还怎么练习呢?
2、准备工作(1)——数据库
1. 安装 mysql
和 Navicat
mysql
是必须的,Navicat
是可视化工具,查看数据库比较方便
直接通过官网安装即可,网上教程一大把,这里就不介绍具体怎么安装了
2. 连接数据库
打开 Navicat
,点击左上角 Connection
, 然后点击 Mysql
输入你的 Mysql
的密码,即可连接你本地的数据库;
3. 新建数据库
此时数据库是空的,我们后面通过代码来建表
3、准备工作(2)—— TypeORM
连接数据库
1. 安装依赖
yarn add @nestjs/typeorm typeorm mysql2
2. app.modules.ts 文件中配置
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'username', // 改成你自己的用户名
password: '********', // 改成你自己的密码
database: 'todos',
entities: ['dist/**/entities/*.entity{.ts,.js}'],
synchronize: true,
}),
],
controllers: [AppController],
providers: [AppService],
})
...
4、建表
todos 下新建 entities/todos.entity.ts 文件
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity('todos')
export class TodosEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
description: string;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
create_time: Date;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
update_time: Date;
}
写法可以查看 TypeORM
官方文档
5、开始CRUD
src/todos/todos.service.ts
文件中,写业务逻辑,比如“查询任务”
要引入我们前面写好的 TodosEntity
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TodosEntity } from './entities/todos.entity';
@Injectable()
export class TodosService {
constructor(
@InjectRepository(TodosEntity)
private readonly todosRespository: Repository<TodosEntity>,
) {}
// 查询任务
async findAll() {
return await this.todosRespository.find();
}
}
-
src/todos/todos.module.ts
文件中也要引入我们前面写好的
TodosEntity
@Module({ imports: [TypeOrmModule.forFeature([TodosEntity])], ... })
-
src/todos/todos.controller.ts
文件中import { Get } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { TodosService } from './todos.service'; import { CreateTodoDto } from './dto/create-todo.dot'; @Controller('todos') export class TodosController { constructor(private readonly todosSerive: TodosService) {} @Get() async get() { return await this.todosSerive.findAll(); } }
此时,我们的查询接口就写好了,增删改同理,这里就不做赘述了。可以自己写,也可以参考我的代码。源码:github.com/jiayinya/ne…
对于数据库的增删改操作的 API,也是查看 TypeORM
官方文档
六、Swagger文档
1. 引入Swagger
(1)安装依赖
yarn add @nestjs/swagger swagger-ui-express
(2)main.ts配置
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 设置swagger
const config = new DocumentBuilder()
.setTitle('nest-demo-todos')
.setDescription('nest-demo-todos接口文档')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);
await app.listen(3000);
}
此时,访问 http://localhost:3000/docs 即可查看 swagger 文档。
2. 优化Swagger文档
我们此时的文档,上面只有接口,什么注释都没有,使用的人根本不知道这个接口/这个字段代表什么意思! 所以,我们需要给文档加上相应的说明。
(1)接口标签 —— 根据Controller来分类
// src/todos/todos.controller.ts
@ApiTags('任务')
@Controller('todos')
export class TodosController {
...
}
(2)接口说明 —— 每个接口含义
// src/todos/todos.controller.ts
@ApiTags('任务')
@Controller('todos')
export class TodosController {
...
@ApiOperation({ summary: '查询任务' })
@Get()
async get() {
return await this.todosSerive.findAll();
}
}
(3)DTO —— 每个字段含义
todos 目录下,新建 dto/create-todo.dot.ts
// src/todos/dto/create-todo.dot.ts
import { ApiProperty } from '@nestjs/swagger';
export class CreateTodoDto {
@ApiProperty({ description: '任务标题' })
readonly title: string;
@ApiProperty({ description: '任务详细描述' })
readonly description: string;
}
// src/todos/todos.controller.ts
...
export class TodosController {
...
@Post()
async add(@Body() body: CreateTodoDto) {
return await this.todosSerive.addTodo(body);
}
}
直到此时,我们的接口文档看起来是非常清晰明了的了。
七、接口格式统一
我们现在是直接将最后结果返回的,直接是 todos 的数组集合,如果是string,直接返回string, 我们希望最后的结构是这样的。
// 成功
{
code: 1,
data: [],
message: 'ok'
}
// 失败
{
code: -1,
data: {},
message: '我是错误原因'
}
怎么做呢?我们需要一个过滤器和拦截器
- 过滤器:拦截错误请求
- 拦截器:拦截成功的返回数据
创建过滤器
命令:nest g filter core/filter/http-exception
// src/core/filter/http-exception/http-exception.filter.ts
import {ArgumentsHost,Catch, ExceptionFilter, HttpException} from '@nestjs/common';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp(); // 获取请求上下文
const response = ctx.getResponse(); // 获取请求上下文中的 response对象
const status = exception.getStatus(); // 获取异常状态码
// 设置错误信息
const message = exception.message
? exception.message
: `${status >= 500 ? 'Service Error' : 'Client Error'}`;
const errorResponse = {
data: {},
message: message,
code: -1,
};
// 设置返回的状态码, 请求头,发送错误信息
response.status(status);
response.header('Content-Type', 'application/json; charset=utf-8');
response.send(errorResponse);
}
}
创建拦截器
命令:nest g interceptor core/interceptor/transform
// src/core/interceptor/transform/transform.interceptor.ts
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: 1,
msg: '请求成功',
};
}),
);
}
}
全局注册过滤器和拦截器
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './core/filter/http-exception/http-exception.filter';
import { TransformInterceptor } from './core/interceptor/transform/transform.interceptor';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局注册过滤器
app.useGlobalInterceptors(new TransformInterceptor());
// 全局注册拦截器
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
哎呀,我苍了天了,终于快结束了,写的我好累。我们还有最后一步,就是需要对字段进行校验。
八、字段校验
我们写到这里,其实基本完整的流程都已经结束了。但是,会存在一个小问题。
比如,我创建任务的时候,如果一不小心 description
字段忘记传了,接口就会报错 500,客户端会以为服务端接口挂了。可是接口实际好好的。我们希望这种情况下,接口不要500,而是返回给客户端错误信息,告诉它你的 description
字段没有传,这个字段是必传的,该怎么做呢?
(1)安装依赖
yarn add class-validator class-transformer
(2)添加字段校验
在 src/todos/dto/create-todo.dot.ts
文件中,添加字段的校验及提示信息
import { IsNotEmpty } from 'class-validator';
export class CreateTodoDto {
@IsNotEmpty({ message: '任务标题不能为空' })
readonly title: string;
@IsNotEmpty({ message: '任务详细描述必传' })
readonly description: string;
}
(3)main.ts 中全局注册
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './core/filter/http-exception/http-exception.filter';
import { TransformInterceptor } from './core/interceptor/transform/transform.interceptor';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局注册管道
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
(4)修改过滤器
过滤器中关于错误信息的设置也要修改,因为错误信息此时来自 class-validator
校验的错误提示
// src/core/filter/http-exception/http-exception.filter.ts
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
.....
// 设置错误信息
let message = '';
if (exception instanceof BadRequestException) {
// 获取 class-validator 校验的信息
message = exception.getResponse()['message'].join('、');
} else {
message = exception.message
? exception.message
: `${status >= 500 ? 'Service Error' : 'Client Error'}`;
}
....
}
参考资源:blog.csdn.net/xgangzai/ar…
(原作者写的可能比我写的更详细更清晰一些,我这里主要是自己记录实战过程的笔记。如果需要更详细的解释,可以看上面的链接)