关于 nestjs-expert
nestjs-expert 是用于构建和维护 NestJS 应用的编码技能,针对 Prisma、Postgres 和严格 TypeScript 提供指导。内容涵盖架构、验证、数据库、认证与性能,并包含 NestJS 12 和 Prisma 7 的版本特定做法。
主要功能
- 01NestJS 模块与依赖注入模式
- 02DTO 验证和控制器指导
- 03Prisma 7 与 Postgres 实践
- 04认证与守卫模式
- 05查询性能和事务规则
适用场景
- 01组织 NestJS 功能模块
- 02检查 DTO 验证和控制器边界
- 03配置 Prisma 7 与 Postgres
- 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— 全局错误处理策略