前端开发者学习后端开发入门知识(nest.js全套入门知识)

465 阅读5分钟

本文是nestjs官方教学视频的代码手巧版,也算是自己的学习笔记,需要的朋友建议配合视频食用效果更佳,方便的话请点个小赞。

代码地址

安装

npm i -g @nestjs/cli

新建工程

nest new

新增控制器

nest g co

通过@Controller()传参实现路由功能,该类里的具体实现方法(@Get()@Post())传参实现路由嵌套

参数路由:

 @Get(':id')
 findOne(@Param('id') id:string){
   // @Param() param 可以取到整个路由参数
   return `this actions return #${id} cooffee`
 }

获取post请求body

  @Post()
  create(@Body('name') body){
            return body
          }
      // 取特定的参数
  @Post()
  create(@Body('name') body){
          return body
        }

设置http状态码

  @HttpCode(HttpStatus.GONE)

获取url query参数

findAll(@Query() queryParams){
  const {page,num} = queryParams
  return `this action return ${page}page ${num}num coffees`
}

新建服务

nest g s

服务的具体实现

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';

@Injectable()
export class CoffeesService {
  findOne(id:string){
    throw `random error`  // 抛出程序执行异常 返回500
    throw new HttpException(`coffee ${id} not found`,HttpStatus.NOT_FOUND) // 抛出正常异常
  }
}

在对应控制器内注入服务

export class CoffeesController {
  constructor(private readonly coffeesService:CoffeesService){}
  // 修改控制器内的对应方法
  @Get(':id')
  findOne(@Param('id') id:string){
    return this.coffeesService.findOne(id)
  }
}

新建module

nest g module

@Module({}) 
有四个参数
/* 
controllers:处理该模块下的API
exports: 需要导出的内容
imports:从其他模块引入的内容
providers: 需要注入的服务 
*/

@Module({controllers:[CoffeesController],providers:[CoffeesService]})
//这module里注入了controller和service之后需要把app.module.ts里对应的controller和service删掉

## 生成DTO
nest g class coffees/dto/create-coffee.dto --no-spec

定义DTO

export class CreateCoffeeDto {
readonly name : string
readonly brand : string
}

使用DTO

在controller里

@Post()
create(@Body() CreateCoffeeDto:CreateCoffeeDto){
  return this.coffeesService.create(CreateCoffeeDto)
}

请求参数校验

在main.ts

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
  whitelist:true, // 设置参数白名单,未在DTO中声明的字段将被过滤
  forbidNonWhitelisted:true //若参数中有未在DTO中存在的字段,终止请求
  transform:true // 将请求参数(字符串、数组)转换为DTO需要的格式
}))
await app.listen(3000);
}

安装 class-validator class-transformer

使用

在对应的DTO文件内

import { IsString } from "class-validator";
export class CreateCoffeeDto {
  @IsString()
  readonly name : string
  @IsString()
  readonly brand : string
}

处理更新操作的时候重复定义DTO代码问题

安装 @nestjs/mapped-types 使用

import { PartialType } from "@nestjs/mapped-types";
import { CreateCoffeeDto } from "./create-coffee.dto";

export class UpdateCoffeeDto extends PartialType(CreateCoffeeDto){

}
// 返回传入的类的类型 同时所有属性是可选的 并且集成了所有的验证规则 也可通过@IsOptional()添加单个规则给每个字段

配置docker

新建 docker-compose.yml文件

version: "3" 

services:
  db:
    image: postgres // 数据库类型
    restart: always 
    ports:
      - "5432:5432"  
    environment:
      POSTGRES_PASSWORD: pass123

安装typeorm

npm i @nestjs/typeorm typeorm@0.2 pg

由于typeorm0.2和0.3差别比较大 这里用0.2版本

连接postgres数据库

在app.module.ts

