涵盖数据库经典设计、JWT身份验证、日志收集、Swagger 接口文档与企业级工程化实践
GitHub 地址 :消息中心项目(🌟欢迎 Star!)
🌟目标读者
- 前端开发者: 希望了解请求接口后的全链路流程,了解后端的主要工作内容以便在日常对接中能更好的沟通(battle)
- NestJS新手: 你可能了解过NestJS的核心概念,这个项目将有助于你的实践
- 全栈开发者: 二开/直接复用消息中心模块与权限设计方案
🌟 你将能学到
以下教程将按照由浅入深,先实操后原理的节奏进行:
1️⃣ NestJS应用的最小功能闭环
我们将用nestjs cli开发一个Hello World!应用,这是它的效果:
🚴实操:
bash
# 全局安装脚手架
$ npm i -g @nestjs/cli
# 自动生成nest-app项目
$ nest new nest-app
$ cd nest-app
$ npm i
$ npm run start:dev
# 此时你可以在浏览器访问127.0.0.1:3000
♂️原理
可以看到一个最小的NestJS应用至少包含三部分:Module 、Controller 和Service 。它们的主要作用分别是整合资源和依赖 、路由处理 和接口的逻辑处理 。(可能有点抽象,没关系,先有个印象就好)
2️⃣TypeORM操作数据库
我们先从用户的 CRUD 开始学习
🚴实操
bash
# 创建users Module
$ nest g mo users
# 创建users Controller
$ nest g co users --no-spec
# 创建users Service
$ nest g s users --no-spec
# 安装依赖
$ npm i typeorm @nestjs/typeorm mysql
接着在users目录下创建user.entity.ts
typescript
// src/users/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
@Entity()
// 数据库User表的映射
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({unique: true})
username: string;
@Column()
password: string;
@Column({default: 'user'})
role: string;
@CreateDateColumn()
createdAt: Date;
}
然后登录MySQL,创建数据库 nest_app
bash
# 注意:应先安装MySQL 8.x以上版本
$ mysql -u root -p
# 然后输入密码,按回车
sql
CREATE DATABASE nest_app;
-- 开发环境我们配置了自动生成表(如下users.module.ts),所以手动创建数据库即可 --
最后更新users.controller.ts、users.module.ts和users.service.ts的代码:
typescript
// users.controller.ts
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { UsersService } from './users.service';
import { User } from './user.entity';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) { }
@Post('register')
register(@Body() user: Partial<User>) {
return this.usersService.register(user);
}
@Delete('delete/:id')
delete(@Param('id') id: string) {
return this.usersService.delete(+id);
}
@Put('update')
updatePassword(@Body() data){
return this.usersService.updatePassword(data.id, data.password);
}
@Get()
getUsers() {
return this.usersService.getUsers();
}
}
typescript
// users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports:[TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'a7758258',
database: 'nest_app',
autoLoadEntities: true,
synchronize: true, // 根据entity自动生成数据库表结构,生产环境应关闭
}
),
TypeOrmModule.forFeature([User]),
],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
typescript
// users.service.ts
import { Injectable } from '@nestjs/common';
import { User } from './user.entity';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
@Injectable()
export class UsersService {
// NestJS通过IOC容器自动注入数据库User表实例
constructor(@InjectRepository(User) private readonly userRepository: Repository<User>) { }
register(user: Partial<User>): Promise<User> {
return this.userRepository.save(user);
}
delete(id: number) {
return this.userRepository.delete(id);
}
updatePassword(id: number, password: string) {
return this.userRepository.update(id, {password});
}
getUsers() {
return this.userRepository.find();
}
}
现在,你可以通过postman请求这5个接口实现简单的增删改查操作:
ruby
http://127.0.0.1:3000
http://127.0.0.1:3000/users/register
http://127.0.0.1:3000/users/delete/:id
http://127.0.0.1:3000/users/update
http://127.0.0.1:3000/users
举两个例子


♂️原理
TypeORM
它是一个对象关系映射(Object Relational Mapping)工具,把数据库的操作映射成为对象的相关操作。
- 比如,entity User类在数据库的映射是一张表结构,5个属性分别映射user表的5个列:

