mcpField
N
nestjs-expert

nestjs-expert

github.com/shipshitdev开发3989 次安装1 天前更新
代码仓库

关于 nestjs-expert

nestjs-expert 是用于构建和维护 NestJS 应用的编码技能,针对 Prisma、Postgres 和严格 TypeScript 提供指导。内容涵盖架构、验证、数据库、认证与性能,并包含 NestJS 12 和 Prisma 7 的版本特定做法。

主要功能

  1. 01NestJS 模块与依赖注入模式
  2. 02DTO 验证和控制器指导
  3. 03Prisma 7 与 Postgres 实践
  4. 04认证与守卫模式
  5. 05查询性能和事务规则

适用场景

  1. 01组织 NestJS 功能模块
  2. 02检查 DTO 验证和控制器边界
  3. 03配置 Prisma 7 与 Postgres
  4. 04避免不安全查询和无事务的依赖写入

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/shipshitdev/skills --skill nestjs-expert

安装后在 Claude Code 中输入/nestjs-expert确认已加载。

附带文件plugin.jsonreferences/full-guide.md

详细介绍

NestJS 专家

技术栈:NestJS 12 + Prisma 7 + Postgres + TypeScript 严格模式。

NestJS 12 和 Better Auth 提供的包仅支持 ESM。CommonJS 构建(module: commonjs 搭配 moduleResolution: bundler)仍然可用,因为 Node 22.12+ 会通过 require(esm) 加载 ESM,因此 API 需运行在 Node >= 22.12 上;较旧版本的 Node 会在启动时因 ERR_REQUIRE_ESM 失败。版本 11 是旧版系列(legacy dist-tag):不要混用 11 和 12 的包。

模块架构

每个功能都是独立的模块。不要直接跨模块导入,而应使用导出的提供者。

src/
├── app.module.ts           # Root — imports feature modules only
├── common/                 # Shared guards, pipes, filters, interceptors
│   ├── filters/
│   ├── guards/
│   ├── interceptors/
│   └── pipes/
├── config/                 # ConfigModule setup
├── prisma/                 # PrismaModule + PrismaService (global, one client)
└── {feature}/
    ├── {feature}.module.ts
    ├── {feature}.controller.ts
    ├── {feature}.service.ts
    ├── dto/
    │   ├── create-{feature}.dto.ts
    │   └── update-{feature}.dto.ts
    └── {feature}.types.ts

依赖注入规则

  • 尽可能注入接口,而非具体类
  • 除非需要请求作用域,否则使用 @Injectable({ scope: Scope.DEFAULT })(单例)
  • 循环依赖 = 架构问题——仅在最后迫不得已时才用 forwardRef 修复
  • 使用 Test.createTestingModule 进行测试——始终模拟外部服务

控制器

@Controller('resources')
@UseGuards(JwtAuthGuard)
@UseInterceptors(ResponseTransformInterceptor)
export class ResourceController {
  constructor(private readonly resourceService: ResourceService) {}

  @Get()
  async findAll(@Query() query: PaginationQueryDto) {
    return this.resourceService.findAll(query);
  }

  @Post()
  @HttpCode(HttpStatus.CREATED)
  async create(@Body() dto: CreateResourceDto, @CurrentUser() user: AuthUser) {
    return this.resourceService.create(dto, user.id);
  }
}

规则:

  • 控制器应保持精简——不包含业务逻辑,不调用数据库
  • 始终使用 DTO 为 @Body()、@Query()、@Param() 指定类型
  • 使用自定义装饰器 @CurrentUser(),绝不使用 @Req()

DTO + 验证

import { IsString, IsEnum, IsOptional, MinLength, MaxLength } from 'class-validator';
import { Transform } from 'class-transformer';

export class CreateResourceDto {
  @IsString()
  @MinLength(1)
  @MaxLength(255)
  name: string;

  @IsEnum(ResourceStatus)
  status: ResourceStatus;

  @IsOptional()
  @IsString()
  @Transform(({ value }) => value?.trim())
  description?: string;
}

在 main.ts 中配置全局验证管道:

app.useGlobalPipes(new ValidationPipe({
  whitelist: true,        // strip unknown props
  forbidNonWhitelisted: true,
  transform: true,        // auto-transform primitives
  transformOptions: { enableImplicitConversion: true },
}));

Prisma / Postgres

Schema 位于 prisma/schema.prisma;使用一个全局 PrismaService 封装客户端。功能服务依赖 PrismaService——控制器绝不直接访问它。

Prisma 7:prisma-client 生成器需要显式设置 output(从该路径导入 PrismaClient,而不是从 @prisma/client 导入);客户端需要驱动适配器(@prisma/adapter-pg);连接 URL 位于 prisma.config.ts。CommonJS Nest 构建需在生成器代码块中设置 moduleFormat = "cjs"。output 相对于 schema 文件解析,因此,prisma/schema.prisma 中设置为 ../src/generated/prisma 时,客户端会写入 src/generated/prisma,下方的服务会从 src/prisma/ 导入该客户端。将该目录加入 git 忽略,并在 CI 和 Docker 构建中重新生成。