imports: [CoffeesModule,TypeOrmModule.forRoot({
  // type:'postgres',
  // host:'localhost',
  // port:5432,
  // username:'postgres',
  // password:'pass123',
  // database:'postgres',
  // autoLoadEntities:true,
  // synchronize:true

  type:'mysql',
  host:'localhost',
  port:3306,
  username:'root',
  password:'12345678',
  database:'coffee',
  autoLoadEntities:true, // 自动加载模块,而不是指定实体数组
  synchronize:true // TypeORM实体每次运行应用程序时自动同步数据库,生产环境需要关闭。
})],

配置Entity实体类

新建 src/coffees/entites/coffee.entity.ts

import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
//由于在app.module.ts里初始化连接数据库的时候配置了自动同步 所以 @Entity 会自动把配置的Entity实体类在数据库中生成一个SQL表,数据库表明是类名的小写,如果要指定表明可以在装饰器里传参
@Entity()
export class Coffee{
  // 表示主键primary key
  @PrimaryGeneratedColumn()
  id:number;

  @Column()
  name:string;

  @Column()
  brand:string;

  @Column('json',{nullable:true})
  // 数据格式是json 并且是可以为空的
  flavors:string[]
}

将实体类引入到对应的Module中

在coffees.module.ts

import { Coffee } from './entities/coffee.entity';

@Module({
  imports:[TypeOrmModule.forFeature([Coffee])],
  // 模块内引入使用forFeature
  controllers:[CoffeesController],
  providers:[CoffeesService]})
export class CoffeesModule {}

将实体类注入到对应的Service中

在coffees.service.ts

@Module({
imports:[TypeOrmModule.forFeature([Coffee])],
controllers:[CoffeesController],
providers:[CoffeesService]})
export class CoffeesModule {
  constructor(
    @InjectRepository(Coffee)
    private readonly coffeeRepository:Repository<Coffee>
  ){

  }
}

在service中实现增删查改接口

import { HttpException, HttpStatus, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateCoffeeDto } from './dto/create-coffee.dto';
import { UpdateCoffeeDto } from './dto/update-coffee.dto';
import { Coffee } from './entities/coffee.entity';

@Injectable()
export class CoffeesService {
  constructor(
    @InjectRepository(Coffee)
    private readonly coffeeRepository:Repository<Coffee>
  ){}
    findAll(){
      return this.coffeeRepository.find()
    }
    async findOne(id:string){
      const coffee = await this.coffeeRepository.findOne(id)
      if (!coffee) {
        throw new NotFoundException(`Coffee ${id} not found`)
      }
      return coffee
    }
    create(createCoffeeDto:CreateCoffeeDto){
      //先创建实体
      const coffee = this.coffeeRepository.create(createCoffeeDto)
      // 存入数据库
      return this.coffeeRepository.save(coffee)
    }
    async update(id:string,updateCoffeeDto:UpdateCoffeeDto){
      // 调用preload方法 该方法会先创建实体,再在数据库中检查实体是否存在,如果存在就进行替换,如果不存在就会返回undefiend
      const coffee = await this.coffeeRepository.preload({
        id:+id,
        ...updateCoffeeDto
      })
      if (!coffee) {
        throw new NotFoundException(`Coffee ${id} not found`)
      }
      return this.coffeeRepository.save(coffee)
    }
    async remove(id:string){
      const coffee = await this.findOne(id)
      return this.coffeeRepository.remove(coffee)
    }
}

关联表

新建“口味” 实体

nest g class coffees/entities/flavor.entity --no-spec

把生成文件里的class名字里的Entity去掉

@Entity()
export class Flavor {
  @PrimaryGeneratedColumn()
  id:number;

  @Column()
  name:string
}

注册实体

在coffees.module.ts里

imports:[TypeOrmModule.forFeature([Coffee,Flavor])],

关联表关系

在coffee实体里修改需要关联的字段

  // @Column('json',{nullable:true})
  // flavors:string[]
  // 修改为
  @JoinTable() // 指定关联关系的OWNER端
  @ManyToMany(
    type=> Flavor,  //需要关联的实体
    flavor=>flavor.coffees  // 关联的具体字段
  ) // 指定多对多关系
  flavors:string[]