- 再比如,在执行更新操作时
typescript
// 假设id=1,password='123'
// this.userRepository.update(1, {password: '123'})
// 映射的SQL语句是:
// UPDATE user SET password = 123 WHERE id = 1
updatePassword(id: number, password: string) {
return this.userRepository.update(id, {password});
}
- (到这里,你算是入门了NestJS)
3️⃣NestJS核心框架深度实践
🚴实操
参考GitHub项目:消息中心(🌟欢迎 Star!)
bash
$ git clone https://github.com/leiguunjong/nestjs-message-center.git
bash
$ pnpm install
bash
$ mysql -u root -p
sql
CREATE DATABASE message_center;
bash
$ pnpm run start:dev
项目主要功能和特性包括:
- 用户管理 :实现了用户的注册、登录功能,并通过 JWT 进行身份验证,确保系统的安全性
- 权限控制 :采用 RBAC(基于角色的访问控制)模型,实现了细粒度的权限管理,确保不同角色的用户只能访问其被授权的资源
- 消息管理 :提供了完整的消息 CRUD 接口,支持消息的创建、获取、修改和删除操作
- 日志记录 :集成了 Pino 日志系统,开发环境实现了高效、结构化的日志记录,生产环境生成日级别的日志文件,便于问题的追踪和调试
- 接口文档 :通过集成 Swagger,自动生成了 API 文档,方便前后端协作和测试
- 环境变量配置:支持通过环境变量进行配置,方便在不同环境下进行部署和调试
- 数据库设计 :使用 MySQL 作为数据库,结合 TypeORM 进行数据库操作,支持复杂的数据关系和操作,如主键、外键、联表查询 、联合唯一索引 、级联删除等
- 数据安全 :对敏感数据进行了脱敏存储,确保用户数据的安全性
- 管道和拦截器:对接口请求参数和返回结果作校验和过滤,确保接口的安全性
(上面的概念可能有点多,没关系,我们先从需求背景入手)
可以设想一个APP应用,一般有用户注册/登录 功能,通常登录态 可以保持一段时间;同时会有系统消息 的推送,消息有已读未读状态。
数据库设计
所有复杂的逻辑终归于对数据的操作,因此我们先从数据库的设计入手。
很自然我们需要一张user表 存放用户数据: 也需要一张message表存放消息数据:
因为不同用户对于同一条消息的已读状态可能是不同的,所以需要一张user_message_status表存放用户对消息的已读状态:
这三张表分别映射User、Message和UserMessageStatus entity实体 ,应该注意到,user_message_status表的两个属性userId和messageId作为外键 分别指向user表和message表的主键 。而且一个用户可以阅读多条消息,所以user与user_message_status是一对多关系 ;同样一条消息可以被多个用户阅读,所以message与user_message_status也是一对多的关系:

