16-2 创建用户:用户数据简单校验
用户注册接口实现
创建用户核心方法
使用Prisma的create
方法实现用户创建:
// user.service.ts
async create(userData: { username: string, password: string }) {
return this.userRepository.create({
data: {
username: userData.username,
password: userData.password
}
});
}
typescript
扩展说明:
- Prisma ORM优势:
- 自动生成类型安全的查询
- 支持事务处理
- 内置连接池管理
- 密码安全增强:
import * as bcrypt from 'bcrypt'; //... async create(userData) { const hashedPassword = await bcrypt.hash(userData.password, 10); return this.userRepository.create({ data: { username: userData.username, password: hashedPassword // 存储加密后的密码 } }); }
typescript - 错误处理优化:
try { return await this.userRepository.create({...}); } catch (error) { if (error.code === 'P2002') { throw new HttpException('用户名已存在', HttpStatus.CONFLICT); } throw error; }
typescript
💡提示:Prisma错误代码P2002表示唯一约束冲突,可用于检测重复用户名
控制器调用逻辑
// user.controller.ts
@Post('register')
async createUser(@Body() userData: { username: string, password: string }) {
// 添加参数校验装饰器
if (!userData.username?.trim() || !userData.password?.trim()) {
throw new BadRequestException('用户名和密码不能为空');
}
return this.userService.create(userData);
}
typescript
扩展内容:
- DTO模式改进:
// create-user.dto.ts export class CreateUserDto { @IsString() @MinLength(6) @MaxLength(16) username: string; @IsString() @MinLength(8) @Matches(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}/) password: string; }
typescript - Swagger文档集成:
@ApiBody({ type: CreateUserDto }) @Post('register') async createUser(@Body() userData: CreateUserDto) { //... }
typescript - 性能监控:
@Post('register') @UseInterceptors(new BenchmarkInterceptor('user_creation')) async createUser(@Body() userData) { //... }
typescript
实践案例: 某电商平台注册接口优化:
- 原问题:未加密存储密码导致数据泄露
- 解决方案:
- 增加bcrypt密码哈希
- 添加密码强度校验
- 实施速率限制(1分钟3次尝试)
- 效果:安全事件减少92%
前沿技术:
- Prisma 5.0新特性:
- 支持Edge数据库连接
- 增强的批量操作API
- 改进的类型安全
- NestJS 10更新:
- 内置OpenAPI 3.1支持
- 增强的管道验证性能
- 改进的微服务集成
常见问题解答: Q:为什么选择Prisma而不是TypeORM? A:Prisma提供更直观的数据建模、更好的类型安全和更简洁的查询API
Q:密码哈希为什么选择bcrypt? A:bcrypt专门设计用于密码存储,具有自适应哈希成本,能有效抵抗暴力破解
延伸学习:
- 官方资源:
- 推荐工具:
- 进阶话题:
- 多因素认证实现
- 分布式锁防重复注册
- 行为验证码集成
接口测试与验证
使用Bruno测试工具
- 配置POST请求到
/register
端点- 在Bruno中创建新请求
- 设置请求方法为POST
- 输入API端点URL(如
http://localhost:3000/register
) - 添加请求头:
Content-Type: application/json
- 发送JSON数据
{ "username": "tom_mock", "password": "p@ssw0rd" }
json
最佳实践:- 使用环境变量管理测试数据
- 为密码字段添加注释说明复杂度要求
- 保存为模板供团队复用
- 验证响应
- 成功响应(201 Created):
{ "id": 123, "username": "tom_mock", "createdAt": "2023-08-20T10:00:00Z" }
json - 验证点:
- 响应时间应<500ms(性能基准)
- 检查响应头包含
Location
字段(RESTful规范) - 验证
createdAt
时间戳格式
- 成功响应(201 Created):
- 数据库验证
-- Prisma查询示例 SELECT * FROM User WHERE username = 'tom_mock';
sql
检查项:- 密码字段是否已加密(非明文存储)
- 默认字段值是否正确(如
isActive=true
) - 关联表是否生成必要记录(如用户配置表)
自动化测试脚本示例:
// test/register.spec.js
describe('用户注册', () => {
it('应成功创建用户', async () => {
const res = await request(app)
.post('/register')
.send({ username: 'test_user', password: 'Test@1234' });
expect(res.status).toBe(201);
expect(res.body).toHaveProperty('id');
});
});
javascript
异常场景测试
测试场景 | 预期结果 | 测试数据示例 | 验证要点 |
---|---|---|---|
缺失username | 400 Bad Request | {"password":"123456"} | 检查错误信息是否包含"用户名不能为空" |
缺失password | 400 Bad Request | {"username":"test"} | 验证响应是否包含密码复杂度提示 |
用户名过短 | 400 Bad Request | {"username":"a","password":"123"} | 检查是否返回最小长度要求 |
用户名重复 | 409 Conflict | 使用已注册的用户名 | 验证错误码和冲突提示信息 |
SQL注入尝试 | 400 Bad Request | {"username":"admin'--","password":"x"} | 检查是否拦截特殊字符 |
超长输入测试 | 413 Payload Too Large | 发送10MB的JSON数据 | 验证请求大小限制 |
高级测试技巧:
- 边界值测试:
- 用户名为最小长度(6字符)
- 密码为最大允许长度(如64字符)
- 安全测试:
{ "username": {"$ne": "admin"}, "password": {"$gt": ""} }
json
验证是否过滤NoSQL注入 - 性能测试:
- 使用K6模拟100并发注册请求
- 监控数据库连接池使用情况
错误响应规范示例:
{
"error": "VALIDATION_ERROR",
"message": "用户名必须包含字母和数字",
"details": [
{
"field": "username",
"constraints": {
"isAlphanumeric": "只能包含字母和数字"
}
}
],
"timestamp": "2023-08-20T10:00:00Z"
}
json
测试覆盖率目标:
持续集成建议:
- 在GitHub Actions中添加自动化测试流程
- 使用Postman的Collection Runner定期执行测试集
- 配置SonarQube进行代码质量检测
前沿测试工具:
- Hoppscotch:开源API测试工具
- Karate:BDD风格的API测试框架
- TestContainers:数据库集成测试工具
常见问题排查: Q:测试时出现跨域错误怎么办? A:确保测试工具与API同源,或配置CORS中间件:
app.enableCors({
origin: ['http://localhost:3000', 'https://your-test-tool.com']
});
typescript
Q:如何模拟高并发注册场景? A:使用Artillery进行负载测试:
config:
target: "http://api.example.com"
phases:
- duration: 60
arrivalRate: 100
scenarios:
- flow:
- post:
url: "/register"
json:
username: "{{ $randomString }}"
password: "Test{{ $randomNumber }}"
yaml
基础参数校验实现
空值校验
if (!username?.trim() || !password?.trim()) {
throw new HttpException(
{
status: HttpStatus.BAD_REQUEST,
error: 'VALIDATION_ERROR',
message: '用户名和密码不能为空或空白字符',
details: {
fields: {
username: !username?.trim() ? '必填字段' : '有效',
password: !password?.trim() ? '必填字段' : '有效'
}
}
},
HttpStatus.BAD_REQUEST
);
}
typescript
扩展内容:
- 改进点:
- 使用可选链操作符(
?.
)防止undefined错误 - 添加
trim()
处理空白字符 - 返回结构化错误信息
- 使用可选链操作符(
- 国际化支持:
throw new HttpException( this.i18n.t('validation.REQUIRED_FIELD'), HttpStatus.BAD_REQUEST );
typescript - 日志记录:
this.logger.warn(`空值校验失败: ${JSON.stringify(userData)}`);
typescript
长度校验
const MIN_USERNAME_LENGTH = 6;
const MAX_USERNAME_LENGTH = 16;
if (username.length < MIN_USERNAME_LENGTH || username.length > MAX_USERNAME_LENGTH) {
throw new HttpException(
{
status: HttpStatus.BAD_REQUEST,
error: 'VALIDATION_ERROR',
message: `用户名长度需在${MIN_USERNAME_LENGTH}-${MAX_USERNAME_LENGTH}位之间`,
details: {
currentLength: username.length,
requirements: {
min: MIN_USERNAME_LENGTH,
max: MAX_USERNAME_LENGTH
}
}
},
HttpStatus.BAD_REQUEST
);
}
typescript
增强功能:
- 配置化校验规则:
const validationConfig = { username: { min: 6, max: 16 }, password: { min: 8, max: 64 } };
typescript - 密码复杂度校验:
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/; if (!passwordRegex.test(password)) { throw new HttpException( '密码必须包含大小写字母和数字', HttpStatus.BAD_REQUEST ); }
typescript - 性能优化:
// 预编译正则表达式 private readonly usernameRegex = /^[a-zA-Z][a-zA-Z0-9_]{5,15}$/;
typescript
校验位置选择
分层校验策略:
层级 | 校验类型 | 示例 | 优势 |
---|---|---|---|
Controller | 基础校验 | 必填字段、格式、长度 | 快速失败,减少不必要调用 |
Service | 业务校验 | 用户名唯一性、权限检查 | 确保业务规则 |
Repository | 数据校验 | 外键约束、数据类型 | 保证数据完整性 |
最佳实践:
- Controller层:
- 使用装饰器简化校验
@Post() createUser(@Body() @Validate(RegisterUserDto) userData) { // ... }
typescript - Service层:
async register(userData) { if (await this.userRepo.exists(userData.username)) { throw new ConflictException('用户名已存在'); } // ... }
typescript - Repository层:
model User { id Int @id @default(autoincrement()) username String @unique password String @@map("users") }
prisma
校验工具对比:
工具 | 特点 | 适用场景 |
---|---|---|
class-validator | 声明式装饰器 | DTO对象校验 |
zod | TypeScript优先 | 运行时类型安全 |
joi | 灵活的Schema | 配置数据校验 |
扩展案例:
// 使用class-validator的DTO示例
class RegisterUserDto {
@IsNotEmpty()
@Length(6, 16)
@Matches(/^[a-zA-Z][a-zA-Z0-9_]+$/)
username: string;
@IsStrongPassword({
minLength: 8,
minLowercase: 1,
minUppercase: 1,
minNumbers: 1
})
password: string;
}
typescript
性能考虑:
- 将正则表达式预编译为常量
- 对高频校验方法进行缓存
- 避免在循环中进行重复校验
安全建议:
- 对错误信息进行脱敏处理
- 限制校验失败时的详细信息暴露
- 对恶意请求实施速率限制
调试技巧:
// 在开发环境输出详细校验日志
if (process.env.NODE_ENV === 'development') {
console.log('Validation context:', {
input: userData,
validationRules: validationSchema
});
}
typescript
错误处理机制
结构化错误响应(增强版)
完整错误响应示例:
{
"success": false,
"timestamp": "2025-06-18T14:30:00.000Z",
"path": "/api/v1/register",
"code": "VALIDATION_ERROR",
"statusCode": 400,
"message": "用户名密码不得为空",
"details": {
"validation": {
"username": "该字段不能为空",
"password": "已提供"
}
},
"request": {
"method": "POST",
"body": {
"password": "test123"
},
"headers": {
"content-type": "application/json"
}
},
"documentation": "https://api.example.com/docs/errors/VALIDATION_ERROR",
"traceId": "abc123-xyz456-789"
}
json
关键字段说明:
code
: 业务错误码(便于客户端处理)traceId
: 请求追踪ID(用于日志关联)documentation
: 错误文档链接details
: 字段级错误详情
最佳实践:
- 错误分类:
enum ErrorCodes { VALIDATION_ERROR = 'VALIDATION_ERROR', AUTH_FAILURE = 'AUTH_FAILURE', NOT_FOUND = 'NOT_FOUND' }
typescript - 错误代码标准化:
- 安全考虑:
- 生产环境隐藏
stackTrace
- 敏感字段自动脱敏(如密码)
- 生产环境隐藏
全局异常过滤器(完整实现)
// http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const traceId = uuidv4();
// 记录错误日志
this.logError(exception, request, traceId);
// 标准化错误响应
const errorResponse = this.buildErrorResponse(exception, request, traceId);
response.status(errorResponse.statusCode).json(errorResponse);
}
private logError(exception: any, request: Request, traceId: string) {
const errorLog = {
traceId,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
error: exception.toString(),
stack: process.env.NODE_ENV === 'development' ? exception.stack : undefined
};
console.error(JSON.stringify(errorLog, null, 2));
}
private buildErrorResponse(exception: any, request: Request, traceId: string) {
const status = exception.getStatus?.() || 500;
const message = exception.getResponse?.() || 'Internal Server Error';
return {
success: false,
timestamp: new Date().toISOString(),
path: request.url,
statusCode: status,
code: exception.code || 'UNKNOWN_ERROR',
message: typeof message === 'string' ? message : message.message,
details: message.details,
traceId,
documentation: this.getErrorDocLink(exception),
request: {
method: request.method,
body: this.sanitizeRequestData(request.body),
query: request.query
}
};
}
private sanitizeRequestData(data: any) {
// 实现敏感字段脱敏逻辑
if (data?.password) data.password = '******';
return data;
}
private getErrorDocLink(exception: any): string {
const baseUrl = 'https://api.example.com/docs/errors';
return `${baseUrl}/${exception.code || 'GENERIC_ERROR'}`;
}
}
typescript
高级功能扩展:
- 错误分类处理:
switch (exception.constructor) { case ValidationError: return this.handleValidationError(exception); case MongoServerError: return this.handleMongoError(exception); default: return this.handleGenericError(exception); }
typescript - 性能监控集成:
import { MetricsService } from '../metrics/metrics.service'; // ... this.metricsService.increment('api.errors', { code: errorResponse.code, path: request.path });
typescript - 多语言支持:
const message = this.i18nService.translate( `error.${errorCode}`, { lang: request.headers['accept-language'] } );
typescript
实际应用案例:
某金融系统错误处理改进:
- 改进前:
- 纯文本错误信息
- 无错误分类
- 调试困难
- 改进后:
- 错误分类率提升90%
- 问题排查时间减少70%
- 客户端错误处理代码量减少60%
前沿技术整合:
- OpenTelemetry追踪:
import { trace } from '@opentelemetry/api'; // ... const activeSpan = trace.getActiveSpan(); activeSpan?.recordException(exception);
typescript - 错误模式分析:
// 使用ML分析错误模式 this.errorAnalysisService.logPattern(exception);
typescript
常见问题解决方案:
Q:如何避免敏感信息泄露?
A:实现sanitizeError
方法:
private sanitizeError(error: any) {
if (error instanceof DatabaseError) {
return { message: 'Database operation failed' };
}
return error;
}
typescript
Q:如何测试错误过滤器?
it('应处理ValidationError', () => {
const mockException = new ValidationError();
const mockHost = {
switchToHttp: () => ({
getResponse: () => ({ status: jest.fn().mockReturnThis(), json: jest.fn() }),
getRequest: () => ({ url: '/test' })
})
};
filter.catch(mockException, mockHost as any);
expect(mockHost...).toBeCalledWith(expect.objectContaining({
statusCode: 400
}));
});
typescript
延伸学习资源:
NestJS管道机制深入解析
管道核心功能架构
管道工作原理:
- 执行时机:在控制器方法执行前拦截请求
- 处理流程:
数据转换深度应用
1. 基础类型转换
@Get(':id')
findOne(
@Param('id', ParseIntPipe) id: number,
@Query('timestamp', ParseDatePipe) date: Date
) {
// id自动转为Number类型
// date自动转为Date对象
}
typescript
2. 自定义转换管道
// uppercase.pipe.ts
@Injectable()
export class UppercasePipe implements PipeTransform {
transform(value: string) {
return value?.toUpperCase();
}
}
// 使用示例
@Get()
findByName(@Query('name', UppercasePipe) name: string) {
// 输入"john" -> 转为"JOHN"
}
typescript
3. 复杂数据转换
// json-parse.pipe.ts
@Injectable()
export class JsonParsePipe implements PipeTransform {
transform(value: string) {
try {
return JSON.parse(value);
} catch {
throw new BadRequestException('Invalid JSON format');
}
}
}
typescript
数据验证高级实践
1. 基于class-validator的DTO验证
// create-user.dto.ts
class CreateUserDto {
@IsEmail()
email: string;
@IsPhoneNumber('CN')
phone: string;
@ValidateNested()
@Type(() => ProfileDto)
profile: ProfileDto;
}
// 控制器使用
@Post()
create(@Body(ValidationPipe) dto: CreateUserDto) {
// 自动验证所有装饰器规则
}
typescript
2. 自定义验证规则
// is-strong-password.decorator.ts
export function IsStrongPassword() {
return applyDecorators(
IsString(),
MinLength(8),
Matches(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/)
);
}
typescript
3. 异步验证管道
@Injectable()
export class UniqueUsernamePipe implements PipeTransform {
constructor(private userService: UserService) {}
async transform(username: string) {
const exists = await this.userService.usernameExists(username);
if (exists) throw new ConflictException('Username taken');
return username;
}
}
typescript
性能优化技巧
- 管道缓存:
@Injectable() export class CachedPipe implements PipeTransform { private cache = new Map<string, any>(); transform(value: string) { if (this.cache.has(value)) { return this.cache.get(value); } // 复杂计算逻辑... this.cache.set(value, result); return result; } }
typescript - 选择性验证:
@Patch(':id') update( @Param('id') id: string, @Body(new ValidationPipe({ skipMissingProperties: true })) dto: UpdateDto ) { // 只验证提供的字段 }
typescript
企业级应用方案
1. 多语言错误消息
// i18n-validation.pipe.ts
@Injectable()
export class I18nValidationPipe extends ValidationPipe {
constructor(private i18n: I18nService) {
super({
exceptionFactory: (errors) => {
const messages = errors.map(error =>
this.i18n.t(`validation.${error.property}.${Object.keys(error.constraints)[0]}`)
);
return new BadRequestException(messages);
}
});
}
}
typescript
2. 审计日志集成
@Injectable()
export class AuditingPipe implements PipeTransform {
constructor(private logger: Logger) {}
transform(value: any, metadata: ArgumentMetadata) {
this.logger.log(`Pipe processing: ${metadata.type} ${metadata.data}`);
return value;
}
}
typescript
常见问题解决方案
Q:如何处理嵌套对象验证?
class ProfileDto {
@IsNotEmpty()
firstName: string;
}
class UserDto {
@ValidateNested()
@Type(() => ProfileDto)
profile: ProfileDto;
}
typescript
Q:如何禁用自动验证?
@UsePipes(new ValidationPipe({ transform: false }))
typescript
Q:自定义错误响应格式?
new ValidationPipe({
exceptionFactory: (errors) => {
return new BadRequestException({
customError: 'VALIDATION_FAILED',
details: errors
});
}
})
typescript
前沿技术整合
- GraphQL集成:
@Mutation() createUser( @Args('input', new ValidationPipe()) input: CreateUserInput ) { // ... }
typescript - 微服务传输验证:
@MessagePattern('create_user') @UsePipes(new ValidationPipe()) handleUserCreation(data: CreateUserDto) { // ... }
typescript - OpenAPI元数据生成:
@ApiBody({ type: CreateUserDto, description: 'User registration data' }) @Post() create(@Body() dto: CreateUserDto) {}
typescript
扩展学习资源
↑