TypeScript+Mongoose实现CRUD:请求查询参数校验最佳实践
控制器中校验请求参数并符合Mongoose文档定义的最佳实践
问题描述
我希望在控制器中先校验所有req.query是否符合IGroupDocument的定义,确认无误后再向数据库集合中添加文档。请问实现该需求的最佳实践是什么?
相关代码定义
IGroupDocument 接口
import { Document, Model } from "mongoose"; export interface IGroup { firstName: string; lastName: string; age?: number; email: string; dateOfEntry?: Date; } export interface IGroupDocument extends IGroup, Document {}
原始控制器代码
function create(req: Request, res: Response) { // req.query validation: if firstName, lastName and email exist and type string, and then make a document from req.query call newGroup. GroupModel.create(newGroup) res.send(`${req.query.name} created`) }
最佳实践实现方案
核心思路
采用分层校验的思路:控制器层负责请求参数的格式、类型、必填项校验,Mongoose层负责数据库层面的规则校验(如字段类型、格式约束),同时结合TypeScript的类型系统保障编译时的类型安全,避免运行时类型错误。
方案一:自定义类型守卫 + Mongoose Schema校验
适合小型项目或不想引入额外依赖的场景
1. 定义匹配接口的Mongoose Schema
// group.schema.ts import mongoose, { Schema } from 'mongoose'; import { IGroupDocument } from './group.interface'; const GroupSchema: Schema = new Schema({ firstName: { type: String, required: true, trim: true }, lastName: { type: String, required: true, trim: true }, age: { type: Number, min: 0 }, email: { type: String, required: true, trim: true, match: /^\S+@\S+\.\S+$/ }, dateOfEntry: { type: Date } }); export const GroupModel = mongoose.model<IGroupDocument>('Group', GroupSchema);
2. 控制器中实现类型守卫和校验
import { Request, Response } from 'express'; import { GroupModel } from './group.schema'; import { IGroup } from './group.interface'; // 自定义类型守卫,验证req.query是否符合IGroup类型 function isGroupQuery(query: any): query is IGroup { return ( typeof query.firstName === 'string' && query.firstName.trim() !== '' && typeof query.lastName === 'string' && query.lastName.trim() !== '' && typeof query.email === 'string' && /^\S+@\S+\.\S+$/.test(query.email) && (query.age === undefined || (!isNaN(Number(query.age)) && Number(query.age) >= 0)) && (query.dateOfEntry === undefined || !isNaN(Date.parse(query.dateOfEntry))) ); } async function create(req: Request, res: Response) { try { // 第一步:运行时校验请求参数格式 if (!isGroupQuery(req.query)) { return res.status(400).send('参数错误:firstName、lastName为必填非空字符串,email需符合邮箱格式,age为非负数字,dateOfEntry为合法日期格式'); } // 转换参数类型(req.query所有值都是字符串,需转成接口定义的类型) const groupData: IGroup = { firstName: req.query.firstName.trim(), lastName: req.query.lastName.trim(), email: req.query.email.trim(), ...(req.query.age && { age: Number(req.query.age) }), ...(req.query.dateOfEntry && { dateOfEntry: new Date(req.query.dateOfEntry) }) }; // 第二步:Mongoose自动校验Schema规则,通过后创建文档 const newGroup = await GroupModel.create(groupData); res.status(201).send(`${newGroup.firstName} ${newGroup.lastName} 创建成功`); } catch (err) { // 捕获Mongoose校验错误或其他异常 const errorMsg = err instanceof Error ? err.message : '未知错误'; res.status(400).send(`创建失败:${errorMsg}`); } }
方案二:使用class-validator + class-transformer(大型项目推荐)
通过数据传输对象(DTO)统一管理校验规则,代码更易维护和扩展
1. 安装依赖
npm install class-validator class-transformer
2. 定义DTO并添加校验规则
// group.dto.ts import { IsString, IsEmail, IsOptional, IsNumber, Min, IsDateString, Trim } from 'class-validator'; export class CreateGroupDto { @IsString() @Trim() firstName: string; @IsString() @Trim() lastName: string; @IsOptional() @IsNumber() @Min(0) age?: number; @IsEmail() @Trim() email: string; @IsOptional() @IsDateString() dateOfEntry?: Date; }
3. 控制器中使用DTO校验
import { Request, Response } from 'express'; import { GroupModel } from './group.schema'; import { plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; import { CreateGroupDto } from './group.dto'; async function create(req: Request, res: Response) { try { // 将req.query转换为DTO实例,自动完成类型转换 const groupDto = plainToInstance(CreateGroupDto, req.query); // 执行校验 const errors = await validate(groupDto); if (errors.length > 0) { // 格式化错误信息返回给前端 const errorMessages = errors.map(err => Object.values(err.constraints!).join(', ')).join('; '); return res.status(400).send(`参数错误:${errorMessages}`); } // DTO已校验通过,直接传入Mongoose创建文档 const newGroup = await GroupModel.create(groupDto); res.status(201).send(`${newGroup.firstName} ${newGroup.lastName} 创建成功`); } catch (err) { const errorMsg = err instanceof Error ? err.message : '未知错误'; res.status(400).send(`创建失败:${errorMsg}`); } }
关键注意事项
- 不要跳过Mongoose校验:即使控制器层做了校验,Mongoose的Schema校验依然能防止非法数据进入数据库(比如直接调用服务层的场景)
- 处理类型转换:
req.query的所有值都是字符串类型,必须手动或通过DTO转换为接口定义的类型(数字、日期等) - 返回清晰的错误信息:把校验失败的具体原因返回给前端,方便调试和用户理解
内容的提问来源于stack exchange,提问作者YSH
相关产品推荐
相关产品推荐