体现在entity实体的设计:
typescript
// user-message-status.entity.ts
// ... 其他逻辑
// 多对一关系
@ManyToOne(()=>User)
user: User
@ManyToOne(()=>Message)
message: Message
// ...
// message.entity.ts
// ... 其他逻辑
// 一对多关系
@OneToMany(
() => UserMessageStatus,
(status) => status.message
)
statuses: UserMessageStatus[];
// ...
// user.entity.ts
// ... 其他逻辑
@OneToMany(
() => UserMessageStatus,
(status) => status.user
)
statuses: UserMessageStatus[];
// ...
因为某个用户对于某条消息的已读状态是唯一的,即在user_message_status表中,userId和messageId的组合应该唯一,所以我们建立了联合唯一索引:
typescript
// user-message-status.entity.ts
@Entity()
@Unique(['userId', 'messageId']) // Joint unique index 联合唯一索引
export class UserMessageStatus {
// ...
}
在删除某一条message数据时,我们需要同时删除该消息的已读记录,避免孤儿数据的产生,所以我们要配置级联删除:
typescript
// message.entity.ts
@OneToMany(
() => UserMessageStatus,
(status) => status.message,
{
cascade: true // cascading delete 启用级联删除
}
)
statuses: UserMessageStatus[];
// message.service.ts
async deleteMessage(id: number): Promise<OutputDto> {
// 先加载关联实体(触发级联删除)
// Related entities are loaded first (triggering a cascade delete)
const message = await this.msgRepository.findOne({
where: { id },
relations: ['statuses']
});
if (!message) {
this.logger.error('message id not found');
throw new NotFoundException({ code: 1201, msg: 'message id not found' });
}
return this.msgRepository.remove(message)
.then(() => { return { code: 1202, msg: 'delete message success' } })
.catch(err => {
this.logger.error(err);
throw new InternalServerErrorException({ code: 1203, msg: 'delete message fail' });
}
);
}
同时需要在数据库层面配置:
sql
ALTER TABLE user_message_status
ADD CONSTRAINT FK_message_cascade
FOREIGN KEY (messageId) REFERENCES message(id)
ON DELETE CASCADE;
JWT与RBAC
我们希望用户登录不需要每次都输入用户名密码,所以需要使用JWT(JSON Web Token)进行身份验证,通常的方式是:
-
用户通过用户名密码登录成功后,后端会返回一个长字符串,原始Token的长这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjc2LCJ1c2VybmFtZSI6ImRqa2ZiamRmcXdlIiwiaWF0IjoxNzQ0MTIwNjI1LCJleHAiOjE3NDQ5ODQ2MjV9.IIuOARLtBun_nPKtFy0Nlz3PV5Zrhq5zgQartUQi_vA
-
下次需要身份验证时,前端请求接口会带上Token
-
后端根据Token判断是哪个用户
-
后端返回Token:
typescript
// auth.service.ts
async login(username: string, password: string): Promise<LoginOutputDto> {
// ... 其他逻辑
const payload = { sub: user.id, username: user.username };
return {
access_token: await this.jwtService.signAsync(payload)
};
}
- 解析前端请求的Token:
typescript
// auth.guard.ts
async canActivate(context: ExecutionContext): Promise<boolean> {
// ...其他逻辑
try {
// 解析token
const payload = await this.jwtService.verifyAsync(
token,
{
secret: this.configService.get<string>('JWT_SECRET')
}
);
// ...
} catch (err) {
this.logger.error(err);
throw new UnauthorizedException(err?.message);
}
}
我们希望一些接口或资源需要特定权限的用户才可以访问,比如:
typescript
// message.controller.ts
@Post()
// 需要管理员权限才能创建消息
@Roles(Role.Admin)
create(@Body() msg: MessageDto): Promise<Message> {
return this.messageService.create(msg);
}
@Get()
// 获取消息普通用户即可
@Roles(Role.User)
getMessage() {
return this.messageService.getMessage();
}
由此需要引入RBAC (Role Based Access Control)概念,它是一种基于角色的访问控制系统,以下是它的主要实现逻辑:
typescript
// roles.guard.ts
// 返回true代表有权限
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(
ROLES_KEY,
[
context.getHandler(),
context.getClass(),
]
);
if (!requiredRoles) {
return true;
}
const role = context.switchToHttp().getRequest()?.user?.role;
if (role === 'admin') {
return true;
}
return requiredRoles.some(r => r === role);
}
// message.controller.ts
// 使用角色鉴权
@UseGuards(AuthGuard, RolesGuard)
export class MessagesController {
// ...其他逻辑
// 需要的角色权限
@Roles(Role.Admin)
create(@Body() msg: MessageDto): Promise<Message> {
return this.messageService.create(msg);
}
}
上述代码片段第23行为什么要在UseGuards前面加上AuthGuard呢?原因是:从前面JWT的介绍可以知道AuthGuard的主要逻辑是解析Token的,也就是验证登录相关,假如登录验证没通过,也就没有必要再验证用户的角色了。所以RABC一般会结合JWT使用。
数据安全
管道和拦截器
前面使用了守卫 (AuthGuard、RolesGuard)进行了鉴权相关的安全验证,而为了确保接口的安全性,通常还会对请求参数和返回结果作校验和过滤,分别需要使用管道 和拦截器 机制。
管道的主要实现逻辑:
typescript
// main.ts
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
// ...其他逻辑
// 利用管道验证和转换输入数据
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // 去除前端传入的DTO定义以外的字段
forbidNonWhitelisted: true, // DTO以外的字段会报错,优先级比whitelist配置高
transform: true,
})
);
}
// register-input.dto.ts
import { IsString, IsNotEmpty, Length } from 'class-validator';
export class RegisterInputDto {
@IsString()
@IsNotEmpty()
username: string;
@IsString()
@Length(4,20)
password: string;
}
上述提到了DTO 的概念,我们可以从他字面含义Data Transfer Object 来理解,就是被用来传输数据的对象 。
接下来我们看看使用管道的效果:
当前端传入字段非法或者值未校验通过时,管道会自动返回前端一个400的客户端错误,并附带错误信息。如果希望中文信息可以手动配置装饰器,如:
typescript
// ...其他逻辑
@IsNotEmpty({message: '用户名不能为空'})
username: string;
拦截器的主要实现逻辑:
typescript
// main.ts
import { Reflector } from '@nestjs/core';
import { ClassSerializerInterceptor } from '@nestjs/common';
async function bootstrap() {
// ...其他逻辑
// 利用拦截器过滤输出数据的敏感字段
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
}
// user.entity.ts
import { Exclude, Expose } from 'class-transformer';
@Entity()
export class User {
// ...其他逻辑
@Column({ unique: true })
@Expose() // 对前端暴露
username: string;
@Column()
@Exclude() // 对前端隐藏
password: string;
@CreateDateColumn()
@Expose() // 对前端暴露
createdAt: Date;
// user.service.ts
import {ClassSerializerInterceptor, Injectable, UseInterceptors} from '@nestjs/common';
@Injectable()
// 使用拦截器
@UseInterceptors(ClassSerializerInterceptor)
export class UsersService {
// ...其他逻辑
async getUsers(): Promise<User[]> {
// 返回的用户列表经过拦截器处理
// 最终会按照User实体的字段装饰器(@Expose、@Exclude等)进行暴露/隐藏相关字段
return this.userRepository.find();
}
}
我们看下使用拦截器的效果:

脱敏存储
一些敏感数据如密码等以明文形式存入数据库是风险操作,这里使用bcryptjs对数据进行加密:
typescript
// auth.service.ts
import * as bcrypt from 'bcryptjs';
// ...其他逻辑
async register(username: string, password: string) {
// 加密
const _password = bcrypt.hashSync(password, 10);
return this.usersService.register({ username, password: _password });
}
async login(username: string, password: string): Promise<LoginOutputDto> {
// ...其他逻辑
// 解密
const isMatching = bcrypt.compareSync(password, user.password);
if (!isMatching) {
this.logger.error('login password error');
throw new UnauthorizedException(errMsg);
}
// ...
}
环境变量
应用通常运行在不同的环境中。根据环境,应使用不同的配置设置。我们创建.env 、.env.development 和.env.production 文件分别存放通用配置、开发环境配置和生产环境配置,并且结合cross-env(一个用来切换环境的npm工具包)在package.json配置:
json
// package.json
"scripts": {
"start": "cross-env NODE_ENV=development nest start",
"start:dev": "cross-env NODE_ENV=development start --watch",
"start:debug": "cross-env NODE_ENV=development nest start --debug --watch",
"start:prod": "cross-env NODE_ENV=production node dist/main"
},
在根模块根据不同NODE_ENV读取不同的文件:
typescript
// app.Module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: [
`.env.${process.env.NODE_ENV || 'development'}`,
'.env',
],
}),
})
export class AppModule { }
至此我们配置好了环境变量,接下来举个连接数据库的例子去获取我们定义好的环境变量:
typescript
// message.Module.ts
import { ConfigService } from '@nestjs/config';
@Module({
imports: [
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get<string>('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get<string>('DB_USER'),
password: configService.get<string>('DB_PASSWORD'),
database: configService.get<string>('DB_NAME'),
synchronize: configService.get<string>('NODE_ENV') === 'development',
autoLoadEntities: true,
})
}),
],
})
export class MessagesModule { }
日志管理
为了便于问题的调试和追踪,我们集成了Pino 日志系统,在开发环境,使用pino-pretty 进行高效、结构化的日志展示;在生产环境,使用pino-roll生成日级别的日志文件。主要实现逻辑:
typescript
// main.ts
import { Logger } from 'nestjs-pino';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// app层级使用pino logger
app.useLogger(app.get(Logger));
// ...其他逻辑
}
// app.Module.ts
import { LoggerModule } from 'nestjs-pino';
@Module({
imports: [
LoggerModule.forRoot({
pinoHttp: {
transport: {
targets: [
process.env.NODE_ENV === 'development'
? {
level: 'debug',
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss',
ignore: 'pid,hostname'
}
}
: {
level: 'info',
target: 'pino-roll',
options: {
file: join(__dirname,'logs',`log`),
frequency: 'daily',
dateFormat: 'yyyy-MM-dd',
mkdir: true,
size: '10M',
}
}
]
}
}
}),
],
})
export class AppModule { }
至此我们集成了Pino日志到NestJS应用,接下来举个使用logger打印日志的例子:
typescript
// message.service.ts
import { PinoLogger, InjectPinoLogger } from "nestjs-pino";
// ...其他逻辑
constructor(
@InjectPinoLogger(MessagesService.name)
private readonly logger: PinoLogger
) { }
async updateReadStatus(userId: number, messageId: number): Promise<OutputDto> {
return this.umsRepository.save({ userId, messageId, isRead: true })
.then(() => ({ code: 1101, msg: 'update read status success' }))
.catch(err => {
// 打印错误
this.logger.error(err);
// ...其他逻辑
});
}
在生产环境中,我们可以在dist/logs文件夹下查看生成的日志文件:
bash
$ pnpm run start:prod

