本文是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
}
设置级联插入
- 修改多对多关联的字段配置,在coffee.entity.ts里
@JoinTable() // 指定关联关系的OWNER端
@ManyToMany(
type=> Flavor, //需要关联的实体
flavor=>flavor.coffees // 关联的具体字段
{
cascade:true //新创建的Coffee的Flavor将自动插入到数据库
}
) // 指定多对多关系
flavors:string[]
- 将Flavor Repository 注入到CoffeesService里
@InjectRepository(Flavor)
private readonly flavorRepository: Repository<Flavor>,
- 新建一个私有的预查询方法,用于在新增或者更新的时候调用
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
- 配置event.entity.ts
@Entity()
export class Event {
@PrimaryGeneratedColumn()
id:number;
@Column()
type:string;
@Column()
name:string;
@Column()
payload:Record<string,any>
}
- 将event添加到CoffeesModule中
imports:[TypeOrmModule.forFeature([Coffee,Flavor,Event])],
- 将事件添加到咖啡实体中 在coffee.entitity.ts中增加一列
@Column({default:0})
recommendations:number
- 在coffees.service.ts的constructor中创建事务连接
private readonly connect:Connection //创建事务连接
- 在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相互引用
- 新建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 {}
- 在被应用的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 {}
- 在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
- 安装 @nestjs/config
- 创建.env文件
DATABASE_HOST:'localhost',
DATABASE_PORT:3306,
DATABASE_USERNAME:'root',
DATABASE_PASSWORD:'12345678',
DATABASE_DATABASE:'coffee',
- 将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)
}
全局异常过滤器
- 创建 nest g filter common/filter/http-exception
- 配置过滤器
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()
})
}
}
- 注入应用程序
// main.ts
app.useGlobalFilters(new HttpExceptionFilter())
await app.listen(3000);
全局守卫
- 创建 nest g filter common/filter/http-exception
- 配置
// 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'; } } - 注入 app.useGlobalGuards(new ApiKeyGuard())
定义单个控制器为公共请求,跳过守卫验证
- 使用自带的 @SetMetadata 装饰器
// coffees.controller.ts @SetMetadata('isPublic',true) @Get() findAll(@Query() paginationQuery:PaginationQueryDto){ return this.coffeesService.findAll(paginationQuery) } - 在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';
}
}
- 生成公共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 {}
- 删除main.ts里的 app.useGlobalGuards(new ApiKeyGuard())
创建拦截器
- 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里
}
}
- 在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
- nest g pipe common/pipes/parse-int
- 配置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;
}
}
- 使用pipe
// coffees.controller.ts
@Get(':id')
// 这里使用的是自定义的Pipe 而不是nest自带的
findOne(@Param('id',ParseIntPipe) id:number){
console.log(id);
return this.coffeesService.findOne(''+ id)
}
自定义中间件
- nest g middleware common/middleware/logging
- 配置
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('*')
}
}
自定义装饰器
- 新建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
}
)
- 在具体的方法中使用
@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
- npm i @nestjs/swagger swagger-ui-express
- 在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);
- 修改update dto文件
import { PartialType } from "@nestjs/swagger";
import { CreateCoffeeDto } from "./create-coffee.dto";
export class UpdateCoffeeDto extends PartialType(CreateCoffeeDto){
}
- 配置字段描述
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 : []
}
- 配置响应描述 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)
}
- 设置接口方法分组标签
@ApiTags('coffee')
@Controller('coffees')