在被关联flavor的实体里增加关联的字段

@ManyToMany(     //由于已经在Coffee里面@JoinTable()指定了关联的所有者,所以这里不需要在指定了
type =>  Coffee,    //关联的实体
coffee => coffee.flavors  // 关联的字段
)
coffees:Coffee[]

处理查询的时候查出所关联的字段

在对应的service里修改查询方法

findAll(){
    return this.coffeeRepository.find({
      relations:['flavor'] // 指定关联的字段
    })
  }
  async findOne(id:string){
    const coffee = await this.coffeeRepository.findOne(id,{
      relations:['flavor'] // 指定关联的字段
    })
    if (!coffee) {
      throw new NotFoundException(`Coffee ${id} not found`)
    }
    return coffee
  }

设置级联插入

  1. 修改多对多关联的字段配置,在coffee.entity.ts里
 @JoinTable() // 指定关联关系的OWNER端
 @ManyToMany(
   type=> Flavor,  //需要关联的实体
   flavor=>flavor.coffees  // 关联的具体字段
   {
     cascade:true  //新创建的Coffee的Flavor将自动插入到数据库
    }
 ) // 指定多对多关系
 flavors:string[]
  1. 将Flavor Repository 注入到CoffeesService里
@InjectRepository(Flavor)
 private readonly flavorRepository: Repository<Flavor>,
  1. 新建一个私有的预查询方法,用于在新增或者更新的时候调用
 private async preloadFlavorByName(name:string):Promise<Flavor>{
   const existingFlavor = await this.flavorRepository.findOne({name})
   // 先查找数据库中是否已存在传入名称的口味实体,如果有则返回
   if (existingFlavor) {
     return existingFlavor
   }
   // 如果没有就用传入的名称新建一个
   return this.flavorRepository.create({name})
 }

处理分页

新建分页DTO

nest g class common/dto/pagination-query.dto --no-spec

在common/dto/pagination-query.dto.ts里配置

 export class PaginationQueryDto {
   @IsOptional() // 参数可选
   @IsPositive() // 参数为大于0的正数
   @Type(()=> Number) //指定转换为number,这里也可以在全局main.ts配置全局转换
   limit:number;

   @IsOptional()
   @IsPositive()
   @Type(()=>Number)
   offest:number
 }

在controller和service里面使用

在conttoller里使用

 @Get()
 findAll(@Query() paginationQuery:PaginationQueryDto){
   return this.coffeesService.findAll(paginationQuery)
 }

在service里使用

 findAll(paginationQuery:PaginationQueryDto){
   const {limit,offset} = paginationQuery
   return this.coffeeRepository.find({
     relations:['flavors'], // 指定关联的字段
     skip:offset, // 需要跳过的数据
     take:limit  // 取得数据条数
   })
 }

数据库事务处理

创建事件event.dto

nest g class events/entities/event.entity --no-spec

  1. 配置event.entity.ts
@Entity()
export class Event {
  @PrimaryGeneratedColumn()
  id:number;

  @Column()
  type:string;

  @Column()
  name:string;

  @Column()
  payload:Record<string,any>
}
  1. 将event添加到CoffeesModule中
imports:[TypeOrmModule.forFeature([Coffee,Flavor,Event])],
  1. 将事件添加到咖啡实体中 在coffee.entitity.ts中增加一列
@Column({default:0})
recommendations:number
  1. 在coffees.service.ts的constructor中创建事务连接
private readonly connect:Connection //创建事务连接
  1. 在coffees.service里实现最终的方法
    async recommendCoffee(coffee:Coffee){
      const queryRunner = this.connect.createQueryRunner() //创建一个queryRunner
      await queryRunner.connect() // 连接到数据库
      await queryRunner.startTransaction() // 开始事务进程
      try {
        coffee.recommendations++
        const recommendEvent = new Event() // 创建一个新事件
        recommendEvent.name = 'recommend_coffee'
        recommendEvent.type = 'coffee'
        recommendEvent.payload = {coffeeId : coffee.id}
        await queryRunner.manager.save(coffee)
        await queryRunner.manager.save(recommendEvent)
        await queryRunner.commitTransaction()
      } catch (error) {
        await queryRunner.rollbackTransaction() //如果事务执行出错则执行回滚
      } finally{
        await queryRunner.release() // 事务执行完成则释放连接
      }
    }