接口文档
为了方便前后端协作和测试,我们集成了Swagger,自动生成 API 文档。 我们先来看看Swagger文档长什么样:
bash
$ pnpm run start
# 启动项目后我们可以在浏览器访问 127.0.0.1:3000/api
有了swagger文档,前端就可以快速调试接口了,避免了反复沟通参数格式。
主要实现逻辑:
typescript
// main.ts
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
const config = new DocumentBuilder()
.setTitle(appName)
.setDescription(`${appName} API document`)
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
// message.controller.ts
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
@ApiTags('messages')
@ApiBearerAuth()
@Controller('messages')
@UseGuards(AuthGuard, RolesGuard)
export class MessagesController {
@Delete('delete/:id')
@ApiOperation({ summary: '删除消息', description: '需要管理员权限' })
@ApiResponse({ status: HttpStatus.OK, description: '删除消息成功' })
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'message id不存在' })
@ApiResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, description: '服务端错误' })
@Roles(Role.Admin)
deleteMessage(@Param('id') id: string) {
return this.messageService.deleteMessage(+id);
}
}
♂️原理
IOC&AOP
至此,我们学习了消息中心项目的所有功能模块和主要逻辑实现。现在回过头来,我们引入NestJS框架的两个核心思想:IOC和AOP
IOC(Inverse of Control 控制反转)
-
核心思想:对象的创建、依赖管理由 NestJS 的 IOC 容器自动处理,开发者只需通过装饰器声明依赖关系,无需手动 new 对象
-
实现方式:
-
依赖注入 (DI):通过 @Injectable() 装饰器标记服务类,并在构造函数中声明依赖。
-
模块化 :使用 @Module 装饰器定义模块,通过 providers 和 controllers 管理依赖关系。
-
-
代码实例:
typescript
// message.service.ts
// 1. 定义服务类(标记为可注入)
@Injectable()
export class MessagesService {
// ...
}
// message.controller.ts
export class MessagesController {
// 2. 在控制器中注入服务
constructor(private readonly messageService: MessagesService) { }
}
// message.Module.ts
@Module({
// 3. 模块中注册服务与控制器
controllers: [MessagesController],
providers: [MessagesService]
})
export class MessagesModule { }
AOP(Aspect Oriented Programming 面向切面编程)
-
核心思想:通过拦截器(Interceptors)、守卫(Guards)、管道(Pipes)等机制,将横切关注点(如日志、验证、异常处理)从业务逻辑中剥离。
-
实现方式:
- 装饰器 :使用
@UseInterceptors()
、@UseGuards()
等装饰器动态增强类或方法。 - 内置工具 :NestJS 提供多种内置 AOP 工具(如
ValidationPipe
用于数据验证)。
- 装饰器 :使用
-
代码示例1:
typescript
// 1. 定义守卫
// auth.guard.ts
@Injectable()
export class AuthGuard implements CanActivate {
// ...
}
// roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
// ...
}
// message.controller.ts
@Controller('messages')
// 2. 在路由控制器切面使用守卫
@UseGuards(AuthGuard, RolesGuard)
export class MessagesController {
// ...
}
- 代码示例2:
typescript
// user.service.ts
@Injectable()
// 在service切面使用拦截器
@UseInterceptors(ClassSerializerInterceptor)
export class UsersService {
// ...
}
- 在NestJS生命周期中,每一个功能模块都可以看作是一个切面
NestJS 中 IOC 和 AOP 的协同
- IOC 容器管理切面 :拦截器、守卫等切面逻辑本身也是通过 IOC 容器创建的(标记为
@Injectable()
)。 - 动态代理机制 :NestJS 底层使用 装饰器 和 反射 技术实现 AOP,结合 IOC 的依赖注入能力动态植入逻辑。
- 通过 IOC 和 AOP,NestJS 实现了代码的模块化、解耦和复用,使开发者可以专注于业务逻辑,而非基础设施的重复实现。
谢谢你能看到这里(手动比心),第一次写技术分享文章,你的一键三连是对我的最大鼓励!