chore: import zh skill nestjs-expert

This commit is contained in:
wehub-skill-sync
2026-07-13 21:36:02 +08:00
commit 75dbfd305b
8 changed files with 2331 additions and 0 deletions
+9
View File
@@ -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 清单整理
- 原作者、版权和许可证信息以上游仓库为准
+208
View File
@@ -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/)
+166
View File
@@ -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()` | 应用守卫 |
+225
View File
@@ -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()` | 文档响应 |
+153
View File
@@ -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
+140
View File
@@ -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 | 每个请求的状态 |
+186
View File
@@ -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` | 设置/拆卸 |