Nest.js
IoC, DI
- IoC: Inversion of Control 控制反转
- DI: Dependency Injection 依赖注入
创建 nest 项目
bash
# vue
pnpm create vue@latest
# vite
pnpm create vite@latest
# nest
pnpm add -g @nestjs/cli
nest new nest-demo
nest 命令行
nest --help
bash
# [create] src/user/user.module.ts
# [update] src/app.module.ts
nest generate module user
nest g mo user
bash
# [create] src/user/user.controller.ts
# [update] src/user/user.module.ts
nest generate controller user
nest g co user
bash
# [create] src/user/user.service.ts
# [update] src/user/user.module.ts
nest generate service user
nest g s user
bash
# [create] src/user/user.module.ts
# [create] src/user/user.controller.ts
# [create] src/user/user.service.ts
# [create] src/user/dto/create-user.dto.ts
# [create] src/user/dto/update-user.dto.ts
# [create] src/user/entities/user.entity.ts
# [update] src/app.module.ts
rm -rf src/user
nest generate resource user
nest g res user
app 模块
ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { VersioningType } from "@nestjs/common";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableVersioning({
type: VersioningType.URI,
}); // 开启 url 版本
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
ts
import { Controller, Get } from "@nestjs/common";
import { AppService } from "./app.service";
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
ts
import { Injectable } from "@nestjs/common";
@Injectable()
export class AppService {
getHello(): string {
return "Hello World!";
}
}
user 子模块
loader, action: 参考 react-router 路由操作
ts
import { Module } from "@nestjs/common";
import { UserService } from "./user.service";
import { UserController } from "./user.controller";
@Module({
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
ts
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Version,
Req,
Query,
Headers,
HttpCode,
} from "@nestjs/common";
import { UserService } from "./user.service";
import { CreateUserDto } from "./dto/create-user.dto";
import { UpdateUserDto } from "./dto/update-user.dto";
import { Request as ExpressRequest } from "express";
// @Controller('user')
@Controller({
path: "user",
version: "1", // v1
})
export class UserController {
constructor(private readonly userService: UserService) {}
// POST 请求, 请求体参数
// curl -X POST -d "k1=v1&k2=v2" http://localhost:3000/v1/user
@Post()
create(
@Req() req: ExpressRequest,
@Body() body: CreateUserDto,
@Body("k1") k1: string,
@Body("k2") k2: string,
) {
console.log("[create] req.body:", req.body); // { k1: 'v1', k2: 'v2' }
console.log("[create] body:", body); // { k1: 'v1', k2: 'v2' }
console.log("[create] k1:", k1); // v1
console.log("[create] k2:", k2); // v2
return this.userService.create(body);
}
// GET 请求, 查询参数
// curl http://localhost:3000/v1/user?k3=v3&k4=v4
@Get()
findAll(
@Req() /** @Request() */ req: ExpressRequest,
@Query() query: unknown,
@Query("k3") k3: string,
@Query("k4") k4: string,
) {
console.log("[findAll] req.query:", req.query); // { k3: 'v3', k4: 'v4' }
console.log("[findAll] query:", query); // { k3: 'v3', k4: 'v4' }
console.log("[findAll] k3:", k3); // v3
console.log("[findAll] k4:", k4); // v4
return this.userService.findAll();
}
// GET 请求, url 路径参数
// curl http://localhost:3000/v2/user/3/whoami
@Get(":id/:name")
@Version("2") // v2
findOne(
@Req() req: ExpressRequest,
@Param() params: unknown,
@Param("id") id: string,
@Param("name") name: string,
) {
console.log("[findOne] req.params:", req.params); // { id: '3', name: 'whoami' }
console.log("[findOne] params:", params); // { id: '3', name: 'whoami' }
console.log("[findOne] id:", id); // 3
console.log("[findOne] name:", name); // whoami
return this.userService.findOne(Number.parseInt(id, 10));
}
// curl -X PATCH -d "k5=v5&k6=v6" http://localhost:3000/v1/user/3
@Patch(":id")
@HttpCode(200) // 返回 http 状态码 200
update(
@Param("id") id: string,
@Body() updateUserDto: UpdateUserDto,
@Headers() headers: unknown,
) {
console.log("[update] id:", id); // 3
console.log("[update] updateUserDto:", updateUserDto); // { k5: 'v5', k6: 'v6' }
console.log("[update] headers:", headers);
return this.userService.update(+id, updateUserDto);
}
@Delete(":id")
remove(@Param("id") id: string) {
return this.userService.remove(+id);
}
}
ts
import { Injectable } from "@nestjs/common";
import { CreateUserDto } from "./dto/create-user.dto";
import { UpdateUserDto } from "./dto/update-user.dto";
@Injectable()
export class UserService {
create(createUserDto: CreateUserDto) {
return "This action adds a new user";
}
findAll() {
return "This loader returns all user";
}
findOne(id: number) {
return `This loader returns a #${id} user`;
}
update(id: number, updateUserDto: UpdateUserDto) {
return `This action updates a #${id} user`;
}
remove(id: number) {
return `This action removes a #${id} user`;
}
}
ts
// @/user/entities/user.entity.ts
export class User {}
// @/user/dto/create-user.dto.ts
export class CreateUserDto {}
// @/user/dto/update-user.dto.ts
import { PartialType } from "@nestjs/mapped-types";
import { CreateUserDto } from "./create-user.dto";
export class UpdateUserDto extends PartialType(CreateUserDto) {}
会话管理
bash
pnpm add express-session
pnpm add @types/express-session -D
# 验证码
pnpm add svg-captcha
inject/provide
AppService --- inject --> AppModule (IoC container) --- provide --> AppController
自定义注入名、自定义注入值
ts
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { UserModule } from "./user/user.module";
@Module({
imports: [UserModule],
controllers: [AppController],
// providers: [AppService],
providers: [
// 自定义注入名 MyAppService
{
provide: "MyAppService",
useClass: AppService,
},
// 自定义注入值 kun = ['sing', 'dance', 'rap', 'basketball']
{
provide: "kun",
useValue: ["sing", "dance", "rap", "basketball"],
},
{
provide: "DecoratedAppService",
// inject: [AppService],
inject: ["MyAppService"],
// 支持异步
async useFactory(appService: AppService) {
return new Promise((resolve) => {
console.log("[Debug] typeof appService", typeof appService);
appService.getHello = function () {
return "I love you";
};
resolve(appService);
});
},
},
],
})
export class AppModule {}
ts
import { Controller, Get, Inject } from "@nestjs/common";
import { AppService } from "./app.service";
@Controller()
export class AppController {
// constructor(private readonly appService: AppService) {}
constructor(
@Inject("MyAppService") private readonly appService: AppService,
@Inject("kun") private readonly kun: string[],
@Inject("DecoratedAppService")
private readonly decoratedAppService: AppService,
) {}
// curl http://localhost:3000
@Get()
getHello(): string {
console.log("[getHello] Injected kun:", this.kun);
console.log(
"[getHello] Injected decoratedAppService.getHello():",
this.decoratedAppService.getHello(),
);
return this.appService.getHello();
}
}
模块
共享模块 exports
- 使用 exports 导出, 父模块中可用
- 类似 Vue 的 defineExpose, React 的 useImperativeHandle
bash
nest generate resource common
nest g res common
ts
import { Module } from "@nestjs/common";
import { CommonService } from "./common.service";
import { CommonController } from "./common.controller";
@Module({
controllers: [CommonController],
providers: [CommonService],
exports: [CommonService],
})
export class CommonModule {}
ts
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { UserModule } from "./user/user.module";
import { CommonModule } from "./common/common.module";
@Module({
// UserModule export
imports: [UserModule, CommonModule],
controllers: [AppController],
// AppModule provide
providers: [
{
provide: "MyAppService",
useClass: AppService,
},
],
})
export class AppModule {}
ts
import { Controller, Get, Inject } from "@nestjs/common";
import { AppService } from "./app.service";
import { CommonService } from "./common/common.service";
@Controller()
export class AppController {
constructor(
// AppModule provide
@Inject("MyAppService") private readonly appService: AppService,
// UserModule export
private readonly commonService: CommonService,
) {}
// curl http://localhost:3000
@Get()
getHello(): string {
console.log(this.commonService.findAll());
return this.appService.getHello();
}
}
全局模块 @Global() + exports
使用 @Global() + exports 导出, 所有模块中 (全局) 可用
ts
import { Global, Module } from "@nestjs/common";
import { CommonService } from "./common.service";
import { CommonController } from "./common.controller";
const config = {
provide: "commonConfig",
useValue: { port: 3000 },
};
@Global()
@Module({
controllers: [CommonController],
providers: [CommonService, config],
exports: [CommonService, config],
})
export class CommonModule {}
ts
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { UserModule } from "./user/user.module";
import { CommonModule } from "./common/common.module";
@Module({
imports: [UserModule, CommonModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
ts
import { Module } from "@nestjs/common";
import { UserService } from "./user.service";
import { UserController } from "./user.controller";
@Module({
controllers: [UserController],
// providers: [UserService],
providers: [
{
provide: "MyUserService",
useClass: UserService,
},
],
})
export class UserModule {}
ts
import { Controller, Param, Delete, Inject } from "@nestjs/common";
import { UserService } from "./user.service";
@Controller({
path: "user",
version: "1", // v1
})
export class UserController {
constructor(
// UserModule provide
@Inject("MyUserService") private readonly userService: UserService,
// Global CommonModule export
@Inject("commonConfig") private readonly config: unknown,
) {}
// curl -X DELETE http://localhost:3000/v1/user/3
@Delete(":id")
remove(@Param("id") id: string) {
console.log("[remove] this.config:", this.config); // { port: 3000 }
return this.userService.remove(+id);
}
}
动态模块 (静态方法)
ts
import { DynamicModule, Global, Module } from "@nestjs/common";
import { CommonService } from "./common.service";
import { CommonController } from "./common.controller";
const defaultConfig = {
provide: "commonConfig",
useValue: { port: 3000 },
};
@Global()
@Module({
controllers: [CommonController],
providers: [CommonService, defaultConfig],
exports: [CommonService, defaultConfig],
})
export class CommonModule {
static decorate(configValue: Record<string, unknown>): DynamicModule {
const dynamicConfig = {
provide: "commonConfig",
useValue: configValue,
};
return {
module: CommonModule,
providers: [CommonService, dynamicConfig],
exports: [CommonService, dynamicConfig],
};
}
}
ts
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { UserModule } from "./user/user.module";
import { CommonModule } from "./common/common.module";
@Module({
imports: [UserModule, CommonModule.decorate({ port: 3001 })],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
ts
import { Module } from "@nestjs/common";
import { UserService } from "./user.service";
import { UserController } from "./user.controller";
@Module({
controllers: [UserController],
// providers: [UserService],
providers: [
{
provide: "MyUserService",
useClass: UserService,
},
],
})
export class UserModule {}
ts
import { Controller, Param, Delete, Inject } from "@nestjs/common";
import { UserService } from "./user.service";
@Controller({
path: "user",
version: "1", // v1
})
export class UserController {
constructor(
// UserModule provide
@Inject("MyUserService") private readonly userService: UserService,
// Global CommonModule.decorate export
@Inject("commonConfig") private readonly config: unknown,
) {}
// curl -X DELETE http://localhost:3000/v1/user/3
@Delete(":id")
remove(@Param("id") id: string) {
console.log("[remove] this.config:", this.config); // { port: 3001 }
return this.userService.remove(+id);
}
}
中间件
bash
# [create] src/logger/logger.middleware.ts
nest generate middleware logger
nest g mi logger
依赖注入中间件
ts
import { Injectable, NestMiddleware } from "@nestjs/common";
import {
Request as ExpressRequest,
Response as ExpressResponse,
NextFunction,
} from "express";
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: ExpressRequest, res: ExpressResponse, next: NextFunction) {
console.log("[logger] Object.keys(req):", Object.keys(req));
console.log("[logger] Object.keys(res):", Object.keys(res));
next();
// res.send("Intercepted by logger")
}
}
ts
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
import { UserService } from "./user.service";
import { UserController } from "./user.controller";
import { LoggerMiddleware } from "src/logger/logger.middleware";
@Module({
controllers: [UserController],
// providers: [UserService],
providers: [
{
provide: "MyUserService",
useClass: UserService,
},
],
})
export class UserModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// 作用于 UserController 中的所有请求
// consumer.apply(LoggerMiddleware).forRoutes(UserController);
// 只作用于 UserController 中, 路由前缀 /v1/user 的请求
// consumer.apply(LoggerMiddleware).forRoutes('/v1/user');
// 只作用于 UserController 中, 路由前缀 /v1/user 的 GET 请求
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: "/v1/user", method: RequestMethod.GET });
}
}
全局中间件、允许跨域
全局中间件即 express 函数中间件
ts
// @/main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { VersioningType } from "@nestjs/common";
import * as expressSession from "express-session";
import { Handler as ExpressHandler } from "express";
import { NestExpressApplication } from "@nestjs/platform-express";
const globalMiddleware: ExpressHandler = (req, res, next) => {
console.log("[globalMiddleware] req.originalUrl:", req.originalUrl);
next();
};
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(
AppModule,
{ cors: true }, // 开启跨域
);
// 开启 url 版本
app.enableVersioning({
type: VersioningType.URI,
});
app.use(
expressSession({
secret: "528",
rolling: true,
name: "cookieKey",
cookie: { httpOnly: true, maxAge: 1000 * 60 * 60 * 60 * 24 * 7 },
}),
);
// 使用全局中间件
app.use(globalMiddleware);
// 开启跨域
app.enableCors();
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
静态资源目录, 文件上传、(流式) 下载
bash
pnpm add multer
pnpm add @types/multer -D
pnpm add compressing
nest generate resource upload
nest g res upload
ts
import { Module } from "@nestjs/common";
import { UploadService } from "./upload.service";
import { UploadController } from "./upload.controller";
import { MulterModule } from "@nestjs/platform-express";
import { diskStorage } from "multer";
import { basename, extname, join } from "node:path";
import { randomBytes } from "node:crypto";
// const __filename = fileURLToPath(import.meta.url);
// const __dirname = dirname(__filename);
@Module({
imports: [
MulterModule.register({
storage: diskStorage({
destination: join(__dirname, "../static"),
filename: (req, file, callback) => {
console.log("[multer] file.fieldname:", file.fieldname); // fileEntity
const fnameWithExt = file.originalname;
console.log("[multer] file.originalname:", fnameWithExt); // example.jpg
const extWithDot = extname(fnameWithExt);
console.log("[multer] extWithDot:", extWithDot); // .jpg
const fnameNoExt = basename(fnameWithExt, extWithDot);
console.log("[multer] fnameNoExt:", fnameNoExt); // example
const hash = randomBytes(4).toString("hex").slice(0, 8);
const renamedFilenameNoExt = `${fnameNoExt}.${hash}${extWithDot}`;
return callback(null, renamedFilenameNoExt);
},
}),
}),
],
controllers: [UploadController],
providers: [UploadService],
})
export class UploadModule {}
ts
import {
Controller,
Post,
UseInterceptors,
UploadedFile,
Get,
Param,
Res,
} from "@nestjs/common";
import { UploadService } from "./upload.service";
import { FileInterceptor } from "@nestjs/platform-express";
import { join } from "node:path";
import { Response as ExpressResponse } from "express";
import { createReadStream } from "node:fs";
@Controller("upload")
export class UploadController {
constructor(private readonly uploadService: UploadService) {}
// 上传
@Post()
@UseInterceptors(
FileInterceptor("fileEntity" /** fieldName */), // 上传单个文件
// FilesInterceptor('multiFileEntities' /** fieldName */), // 上传多个文件
)
uploadSingleFile(@UploadedFile() file: Express.Multer.File) {
console.log("[uploadSingleFile] file.fieldname:", file.fieldname); // fileEntity
console.log("[uploadSingleFile] file.originalname:", file.originalname); // example.jpg
console.log("[uploadSingleFile] file.mimetype:", file.mimetype); // image/jpeg
console.log("[uploadSingleFile] file.destination:", file.destination); // /path/to/dist/static
console.log("[uploadSingleFile] file.filename:", file.filename); // example.[hash8].jpg
console.log("[uploadSingleFile] file.path:", file.path); // /path/to/dist/static/example.[hash8].jpg
console.log("[uploadSingleFile] file.size:", file.size); // ? (bytes)
return "200 OK";
}
// 下载
// http://localhost:3000/upload/example.[hash8].jpg
@Get(":fnameWithExt")
download(
@Param("fnameWithExt") fnameWithExt: string,
@Res() res: ExpressResponse,
) {
console.log("[download] fnameWithExt:", fnameWithExt);
const assetUrl = join(__dirname, "../static", fnameWithExt);
res.download(assetUrl);
}
// 流式下载
// http://localhost:3000/upload/stream/example.[hash8].jpg
@Get("stream/:fnameWithExt")
downloadStream(
@Param("fnameWithExt") fnameWithExt: string,
@Res() res: ExpressResponse,
) {
console.log("[downloadStream] fnameWithExt:", fnameWithExt);
const assetUrl = join(__dirname, "../static", fnameWithExt);
res.setHeader("Content-Type", "application/octet-stream");
const fileStream = createReadStream(assetUrl);
fileStream.pipe(res);
}
}
ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { VersioningType } from "@nestjs/common";
import * as expressSession from "express-session";
import { Handler as ExpressHandler } from "express";
import { NestExpressApplication } from "@nestjs/platform-express";
import { join } from "path";
const globalMiddleware: ExpressHandler = (req, res, next) => {
console.log("[globalMiddleware] req.originalUrl:", req.originalUrl);
next();
};
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(
AppModule,
{ cors: true }, // 开启跨域
);
// 开启 url 版本
app.enableVersioning({
type: VersioningType.URI,
});
app.use(
expressSession({
secret: "528",
rolling: true,
name: "cookieKey",
cookie: { httpOnly: true, maxAge: 1000 * 60 * 60 * 60 * 24 * 7 },
}),
);
// 使用全局中间件
app.use(globalMiddleware);
// 开启跨域
app.enableCors();
// 使用静态资源目录
// http://localhost:3000/resources/example.[hash8].jpg
app.useStaticAssets(join(__dirname, "static"), {
prefix: "/resources", // 必须带 /
});
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
rxjs
案例
ts
import { filter, interval, map, Observable, take, of } from "rxjs";
describe("rxjs", () => {
// pnpm test src/rxjs.spec.ts -t test1
it("test1", (done) => {
const results: number[] = [];
const observable = new Observable((subscribe) => {
subscribe.next(1);
subscribe.next(2);
subscribe.next(3);
setTimeout(() => {
subscribe.next(4);
subscribe.complete();
}, 3000);
});
observable.subscribe({
next: (val: number) => results.push(val),
complete: () => {
expect(results).toEqual([1, 2, 3, 4]);
done();
},
});
});
// pnpm test src/rxjs.spec.ts -t test2
it("test2", (done) => {
const results: number[] = [];
interval(500)
.pipe(take(5))
.subscribe({
next: (val) => results.push(val),
complete: () => {
expect(results).toEqual([0, 1, 2, 3, 4]);
done();
},
});
});
// pnpm test src/rxjs.spec.ts -t test3
it("test3", (done) => {
const results: { score: number }[] = [];
const observable = interval(500)
.pipe(
map((item) => ({ score: item })),
filter((item) => item.score % 2 == 0),
)
.subscribe({
next: (val) => {
results.push(val);
if (val.score === 4) {
observable.unsubscribe();
expect(results).toEqual([{ score: 0 }, { score: 2 }, { score: 4 }]);
done();
}
},
});
});
// pnpm test src/rxjs.spec.ts -t test4
it("test4", (done) => {
const results: { score: number }[] = [];
of(0, 1, 2, 3, 4)
.pipe(
map((item) => ({ score: item })),
filter((item) => item.score % 2 == 1),
)
.subscribe({
next: (val) => {
results.push(val);
},
complete: () => {
expect(results).toEqual([{ score: 1 }, { score: 3 }]);
done();
},
});
});
});
interceptor 拦截器, filter 过滤器
bash
# [create] src/resp/resp.interceptor.ts
nest generate interceptor resp
nest g itc resp
# [create] src/err/err.filter.ts
nest generate filter err
nest g f err
ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from "@nestjs/common";
import { map, Observable } from "rxjs";
interface IRes<T> {
data: T;
code: number;
message: string;
}
@Injectable()
export class RespInterceptor<T> implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<IRes<T>> {
console.log(
"[respInterceptor] Object.keys(context):",
Object.keys(context),
);
return next.handle().pipe(
map((item: T) => ({
data: item,
code: 200,
message: "OK",
})),
);
}
}
ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from "@nestjs/common";
import {
Request as ExpressRequest,
Response as ExpressResponse,
} from "express";
@Catch()
export class ErrFilter<T extends HttpException> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const req = ctx.getRequest<ExpressRequest>();
const resp = ctx.getResponse<ExpressResponse>();
const statusCode = exception.getStatus();
resp.status(statusCode).json({
timestamp: Date.now(),
cause: exception.cause,
statusCode,
reqUrl: req.url,
errMsg: exception.message,
errName: exception.name,
errStack: exception.stack,
});
}
}
ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { VersioningType } from "@nestjs/common";
import * as expressSession from "express-session";
import { Handler as ExpressHandler } from "express";
import { NestExpressApplication } from "@nestjs/platform-express";
import { join } from "path";
import { RespInterceptor } from "./resp/resp.interceptor";
import { ErrFilter } from "./err/err.filter";
const globalMiddleware: ExpressHandler = (req, res, next) => {
console.log("[globalMiddleware] req.originalUrl:", req.originalUrl);
next();
};
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(
AppModule,
{ cors: true }, // 开启跨域
);
// 开启 url 版本
app.enableVersioning({
type: VersioningType.URI,
});
app.use(
expressSession({
secret: "528",
rolling: true,
name: "cookieKey",
cookie: { httpOnly: true, maxAge: 1000 * 60 * 60 * 60 * 24 * 7 },
}),
);
// 使用全局中间件
app.use(globalMiddleware);
// 开启跨域
app.enableCors();
// 使用静态资源目录
// http://localhost:3000/resources/example.[hash8].jpg
app.useStaticAssets(join(__dirname, "static"), {
prefix: "/resources", // 必须带 /
});
// 使用全局响应拦截器
app.useGlobalInterceptors(new RespInterceptor());
// 使用全局异常过滤器
app.useGlobalFilters(new ErrFilter());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
管道
内置管道
- ValidationPipe 字段校验管道
- ParseBoolPipe 解析布尔值的管道
- ParseIntPipe 解析整数的管道
- ParseFloatPipe 解析浮点数的管道
- ParseArrayPipe 解析数组的管道
- ParseUUIDPipe 解析 UUID 的管道
- ParseEnumPipe 解析枚举的管道
- DefaultValuePipe 默认值管道
ts
import {
Controller,
Get,
Body,
Patch,
Param,
Delete,
ParseIntPipe,
ParseUUIDPipe,
} from "@nestjs/common";
import { CommonService } from "./common.service";
import { UpdateCommonDto } from "./dto/update-common.dto";
@Controller("common")
export class CommonController {
constructor(private readonly commonService: CommonService) {}
// curl http://localhost:3000/common/1
@Get(":id")
findOne(@Param("id") id: string) {
console.log("[findOne] typeof id", typeof id); // string
return this.commonService.findOne(Number.parseInt(id, 10));
}
// curl -X PATCH http://localhost:3000/common/1
@Patch(":id")
update(
@Param("id", ParseIntPipe) id: number,
@Body() updateCommonDto: UpdateCommonDto,
) {
console.log("[update] typeof id:", typeof id); // number
return this.commonService.update(id, updateCommonDto);
}
// curl -X DELETE http://localhost:3000/common/[uuid]
@Delete(":uuid")
remove(@Param("uuid", ParseUUIDPipe) uuid: string) {
console.log("[remove] typeof uuid:", typeof uuid); // string
return this.commonService.remove(uuid);
}
}
案例: 全局字段校验管道
bash
nest generate resource login
nest g res login
# [create] src/login/login.pipe.ts
nest generate pipe login
nest g pi login
pnpm add class-validator class-transformer
ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
} from "@nestjs/common";
import {
Request as ExpressRequest,
Response as ExpressResponse,
} from "express";
interface DetailedErrMsg {
message: string[];
}
@Catch()
export class ErrFilter<T extends HttpException> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const req = ctx.getRequest<ExpressRequest>();
const resp = ctx.getResponse<ExpressResponse>();
// const statusCode = exception.getStatus();
const statusCode = exception.getStatus?.() ?? HttpStatus.BAD_REQUEST;
resp.status(statusCode).json({
timestamp: Date.now(),
cause: exception.cause,
statusCode,
reqUrl: req.url,
errMsg: exception.message,
// detailedErrMsg 包含全局字段校验管道的错误消息
detailedErrMsg:
(Reflect.get(exception, "response") as DetailedErrMsg | undefined)
?.message ?? [],
errName: exception.name,
errStack: exception.stack,
});
}
}
ts
import { IsNotEmpty, IsString, Length, IsNumber } from "class-validator";
export class CreateLoginDto {
@IsString()
@IsNotEmpty()
@Length(3, 7, {
message: "name.length >= 3 && name.length <= 7",
})
name: string;
@IsNumber()
age: number;
}
ts
import { Controller, Post, Body } from "@nestjs/common";
import { LoginService } from "./login.service";
import { CreateLoginDto } from "./dto/create-login.dto";
@Controller("login")
export class LoginController {
constructor(private readonly loginService: LoginService) {}
// curl -X POST -H "Content-Type:application/json" -d '{"name":"whoami","age":23}' http://localhost:3000/login
@Post()
create(
@Body() createLoginDto: CreateLoginDto,
@Body("name") name: string,
@Body("age") age: string,
) {
// { name: 'whoami', age: 23 }
console.log("[create] createLoginDto:", createLoginDto);
// whoami
console.log("[create] name:", name);
// 23
console.log("[create] age:", age);
return this.loginService.create(createLoginDto);
}
}
ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ValidationPipe, VersioningType } from "@nestjs/common";
import * as expressSession from "express-session";
import { Handler as ExpressHandler } from "express";
import { NestExpressApplication } from "@nestjs/platform-express";
import { join } from "path";
import { RespInterceptor } from "./resp/resp.interceptor";
import { ErrFilter } from "./err/err.filter";
const globalMiddleware: ExpressHandler = (req, res, next) => {
console.log("[globalMiddleware] req.originalUrl:", req.originalUrl);
next();
};
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(
AppModule,
{ cors: true }, // 开启跨域
);
// 开启 url 版本
app.enableVersioning({
type: VersioningType.URI,
});
app.use(
expressSession({
secret: "528",
rolling: true,
name: "cookieKey",
cookie: { httpOnly: true, maxAge: 1000 * 60 * 60 * 60 * 24 * 7 },
}),
);
// 使用全局中间件
app.use(globalMiddleware);
// 开启跨域
app.enableCors();
// 使用静态资源目录
// http://localhost:3000/resources/example.[hash8].jpg
app.useStaticAssets(join(__dirname, "static"), {
prefix: "/resources", // 必须带 /
});
// 使用全局响应拦截器
app.useGlobalInterceptors(new RespInterceptor());
// 使用全局异常过滤器
app.useGlobalFilters(new ErrFilter());
// 使用全局字段校验管道
app.useGlobalPipes(new ValidationPipe());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
自定义管道
案例: 自定义字段校验管道
ts
import { IsNotEmpty, IsString, Length, IsNumber } from "class-validator";
export class CreateLoginDto {
@IsString()
@IsNotEmpty()
@Length(3, 7, {
message: "name.length >= 3 && name.length <= 7",
})
name: string;
@IsNumber()
age: number;
}
ts
import { Controller, Post, Body } from "@nestjs/common";
import { LoginService } from "./login.service";
import { CreateLoginDto } from "./dto/create-login.dto";
import { LoginPipe } from "./login.pipe";
@Controller("login")
export class LoginController {
constructor(private readonly loginService: LoginService) {}
// curl -X POST -H "Content-Type:application/json" -d '{"name":"whoami","age":23}' http://localhost:3000/login
@Post()
create(
@Body(LoginPipe) createLoginDto: CreateLoginDto,
@Body("name", LoginPipe) name: string,
@Body("age", LoginPipe) age: string,
) {
// { name: 'whoami', age: 23 }
console.log("[create] createLoginDto:", createLoginDto);
// whoami
console.log("[create] name:", name);
// 23
console.log("[create] age:", age);
return this.loginService.create(createLoginDto);
}
}
ts
import {
ArgumentMetadata,
HttpException,
HttpStatus,
Injectable,
PipeTransform,
} from "@nestjs/common";
import { plainToInstance } from "class-transformer";
import { validate, ValidationError } from "class-validator";
import { CreateUserDto } from "src/user/dto/create-user.dto";
type WithProps<T> = T | T[keyof T];
@Injectable()
export class LoginPipe implements PipeTransform {
async transform(
value: WithProps<typeof CreateUserDto>,
metadata: ArgumentMetadata,
) {
/** @Body(LoginPipe) */
// value: { name: 'whoami', age: 23 }
// metadata: { metatype: [class CreateLoginDto], type: 'body', data: undefined }
/** @Body('name', LoginPipe) */
// value: whoami
// metadata: { metatype: [Function: String], type: 'body', data: 'name' }
/** @Body('age', LoginPipe) */
// value: 23
// metadata: { metatype: [Function: String], type: 'body', data: 'age' }
console.log("[loginPipe] value:", value);
console.log("[loginPipe] metadata:", metadata);
if (metadata.metatype) {
const typedValue = plainToInstance(metadata.metatype, value) as WithProps<
typeof CreateUserDto
>;
// typedValue: CreateLoginDto { name: 'whoami', age: 23 }
// typedValue: whoami
// typedValue: 23;
console.log("[loginPipe] typedValue:", typedValue);
const aggregateErrors: ValidationError[] = await validate(typedValue);
console.log("[loginPipe] aggregateErrors:", aggregateErrors);
if (aggregateErrors.length) {
throw new HttpException(aggregateErrors, HttpStatus.BAD_REQUEST);
}
}
return value;
}
}
守卫
bash
# [create] src/login/login.guard.ts
nest generate guard login
nest g gu login
ts
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Observable } from "rxjs";
import { Request as ExpressRequest } from "express";
@Injectable()
export class LoginGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
console.log("[loginGuard] Object.keys(context):", Object.keys(context));
console.log(
"[loginGuard] context.getHandler():",
context.getHandler().toString(),
);
const roles = this.reflector.get<string[] | undefined>(
"roles",
context.getHandler(),
);
// roles: ['admin']
console.log("[loginGuard] roles:", roles);
const ctx = context.switchToHttp();
const req = ctx.getRequest<ExpressRequest>();
const queryRole = req.query.role;
// req.query.role: admin
console.log("[loginGuard] req.query.role:", queryRole);
return (
!roles ||
(Boolean(queryRole) &&
typeof queryRole === "string" &&
roles.includes(queryRole))
);
}
}
ts
import {
Controller,
Post,
Body,
UseGuards,
Get,
SetMetadata,
} from "@nestjs/common";
import { LoginService } from "./login.service";
import { CreateLoginDto } from "./dto/create-login.dto";
import { LoginGuard } from "./login.guard";
@Controller("login")
// 本模块中使用 LoginGuard 守卫
@UseGuards(LoginGuard)
export class LoginController {
constructor(private readonly loginService: LoginService) {}
// curl -X POST -H "Content-Type:application/json" -d '{"name":"whoami","age":23}' http://localhost:3000/login
@Post()
create(
@Body() createLoginDto: CreateLoginDto,
@Body("name") name: string,
@Body("age") age: string,
) {
// { name: 'whoami', age: 23 }
console.log("[create] createLoginDto:", createLoginDto);
// whoami
console.log("[create] name:", name);
// 23
console.log("[create] age:", age);
return this.loginService.create(createLoginDto);
}
// curl http://localhost:3000/login?role=admin
@Get()
// 守卫元数据
@SetMetadata("roles", ["admin"])
findAll() {
return this.loginService.findAll();
}
}
ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ValidationPipe, VersioningType } from "@nestjs/common";
import * as expressSession from "express-session";
import { Handler as ExpressHandler } from "express";
import { NestExpressApplication } from "@nestjs/platform-express";
import { join } from "path";
import { RespInterceptor } from "./resp/resp.interceptor";
import { ErrFilter } from "./err/err.filter";
import { LoginGuard } from "./login/login.guard";
const globalMiddleware: ExpressHandler = (req, res, next) => {
console.log("[globalMiddleware] req.originalUrl:", req.originalUrl);
next();
};
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(
AppModule,
{ cors: true }, // 开启跨域
);
// 开启 url 版本
app.enableVersioning({
type: VersioningType.URI,
});
app.use(
expressSession({
secret: "528",
rolling: true,
name: "cookieKey",
cookie: { httpOnly: true, maxAge: 1000 * 60 * 60 * 60 * 24 * 7 },
}),
);
// 使用全局中间件
app.use(globalMiddleware);
// 开启跨域
app.enableCors();
// 使用静态资源目录
// http://localhost:3000/resources/example.[hash8].jpg
app.useStaticAssets(join(__dirname, "static"), {
prefix: "/resources", // 必须带 /
});
// 使用全局响应拦截器
app.useGlobalInterceptors(new RespInterceptor());
// 使用全局异常过滤器
app.useGlobalFilters(new ErrFilter());
// 使用全局字段校验管道
app.useGlobalPipes(new ValidationPipe());
// 使用全局守卫
app.useGlobalGuards(new LoginGuard());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
责任链模式
request 请求 -> middleware 中间件 -> guard 守卫 -> interceptor 前置拦截器 -> controller 控制器 -> interceptor 后置拦截器 -> filter (异常) 过滤器 -> response 响应
ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from "@nestjs/common";
import { map, Observable } from "rxjs";
interface IRes<T> {
data: T;
code: number;
message: string;
}
@Injectable()
export class RespInterceptor<T> implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<IRes<T>> {
// 前置拦截器
console.log(
"[respInterceptor] Object.keys(context):",
Object.keys(context),
);
// controller
// 后置拦截器
return next.handle().pipe(
map((item: T) => {
console.log("[respInterceptor] rxjs");
return {
data: item,
code: 200,
message: "OK",
};
}),
);
}
}
自定义装饰器
- 方法装饰器
- 参数装饰器
bash
# [create] /src/custom/custom.decorator.ts
nest generate decorator custom
nest g d custom
ts
import { createParamDecorator, SetMetadata } from "@nestjs/common";
import { Request as ExpressRequest } from "express";
// 方法装饰器
// Usage: @SetRoles('admin', 'user')
export const SetRoles = (...args: string[]) => SetMetadata("roles", args);
// 参数装饰器
// Usage: @ReqUrl('yourData')
export const ReqUrl = createParamDecorator((data, context) => {
console.log("[reqUrl] data:", data); // myData
const ctx = context.switchToHttp();
const req = ctx.getRequest<ExpressRequest>();
return req.url; // /login/1?role=user
});
ts
import {
Controller,
Post,
Body,
UseGuards,
Get,
Param,
SetMetadata,
} from "@nestjs/common";
import { LoginService } from "./login.service";
import { CreateLoginDto } from "./dto/create-login.dto";
import { LoginGuard } from "./login.guard";
import { ReqUrl, SetRoles } from "src/custom/custom.decorator";
@Controller("login")
// 本模块中使用 LoginGuard 守卫
@UseGuards(LoginGuard)
export class LoginController {
constructor(private readonly loginService: LoginService) {}
// curl -X POST -H "Content-Type:application/json" -d '{"name":"whoami","age":23}' http://localhost:3000/login
@Post()
create(
@Body() createLoginDto: CreateLoginDto,
@Body("name") name: string,
@Body("age") age: string,
) {
// { name: 'whoami', age: 23 }
console.log("[create] createLoginDto:", createLoginDto);
// whoami
console.log("[create] name:", name);
// 23
console.log("[create] age:", age);
return this.loginService.create(createLoginDto);
}
// curl http://localhost:3000/login?role=admin
@Get()
@SetMetadata("roles", ["admin"])
findAll() {
return this.loginService.findAll();
}
// curl http://localhost:3000/login/1?role=user
@Get(":id")
@SetRoles("admin", "user")
findOne(@Param("id") id: string, @ReqUrl("myData") reqUrl: string) {
console.log("[findOne] reqUrl:", reqUrl);
return this.loginService.findOne(Number.parseInt(id, 10));
}
}
集成 swagger
pnpm add @nestjs/swagger swagger-ui-express
swagger 类注解
@ApiTags()
@ApiBearerAuth()
swagger 方法注解
@ApiOperation()
@ApiQuery()
查询参数@ApiParam()
url 路径参数@ApiResponse()
@ApiProperty()
ts
import { INestApplication } from "@nestjs/common";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
const swaggerDocumentConfig = new DocumentBuilder()
.setTitle("nest-demo")
.setDescription("nest-demo")
.setVersion("1")
.build();
export function setupSwaggerDocument(app: INestApplication) {
const swaggerDocument = SwaggerModule.createDocument(
app,
swaggerDocumentConfig,
);
SwaggerModule.setup("/api-docs", app, swaggerDocument);
}
typeorm 连接 mysql
bash
mkdir ./sql
pnpm add mysql2 @nestjs/typeorm typeorm
nest generate resource emp # employee
nest g res emp # employee
yml
services:
# docker compose up mysql -d
mysql:
image: "mysql:latest"
volumes:
- ./sql:/docker-entrypoint-initdb.d
ports:
- 3307:3306
environment:
- MYSQL_DATABASE=db0
- MYSQL_USER=whoami
- MYSQL_PASSWORD=pass
- MYSQL_RANDOM_ROOT_PASSWORD="yes"
# docker compose down mysql -v
ts
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { UserModule } from "./user/user.module";
import { CommonModule } from "./common/common.module";
import { UploadModule } from "./upload/upload.module";
import { LoginModule } from "./login/login.module";
import { TypeOrmModule } from "@nestjs/typeorm";
import { EmpModule } from "./emp/emp.module";
@Module({
imports: [
UserModule,
CommonModule.decorate({ port: 3001 }),
UploadModule,
LoginModule,
EmpModule,
TypeOrmModule.forRoot({
type: "mysql",
host: "localhost",
port: 3307,
username: "whoami",
password: "pass",
database: "db0",
retryDelay: 500,
retryAttempts: 3,
// entities: [join(__dirname, './**/*.entity{.js,.ts}')],
autoLoadEntities: true, // 自动加载 entity
synchronize: true, // 自动将 entity 同步到数据库
}),
],
controllers: [AppController],
// providers: [AppService],
providers: [
{
provide: "MyAppService",
useClass: AppService,
},
],
})
export class AppModule {}
ts
import {
Column,
CreateDateColumn,
Entity,
Generated,
Index,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from "typeorm";
@Entity()
export class Emp {
// 自增主键
@PrimaryGeneratedColumn()
id: number;
// 索引
@Generated("uuid")
@Index()
uuid: string;
@Column({ type: "varchar", length: 255, nullable: false })
username: string;
@Column({ select: false })
password: string;
// 0 as female, 1 as male, 2 as unknown
@Column({
type: "enum",
enum: [0, 1, 2],
default: 2,
comment: "0 as female, 1 as male, 2 as unknown",
})
gender: number;
@Column("simple-array")
// 使用 roles.join(',') 持久化到数据库
roles: string[];
@Column("simple-json")
// 使用 JSON.stringify(user) 持久化到数据库
userInfo: { name: string; age: number };
@CreateDateColumn({ type: "timestamp" })
createTime: Date;
@UpdateDateColumn({ type: "timestamp" })
updateTime: Date;
}