本文内容指南
- 异常拦截
- 返回体封装
- class-validator 参数校验管道
- typeorm 数据库操作
- ...
异常过滤
过滤分为全局过滤器、控制器过滤器、路由过滤器,它们用来更友好地返回服务端的错误响应。
@Catch(HttpException)捕获Http异常进行处理,统一返回状态码和错误信息。
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response
.status(status)
.json({
statusCode: status,
message:exception.message,
timestamp: new Date().toISOString(),
path: request.url,
});
}
绑定过滤器
在入口文件通过useGlobalFilters方式绑定全局的过滤器,当然也可以在Controller类或者方法通过@UseFilters()装饰器使用。
import { HttpExceptionFilter } from './filter/http-exception.filter'
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
使用map方法改写返回值,封装data、statusCode、message返回结构。
拦截器封装响应体
拦截器主要用于在请求处理之前和之后对请求进行修改、干预或拦截。它们可以修改请求和响应的数据、转换数据格式、记录日志等,以及处理全局任务。这里通过拦截器封装响应体:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(map(data => {
return {
statusCode: 200,
message: '请求成功!',
data
}
}));
}
}
绑定拦截器
在入口文件通过useGlobalInterceptors方式绑定全局的拦截器,@UseInterceptor()装饰器可以绑定类或者方法的拦截器。
import { HttpExceptionFilter } from './filter/http-exception.filter'
import { TransformInterceptor } from './interceptor/transform.interceptor'
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new TransformInterceptor());
await app.listen(3000);
}
bootstrap();
class-validator 参数校验管道
安装依赖:
pnpm add class-validator class-transformer
创建数据模型类:
验证修饰符有IsString,MinLength,MaxLength,IsInt,Min,Max,IsDate等,message参数可以定义错误提示信息。
import { IsString, IsInt,MinLength,MaxLength} from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(2, { message: "最小长度2" })
@MaxLength(20, { message: "最大长度20" })
name: string;
@IsInt()
age: number;
}
class-validator 装饰器方法见 wdk-docs.github.io/nestjs-docs…
创检验管道类:
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToInstance(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
const message=errors.map(e=> Object.values(e.constraints));
throw new BadRequestException(`字段校验失败:${message}`);
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
绑定校验管道
管道可以是参数、方法、控制器、全局范围。 这里绑定到@Body装饰器上校验post body。
@Post()
async create(
@Body(new ValidationPipe()) createUserDto: CreateUserDto,
) {
this.usersService.create(CreateUserDto);
}
typeorm 操作数据库
需要安装 typeorm相关依赖:
@nestjs/typeorm typeorm mysql2
创建实体
import { Entity, Column, PrimaryColumn,CreateDateColumn } from 'typeorm';
@Entity('user_info')
export class User {
@PrimaryColumn()
id: number;
@Column()
userName: string;
@CreateDateColumn()
createTime: Date;
}
- Entity 传参可以指定表名
- PrimaryColumn 是主键列,每个实体必须至少有一个主键列。
- PrimaryGeneratedColumn 是自动生成的主键列。
@ Column可以指定列选项:
type: ColumnType- 列类型。其中之一在上面.name: string- 数据库表中的列名。length: number- 列类型的长度。 例如,如果要创建varchar(150)类型,请指定列类型和长度选项。default: string- 添加数据库级列的DEFAULT值。primary: boolean- 将列标记为主要列。 使用方式和@ PrimaryColumn相同。unique: boolean- 将列标记为唯一列(创建唯一约束)。comment: string- 数据库列备注,并非所有数据库类型都支持。
还有有几种特殊的列类型可以使用:
@CreateDateColumn是一个特殊列,自动为实体插入日期。无需设置此列,该值将自动设置。@UpdateDateColumn是一个特殊列,在每次调用实体管理器或存储库的save时,自动更新实体日期。无需设置此列,该值将自动设置。@VersionColumn是一个特殊列,在每次调用实体管理器或存储库的save时自动增长实体版本(增量编号)。无需设置此列,该值将自动设置。
实体管理器Repository可以对实体进行增删改查,nestjs中通过InjectRepository向Service注入Repository:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
findAll(): Promise<User[]> {
return this.usersRepository.find();
}
findOne(id: number): Promise<User | null> {
return this.usersRepository.findOneBy({ id });
}
async remove(id: number): Promise<void> {
await this.usersRepository.delete(id);
}
}
Repository API
find
select- 表示必须选择对象的哪些属性relations- 关系需要加载主体。 也可以加载子关系(join和leftJoinAndSelect的简写)join- 需要为实体执行联接,扩展版对的"relations"。where-查询实体的简单条件。OR 运算符查询可以用数组order- 选择排序skip- 偏移(分页)take- limit (分页) - 得到的最大实体数。cache启用或禁用查询结果缓存lock- 启用锁查询。 只能在findOne方法中使用。
find方法示例:
userRepository.find({
select: ["id", "userName"],
relations: ["photos"],
where: {
firstName: "Timber",
lastName: "Saw"
},
order: {
name: "ASC",
id: "DESC"
},
skip: 1,
take: 10,
cache: true
});
Repository 其它API:
create- 创建新实例。 接受具有用户属性的对象文字,该用户属性将写入新创建的用户对象(可选)。save- 保存给定实体或实体数组。 如果该实体已存在于数据库中,则会更新该实体。 如果数据库中不存在该实体,则会插入该实体。insert- 插入新实体或实体数组。remove- 删除给定的实体或实体数组。update- 通过给定的更新选项或实体 ID 部分更新实体。delete-根据实体 id, ids 或给定的条件删除实体count- 符合指定条件的实体数量。对分页很有用。findOne- 查找匹配某些 ID 或查找选项的第一个实体。query- 执行原始 SQL 查询。
命名策略:驼峰命转下划线
实体中的列名用的是驼峰命名法,但是数据库里一般是下划线命名,通过@Columns({name:'user_name'})指定每个字段数据库列名又太繁琐。
sequelize-typescript可以通过underscored选项开启转换,typeorm中可以通过namingStrategy指定命名策略。
import { DefaultNamingStrategy } from "typeorm";
export class MyNamingStrategy extends DefaultNamingStrategy {
tableName(targetName: string, userSpecifiedName: string | undefined): string {
if (userSpecifiedName) return userSpecifiedName;
return parseName(targetName);
}
columnName(propertyName: string, customName: string, embeddedPrefixes: string[]): string {
if (customName) return customName;
return parseName(propertyName);
}
}
function parseName(targetName: string): string {
if(!targetName) return "";
let str : string = "";
for(let i = 0; i < targetName.length; ++i) {
let code = targetName[i];
if (code >= "A" && code <= "Z") {
if(i != 0 ) {
str = str.concat("_");
}
str = str.concat(code.toLocaleLowerCase());
}else {
str = str.concat(code);
}
}
return str;
}
初始化typeorm是提供namingStrategy选项:
TypeOrmModule.forRoot({
type: 'mysql',
namingStrategy: new MyNamingStrategy()
...
})
JWT 认证
JWT认证流程:
- 客户端使用用户名密码登录
- 登录成功后服务端下发JWT
- 客户端后续请求携带JWT headers 实现身份认证
- 服务的中还需创建受保护的路由守卫,只有携带JWT才能访问路由
- 创建auth 模块
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { jwtConstants } from './constants';
@Module({
imports: [
UsersModule,
JwtModule.register({
global: true,
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}
Controller 中添加登录方法
import { Body, Controller, Post, HttpCode, HttpStatus } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@HttpCode(HttpStatus.OK)
@Post('login')
signIn(@Body() signInDto: Record<string, any>) {
return this.authService.signIn(signInDto.username, signInDto.password);
}
}
- auth service 检查用户名密码密码是否正确
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService
) {}
async signIn(username, pass) {
const user = await this.usersService.findOne(username);
if (user?.password !== pass) {
throw new UnauthorizedException();
}
const payload = { sub: user.userId, username: user.username };
return {
access_token: await this.jwtService.signAsync(payload),
};
}
}
- 创建登录守卫
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { Request } from 'express';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(
token,
{
secret: jwtConstants.secret
}
);
// 💡 We're assigning the payload to the request object here
// so that we can access it in our route handlers
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
Controller 中使用守卫
@UseGuards(AuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
RBAC角色访问控制
RBAC是基于角色的权限访问控制
首先创建Role枚举系统的角色:
export enum Role {
User = 'user',
Admin = 'admin',
}
@Roles装饰器
然后创建一个装饰器@Roles,表示哪些角色可以访问相应的资源
import { SetMetadata } from '@nestjs/common';
import { Role } from '../enums/role.enum';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
SetMetadata 是 Nest.js 内置的一个装饰器方法,它用于为路由方法添加元数据,它接收两个参数,分别是描述元数据的 key 和元数据值。
@Roles装饰器通过SetMetadata给类添加了角色信息的数据。之后在守卫中取数据时需要用Reflector检索和解析元数据。
Reflector.getMetadata(metadataKey: string, target: object):获取指定目标上的元数据。
创建后,就可以在Controller中使用:
@Post()
@Roles(Role.Admin)
create(@Body() createUserDto: CreateUserDto) {
this.usersService.create(createUserDto);
}
创建角色守卫
最好,创建守卫来拦截路由,用户角色和路由需要的角色匹配时才让请求通过。
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
在守卫canActivate方法中比较角色,通过则返回true,处理用户请求;否则返回false,忽略用户请求。
this.reflector.get 可以通过key 和 类上下文获取SetMetadata存储在类中的数据,getAllAndOverride 可以覆盖多层守卫中的默认值,比如下面的用例:
@Roles(['user'])
@Controller('users')
export class UsersController {
@Post()
@Roles(['admin'])
async create(@Body() createUserDto: CreateUserDto) {
this.usersService.create(createUserDto);
}
}
注册守卫
守卫分为全局守卫、控制器守卫、方法守卫,执行顺序:全局守卫>控制器守卫>方法守卫.
这里将 RolesGuard 注册为全局守卫,根据@Role装饰器提供的角色信息对所有请求进行角色权限校验.
import { APP_GUARD } from '@nestjs/core'
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
接口频率限制
接口频率限制可以用 @nestjs/throttler
pnpm add @nestjs/throttler
@Module({
imports: [
ThrottlerModule.forRoot([{
ttl: 60000, // 超时时间
limit: 10, // ttl内的最大请求数
}]),
],
providers:[
{
provide: APP_GUARD,
useClass: ThrottlerGuard // 全局注册
}
]
})
export class AppModule {}
热更新
- 安装依赖
pnpm add -D webpack-node-externals run-script-webpack-plugin webpack
- 根目录下新建配置文件webpack-hmr.config.js
const nodeExternals = require('webpack-node-externals');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');
module.exports = function (options, webpack) {
return {
...options,
entry: ['webpack/hot/poll?100', options.entry],
externals: [
nodeExternals({
allowlist: ['webpack/hot/poll?100'],
}),
],
plugins: [
...options.plugins,
new webpack.HotModuleReplacementPlugin(),
new webpack.WatchIgnorePlugin({
paths: [/.js$/, /.d.ts$/],
}),
new RunScriptWebpackPlugin({ name: options.output.filename, autoRestart: false }),
],
};
};
- main.ts启用热更新
declare const module: any;
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => app.close());
}
}
bootstrap();
- packge.json启动命令:
"start:dev": "nest build --webpack --webpackPath webpack-hmr.config.js --watch"
其它功能
设置API前缀
app.setGlobalPrefix('/api')
开启cors
app.enableCors()
swagger 接口文档
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
function enableSwagger(app){
const config = new DocumentBuilder()
.setTitle('Cats example')
.setDescription('The cats API description')
.setVersion('1.0')
.addTag('cats')
.build();
const document = SwaggerModule.createDocument(app, config);
// 第一个参数是swagger文档路径
SwaggerModule.setup('docs', app, document);
}
enableSwagger(app)
swc 编译
pnpm add -D @swc/cli @swc/core
nest-cli.json添加选项:
{
"compilerOptions": {
"builder": "swc"
}
}
注册bodyParser
nestjs支持解析json 和 urlencoded,useBodyParser可以解析其他格式数据:
app.useBodyParser('text');