本章的目的
学习使用以下工具
- 学会配置swagger
- 读取配置文件
- redis的使用
- Filter, guard,interceptor的使用(后面章节有详细介绍)
新建项目
nest new project-name
我选择ppm
安装并配置 Swagger
pnpm install --save @nestjs/swagger
修改 main.ts(初始化 Swagger 文档)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('backend')
.setDescription('接口文档')
.setVersion('1.0')
.build();
const documentFactory = () => SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, documentFactory);
await app.listen(process.env.PORT ?? 1110);
}
bootstrap();
配置环境变量与全局配置
包安装
pnpm i --save @nestjs/config
1.修改 app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { ApiConfigService } from 'config/ApiConfigService';
// 根据环境确定配置文件路径
const envFilePath = process.env.NODE_ENV
? `.env.${process.env.NODE_ENV}`
: '.env';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath,
isGlobal: true,
load: [],
}),
],
controllers: [AppController],
providers: [AppService, ApiConfigService],
exports: [],
})
export class AppModule {}
2.创建环境变量文件
新建 .env.development:
NODE_ENV = development
PORT = 1110
SWAGGER_ENABLED = false
SWAGGER_TITLE = Swagger
SWAGGER_DESCRIPTION = Swagger
SWAGGER_PREFIX = /api
SWAGGER_VERSION = 1.0
3.配置启动脚本(修改 package.json)
{
"scripts": {
"start:dev": "NODE_ENV=development nest start --watch",
"start:test": "NODE_ENV=test nest start --watch"
}
}
4.创建配置服务 config/index.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
export interface ApiConfig {
port: number;
swagger: {
enabled: boolean;
title: string;
description: string;
version: string;
prefix: string;
};
}
@Injectable()
export class ApiConfigService {
constructor(private configService: ConfigService) {}
get port(): number {
return this.configService.get<number>('port', 1110);
}
get swagger(): ApiConfig['swagger'] {
return {
enabled: this.getBoolean('SWAGGER_ENABLED', false),
title: this.configService.get<string>('SWAGGER_TITLE', ''),
prefix: this.configService.get<string>('SWAGGER_PREFIX', ''),
description: this.configService.get<string>('SWAGGER_DESCRIPTION', ''),
version: this.configService.get<string>('SWAGGER_VERSION', ''),
};
}
private getBoolean(key: string, defaultValue: boolean): boolean {
const value = this.configService.get<string>(key, String(defaultValue));
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'string') {
return value.toLowerCase() === 'true' || value === '1';
}
return Boolean(value);
}
}
5.修改app.module.ts
providers: [AppService, ApiConfigService],
6.优化 main.ts(通过配置服务动态初始化)
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const { swagger, port } = app.get(ApiConfigService);
if (swagger.enabled) {
const config = new DocumentBuilder()
.setTitle(swagger.title)
.setDescription(swagger.description)
.setVersion(swagger.version)
.build();
const documentFactory = () => SwaggerModule.createDocument(app, config);
SwaggerModule.setup(swagger.prefix, app, documentFactory);
}
await app.listen(port);
}
集成redis
安装依赖
pnpm i ioredis --save-dev
1.创建 Redis 模块(redis.module.ts)
import { Module } from '@nestjs/common';
import { ApiConfigService } from '@/config/config.service';
import { REDIS_DB } from '@/constants';
import Redis from 'ioredis';
@Module({
providers: [
ApiConfigService,
{
provide: REDIS_DB,
inject: [ApiConfigService],
useFactory: (config: ApiConfigService) => {
const redis = config.redis;
return new Redis({
port: redis.port,
host: redis.host,
password: redis.password,
db: redis.db,
});
},
},
],
exports: [REDIS_DB],
})
export class RedisModule {}
2.注册 Redis 模块(修改 app.module.ts)
imports: [
// ...其他模块
RedisModule, // 添加 Redis 模块
]
3.创建常量定义(constants/index.ts)
export const REDIS_DB = Symbol('REDIS_DB');
4.配置路径别名(修改 tsconfig.json)
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"] // 支持 @/ 路径别名
}
}
}
5. 添加 Redis 环境变量(.env)
.env
REDIS_HOST =
REDIS_PORT =
REDIS_PASSWORD =
REDIS_DB =
扩展配置服务(ApiConfigService 添加 Redis 配置)
// 扩展 ApiConfig 接口
export interface ApiConfig {
// ...其他配置
redis: {
host: string;
port: number;
password: string;
db: number;
};
}
// 添加 Redis 配置读取方法
@Injectable()
export class ApiConfigService {
// ...其他方法
get redis(): ApiConfig['redis'] {
return {
host: this.configService.get<string>('REDIS_HOST', ''),
port: this.configService.get<number>('REDIS_PORT', 0),
password: this.configService.get<string>('REDIS_PASSWORD', ''),
db: this.configService.get<number>('REDIS_DB', 0),
};
}
}
6.使用事例(app.service 添加)
import { HttpException, Inject, Injectable } from '@nestjs/common';
import { REDIS_DB } from './constants';
import Redis from 'ioredis';
@Injectable()
export class AppService {
constructor(@Inject(REDIS_DB) private readonly redis: Redis) {}
async getHello(): Promise<any> {
await this.redis.set('mykey', 'hello world');
return {
redisKey: await this.redis.get('mykey'),
};
}
}
添加全局拦截
1.http的异常 filter
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(HttpExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Internal server error';
let code = 500;
let errors: any = undefined;
if (exception instanceof HttpException) {
status = exception.getStatus();
const res = exception.getResponse();
if (typeof res === 'string') {
message = res;
} else if (typeof res === 'object' && res !== null) {
// Nest's HttpException often returns object with message and error
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
message = res.message || res.error || message;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
errors = res.errors || res;
}
code = status;
} else if (exception instanceof Error) {
message = exception.message;
}
const responseBody = {
success: false,
code,
message,
path: request.url,
timestamp: new Date().toISOString(),
errors,
};
this.logger.error(
`${request.method} ${request.url} -> ${message}`,
(exception as any)?.stack,
);
response.status(status).json(responseBody);
}
}
2.jwt 令牌 Guard
pnpm i nestjs-cls jsonwebtoken @nestjs/passport
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
Logger,
} from '@nestjs/common';
import { Request } from 'express';
import * as jwt from 'jsonwebtoken';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from './public.decorator';
import { ClsService } from 'nestjs-cls';
@Injectable()
export class JwtAuthGuard implements CanActivate {
private readonly logger = new Logger(JwtAuthGuard.name);
constructor(
private readonly reflector: Reflector,
private readonly clsService: ClsService,
) {}
canActivate(context: ExecutionContext): boolean {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
const req = context.switchToHttp().getRequest<Request>();
const auth = req.headers['authorization'];
if (!auth) {
throw new UnauthorizedException('未提供认证信息');
}
const parts = auth.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer')
throw new UnauthorizedException('无效的认证格式');
const token = parts[1];
try {
const secret = process.env.JWT_SECRET || 'jwt-secret';
const payload = jwt.verify(token, secret);
(req as any).user = payload;
this.clsService.set('userCode', payload.userCode ?? payload?.id);
this.clsService.set('user', payload);
return true;
} catch (err) {
throw new UnauthorizedException('无效或过期的 token');
}
}
}
3.reponse拦截器 interceptors
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface ResponseFormat<T = any> {
success: boolean;
code: number;
message: string;
data?: T;
}
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
if (
data &&
typeof data === 'object' &&
'success' in data &&
'code' in data &&
'message' in data
) {
return data;
}
return {
success: true,
code: 200,
message: 'OK',
data,
} as ResponseFormat<any>;
}),
);
}
}
4.main.ts修改
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new ResponseInterceptor());
5.app.module.ts 修改
{
import:[
//... 其他配置
ClsModule.forRoot({
global: true,
middleware: {
mount: true,
},
}),
],
providers: [
AppService,
ApiConfigService,
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
]
}
本章结束