🚀 NestJS+TypeORM+MySQL+RBAC构建消息中心后台实战

涵盖数据库经典设计、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应用至少包含三部分:ModuleControllerService 。它们的主要作用分别是整合资源和依赖路由处理接口的逻辑处理 。(可能有点抽象,没关系,先有个印象就好)

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 的依赖注入能力动态植入逻辑。
  • 通过 IOCAOP,NestJS 实现了代码的模块化、解耦和复用,使开发者可以专注于业务逻辑,而非基础设施的重复实现。

谢谢你能看到这里(手动比心),第一次写技术分享文章,你的一键三连是对我的最大鼓励!

相关推荐
天天扭码9 小时前
前端如何实现RAG?一文带你速通,使用RAG实现长期记忆
前端·node.js·ai编程
hxmmm13 小时前
自定义封装 vue多页项目新增项目脚手架
前端·javascript·node.js
濮水大叔13 小时前
VonaJS是如何做到文件级别精确HMR(热更新)的?
typescript·node.js·nestjs
小胖霞16 小时前
全栈系列(15)github Actions自动化部署前端vue
前端·node.js·github
LYFlied16 小时前
【一句话概述】Webpack、Vite、Rollup 核心区别
前端·webpack·node.js·rollup·vite·打包·一句话概述
程序员爱钓鱼21 小时前
Node.js 编程实战:MongoDB 基础与 Mongoose 入门
后端·node.js·trae
程序员爱钓鱼21 小时前
Node.js 编程实战:MySQL PostgreSQL数据库操作详解
后端·node.js·trae
古韵1 天前
当 API 文档走进编辑器会怎样?
vue.js·react.js·node.js
小胖霞2 天前
企业级全栈项目(14) winston记录所有日志
vue.js·前端框架·node.js
Anita_Sun2 天前
🎨 基础认知篇:打破单线程误区
node.js