chore: import zh skill nestjs-expert
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- Skill 名称:`nestjs-expert`
|
||||
- 中文类目:Node.js/TypeScript后端框架开发
|
||||
- 上游仓库:`jeffallan__claude-skills`
|
||||
- 上游路径:`skills/nestjs-expert/SKILL.md`
|
||||
- 上游链接:https://github.com/jeffallan/claude-skills/blob/HEAD/skills/nestjs-expert/SKILL.md
|
||||
- 本仓库为 WeHub 中文 Skill 汉化包,基于 skill 市场筛选 Top200 清单整理
|
||||
- 原作者、版权和许可证信息以上游仓库为准
|
||||
@@ -0,0 +1,208 @@
|
||||
---
|
||||
name: nestjs-expert
|
||||
description: 创建并配置适用于企业级 TypeScript 后端应用的 NestJS 模块、控制器、服务、DTO、守卫和拦截器。在构建 NestJS REST API 或 GraphQL 服务、实现依赖注入、搭建模块化架构、添加 JWT/Passport 身份验证、集成 TypeORM 或 Prisma,以及处理 .module.ts、.controller.ts 和 .service.ts 文件时使用。在 NestJS 项目中调用守卫、拦截器、管道、验证、Swagger 文档以及单元/E2E 测试。
|
||||
license: MIT
|
||||
metadata:
|
||||
author: https://github.com/Jeffallan
|
||||
version: "1.1.0"
|
||||
domain: backend
|
||||
triggers: NestJS, Nest, Node.js backend, TypeScript backend, dependency injection, controller, service, module, guard, interceptor
|
||||
role: specialist
|
||||
scope: implementation
|
||||
output-format: code
|
||||
related-skills: fullstack-guardian, test-master, devops-engineer
|
||||
---
|
||||
|
||||
# NestJS Expert
|
||||
|
||||
高级 NestJS 专家,精通企业级、可扩展的 TypeScript 后端应用。
|
||||
|
||||
## 核心工作流
|
||||
|
||||
1. **分析需求** — 确定模块、端点、实体及其关系
|
||||
2. **设计结构** — 规划模块组织及模块间的依赖关系
|
||||
3. **实现** — 创建模块、服务和控制器,并正确进行依赖注入连接
|
||||
4. **安全加固** — 添加守卫、验证管道和身份验证
|
||||
5. **验证** — 运行 `npm run lint`、`npm run test`,并通过 `nest info` 确认依赖注入图
|
||||
6. **测试** — 为服务编写单元测试,为控制器编写 E2E 测试
|
||||
|
||||
## 参考指南
|
||||
|
||||
根据上下文加载详细指导:
|
||||
|
||||
| 主题 | 参考文档 | 加载时机 |
|
||||
|-------|-----------|-----------|
|
||||
| 控制器 | `references/controllers-routing.md` | 创建控制器、路由、Swagger 文档时 |
|
||||
| 服务 | `references/services-di.md` | 服务、依赖注入、提供者时 |
|
||||
| DTO | `references/dtos-validation.md` | 验证、class-validator、DTO 时 |
|
||||
| 身份验证 | `references/authentication.md` | JWT、Passport、守卫、授权时 |
|
||||
| 测试 | `references/testing-patterns.md` | 单元测试、E2E 测试、模拟时 |
|
||||
| Express 迁移 | `references/migration-from-express.md` | 从 Express.js 迁移到 NestJS 时 |
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 带 DTO 验证和 Swagger 的控制器
|
||||
|
||||
```typescript
|
||||
// create-user.dto.ts
|
||||
import { IsEmail, IsString, MinLength } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateUserDto {
|
||||
@ApiProperty({ example: 'user@example.com' })
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ example: 'strongPassword123', minLength: 8 })
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
password: string;
|
||||
}
|
||||
|
||||
// users.controller.ts
|
||||
import { Body, Controller, Post, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { UsersService } from './users.service';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
|
||||
@ApiTags('users')
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiCreatedResponse({ description: 'User created successfully.' })
|
||||
create(@Body() createUserDto: CreateUserDto) {
|
||||
return this.usersService.create(createUserDto);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 带依赖注入和错误处理的服务
|
||||
|
||||
```typescript
|
||||
// users.service.ts
|
||||
import { Injectable, ConflictException, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { User } from './entities/user.entity';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly usersRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
async create(createUserDto: CreateUserDto): Promise<User> {
|
||||
const existing = await this.usersRepository.findOneBy({ email: createUserDto.email });
|
||||
if (existing) {
|
||||
throw new ConflictException('Email already registered');
|
||||
}
|
||||
const user = this.usersRepository.create(createUserDto);
|
||||
return this.usersRepository.save(user);
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<User> {
|
||||
const user = await this.usersRepository.findOneBy({ id });
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User #${id} not found`);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 模块定义
|
||||
|
||||
```typescript
|
||||
// users.module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UsersService } from './users.service';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService], // 仅在其他模块需要此服务时导出
|
||||
})
|
||||
export class UsersModule {}
|
||||
```
|
||||
|
||||
### 服务的单元测试
|
||||
|
||||
```typescript
|
||||
// users.service.spec.ts
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { ConflictException } from '@nestjs/common';
|
||||
import { UsersService } from './users.service';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
const mockRepo = {
|
||||
findOneBy: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
describe('UsersService', () => {
|
||||
let service: UsersService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
UsersService,
|
||||
{ provide: getRepositoryToken(User), useValue: mockRepo },
|
||||
],
|
||||
}).compile();
|
||||
service = module.get<UsersService>(UsersService);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('当邮箱已存在时抛出 ConflictException', async () => {
|
||||
mockRepo.findOneBy.mockResolvedValue({ id: 1, email: 'user@example.com' });
|
||||
await expect(
|
||||
service.create({ email: 'user@example.com', password: 'pass1234' }),
|
||||
).rejects.toThrow(ConflictException);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 约束
|
||||
|
||||
### 必须遵循
|
||||
- 对所有服务使用 `@Injectable()` 和构造器注入 — 绝不要用 `new` 实例化服务
|
||||
- 在 DTO 上使用 `class-validator` 装饰器验证所有输入,并在全局启用 `ValidationPipe`
|
||||
- 对所有请求/响应体使用 DTO;绝不要将原始 `req.body` 传递给服务
|
||||
- 在服务中抛出带类型的 HTTP 异常(`NotFoundException`、`ConflictException` 等)
|
||||
- 使用 `@ApiTags`、`@ApiOperation` 和响应装饰器记录所有端点
|
||||
- 使用 `Test.createTestingModule` 为每个服务方法编写单元测试
|
||||
- 通过 `ConfigModule` 和 `process.env` 存储所有配置值;绝不要硬编码
|
||||
|
||||
### 严禁操作
|
||||
- 在响应中暴露密码、密钥或内部堆栈跟踪信息
|
||||
- 接受未经验证的用户输入 — 始终应用 `ValidationPipe`
|
||||
- 使用 `any` 类型,除非绝对必要且已记录说明
|
||||
- 在模块之间创建循环依赖 — 仅作为最后手段使用 `forwardRef()`
|
||||
- 在源文件中硬编码主机名、端口或凭据
|
||||
- 在服务方法中跳过错误处理
|
||||
|
||||
## 输出模板
|
||||
|
||||
实现 NestJS 功能时,按以下顺序提供:
|
||||
1. 模块定义(`.module.ts`)
|
||||
2. 带 Swagger 装饰器的控制器(`.controller.ts`)
|
||||
3. 带类型化错误处理的服务(`.service.ts`)
|
||||
4. 带 `class-validator` 装饰器的 DTO(`dto/*.dto.ts`)
|
||||
5. 服务方法的单元测试(`*.service.spec.ts`)
|
||||
|
||||
## 知识参考
|
||||
|
||||
NestJS, TypeScript, TypeORM, Prisma, Passport, JWT, class-validator, class-transformer, Swagger/OpenAPI, Jest, Supertest, Guards, Interceptors, Pipes, Filters
|
||||
|
||||
[文档](https://jeffallan.github.io/claude-skills/skills/backend/nestjs-expert/)
|
||||
@@ -0,0 +1,166 @@
|
||||
# 认证与守卫
|
||||
|
||||
## JWT 策略
|
||||
|
||||
```typescript
|
||||
// jwt.strategy.ts
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private config: ConfigService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: config.get('JWT_SECRET'),
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: { sub: string; email: string; role: string }) {
|
||||
return { userId: payload.sub, email: payload.email, role: payload.role };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## JWT 认证守卫
|
||||
|
||||
```typescript
|
||||
// jwt-auth.guard.ts
|
||||
import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext) {
|
||||
const isPublic = this.reflector.get<boolean>('isPublic', context.getHandler());
|
||||
if (isPublic) return true;
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
handleRequest(err: any, user: any) {
|
||||
if (err || !user) {
|
||||
throw err || new UnauthorizedException('无效的令牌');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
// 公开装饰器
|
||||
export const Public = () => SetMetadata('isPublic', true);
|
||||
```
|
||||
|
||||
## 角色守卫
|
||||
|
||||
```typescript
|
||||
// roles.decorator.ts
|
||||
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
|
||||
|
||||
// roles.guard.ts
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const roles = this.reflector.getAllAndOverride<string[]>('roles', [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (!roles) return true;
|
||||
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
return roles.includes(user.role);
|
||||
}
|
||||
}
|
||||
|
||||
// 用法
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('admin')
|
||||
@Get('admin')
|
||||
adminEndpoint() {}
|
||||
```
|
||||
|
||||
## 认证服务
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
private jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
async validateUser(email: string, password: string): Promise<User | null> {
|
||||
const user = await this.usersService.findByEmail(email);
|
||||
if (user && await bcrypt.compare(password, user.password)) {
|
||||
return user;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async login(user: User) {
|
||||
const payload = { sub: user.id, email: user.email, role: user.role };
|
||||
return {
|
||||
access_token: this.jwtService.sign(payload),
|
||||
refresh_token: this.jwtService.sign(payload, { expiresIn: '7d' }),
|
||||
};
|
||||
}
|
||||
|
||||
async register(dto: CreateUserDto) {
|
||||
const hashedPassword = await bcrypt.hash(dto.password, 10);
|
||||
return this.usersService.create({ ...dto, password: hashedPassword });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 认证模块配置
|
||||
|
||||
```typescript
|
||||
@Module({
|
||||
imports: [
|
||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||
JwtModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
secret: config.get('JWT_SECRET'),
|
||||
signOptions: { expiresIn: '15m' },
|
||||
}),
|
||||
}),
|
||||
UsersModule,
|
||||
],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
```
|
||||
|
||||
## 全局应用守卫
|
||||
|
||||
```typescript
|
||||
// app.module.ts
|
||||
@Module({
|
||||
providers: [
|
||||
{ provide: APP_GUARD, useClass: JwtAuthGuard },
|
||||
{ provide: APP_GUARD, useClass: RolesGuard },
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 组件 | 用途 |
|
||||
|-----------|---------|
|
||||
| `JwtStrategy` | 验证 JWT 令牌 |
|
||||
| `JwtAuthGuard` | 保护路由 |
|
||||
| `RolesGuard` | 基于角色的访问控制 |
|
||||
| `@Public()` | 跳过认证 |
|
||||
| `@Roles('admin')` | 要求角色 |
|
||||
| `@UseGuards()` | 应用守卫 |
|
||||
@@ -0,0 +1,225 @@
|
||||
```typescript
|
||||
// Controllers & Routing
|
||||
|
||||
## Controller with Swagger
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Controller, Get, Post, Patch, Delete,
|
||||
Body, Param, Query, HttpCode, HttpStatus, UseGuards
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
|
||||
import { ParseUUIDPipe, ParseIntPipe } from '@nestjs/common';
|
||||
|
||||
@Controller('users')
|
||||
@ApiTags('users')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create user' })
|
||||
@ApiResponse({ status: 201, type: UserDto })
|
||||
@ApiResponse({ status: 400, description: 'Validation failed' })
|
||||
create(@Body() dto: CreateUserDto): Promise<UserDto> {
|
||||
return this.usersService.create(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all users' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
findAll(
|
||||
@Query('page', new ParseIntPipe({ optional: true })) page = 1,
|
||||
@Query('limit', new ParseIntPipe({ optional: true })) limit = 20,
|
||||
): Promise<UserDto[]> {
|
||||
return this.usersService.findAll({ page, limit });
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
||||
@ApiResponse({ status: 200, type: UserDto })
|
||||
@ApiResponse({ status: 404, description: 'User not found' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string): Promise<UserDto> {
|
||||
return this.usersService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() dto: UpdateUserDto,
|
||||
): Promise<UserDto> {
|
||||
return this.usersService.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
remove(@Param('id', ParseUUIDPipe) id: string): Promise<void> {
|
||||
return this.usersService.remove(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Nested Routes
|
||||
|
||||
```typescript
|
||||
@Controller('posts/:postId/comments')
|
||||
@ApiTags('comments')
|
||||
export class CommentsController {
|
||||
@Get()
|
||||
findAll(@Param('postId', ParseUUIDPipe) postId: string) {
|
||||
return this.commentsService.findByPost(postId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(
|
||||
@Param('postId', ParseUUIDPipe) postId: string,
|
||||
@Body() dto: CreateCommentDto,
|
||||
) {
|
||||
return this.commentsService.create(postId, dto);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Global Prefix & Versioning
|
||||
|
||||
```typescript
|
||||
// main.ts
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.setGlobalPrefix('api');
|
||||
app.enableVersioning({ type: VersioningType.URI });
|
||||
|
||||
// controller.ts
|
||||
@Controller({ path: 'users', version: '1' }) // /api/v1/users
|
||||
export class UsersV1Controller {}
|
||||
|
||||
@Controller({ path: 'users', version: '2' }) // /api/v2/users
|
||||
export class UsersV2Controller {}
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Decorator | Purpose |
|
||||
|-----------|---------|
|
||||
| `@Controller('path')` | Define route prefix |
|
||||
| `@Get()`, `@Post()` | HTTP method |
|
||||
| `@Param('name')` | Path parameter |
|
||||
| `@Query('name')` | Query parameter |
|
||||
| `@Body()` | Request body |
|
||||
| `@HttpCode(201)` | Override status code |
|
||||
| `@ApiTags()` | Swagger grouping |
|
||||
| `@ApiOperation()` | Endpoint description |
|
||||
| `@ApiResponse()` | Document response |
|
||||
```
|
||||
|
||||
# 控制器与路由
|
||||
|
||||
## 带 Swagger 的控制器
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Controller, Get, Post, Patch, Delete,
|
||||
Body, Param, Query, HttpCode, HttpStatus, UseGuards
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
|
||||
import { ParseUUIDPipe, ParseIntPipe } from '@nestjs/common';
|
||||
|
||||
@Controller('users')
|
||||
@ApiTags('users')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create user' })
|
||||
@ApiResponse({ status: 201, type: UserDto })
|
||||
@ApiResponse({ status: 400, description: 'Validation failed' })
|
||||
create(@Body() dto: CreateUserDto): Promise<UserDto> {
|
||||
return this.usersService.create(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all users' })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
findAll(
|
||||
@Query('page', new ParseIntPipe({ optional: true })) page = 1,
|
||||
@Query('limit', new ParseIntPipe({ optional: true })) limit = 20,
|
||||
): Promise<UserDto[]> {
|
||||
return this.usersService.findAll({ page, limit });
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
||||
@ApiResponse({ status: 200, type: UserDto })
|
||||
@ApiResponse({ status: 404, description: 'User not found' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string): Promise<UserDto> {
|
||||
return this.usersService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() dto: UpdateUserDto,
|
||||
): Promise<UserDto> {
|
||||
return this.usersService.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
remove(@Param('id', ParseUUIDPipe) id: string): Promise<void> {
|
||||
return this.usersService.remove(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 嵌套路由
|
||||
|
||||
```typescript
|
||||
@Controller('posts/:postId/comments')
|
||||
@ApiTags('comments')
|
||||
export class CommentsController {
|
||||
@Get()
|
||||
findAll(@Param('postId', ParseUUIDPipe) postId: string) {
|
||||
return this.commentsService.findByPost(postId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(
|
||||
@Param('postId', ParseUUIDPipe) postId: string,
|
||||
@Body() dto: CreateCommentDto,
|
||||
) {
|
||||
return this.commentsService.create(postId, dto);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 全局前缀与版本控制
|
||||
|
||||
```typescript
|
||||
// main.ts
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.setGlobalPrefix('api');
|
||||
app.enableVersioning({ type: VersioningType.URI });
|
||||
|
||||
// controller.ts
|
||||
@Controller({ path: 'users', version: '1' }) // /api/v1/users
|
||||
export class UsersV1Controller {}
|
||||
|
||||
@Controller({ path: 'users', version: '2' }) // /api/v2/users
|
||||
export class UsersV2Controller {}
|
||||
```
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 装饰器 | 用途 |
|
||||
|-----------|---------|
|
||||
| `@Controller('path')` | 定义路由前缀 |
|
||||
| `@Get()`, `@Post()` | HTTP 方法 |
|
||||
| `@Param('name')` | 路径参数 |
|
||||
| `@Query('name')` | 查询参数 |
|
||||
| `@Body()` | 请求体 |
|
||||
| `@HttpCode(201)` | 覆盖状态码 |
|
||||
| `@ApiTags()` | Swagger 分组 |
|
||||
| `@ApiOperation()` | 端点描述 |
|
||||
| `@ApiResponse()` | 文档响应 |
|
||||
@@ -0,0 +1,153 @@
|
||||
# DTO 与验证
|
||||
|
||||
## DTO 模式
|
||||
|
||||
```typescript
|
||||
import {
|
||||
IsEmail, IsString, IsOptional, IsBoolean, IsInt,
|
||||
MinLength, MaxLength, Min, Max, IsUUID, IsEnum,
|
||||
IsArray, ArrayMinSize, ValidateNested, Matches
|
||||
} from 'class-validator';
|
||||
import { Type, Transform } from 'class-transformer';
|
||||
import { ApiProperty, ApiPropertyOptional, PartialType, OmitType, PickType } from '@nestjs/swagger';
|
||||
|
||||
export class CreateUserDto {
|
||||
@ApiProperty({ example: 'user@example.com' })
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ minLength: 8 })
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
@Matches(/^(?=.*[A-Z])(?=.*\d)/, { message: '密码必须包含大写字母和数字' })
|
||||
password: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(50)
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: UserRole, default: UserRole.USER })
|
||||
@IsOptional()
|
||||
@IsEnum(UserRole)
|
||||
role?: UserRole = UserRole.USER;
|
||||
}
|
||||
|
||||
// 用于更新的 Partial(所有字段可选)
|
||||
export class UpdateUserDto extends PartialType(
|
||||
OmitType(CreateUserDto, ['password'] as const)
|
||||
) {}
|
||||
|
||||
// 选取特定字段
|
||||
export class LoginDto extends PickType(CreateUserDto, ['email', 'password'] as const) {}
|
||||
```
|
||||
|
||||
## 嵌套验证
|
||||
|
||||
```typescript
|
||||
export class CreateOrderDto {
|
||||
@ApiProperty({ type: [OrderItemDto] })
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => OrderItemDto)
|
||||
items: OrderItemDto[];
|
||||
|
||||
@ApiProperty({ type: AddressDto })
|
||||
@ValidateNested()
|
||||
@Type(() => AddressDto)
|
||||
shippingAddress: AddressDto;
|
||||
}
|
||||
|
||||
export class OrderItemDto {
|
||||
@IsUUID()
|
||||
productId: string;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
quantity: number;
|
||||
}
|
||||
```
|
||||
|
||||
## 自定义验证
|
||||
|
||||
```typescript
|
||||
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';
|
||||
|
||||
// 自定义装饰器
|
||||
export function IsStrongPassword(options?: ValidationOptions) {
|
||||
return function (object: object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'isStrongPassword',
|
||||
target: object.constructor,
|
||||
propertyName,
|
||||
options,
|
||||
validator: {
|
||||
validate(value: string) {
|
||||
return /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$/.test(value);
|
||||
},
|
||||
defaultMessage(): string {
|
||||
return '密码必须包含大写字母、小写字母、数字和特殊字符';
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
@IsStrongPassword()
|
||||
password: string;
|
||||
```
|
||||
|
||||
## 转换与清理
|
||||
|
||||
```typescript
|
||||
export class QueryDto {
|
||||
@Transform(({ value }) => parseInt(value, 10))
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page: number = 1;
|
||||
|
||||
@Transform(({ value }) => value?.trim().toLowerCase())
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
search?: string;
|
||||
|
||||
@Transform(({ value }) => value === 'true')
|
||||
@IsBoolean()
|
||||
isActive: boolean = true;
|
||||
}
|
||||
```
|
||||
|
||||
## 全局启用验证
|
||||
|
||||
```typescript
|
||||
// main.ts
|
||||
app.useGlobalPipes(new ValidationPipe({
|
||||
whitelist: true, // 剥离未知属性
|
||||
forbidNonWhitelisted: true, // 遇到未知属性时抛出异常
|
||||
transform: true, // 自动转换类型
|
||||
transformOptions: {
|
||||
enableImplicitConversion: true,
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 装饰器 | 用途 |
|
||||
|-----------|---------|
|
||||
| `@IsString()` | 字符串类型 |
|
||||
| `@IsEmail()` | 有效邮箱 |
|
||||
| `@MinLength(n)` | 最小字符串长度 |
|
||||
| `@IsInt()`, `@Min(n)` | 整数验证 |
|
||||
| `@IsEnum(Enum)` | 枚举值 |
|
||||
| `@IsOptional()` | 可选字段 |
|
||||
| `@ValidateNested()` | 验证嵌套对象 |
|
||||
| `@Type(() => Class)` | 转换为类实例 |
|
||||
| `@Transform()` | 自定义转换 |
|
||||
| `PartialType()` | 所有字段变为可选 |
|
||||
| `OmitType()` | 排除字段 |
|
||||
| `PickType()` | 仅保留指定字段 |
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,140 @@
|
||||
# 服务与依赖注入
|
||||
|
||||
## 服务模式
|
||||
|
||||
```typescript
|
||||
import { Injectable, Logger, NotFoundException, ConflictException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
private readonly logger = new Logger(UsersService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly repo: Repository<User>,
|
||||
private readonly emailService: EmailService,
|
||||
) {}
|
||||
|
||||
async create(dto: CreateUserDto): Promise<User> {
|
||||
try {
|
||||
const user = this.repo.create(dto);
|
||||
const saved = await this.repo.save(user);
|
||||
await this.emailService.sendWelcome(saved.email);
|
||||
return saved;
|
||||
} catch (error) {
|
||||
if (error.code === '23505') {
|
||||
throw new ConflictException('邮箱已存在');
|
||||
}
|
||||
this.logger.error(`创建用户失败:${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<User> {
|
||||
const user = await this.repo.findOne({ where: { id } });
|
||||
if (!user) {
|
||||
throw new NotFoundException(`用户 ${id} 未找到`);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateUserDto): Promise<User> {
|
||||
const user = await this.findOne(id);
|
||||
Object.assign(user, dto);
|
||||
return this.repo.save(user);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const result = await this.repo.delete(id);
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`用户 ${id} 未找到`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 带提供者的模块
|
||||
|
||||
```typescript
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService], // 向其他模块公开
|
||||
})
|
||||
export class UsersModule {}
|
||||
```
|
||||
|
||||
## 自定义提供者
|
||||
|
||||
```typescript
|
||||
// 值提供者
|
||||
{ provide: 'API_KEY', useValue: process.env.API_KEY }
|
||||
|
||||
// 工厂提供者
|
||||
{
|
||||
provide: 'CONFIG',
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
apiUrl: configService.get('API_URL'),
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}
|
||||
|
||||
// 类提供者
|
||||
{ provide: LoggerService, useClass: CustomLoggerService }
|
||||
|
||||
// 异步工厂
|
||||
{
|
||||
provide: 'DATABASE_CONNECTION',
|
||||
useFactory: async () => {
|
||||
const connection = await createConnection();
|
||||
return connection;
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 注入模式
|
||||
|
||||
```typescript
|
||||
// 构造器注入(推荐)
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
// 令牌注入
|
||||
constructor(@Inject('API_KEY') private apiKey: string) {}
|
||||
|
||||
// 可选注入
|
||||
constructor(@Optional() private readonly cache?: CacheService) {}
|
||||
|
||||
// 属性注入(谨慎使用)
|
||||
@Inject() private readonly logger: Logger;
|
||||
```
|
||||
|
||||
## 作用域
|
||||
|
||||
```typescript
|
||||
// 默认值:单例(整个应用共享)
|
||||
@Injectable()
|
||||
export class SharedService {}
|
||||
|
||||
// 请求作用域:每个请求一个新实例
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class RequestService {
|
||||
constructor(@Inject(REQUEST) private request: Request) {}
|
||||
}
|
||||
|
||||
// 瞬态作用域:每次注入一个新实例
|
||||
@Injectable({ scope: Scope.TRANSIENT })
|
||||
export class HelperService {}
|
||||
```
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 模式 | 使用场景 |
|
||||
|---------|----------|
|
||||
| 构造器 DI | 大多数情况(推荐) |
|
||||
| `@Inject(token)` | 非类令牌 |
|
||||
| `@Optional()` | 可选依赖 |
|
||||
| 工厂提供者 | 动态配置 |
|
||||
| Scope.REQUEST | 每个请求的状态 |
|
||||
@@ -0,0 +1,186 @@
|
||||
# 测试模式
|
||||
|
||||
## 单元测试设置
|
||||
|
||||
```typescript
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
|
||||
describe('UsersService', () => {
|
||||
let service: UsersService;
|
||||
let repo: jest.Mocked<Repository<User>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
UsersService,
|
||||
{
|
||||
provide: getRepositoryToken(User),
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get(UsersService);
|
||||
repo = module.get(getRepositoryToken(User));
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
});
|
||||
```
|
||||
|
||||
## 服务测试
|
||||
|
||||
```typescript
|
||||
describe('create', () => {
|
||||
it('应创建用户', async () => {
|
||||
const dto = { email: 'test@test.com', password: 'pass', name: 'Test' };
|
||||
const user = { id: '1', ...dto };
|
||||
|
||||
repo.create.mockReturnValue(user as User);
|
||||
repo.save.mockResolvedValue(user as User);
|
||||
|
||||
const result = await service.create(dto);
|
||||
|
||||
expect(repo.create).toHaveBeenCalledWith(dto);
|
||||
expect(repo.save).toHaveBeenCalledWith(user);
|
||||
expect(result).toEqual(user);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('应返回用户', async () => {
|
||||
const user = { id: '1', email: 'test@test.com' };
|
||||
repo.findOne.mockResolvedValue(user as User);
|
||||
|
||||
const result = await service.findOne('1');
|
||||
expect(result).toEqual(user);
|
||||
});
|
||||
|
||||
it('应抛出 NotFoundException', async () => {
|
||||
repo.findOne.mockResolvedValue(null);
|
||||
await expect(service.findOne('1')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 控制器测试
|
||||
|
||||
```typescript
|
||||
describe('UsersController', () => {
|
||||
let controller: UsersController;
|
||||
let service: jest.Mocked<UsersService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
controllers: [UsersController],
|
||||
providers: [
|
||||
{
|
||||
provide: UsersService,
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get(UsersController);
|
||||
service = module.get(UsersService);
|
||||
});
|
||||
|
||||
it('应创建用户', async () => {
|
||||
const dto = { email: 'test@test.com', password: 'pass', name: 'Test' };
|
||||
const user = { id: '1', ...dto };
|
||||
service.create.mockResolvedValue(user as User);
|
||||
|
||||
const result = await controller.create(dto);
|
||||
expect(result).toEqual(user);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## E2E 测试
|
||||
|
||||
```typescript
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
|
||||
describe('UsersController (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
let authToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleFixture = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
|
||||
await app.init();
|
||||
|
||||
// 获取认证令牌
|
||||
const response = await request(app.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send({ email: 'test@test.com', password: 'password' });
|
||||
authToken = response.body.access_token;
|
||||
});
|
||||
|
||||
afterAll(() => app.close());
|
||||
|
||||
it('/users (POST)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.post('/users')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({ email: 'new@test.com', password: 'Test1234', name: 'New' })
|
||||
.expect(201)
|
||||
.expect((res) => {
|
||||
expect(res.body.email).toBe('new@test.com');
|
||||
});
|
||||
});
|
||||
|
||||
it('/users/:id (GET) - 404', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/users/nonexistent-id')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Mock 工厂函数
|
||||
|
||||
```typescript
|
||||
export const createMockRepository = <T>() => ({
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
where: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
getOne: jest.fn(),
|
||||
getMany: jest.fn(),
|
||||
})),
|
||||
});
|
||||
```
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 模式 | 使用场景 |
|
||||
|------|----------|
|
||||
| `Test.createTestingModule()` | 创建测试模块 |
|
||||
| `jest.fn()` | Mock 函数 |
|
||||
| `mockResolvedValue()` | Mock 异步返回值 |
|
||||
| `mockReturnValue()` | Mock 同步返回值 |
|
||||
| `supertest` | E2E HTTP 测试 |
|
||||
| `beforeAll` / `afterAll` | 设置/拆卸 |
|
||||
Reference in New Issue
Block a user