在 prisma.config.ts 中设置 datasource: { url: process.env.DATABASE_URL },不要使用 env("DATABASE_URL"):变量缺失时,env() 会抛出错误,导致在干净的 CI 和没有 .env 的 Docker 构建中执行 prisma generate 失败。缺少 URL 时,migrate 命令仍会报出清晰的错误。

// src/prisma/prisma.service.ts
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../generated/prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  constructor() {
    super({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) });
  }

  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}
// prisma/schema.prisma
generator client {
  provider     = "prisma-client"
  output       = "../src/generated/prisma"
  moduleFormat = "cjs"
}

datasource db {
  provider = "postgresql"
}

model Resource {
  id        String         @id @default(uuid()) @db.Uuid
  name      String
  status    ResourceStatus @default(ACTIVE)
  userId    String         @db.Uuid
  user      User           @relation(fields: [userId], references: [id])
  createdAt DateTime       @default(now())
  updatedAt DateTime       @updatedAt
  deletedAt DateTime?

  @@index([userId, createdAt(sort: Desc)])
}

enum ResourceStatus {
  ACTIVE
  ARCHIVED
}
// service
@Injectable()
export class ResourceService {
  constructor(private readonly prisma: PrismaService) {}

  async findAll(userId: string, query: PaginationQueryDto) {
    const { page = 1, limit = 20 } = query;
    return this.prisma.resource.findMany({
      where: { userId, deletedAt: null },
      orderBy: { createdAt: 'desc' },
      skip: (page - 1) * limit,
      take: limit,
      select: { id: true, name: true, status: true, createdAt: true },
    });
  }
}

规则:

  • ID 使用 String(uuid 或 cuid),并显式声明 @relation 关系字段——绝不使用没有外键的手写 ID 字符串
  • 在服务层将 ID 声明为 string;以 Prisma 生成的类型为准(不要手写模型接口)
  • 使用 select 仅返回所需字段;如果原始行包含机密,绝不返回原始行
  • 多步写入使用 prisma.$transaction;绝不在没有事务的情况下连续执行相互依赖的写入
  • 优先使用类型化客户端;使用 $queryRaw 时,采用带标签的模板(Prisma.sql),以确保值保持参数化——绝不将用户输入传给 $queryRawUnsafe
  • 软删除:使用 deletedAt DateTime?,绝不硬删除用户数据;读取时筛选 deletedAt: null
  • Schema 变更通过 prisma migrate dev / prisma migrate deploy 执行,绝不手动修改数据库

身份验证模式

// JWT strategy
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: configService.get<string>('JWT_SECRET'),
      ignoreExpiration: false,
    });
  }

  async validate(payload: JwtPayload): Promise<AuthUser> {
    // return value is injected as req.user
    return { id: payload.sub, email: payload.email };
  }
}

// custom decorator
export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().user,
);

守卫

@Injectable()
export class ResourceOwnerGuard implements CanActivate {
  constructor(private readonly resourceService: ResourceService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const { user, params } = context.switchToHttp().getRequest();
    const resource = await this.resourceService.findById(params.id);
    return resource?.userId === user.id;
  }
}

异常过滤器

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(GlobalExceptionFilter.name);

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();

    if (exception instanceof HttpException) {
      return response.status(exception.getStatus()).json({
        statusCode: exception.getStatus(),
        message: exception.message,
      });
    }

    this.logger.error('Unhandled exception', exception instanceof Error ? exception.stack : exception);
    return response.status(500).json({ statusCode: 500, message: 'Internal server error' });
  }
}

配置

// config/app.config.ts
export default registerAs('app', () => ({
  port: parseInt(process.env.PORT ?? '3000', 10),
  jwtSecret: process.env.JWT_SECRET,
  databaseUrl: process.env.DATABASE_URL,
}));

// access in service
constructor(private config: ConfigService) {}
const port = this.config.get<number>('app.port');

绝不在配置文件之外直接使用 process.env。

性能规则

  • 读取查询中只使用 select 获取所需列
  • 为每个用于 where 筛选或 orderBy 排序的字段添加 @@index
  • 多字段查询使用复合索引:@@index([userId, createdAt(sort: Desc)])
  • 避免 N+1:使用 include/select 关联,或通过 findMany({ where: { id: { in: ids } } }) 批量查询
  • 使用 skip/take 分页;对于大型表,使用游标分页
  • 使用 @nestjs/cache-manager(Redis)缓存开销较大的读取结果

常见错误

错误做法正确做法
将业务逻辑写在控制器中移至服务中
在任何地方使用 any 类型定义接口/DTO
使用 console.log使用 new Logger(ClassName.name)
直接使用 req.user使用 @CurrentUser() 装饰器
对环境变量硬编码使用 ConfigService
使用 findMany() 返回所有列仅 select 调用方需要的内容
使用 $queryRawUnsafe 处理用户输入使用类型化客户端或带标签的 $queryRaw
不使用事务执行相互依赖的写入操作使用 prisma.$transaction

相关技能

  • nestjs-queue-architect — BullMQ 异步作业模式
  • postgres-ops — 迁移、索引、查询计划
  • error-handling-expert — 全局错误处理策略

评论

支持 Markdown