- 后端
- GraphQL
- API设计
【免费下载链接】type-graphql
Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
TypeGraphQL 是一个面向 TypeScript + Node.js 的 GraphQL 开发库,核心思路是用普通 TypeScript 类和少量装饰器(decorator)声明式地定义 GraphQL schema,从而消除 SDL 文件、TypeScript 接口与 Resolver 之间反复同步的样板代码。本文以 docs/introduction.md 为主线,先剖析传统 GraphQL 开发的痛点,再逐步演示对象类型、Resolver、输入校验与 schema 构建的完整流程,并辅以本仓库源码(如 ObjectType 装饰器、Field 装饰器、buildSchema)说明底层原理,读完后你将能够独立用 TypeGraphQL 搭建一个可运行、可校验、可鉴权的 GraphQL API。
传统 TypeScript + GraphQL 开发:问题到底出在哪里
GraphQL 本身非常优秀,它解决了 REST API 常见的 overfetching(过度获取)与 underfetching(获取不足)问题。但在 Node.js 中用 TypeScript 开发 GraphQL API 时,开发体验却常常“有点痛苦”。文档 docs/introduction.md 描述了一条典型的传统开发路径,整个过程要横跨多套“语言”和多种文件:
- 先用 SDL 定义 schema 类型(
.graphql文件); - 用 ORM 类定义数据模型(例如 TypeORM 的实体类),代表数据库中的表结构;
- 为 queries、mutations 和字段编写 resolver;
- 为所有参数、输入和对象类型手写 TypeScript 接口;
- 最终实现 resolver,并写出一长串泛型签名:
export const getRecipesResolver: GraphQLFieldResolver<void, Context, GetRecipesArgs> = async ( _, args, ctx, ) => { // Common tasks repeatable for almost every resolver const auth = Container.get(AuthService); if (!auth.check(ctx.user)) { throw new NotAuthorizedError(); } await joi.validate(getRecipesSchema, args); const repository = TypeORM.getRepository(Recipe); // Business logic, e.g.: return repository.find({ skip: args.offset, take: args.limit }); };从源码结构看,这类写法最大的隐患是代码冗余与多份“真相”难以同步。每给实体新增一个字段,都要按顺序修改多处文件:
- 修改 ORM 实体类;
- 修改 SDL 中的 schema 类型;
- 更新对应的 TypeScript 接口;
- 必要时同步更新校验规则(如上例中的
joi.validate)。
任何一处遗漏或类型失误都会造成 schema、类型与实现不一致。同时,字段名拼写错误不会被编译器捕获,IDE 的“重命名(F2)”功能也无法跨文件正确工作——因为同一字段在多处重复声明,编辑器无法识别它们之间的关联。
TypeGraphQL 的核心思想:单一事实来源(Single Source of Truth)
TypeGraphQL 的设计目标正是消除上述痛点。它的核心主张是:只用一个真相来源——用 TypeScript 类和少量装饰器定义 schema,其余的一切(SDL、接口、校验、鉴权)都从这个类自动推导或就近声明。
@ObjectType() class Recipe { @Field() title: string; @Field(type => [Rate]) ratings: Rate[]; @Field({ nullable: true }) averageRating?: number; }在这段代码中:
@ObjectType()把Recipe类标记为 GraphQL 的 object type;@Field()声明类属性将映射为 GraphQL 字段;@Field(type => [Rate])声明一个Rate数组类型;@Field({ nullable: true })声明可空字段。
文档强调,TypeGraphQL 还内置了一批实用能力:validation(校验)、authorization(鉴权)和 dependency injection(依赖注入),把过去每个 resolver 里都要手工重复的样板任务(取用户、查权限、校验参数、查仓储)收编为框架级机制。
装饰器背后的源码实现
从本仓库源码可以印证这套“类即 schema”的机制:
- ObjectType.ts 支持三种调用形态(无参、options 对象、名称 + options),最终通过
getMetadataStorage().collectObjectMetadata(...)把类名、描述、实现的接口类等元数据收集起来; - Field.ts 会读取 TypeScript 反射元数据(
design:type或design:returntype),配合findType推断字段类型,再通过collectClassFieldMetadata登记字段名、schema 名、可空性、描述与废弃原因等; - 所有装饰器收集到的元数据最终统一汇入 MetadataStorage(内部维护
queries、mutations、objectTypes、fieldResolvers等数组),供 SchemaGenerator 在构建 schema 时消费。
因此,TypeGraphQL 运行时并不维护“SDL 文件 + 接口”两份定义,schema 完全由这些类与装饰器元数据生成。
用类定义对象类型:@ObjectType 与 @Field 的完整用法
为了直观体验,我们以文档 docs/getting-started.md 中的“食谱(Recipe)API”为例。目标是在 SDL 中得到如下类型:
type Recipe { id: ID! title: String! description: String creationDate: Date! ingredients: [String!]! }首先定义纯 TypeScript 类,只写属性与类型:
class Recipe { id: string; title: string; description?: string; creationDate: Date; ingredients: string[]; }然后加上装饰器,把类“翻译”成 GraphQL 类型:
@ObjectType() class Recipe { @Field(type => ID) id: string; @Field() title: string; @Field({ nullable: true }) description?: string; @Field() creationDate: Date; @Field(type => [String]) ingredients: string[]; }几个关键语法点,对应 types-and-fields.md 中的完整规则:
- 简单类型(
string、boolean、Date):直接@Field()即可,TypeScript 反射元数据足够; - 数组/泛型类型:受 TypeScript 反射能力限制,必须显式用箭头函数标注,如
@Field(type => [String]);嵌套数组用[[Int]]表示深度为 2 的整数数组; - 为什么用函数而非
{ type: Rate }对象:函数写法(type => [Rate])能够规避循环依赖问题(如Post <--> User相互引用),因此成为约定;想少敲键盘可以用简写@Field(() => Rate); - 可空性:默认所有字段非空(与 TS 属性语义一致)。可空属性需同时满足两个条件:类属性上加
?,装饰器传{ nullable: true };若要整个 schema 默认可空,可在buildSchema中设置nullableByDefault: true(详见 bootstrap.md); - 列表的精细可空性:
{ nullable: true | false }只作用于整个列表([Item!]或[Item!]!);需要稀疏数组时用nullable: "items"(产出[Item]!)或nullable: "itemsAndList"(产出[Item]); - 字段选项:
@Field还支持name(schema 中的字段名)、description、deprecationReason、complexity等高级选项,详见 Field.ts 中FieldOptions的定义。
仓库中的真实示例 examples/simple-usage/recipe.type.ts 展示了更丰富的用法:用@ObjectType({ description: ... })给类型加描述、用 getter 映射计算字段(如averageRating)、用deprecationReason标记废弃字段等。
编写 Resolver:@Resolver、@Query 与 @Mutation
类型定义好之后,下一步是创建 resolver(controller)类来承载查询与变更逻辑。以下示例来自 getting-started.md,构造函数中注入RecipeService:
@Resolver(Recipe) class RecipeResolver { constructor(private recipeService: RecipeService) {} @Query(returns => Recipe) async recipe(@Arg("id") id: string) { const recipe = await this.recipeService.findById(id); if (recipe === undefined) { throw new RecipeNotFoundError(id); } return recipe; } @Query(returns => [Recipe]) recipes(@Args() { skip, take }: RecipesArgs) { return this.recipeService.findAll({ skip, take }); } @Mutation(returns => Recipe) @Authorized() addRecipe( @Arg("newRecipeData") newRecipeData: NewRecipeInput, @Ctx("user") user: User, ): Promise<Recipe> { return this.recipeService.addNew({ data: newRecipeData, user }); } @Mutation(returns => Boolean) @Authorized(Roles.Admin) async removeRecipe(@Arg("id") id: string) { try { await this.recipeService.removeById(id); return true; } catch { return false; } } }要点说明:
@Resolver(Recipe)声明该 resolver 服务于Recipe类型;从源码看,Resolver.ts 会把 resolver 类注册到元数据存储中,并解析目标 object type;@Query/@Mutation分别对应 GraphQL 的 query 与 mutation 根字段,其实现(Query.ts)通过getResolverMetadata收集返回类型与选项,然后调用collectQueryHandlerMetadata登记;- 参数装饰器分工明确:
@Arg("id")取单个参数、@Args()展开一组参数、@Ctx("user")取上下文对象; returns => Recipe函数式返回类型声明,与@Field(type => [Rate])同理,既用于推断泛型返回类型,也用于规避循环依赖;具体规则见 resolvers.md;@Authorized()与@Authorized(Roles.Admin)分别表示“仅登录用户可访问”与“满足指定角色才可访问”。源码层面,Authorized.ts 支持零参数、角色数组与可变参数三种形态,会把角色元数据登记到类或字段上;运行时鉴权逻辑由 authChecker 统一执行。
实际项目中,还需要配套实现RecipeService(业务层)与RecipeNotFoundError(自定义错误)。完整的可运行版本可参考 examples/simple-usage/recipe.resolver.ts——它实现了ResolverInterface<Recipe>、使用FieldResolver处理派生字段(如按最低评分过滤后的ratingsCount),并配合@Root()访问父级对象。
输入类型与参数:@InputType、@ArgsType 与自动校验
上文用到的NewRecipeInput与RecipesArgs同样是类,只是用不同的装饰器标注:
@InputType() class NewRecipeInput { @Field() @MaxLength(30) title: string; @Field({ nullable: true }) @Length(30, 255) description?: string; @Field(type => [String]) @ArrayMaxSize(30) ingredients: string[]; } @ArgsType() class RecipesArgs { @Field(type => Int) @Min(0) skip: number = 0; @Field(type => Int) @Min(1) @Max(50) take: number = 25; }两个关键设计:
- 类型分工:
@InputType()用于 mutation/query 的输入对象(GraphQLinput),@ArgsType()用于一组参数(会被展开为多个独立参数)。两者都用@Field声明字段,因此天然复用对象类型的字段声明语法。 - 声明式校验:
@Length、@Min、@Max、@ArrayMaxSize等来自class-validator库的装饰器。TypeGraphQL 会在运行时自动执行这些校验,无需像传统写法那样手工调用joi.validate(...)。本仓库 package.json 将class-validator声明为可选依赖(>=0.14.3),并依赖class-transformer完成输入实例化。
仓库 examples/simple-usage/recipe.input.ts 给出了最小输入类型示例;完整的自动校验配置、自定义验证器(validate函数)以及ValidateArgs选项可参见 validation.md。
构建 Schema:buildSchema 与 Schema Generator
所有类型、resolver、输入类定义完毕后,最后一步是把它们交给buildSchema生成可执行的 GraphQL schema:
const schema = await buildSchema({ resolvers: [RecipeResolver], }); // ... Server源码 buildSchema.ts 展示了这一函数的行为:
resolvers是必填的非空数组(NonEmptyArray<Function>),空数组会直接抛出 “Emptyresolversarray property found inbuildSchemaoptions!” 错误;- 内部调用
SchemaGenerator.generateFromMetadata(...),把所有装饰器收集的元数据转化为真正的GraphQLSchema; - 支持
emitSchemaFile选项(字符串路径 / 配置对象 /true),可将生成的 SDL 写入文件(默认./schema.graphql),方便对比与审查; - 另有同步版本
buildSchemaSync,适用于非异步场景。
以本文的 Recipe 示例为例,打印出的 schema 大致如下:
type Recipe { id: ID! title: String! description: String creationDate: Date! ingredients: [String!]! } input NewRecipeInput { title: String! description: String ingredients: [String!]! } type Query { recipe(id: ID!): Recipe recipes(skip: Int = 0, take: Int = 25): [Recipe!]! } type Mutation { addRecipe(newRecipeData: NewRecipeInput!): Recipe! removeRecipe(id: ID!): Boolean! }注意recipes查询中skip、take的默认值(0 与 25)正是来自RecipesArgs类的属性初始化器——这印证了“类即 schema”的单一真相来源设计。
把 schema 接入 HTTP 服务器的完整流程可参考 examples/simple-usage/index.ts:先import "reflect-metadata",再用buildSchema({ resolvers, emitSchemaFile })构建,最后交给ApolloServer并通过startStandaloneServer监听 4000 端口启动。
不止于此:接口、枚举、联合类型与更多高级能力
如 introduction.md 结尾所述,上面的例子只是冰山一角。TypeGraphQL 对 GraphQL 的完整类型体系都有支持:
- 接口(Interface):
@InterfaceType()+implements,配合类继承(见 interfaces.md 与 inheritance.md); - 枚举(Enum):
registerEnumType把 TS 枚举注册为 GraphQL 枚举(见 enums.md); - 联合类型(Union):
createUnionType声明多类型联合,并自定义resolveType(见 unions.md); - 自定义标量(Scalar):
@Scalar装饰器注册GraphQLScalarType,或直接复用graphql-scalars等第三方实现(见 scalars.md); - 字段级 Resolver:
@FieldResolver在 examples/simple-usage/recipe.resolver.ts 中有现成案例; - 鉴权检查器:通过
authChecker自定义授权逻辑,仓库测试 tests/functional/authorization.ts 覆盖了多种角色组合场景; - 依赖注入:与 TypeDI、tsyringe 等容器集成(见 dependency-injection.md 与 examples 中的 tsyringe、using-container 示例);
- ORM 集成:TypeORM、MikroORM、Typegoose 等都有配套示例(见 examples.md)。
如何开始
- 参照 installation.md 安装
type-graphql、graphql、reflect-metadata与class-validator(如需校验); - 在入口文件顶部
import "reflect-metadata",并在tsconfig.json中开启emitDecoratorMetadata与experimentalDecorators(esm场景的配置详见 esm.md); - 按本文顺序定义
@ObjectType类型、@Resolver类、@InputType/@ArgsType输入类; - 调用
buildSchema({ resolvers })得到 schema,接入 Apollo Server / Express / Fastify 等任意 HTTP 层; - 可运行 examples/simple-usage 目录中的示例(
npm run example:simple-usage,具体脚本见 package.json)验证完整链路。
小结
本文梳理了 TypeGraphQL 解决的核心问题——传统 TS + GraphQL 开发中 SDL、接口、校验、鉴权与业务逻辑多份定义难以同步的冗余之痛——并沿着“对象类型 → Resolver → 输入与校验 → schema 构建”这条主线,完整还原了食谱 API 的构建过程。借助@ObjectType、@Field、@Query、@Mutation、@InputType、@ArgsType、@Authorized等装饰器,配合buildSchema生成可执行 schema,所有定义都能收敛到 TypeScript 类这一个真相来源,让字段重命名、类型检查、输入校验与权限控制获得编译期与运行期的双重保障。更高级的接口、枚举、联合类型、自定义标量、字段解析器与 ORM 集成,都可以在此基础上按需查阅本仓库 docs 目录下的对应指南逐步深入。
- 后端
- GraphQL
- API设计
【免费下载链接】type-graphql
Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
相关推荐
TypeGraphQL 入门指南:用 TypeScript 类与装饰器声明式构建 GraphQL Schema 与 Resolver
TypeGraphQL 入门指南:用 TypeScript 类与装饰器声明式构建 GraphQL Schema 与 Resolver 导读 TypeGraphQ
后端GraphQLAPI设计LaTeX公式秒变Word格式:告别复制粘贴的烦恼,让数学表达更自由
LaTeX公式秒变Word格式:告别复制粘贴的烦恼,让数学表达更自由 还在为学术论文中复杂的数学公式而头疼吗?每次从网页复制LaTeX公式到Word,结果总是一
后端GraphQLAPI设计TypeGraphQL 入门实战:用 TypeScript 类与装饰器构建完整 GraphQL Schema(Getting Started 全解)
TypeGraphQL 入门实战:用 TypeScript 类与装饰器构建完整 GraphQL Schema(Getting Started 全解) 本篇指南基
后端GraphQLAPI设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考