库表添加索引

在event.entity.ts

@Index(['name','type']) // 传递一个列名数组作为参数
 @Entity()
 export class Event {
   @PrimaryGeneratedColumn()
   id:number;

   @Column()
   type:string;

   @Column()
   name:string;

   @Column()
   payload:Record<string,any>
 }

不同module相互引用

  1. 新建coffee-rating.module,在这里注入需要引入的module
import { Module } from '@nestjs/common';
import { CoffeesModule } from 'src/coffees/coffees.module';
import { CoffeeRatingService } from './coffee-rating.service';

@Module({
  imports:[CoffeesModule],
  providers: [CoffeeRatingService]
})
export class CoffeeRatingModule {}
  1. 在被应用的module里面导出该模块的service
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Event } from 'src/events/entities/event.entity';
import { CoffeesController } from './coffees.controller';
import { CoffeesService } from './coffees.service';
import { Coffee } from './entities/coffee.entity';
import { Flavor } from './entities/flavor.entity';

@Module({
  imports:[TypeOrmModule.forFeature([Coffee,Flavor,Event])],
  controllers:[CoffeesController],
  providers:[CoffeesService],
  exports:[CoffeesService]
})
export class CoffeesModule {}
  1. 在coffee.rating.service里注入需要应用的service
import { Injectable } from '@nestjs/common';
import { CoffeesService } from 'src/coffees/coffees.service';

@Injectable()
export class CoffeeRatingService {
  constructor(private readonly coffeeService:CoffeesService){}
}

使用 config Module

  1. 安装 @nestjs/config
  2. 创建.env文件
DATABASE_HOST:'localhost',
DATABASE_PORT:3306,
DATABASE_USERNAME:'root',
DATABASE_PASSWORD:'12345678',
DATABASE_DATABASE:'coffee',
  1. 将ConfigModule添加到app.module.ts并替换app.module.ts 里的数据库连接配置
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CoffeesModule } from './coffees/coffees.module';
import { CoffeeRatingModule } from './coffee-rating/coffee-rating.module';
import { ConfigModule } from '@nestjs/config';

@Module({
 imports: [
   ConfigModule.forRoot(
     // {
     //   envFilePath:'.environment', // 指定env文件(默认加载.env)
     //   ignoreEnvFile:true   //生产环境忽略env配置
     // }
   ),
   CoffeesModule,
   TypeOrmModule.forRoot({
   type:'mysql',
   host:process.env.DATABASE_HOST,
   port:+process.env.DATABASE_PORT,
   username:process.env.DATABASE_USERNAME,
   password:process.env.DATABASE_PASSWORD,
   database:process.env.DATABASE_DATABASE,
   autoLoadEntities:true, // 自动加载模块,而不是指定实体数组
   synchronize:true // TypeORM实体每次运行应用程序时自动同步数据库,生产环境需要关闭。
 }), CoffeeRatingModule],
 controllers: [AppController],
 providers: [AppService],
})
export class AppModule {}

验证环境变量格式

安装 @hapi/joi @types/hapi__joi

npm i @hapi/joi npm i @types/hapi__joi --save-dev

全局pipe 在main.ts里全局注入 或者在app.module.ts里注入

 providers: [
   AppService,
   {
     provide:APP_PIPE,
     useClass:ValidationPipe
   }
 ],

在单个控制器内注入pipe

@UsePipes(ValidationPipe)

在单个方法上注入pipe

  @UsePipes(ValidationPipe)
  @Get()
  findAll(@Query() paginationQuery:PaginationQueryDto){
    return this.coffeesService.findAll(paginationQuery)
  }

