🚀 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 实现了代码的模块化、解耦和复用,使开发者可以专注于业务逻辑,而非基础设施的重复实现。

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

相关推荐
A-Kamen5 小时前
Webpack vs Vite:深度对比与实战示例,如何选择最佳构建工具?
前端·webpack·node.js
OpenTiny社区10 小时前
Node.js技术原理分析系列7——Node.js模块加载方式分析
前端·node.js
kovlistudio12 小时前
红宝书第三十六讲:持续集成(CI)配置入门指南
开发语言·前端·javascript·ci/cd·npm·node.js
小鱼计算机14 小时前
【6】深入学习http模块(万字)-Nodejs开发入门
前端·javascript·http·node.js·http请求
橘右溪18 小时前
Node.js中URL模块详解
node.js
橘右溪18 小时前
Node.js net模块详解
node.js
Mintopia18 小时前
Node.js 中的this
前端·javascript·node.js
玲小珑18 小时前
LLMOps开发(四) Tool + Agent
langchain·node.js·ai编程
橘右溪19 小时前
Node.js cluster模块详解
node.js
·薯条大王19 小时前
Node.js介绍
服务器·node.js