Nest.js初探——实战todo-demo(CRUD)

553 阅读5分钟

完整版源码:github.com/jiayinya/ne…

一、项目简介

这是一个 Nest.js 初探实战的 todo-demo (CRUD) 项目,使用的技术栈为 Nest.js + TypeScript + TypeORM + Mysql + Swagger

这篇文章比较适合完全不了解 Nest.js,并且没接触过服务端的同学哦,可以对如何开发一个 api 有大概的认识。

二、效果预览

image.png image.png image.png

接下来,废话不多说,让我们直接进入实战吧!(这里概念就不多讲了,因为涉及到的概念实在太多,概念这东西,讲了一次也记不住,还是要从实战中去慢慢理解)

三、创建项目

npm i -g @nestjs/cli  // 全局安装Nest
nest new nest-first-demo  // 创建项目

执行完,如下图,会问你要用什么方式来安装依赖包,我选择的是 yarn image.png

此时项目目录是这样的

image.png

四、启动项目

  yarn start:dev

此时,我们打开浏览器访问:http://localhost:3000 ,页面长这个样子: image.png

接下来,解释几个问题:

  1. 为什么打开的是 3000 端口?

src/main.ts 是项目的入口文件, 里面配置了 await app.listen(3000); 这个端口号也是可以修改的

  1. 为什么访问页面会这样展示?

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. 安装 mysqlNavicat

mysql 是必须的,Navicat 是可视化工具,查看数据库比较方便 直接通过官网安装即可,网上教程一大把,这里就不介绍具体怎么安装了

2. 连接数据库

打开 Navicat ,点击左上角 Connection, 然后点击 Mysql image.png

输入你的 Mysql 的密码,即可连接你本地的数据库; image.png

3. 新建数据库

image.png 此时数据库是空的,我们后面通过代码来建表

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

  1. 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();
   }
 }
  1. src/todos/todos.module.ts 文件中

    也要引入我们前面写好的 TodosEntity

     @Module({
       imports: [TypeOrmModule.forFeature([TodosEntity])],
       ...
     })
    
  2. 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…

(原作者写的可能比我写的更详细更清晰一些,我这里主要是自己记录实战过程的笔记。如果需要更详细的解释,可以看上面的链接)