在方法内的参数注入pipe

  @Patch(':id')
  updated(@Param('id') id:string,@Body(ValidationPipe) body:UpdateCoffeeDto){
    return this.coffeesService.update(id,body)
  }

全局异常过滤器

  1. 创建 nest g filter common/filter/http-exception
  2. 配置过滤器
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
import { Response } from "express";
@Catch(HttpException)
export class HttpExceptionFilter<T extends HttpException> implements ExceptionFilter {
  catch(exception: T, host: ArgumentsHost) {
    const ctx = host.switchToHttp() // 访问原生请求或响应对象
    const response = ctx.getResponse<Response>()
    const status = exception.getStatus()
    const exceptionResponse = exception.getResponse()
    const error = typeof response === 'string'
    ? {message:exceptionResponse} 
    : (exceptionResponse as object)
    response.status(status).json({
      ...error,
      timestamp:new Date().toISOString()
    })
  }
}
  1. 注入应用程序
// main.ts
app.useGlobalFilters(new HttpExceptionFilter())
  await app.listen(3000);

全局守卫

  1. 创建 nest g filter common/filter/http-exception
  2. 配置
     // api-key.guard.ts
     import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
     import { Observable } from 'rxjs';
     import { Request } from "express";
     @Injectable()
     export class ApiKeyGuard implements CanActivate {
       canActivate(
         context: ExecutionContext,
       ): boolean | Promise<boolean> | Observable<boolean> {
         const request = context.switchToHttp().getRequest<Request>()
         const authHeader = request.header('Authorization')
         return authHeader === '123456';
       }
     }
    
  3. 注入 app.useGlobalGuards(new ApiKeyGuard())

定义单个控制器为公共请求,跳过守卫验证

  1. 使用自带的 @SetMetadata 装饰器
    // coffees.controller.ts
    @SetMetadata('isPublic',true)
     @Get()
     findAll(@Query() paginationQuery:PaginationQueryDto){
       return this.coffeesService.findAll(paginationQuery)
     }
    
  2. 在api-key.guard.ts中读取请求上下文的配置
  import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
  import { Observable } from 'rxjs';
  import { Request } from "express";
  import { Reflector } from '@nestjs/core';
  @Injectable()
  export class ApiKeyGuard implements CanActivate {
    constructor(
      private readonly reflector:Reflector // 注入Reflector 用来读取请求的上下文配置
    ){}
    canActivate(
      context: ExecutionContext,
    ): boolean | Promise<boolean> | Observable<boolean> {
      const isPublic = this.reflector.get('isPublic',context.getHandler())
      if (isPublic) {
        return true
      }
      const request = context.switchToHttp().getRequest<Request>()
      const authHeader = request.header('Authorization')
      return authHeader === '123456';
    }
  }
  1. 生成公共module
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ApiKeyGuard } from './guards/api-key.guard';

@Module({
  providers:[{provide:APP_GUARD,useClass:ApiKeyGuard}]
})
export class CommonModule {}
  1. 删除main.ts里的 app.useGlobalGuards(new ApiKeyGuard())

创建拦截器

  1. nest g interceptor common/interceptors/wrap-response
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, tap, map } from 'rxjs';

@Injectable()
export class WrapResponseInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('before...');

    // return next.handle().pipe(tap(data=> console.log('after...', data)
    return next.handle().pipe(map(data => ({ data }))) //将返回数据包在data里
  }
}
  1. 在main.ts里注入

超时拦截器 nest g interceptor common/interceptors/timeout

  import { CallHandler, ExecutionContext, Injectable, NestInterceptor, RequestTimeoutException } from '@nestjs/common';
  import { catchError, Observable, throwError, timeout, TimeoutError } from 'rxjs';

  @Injectable()
  export class TimeoutInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
      return next.handle().pipe(
        timeout(3000),
        catchError(err=>{
          if (err instanceof TimeoutError) {
            return throwError(new RequestTimeoutException())
          }
          return throwError(err)
        })
        );
    }
  }

