- 后端
- GraphQL
- API设计
【免费下载链接】type-graphql
Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
TypeGraphQL 的核心魅力在于"用 TypeScript 类与装饰器声明式地构建 GraphQL Schema"。但当多个 Resolver 需要重复执行同一段逻辑(如参数校验、字段投影计算、当前用户提取)时,样板代码会随之膨胀。自定义装饰器正是为此而生:它能将公共逻辑封装为带语义的装饰器,让代码既简洁又易于单元测试。本篇基于 TypeGraphQL 2.0.0-rc.1 官方文档(website/versioned_docs/version-2.0.0-rc.1/custom-decorators.md),结合仓库源码与examples/middlewares-custom-decorators实战示例,系统讲解方法装饰器与参数装饰器的创建、原理与最佳实践。
为什么需要自定义装饰器
在 TypeGraphQL 中,内置装饰器(@Query、@Mutation、@Arg、@Ctx、@Authorized等)已经帮我们把 Resolver 声明得足够干净。但业务逻辑千变万化,比如:
- 每个变更操作前都要基于 Joi/class-validator 校验参数;
- 每个查询都要从
context中取出当前登录用户; - 某些查询需要根据客户端请求的字段集合(GraphQL
info)动态构建数据库 select 投影。
这些逻辑如果散落在每个 Resolver 内部,会产生大量重复代码;如果塞进context再手工取出,又会污染上下文对象。TypeGraphQL 提供了两条官方路径来封装这类公共逻辑:
- 方法装饰器(Method decorators):本质是中间件(Middleware)的语法糖,可挂在 Resolver 方法或字段上;
- 参数装饰器(Parameter decorators):可将计算结果注入为 Resolver 方法的参数。
在 2.0.0-rc.1 文档中,TypeGraphQL 明确支持这两类自定义装饰器;而在当前仓库的最新文档(docs/custom-decorators.md)中,还额外引入了第三类——Resolver 类装饰器,本篇也会一并介绍。
方法装饰器(Method Decorators)
与中间件的关系
TypeGraphQL 的中间件机制允许我们把可复用逻辑写成MiddlewareFn形式的函数,再通过@UseMiddleware附加到 Resolver 上。自定义方法装饰器做的事情本质上就是把"创建中间件"和"挂载中间件"两步合并成一步——它返回的正是@UseMiddleware装饰器的调用结果。
创建方法装饰器:createMethodMiddlewareDecorator
以"基于 Joi schema 校验参数"为例,官方文档给出的工厂函数如下:
export function ValidateArgs(schema: JoiSchema) { return createMethodDecorator(async ({ args }, next) => { // Middleware code that uses custom decorator arguments // e.g. Validation logic based on schema using 'joi' await joiValidate(schema, args); return next(); }); }需要特别说明一点 API 命名的演进:2.0.0-rc.1 文档中使用的辅助函数名为createMethodDecorator,而在当前仓库源码(src/decorators/createMethodMiddlewareDecorator.ts)与最新文档中,该函数已更名为createMethodMiddlewareDecorator,二者功能完全等价。撰写代码时请以当前版本导出的名为准,从type-graphql包中导入:
import { createMethodMiddlewareDecorator } from "type-graphql"; export function ValidateArgs(schema: JoiSchema) { return createMethodMiddlewareDecorator(async ({ args }, next) => { await joiValidate(schema, args); return next(); }); }查看源码可见其实现非常直白——它就是一个对UseMiddleware的类型化包装:
export function createMethodMiddlewareDecorator<TContextType extends object = object>( resolver: MiddlewareFn<TContextType>, ): MethodDecorator { return UseMiddleware(resolver); }也就是说,自定义方法装饰器返回的本质上就是一个@UseMiddleware装饰器,中间件逻辑的完整能力(前置处理、调用next()继续执行、拦截/替换返回值、抛出异常中断)在这里全部可用。中间件签名定义于 src/typings/middleware.ts:
export type NextFn = () => Promise<any>; export type MiddlewareFn<TContext extends object = object> = ( action: ResolverData<TContext>, next: NextFn, ) => Promise<any>;而ResolverData(见 src/typings/resolver-data.ts)与 Resolver 收到的参数一致,包含root、args、context、info四个字段,这意味着自定义装饰器内可以访问到 Resolver 的全部执行上下文。
使用自定义方法装饰器
创建完成后,用法与内置装饰器无异——把它放在 Resolver 方法上方,并传入配置参数即可,还可以与显式的@UseMiddleware混用(执行顺序按装饰器从上到下的声明排列):
@Resolver() export class RecipeResolver { @ValidateArgs(MyArgsSchema) // Custom decorator @UseMiddleware(ResolveTime) // Explicit middleware @Query() randomValue(@Args() { scale }: MyArgs): number { return Math.random() * scale; } }底层元数据收集机制
从 src/decorators/UseMiddleware.ts 的实现可以看到,UseMiddleware支持两种调用形式(数组或可变参数),并会根据是否传入propertyKey区分挂载目标:
- 挂载在类上(
propertyKey == null):调用collectResolverMiddlewareMetadata,中间件对该类的所有 Resolver 生效; - 挂载在方法/属性上:调用
collectMiddlewareMetadata,仅对该字段生效。
此外,若propertyKey是symbol,会抛出SymbolKeysNotSupportedError(src/errors/SymbolKeysNotSupportedError.ts),因此自定义装饰器不支持 symbol 键名的方法。
Resolver 类装饰器(Resolver Class Decorators)
在最新文档(docs/custom-decorators.md)中,TypeGraphQL 还提供了与createMethodMiddlewareDecorator对称的类级辅助函数createResolverClassMiddlewareDecorator,其源码(src/decorators/createResolverClassMiddlewareDecorator.ts)同样只是UseMiddleware(resolver)的包装,返回ClassDecorator:
export function ValidateArgs(schema: JoiSchema) { return createResolverClassMiddlewareDecorator(async ({ args }, next) => { await joiValidate(schema, args); return next(); }); }用法上只需要把装饰器放到 Resolver 类上,该类的所有 Query/Mutation 都会自动应用这段逻辑,无需逐方法重复标注:
@ValidateArgs(MyArgsSchema) // Custom decorator @UseMiddleware(ResolveTime) // Explicit middleware @Resolver() export class RecipeResolver { @Query() randomValue(@Args() { scale }: MyArgs): number { return Math.random() * scale; } }参数装饰器(Parameter Decorators)
核心思想:把返回值注入为方法参数
参数装饰器与中间件/方法装饰器的最大区别在于:它可以返回一个值,该值会被注入到 Resolver 方法的对应参数中。这大大减少了"通过context在中间件与 Resolver 之间传值"的污染性写法——过去我们不得不在中间件里往context塞数据、在 Resolver 里再取出来,现在直接由装饰器产出参数即可。
参数装饰器可以只是一个简单的数据提取器,例如从context中取出当前用户,这让 Resolver 变得对单元测试更友好(测试时直接传入 mock 的context即可):
function CurrentUser() { return createParamDecorator<MyContextType>(({ context }) => context.currentUser); }同样地,该辅助函数在当前仓库源码中的正式名称是createParameterDecorator(src/decorators/createParameterDecorator.ts),2.0.0-rc.1 文档中的createParamDecorator是它的旧名。源码中它的类型签名为:
export type ParameterResolver<TContextType extends object = object> = ( resolverData: ResolverData<TContextType>, ) => any; export function createParameterDecorator<TContextType extends object = object>( resolver: ParameterResolver<TContextType>, paramOptions: CustomParameterOptions = {}, ): ParameterDecorator可以看到,参数解析函数接收完整的ResolverData(root、args、context、info),因此理论上你可以基于任意上下文数据计算注入值。
进阶用法:基于 GraphQL info 计算字段映射
参数装饰器还可以封装更复杂的逻辑。与中间件相比,它提供了更细粒度的"按需执行"控制——例如仅在 Resolver 明确声明@Fields()参数时才计算字段映射,而不是在每个请求里都无条件执行:
function Fields(level = 1): ParameterDecorator { return createParameterDecorator(async ({ info }) => { const fieldsMap: FieldsMap = {}; // Calculate an object with info about requested fields // based on GraphQL 'info' parameter of the resolver and the level parameter // or even call some async service, as it can be a regular async function and we can just 'await' return fieldsMap; }); }注意:把参数装饰器的逻辑写成
async函数会拖慢 GraphQL Resolver 的执行(每次调用都会产生额外的 Promise 开销),所以如无必要,尽量保持参数解析函数为同步逻辑。
在 Resolver 中使用参数装饰器
自定义参数装饰器的使用方式与内置装饰器(@Args、@Arg、@Ctx)完全一致,直接放在参数声明前即可:
@Resolver() export class RecipeResolver { constructor(private readonly recipesRepository: Repository<Recipe>) {} @Authorized() @Mutation(returns => Recipe) async addRecipe( @Args() recipeData: AddRecipeInput, // Custom decorator just like the built-in one @CurrentUser() currentUser: User, ) { const recipe: Recipe = { ...recipeData, // and use the data returned from custom decorator in the resolver code author: currentUser, }; await this.recipesRepository.save(recipe); return recipe; } @Query(returns => Recipe, { nullable: true }) async recipe( @Arg("id") id: string, // Custom decorator that parses the fields from GraphQL query info @Fields() fields: FieldsMap, ) { return await this.recipesRepository.find(id, { // use the fields map as a select projection to optimize db queries select: fields, }); } }在addRecipe中,@CurrentUser()注入的currentUser直接参与业务组装;在recipe中,@Fields()注入的字段映射被用作数据库查询的 select 投影,从而按需优化查询性能。
运行时注入原理
参数装饰器的执行发生在 Resolver 调用之前。查看 src/resolvers/helpers.ts 中的getParamValues逻辑,其中kind === "custom"的分支会:
- 若该自定义参数携带了
arg元数据(见下文"自定义 @Arg 装饰器"),先对参数值执行convertArgToInstance与校验; - 调用
paramInfo.resolver(resolverData)得到注入值; - 最终通过
Promise.all(paramValues)等待所有 Promise 形式的参数值,再调用 Resolver 方法。
对应的元数据结构定义在 src/metadata/definitions/param-metadata.ts:CustomParamMetadata记录了kind: "custom"、目标类、方法名、参数索引,以及可选的options.arg参数注册信息。
进阶:自定义 @Arg 装饰器(Custom Arg Decorators)
有时我们希望自定义装饰器不仅能解析值,还能同时在 GraphQL Schema 中注册/暴露一个参数。在 2.0.0-rc.1 之后,TypeGraphQL 为createParameterDecorator增加了第二个参数CustomParameterOptions,其中arg键可携带@Arg所需的全部元数据(名称、类型函数、选项),从而避免"同时调用Arg()与createParameterDecorator()导致内部元数据冲突"的问题:
function RandomIdArg(argName = "id") { return createParameterDecorator( // here we do the logic of getting provided argument or generating a random one ({ args }) => args[argName] ?? Math.round(Math.random() * MAX_ID_VALUE), { // here we provide the metadata to register the parameter as a GraphQL argument arg: { name: argName, typeFunc: () => Int, options: { nullable: true, description: "Accepts provided id or generates a random one.", }, }, }, ); }结合源码可以看到,当传入paramOptions.arg时,createParameterDecorator.ts 会通过getParamInfo(src/helpers/params.ts)从design:paramtypes元数据或typeFunc推断 GraphQL 类型,并收集kind: "arg"的参数元数据;当运行时存在options.arg时,参数值还会先经过与内置@Arg相同的校验流程(见 src/resolvers/helpers.ts)。
使用方式与普通@Arg几乎一致,Schema 中会自动出现id参数:
@Resolver() export class RecipeResolver { constructor(private readonly recipesRepository: Repository<Recipe>) {} @Query(returns => Recipe, { nullable: true }) async recipe( // custom decorator that will expose an arg in the schema @RandomIdArg("id") id: number, ) { return await this.recipesRepository.findById(id); } }仓库实战示例:middlewares-custom-decorators
官方为自定义装饰器提供了开箱即用的完整示例,位于 examples/middlewares-custom-decorators,集中演示了三种自定义装饰器的落地方式。
基于 class-validator 的参数校验装饰器
examples/middlewares-custom-decorators/decorators/validate-args.ts 用createMethodMiddlewareDecorator封装了 class-validator 校验(注释明确说明也可替换为 Joi 等其他校验库):
import { validate } from "class-validator"; import { ArgumentValidationError, type ClassType, createMethodMiddlewareDecorator, } from "type-graphql"; // Sample implementation of custom validation decorator // This example use 'class-validator' however you can plug-in 'joi' or any other validation library export function ValidateArgs<T extends object>(Type: ClassType<T>) { return createMethodMiddlewareDecorator(async ({ args }, next) => { const instance = Object.assign(new Type(), args); const validationErrors = await validate(instance); if (validationErrors.length > 0) { throw new ArgumentValidationError(validationErrors); } return next(); }); }在 examples/middlewares-custom-decorators/recipe/recipe.resolver.ts 中,它被挂载到recipes查询上,同时通过@Args({ validate: false })关闭内置校验,把校验职责完全交给自定义装饰器,避免重复执行:
@Query(_returns => [Recipe]) @ValidateArgs(RecipesArgs) async recipes( @Args({ validate: false }) // Disable built-in validation here options: RecipesArgs, @CurrentUser() currentUser: User, ): Promise<Recipe[]> { console.log(`User "${currentUser.name}" queried for recipes!`); const start = options.skip; const end = options.skip + options.take; return this.items.slice(start, end); }当前用户提取装饰器
examples/middlewares-custom-decorators/decorators/current-user.ts 展示了最轻量的参数装饰器——直接从context提取数据:
import { createParameterDecorator } from "type-graphql"; import { type Context } from "../context.type"; export function CurrentUser() { return createParameterDecorator<Context>(({ context }) => context.currentUser); }随机 ID 参数装饰器
examples/middlewares-custom-decorators/decorators/random-id-arg.ts 是"自定义 @Arg"的完整实现,它在参数元数据里还附加了validateFn,保证传入的值落在合法区间(0 到MAX_ID_VALUE之间),非法输入直接抛错:
import { Int, createParameterDecorator } from "type-graphql"; const MAX_ID_VALUE = 3; // Number.MAX_SAFE_INTEGER export function RandomIdArg(argName = "id") { return createParameterDecorator( ({ args }) => args[argName] ?? Math.round(Math.random() * MAX_ID_VALUE), { arg: { name: argName, typeFunc: () => Int, options: { nullable: true, description: "Accepts provided id or generates a random one.", validateFn: (value: number): void => { if (value < 0 || value > MAX_ID_VALUE) { throw new Error(`Invalid value for ${argName}`); } }, }, }, }, ); }在同一示例的 recipe.resolver.ts 中,@RandomIdArg("id")被用于recipe查询,未传id时自动生成随机值,使 GraphQL Playground 的调试体验更友好:
@Query(_returns => Recipe, { nullable: true }) async recipe(@RandomIdArg("id") id: number) { console.log(`Queried for recipe with id: ${id}`); return this.items.find(item => item.id === id); }最佳实践与注意事项
- 优先用同步逻辑:参数装饰器中的
async会引入额外 Promise 开销、拖慢 Resolver,能同步完成的数据提取(如从context取值)就不要await。 - 用参数装饰器替代 context 传值:跨中间件与 Resolver 的通信尽量走参数注入,避免把业务数据塞进
context造成污染与隐式依赖。 - 命名即语义:自定义装饰器的价值在于 API 表达力,像
@ValidateArgs、@CurrentUser、@Fields这样的命名能让 Resolver 的意图一目了然;装饰器工厂的参数(如校验 schema、字段层级level)使其可配置、可复用。 - 注意 API 命名差异:2.0.0-rc.1 文档中的
createMethodDecorator/createParamDecorator在当前版本中对应createMethodMiddlewareDecorator/createParameterDecorator,编写代码时以当前type-graphql包实际导出的 API 为准(可查看 src/decorators/index.ts 确认导出清单)。 - 单测友好:将公共逻辑从 Resolver 方法体剥离到装饰器后,Resolver 方法变成了纯逻辑接收方,测试时直接构造参数即可,无需依赖中间件执行环境。
小结
自定义装饰器是 TypeGraphQL 生态中"减少样板代码"的关键机制:方法装饰器(createMethodMiddlewareDecorator)与类装饰器(createResolverClassMiddlewareDecorator)本质是中间件的声明式封装,负责"在执行前后做点什么";参数装饰器(createParameterDecorator)则负责"向方法注入什么",配合其arg元数据还能顺带在 Schema 中注册参数。掌握这三类辅助函数,再结合 examples/middlewares-custom-decorators 中的现成范例,你就可以把校验、鉴权、用户提取、字段投影等横切逻辑沉淀为团队内部的高质量装饰器库,让每个 Resolver 都保持整洁、可读、可测试。
- 后端
- GraphQL
- API设计
【免费下载链接】type-graphql
Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
相关推荐
TypeGraphQL 自定义装饰器完全指南:用 createMethodMiddlewareDecorator 与 createParameterDecorator 消除 Resolver 样板代码
TypeGraphQL 自定义装饰器完全指南:用 createMethodMiddlewareDecorator 与 createParameterDecora
后端GraphQLAPI设计Envoy 集成 QUICHE 深度解析:会话架构、数据流水线与流控水印机制
Envoy 集成 QUICHE 深度解析:会话架构、数据流水线与流控水印机制 导读 本文基于 source/docs/quiche_integration.md
后端GraphQLAPI设计LangChain4j Apache POI 文档解析器:在 Java RAG 流水线中解析 doc、docx、ppt、xls 等 Microsoft Office 文件
LangChain4j Apache POI 文档解析器:在 Java RAG 流水线中解析 doc、docx、ppt、xls 等 Microsoft Offi
后端GraphQLAPI设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考