在main.ts里注入

app.useGlobalInterceptors(new WrapResponseInterceptor(), new TimeoutInterceptor())

在coffees.controller.ts里模拟超时

  @SetMetadata('isPublic',true)
  @Get()
  async findAll(@Query() paginationQuery:PaginationQueryDto){
    await new Promise(resolve => setTimeout(resolve,5000))
    return this.coffeesService.findAll(paginationQuery)
  }

创建自定义pipe

  1. nest g pipe common/pipes/parse-int
  2. 配置pipe
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class ParseIntPipe implements PipeTransform {
  transform(value: string, metadata: ArgumentMetadata) {
    const val = parseInt(value,10)
    if (isNaN(val)) {
      throw new BadRequestException(
        `验证错误 ${val} 不是整数`
      )
    }
    return value;
  }
}
  1. 使用pipe
// coffees.controller.ts
@Get(':id')
 // 这里使用的是自定义的Pipe 而不是nest自带的
  findOne(@Param('id',ParseIntPipe) id:number){
    console.log(id);
    return this.coffeesService.findOne(''+ id)
  }

自定义中间件

  1. nest g middleware common/middleware/logging
  2. 配置
 import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ApiKeyGuard } from './guards/api-key.guard';
import { LoggingMiddleware } from './middleware/logging.middleware';

@Module({
  providers:[{provide:APP_GUARD,useClass:ApiKeyGuard}]
})
export class CommonModule implements NestModule{
  configure(consumer:MiddlewareConsumer){
    // 给所有路由配置中间件
    consumer.apply(LoggingMiddleware).forRoutes('*')
  }
}

自定义装饰器

  1. 新建decorators/portocol.decorator.ts文件
import { createParamDecorator, ExecutionContext } from "@nestjs/common";

export const Protocol = createParamDecorator(
  (defaultValue:string,ctx:ExecutionContext)=>{
    console.log('defaultValue: ', defaultValue);
    const request = ctx.switchToHttp().getRequest()
    return request.protocol
  }
)
  1. 在具体的方法中使用
  @Get()
  async findAll(@Protocol('https') protocol:string, @Query() paginationQuery:PaginationQueryDto){
    console.log(protocol);
    // await new Promise(resolve => setTimeout(resolve,5000))
    return this.coffeesService.findAll(paginationQuery)
  }

添加swagger

  1. npm i @nestjs/swagger swagger-ui-express
  2. 在main.ts里配置
const options = new DocumentBuilder()
  .setTitle('Iluvcoffee')
  .setDescription('Coffee application')
  .setVersion('1.0')
  .build()
const document = SwaggerModule.createDocument(app,options)
SwaggerModule.setup('api',app,document)
await app.listen(3000);
  1. 修改update dto文件
import { PartialType } from "@nestjs/swagger";
import { CreateCoffeeDto } from "./create-coffee.dto";

export class UpdateCoffeeDto extends PartialType(CreateCoffeeDto){

}
  1. 配置字段描述
import { ApiProperty } from "@nestjs/swagger";
import { IsArray, IsString } from "class-validator";
export class CreateCoffeeDto {
  @ApiProperty({description:'名称'})
  @IsString()
  readonly name : string
  @ApiProperty({description:'品牌'})
  @IsString()
  readonly brand : string
  @ApiProperty({description:'口味',example:['sweet']})
  @IsArray()
  readonly flavors : []
}
  1. 配置响应描述 200的响应会自动显示
  @ApiResponse({status:403,description:'Forbidden'})
  @SetMetadata('isPublic',true)
  @Get()
  async findAll(@Protocol('https') protocol:string, @Query() paginationQuery:PaginationQueryDto){
    console.log(protocol);
    // await new Promise(resolve => setTimeout(resolve,5000))
    return this.coffeesService.findAll(paginationQuery)
  }
  1. 设置接口方法分组标签
@ApiTags('coffee')
@Controller('coffees')