第十八课:GraphQL — 类型安全的 API

2 阅读13分钟

覆盖文档:GraphQL(Quick Start, Resolvers, Mutations, Subscriptions, Scalars, Directives, Interfaces, Unions/Enums, Field Middleware, Mapped Types, Plugins, Complexity, Extensions, CLI Plugin, Generating SDL, Sharing Models, Other Features, Federation) 前置知识:第9课 源码重点:@nestjs/graphqlforRoot() 动态模块, Resolver 发现机制, GraphQLSchemaHost


一、GraphQL 入门

[基础] 本节面向首次接触 NestJS GraphQL 的读者。

1.1 什么是 GraphQL

GraphQL 是一种 API 查询语言,客户端可以精确指定需要的数据,避免 REST 的过度获取(over-fetching)和不足获取(under-fetching)。

┌──────────────────────────────────────────────────────┐
│                      REST                            │
│                                                      │
│   GET /users/1        → { id, name, email, ... }     │
│   GET /users/1/posts  → [{ id, title, body, ... }]   │
│   两次请求,返回大量不需要的字段                        │
│                                                      │
├──────────────────────────────────────────────────────┤
│                    GraphQL                            │
│                                                      │
│   query {                                            │
│     user(id: 1) {                                    │
│       name                                           │
│       posts { title }                                │
│     }                                                │
│   }                                                  │
│   一次请求,只返回客户端需要的字段                      │
└──────────────────────────────────────────────────────┘

1.2 Code First vs Schema First

NestJS 支持两种 GraphQL 开发模式:

模式流程推荐场景
Code First(推荐)TypeScript 类 + 装饰器 → 自动生成 SDL与 NestJS 深度集成,装饰器驱动
Schema First先写 .graphql 文件 → 生成 TypeScript 类型团队中 GraphQL schema 优先设计

本课以 Code First 为主线讲解。

1.3 安装与配置

# Apollo 驱动(主流)
npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql

# 或 Mercurius 驱动(Fastify 原生)
npm i @nestjs/graphql @nestjs/mercurius mercurius graphql
// app.module.ts — 配置 GraphQL
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>(ApolloDriver, {
      // Code First:自动生成 schema 文件
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      // 或 autoSchemaFile: true  → 内存中生成,不写入文件

      // 可选配置
      sortSchema: true,           // 字段按字母排序
      playground: true,           // 启用 Playground(开发环境)
      // installSubscriptionHandlers: true,  // 启用 Subscription(已废弃,用 subscriptions 配置)
    }),
    CatsModule,
  ],
})
export class AppModule {}

1.4 定义 GraphQL 类型

使用 @ObjectType()@Field() 装饰器定义 GraphQL 对象类型:

import { ObjectType, Field, Int, ID } from '@nestjs/graphql';

@ObjectType()
export class Cat {
  @Field(() => ID)
  id: string;

  @Field()                    // String 类型自动推断
  name: string;

  @Field(() => Int)           // 显式指定 GraphQL 类型
  age: number;

  @Field(() => String, { nullable: true })   // 可空字段
  breed?: string;

  @Field(() => [String])      // 数组类型
  hobbies: string[];
}

GraphQL 类型映射:

TypeScriptGraphQL装饰器
stringString@Field() 自动推断
booleanBoolean@Field() 自动推断
numberFloat(默认)@Field(() => Float)
numberInt@Field(() => Int)
stringID@Field(() => ID)
DateDateTime@Field()
T[][T]@Field(() => [T])

1.5 编写 Resolver

Resolver 是 GraphQL 中类似 Controller 的角色:

import {
  Resolver,
  Query,
  Mutation,
  Args,
  ResolveField,
  Parent,
  Int,
} from '@nestjs/graphql';
import { Cat } from './cat.model';
import { CatsService } from './cats.service';
import { CreateCatInput } from './dto/create-cat.input';

@Resolver(() => Cat)
export class CatsResolver {
  constructor(private readonly catsService: CatsService) {}

  // Query:查询所有猫
  @Query(() => [Cat], { name: 'cats' })
  findAll() {
    return this.catsService.findAll();
  }

  // Query:按 ID 查询
  @Query(() => Cat, { name: 'cat' })
  findOne(@Args('id', { type: () => Int }) id: number) {
    return this.catsService.findOne(id);
  }

  // Mutation:创建猫
  @Mutation(() => Cat)
  createCat(@Args('createCatInput') createCatInput: CreateCatInput) {
    return this.catsService.create(createCatInput);
  }

  // ResolveField:解析关联字段(计算属性)
  @ResolveField('fullName', () => String)
  getFullName(@Parent() cat: Cat) {
    return `${cat.name} the ${cat.breed || 'Unknown'}`;
  }
}

1.6 输入类型

import { InputType, Field, Int } from '@nestjs/graphql';

@InputType()
export class CreateCatInput {
  @Field()
  name: string;

  @Field(() => Int)
  age: number;

  @Field({ nullable: true })
  breed?: string;
}

@InputType() 对应 GraphQL 的 input 类型,用于 Mutation 和 Query 的参数。@ObjectType() 对应 type,用于返回值。两者不能互换。

1.7 @Args 装饰器

// 简单参数
@Query(() => Cat)
cat(@Args('id', { type: () => Int }) id: number) { ... }

// 对象参数
@Mutation(() => Cat)
createCat(@Args('input') input: CreateCatInput) { ... }

// 多个简单参数
@Query(() => [Cat])
cats(
  @Args('skip', { type: () => Int, defaultValue: 0 }) skip: number,
  @Args('take', { type: () => Int, defaultValue: 25 }) take: number,
) { ... }

// ArgsType — 将多个参数提取为独立类
@ArgsType()
class GetCatsArgs {
  @Field(() => Int, { defaultValue: 0 })
  skip: number;

  @Field(() => Int, { defaultValue: 25 })
  take: number;
}

@Query(() => [Cat])
cats(@Args() args: GetCatsArgs) { ... }

二、类型系统

[中阶] 本节面向已掌握基础 GraphQL 开发的读者。

2.1 自定义标量类型

GraphQL 内置标量:IntFloatStringBooleanID。NestJS 额外提供 GraphQLISODateTimeGraphQLTimestamp

当内置标量不够用时,可以自定义:

import { Scalar, CustomScalar } from '@nestjs/graphql';
import { Kind, ValueNode } from 'graphql';

@Scalar('Date', () => Date)
export class DateScalar implements CustomScalar<number, Date> {
  description = 'Date custom scalar type';

  // 从客户端接收值时(变量)
  parseValue(value: number): Date {
    return new Date(value);
  }

  // 返回给客户端时
  serialize(value: Date): number {
    return value.getTime();
  }

  // 从查询字符串中解析字面量
  parseLiteral(ast: ValueNode): Date {
    if (ast.kind === Kind.INT) {
      return new Date(ast.value);
    }
    return null;
  }
}

在模块中注册:

@Module({
  providers: [DateScalar],  // 注册为 provider
})
export class CommonModule {}

2.2 接口类型

import { InterfaceType, Field, ID } from '@nestjs/graphql';

@InterfaceType({
  // 运行时类型判断:根据实际对象决定返回哪个具体类型
  resolveType(value) {
    if (value.breed) {
      return Cat;
    }
    if (value.tricks) {
      return Dog;
    }
    return null;
  },
})
export abstract class Pet {
  @Field(() => ID)
  id: string;

  @Field()
  name: string;
}

// 实现接口
@ObjectType({ implements: () => [Pet] })
export class Cat extends Pet {
  @Field()
  breed: string;
}

@ObjectType({ implements: () => [Pet] })
export class Dog extends Pet {
  @Field(() => [String])
  tricks: string[];
}
// Resolver 中使用
@Query(() => [Pet])
pets(): Pet[] {
  return this.petsService.findAll();
}

2.3 联合类型

import { createUnionType } from '@nestjs/graphql';

export const SearchResultUnion = createUnionType({
  name: 'SearchResult',
  types: () => [Cat, Dog] as const,
  // resolveType 也可以在这里定义
  resolveType(value) {
    if (value.breed) return Cat;
    if (value.tricks) return Dog;
    return null;
  },
});

// 使用
@Query(() => [SearchResultUnion])
search(@Args('term') term: string) {
  return this.searchService.search(term);
}

2.4 枚举类型

import { registerEnumType } from '@nestjs/graphql';

export enum Role {
  ADMIN = 'ADMIN',
  USER = 'USER',
  MODERATOR = 'MODERATOR',
}

registerEnumType(Role, {
  name: 'Role',
  description: 'User role in the system',
  valuesMap: {
    ADMIN: { description: 'System administrator' },
    MODERATOR: { deprecationReason: 'Use ADMIN instead' },
  },
});

// 使用
@ObjectType()
export class User {
  @Field(() => Role)
  role: Role;
}

2.5 映射类型

@nestjs/mapped-types(REST)类似,@nestjs/graphql 提供 GraphQL 版映射类型:

import {
  PartialType,
  PickType,
  OmitType,
  IntersectionType,
} from '@nestjs/graphql';

// PartialType:所有字段变可选
@InputType()
export class UpdateCatInput extends PartialType(CreateCatInput) {}

// PickType:只保留指定字段
@InputType()
export class CatNameInput extends PickType(CreateCatInput, ['name'] as const) {}

// OmitType:排除指定字段
@InputType()
export class CatWithoutAgeInput extends OmitType(CreateCatInput, ['age'] as const) {}

// IntersectionType:合并两个类型
@InputType()
export class CatWithOwnerInput extends IntersectionType(
  CreateCatInput,
  CreateOwnerInput,
) {}

注意:GraphQL 的映射类型来自 @nestjs/graphql,不是 @nestjs/mapped-types(后者用于 REST DTO)。


三、订阅与中间件

[中阶] 本节面向需要实现实时推送和请求拦截的开发者。

3.1 Subscriptions

GraphQL Subscription 用于服务端向客户端实时推送数据(基于 WebSocket):

npm i graphql-ws
// app.module.ts — 启用 Subscription
GraphQLModule.forRoot<ApolloDriverConfig>(ApolloDriver, {
  autoSchemaFile: true,
  subscriptions: {
    'graphql-ws': true,     // 推荐:graphql-ws 协议
    // 'subscriptions-transport-ws': true,  // 废弃协议,兼容旧客户端
  },
})
import { PubSub } from 'graphql-subscriptions';

const pubSub = new PubSub();  // 开发环境用内存 PubSub

@Resolver(() => Cat)
export class CatsResolver {
  // Mutation:创建猫并发布事件
  @Mutation(() => Cat)
  async createCat(@Args('input') input: CreateCatInput) {
    const cat = await this.catsService.create(input);
    // 发布事件
    pubSub.publish('catAdded', { catAdded: cat });
    return cat;
  }

  // Subscription:监听新增猫事件
  @Subscription(() => Cat, {
    // filter:筛选推送目标(按条件过滤)
    filter(this: CatsResolver, payload, variables) {
      // 只推送指定品种的猫
      return payload.catAdded.breed === variables.breed;
    },
    // resolve:修改推送数据
    resolve(payload) {
      return payload.catAdded;
    },
  })
  catAdded(@Args('breed', { nullable: true }) breed?: string) {
    return pubSub.asyncIterableIterator('catAdded');
  }
}

生产环境 PubSub

内存 PubSub 不适合生产环境(多实例无法共享)。使用外部 PubSub:

npm i graphql-redis-subscriptions ioredis
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';

const pubSub = new RedisPubSub({
  publisher: new Redis({ host: 'localhost', port: 6379 }),
  subscriber: new Redis({ host: 'localhost', port: 6379 }),
});

3.2 Field Middleware

字段中间件在字段级别拦截解析过程,可以在字段解析前后执行自定义逻辑:

import { FieldMiddleware, MiddlewareContext, NextFn } from '@nestjs/graphql';

// 日志中间件
const loggerMiddleware: FieldMiddleware = async (
  ctx: MiddlewareContext,
  next: NextFn,
) => {
  const value = await next();
  console.log(`Field ${ctx.info.fieldName} resolved to:`, value);
  return value;
};

// 权限检查中间件
const checkRoleMiddleware: FieldMiddleware = async (
  ctx: MiddlewareContext,
  next: NextFn,
) => {
  const { info } = ctx;
  const { extensions } = info.parentType.getFields()[info.fieldName];

  const requiredRole = extensions?.role;
  if (requiredRole) {
    const userRole = ctx.context.req.user?.role;
    if (userRole !== requiredRole) {
      return null;  // 无权限则返回 null
    }
  }
  return next();
};

// 应用到字段
@ObjectType()
export class User {
  @Field()
  name: string;

  @Field({ middleware: [checkRoleMiddleware] })
  secretField: string;
}

也可以全局注册字段中间件:

GraphQLModule.forRoot<ApolloDriverConfig>(ApolloDriver, {
  autoSchemaFile: true,
  buildSchemaOptions: {
    fieldMiddleware: [loggerMiddleware],  // 全局字段中间件
  },
})

3.3 Directives(指令)

GraphQL 指令用于在 schema 层面添加自定义行为:

import { Directive, Field, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class Cat {
  @Field()
  @Directive('@upper')         // 自定义指令
  name: string;

  @Field()
  @Directive('@deprecated(reason: "Use fullName instead")')  // 内置指令
  breed: string;
}

实现自定义指令需要使用 mapSchema 进行 schema 转换:

import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
import { defaultFieldResolver, GraphQLSchema } from 'graphql';

function upperDirectiveTransformer(
  schema: GraphQLSchema,
  directiveName: string,
) {
  return mapSchema(schema, {
    [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
      const upperDirective = getDirective(
        schema,
        fieldConfig,
        directiveName,
      )?.[0];

      if (upperDirective) {
        const { resolve = defaultFieldResolver } = fieldConfig;
        fieldConfig.resolve = async function (source, args, context, info) {
          const result = await resolve(source, args, context, info);
          if (typeof result === 'string') {
            return result.toUpperCase();
          }
          return result;
        };
      }
      return fieldConfig;
    },
  });
}

// 配置
GraphQLModule.forRoot<ApolloDriverConfig>(ApolloDriver, {
  autoSchemaFile: true,
  transformSchema: (schema) =>
    upperDirectiveTransformer(schema, 'upper'),
  buildSchemaOptions: {
    directives: [
      new GraphQLDirective({
        name: 'upper',
        locations: [DirectiveLocation.FIELD_DEFINITION],
      }),
    ],
  },
})

3.4 Guards / Interceptors / Filters 在 GraphQL 中

GraphQL 中使用 NestJS 增强器时,需要通过 GqlExecutionContext 获取上下文:

import { ExecutionContext, Injectable, CanActivate } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';

@Injectable()
export class GqlAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    // 将 ExecutionContext 转换为 GqlExecutionContext
    const ctx = GqlExecutionContext.create(context);

    const gqlContext = ctx.getContext();    // GraphQL 上下文(包含 req)
    const args = ctx.getArgs();            // GraphQL 参数
    const info = ctx.getInfo();            // GraphQL 解析信息
    const root = ctx.getRoot();            // 根值

    const request = gqlContext.req;
    return !!request.headers.authorization;
  }
}
// 使用方式与 HTTP 完全相同
@UseGuards(GqlAuthGuard)
@Query(() => [Cat])
cats() {
  return this.catsService.findAll();
}

四、高级特性

[高阶] 本节面向需要深度使用 GraphQL 的高级开发者。

4.1 查询复杂度限制

防止恶意客户端发送超深层嵌套查询耗尽服务器资源:

npm i graphql-query-complexity
import {
  fieldExtensionsEstimator,
  getComplexity,
  simpleEstimator,
} from 'graphql-query-complexity';
import { GraphQLSchemaHost } from '@nestjs/graphql';
import { Plugin } from '@nestjs/apollo';
import {
  ApolloServerPlugin,
  GraphQLRequestListener,
} from '@apollo/server';

@Plugin()
export class ComplexityPlugin implements ApolloServerPlugin {
  constructor(private gqlSchemaHost: GraphQLSchemaHost) {}

  async requestDidStart(): Promise<GraphQLRequestListener<any>> {
    const { schema } = this.gqlSchemaHost;
    const maxComplexity = 20;  // 最大复杂度阈值

    return {
      async didResolveOperation({ request, document }) {
        const complexity = getComplexity({
          schema,
          operationName: request.operationName,
          query: document,
          variables: request.variables,
          estimators: [
            fieldExtensionsEstimator(),
            simpleEstimator({ defaultComplexity: 1 }),
          ],
        });

        if (complexity > maxComplexity) {
          throw new Error(
            `Query too complex: ${complexity}. Maximum: ${maxComplexity}`,
          );
        }
      },
    };
  }
}

为特定字段设置复杂度:

@ObjectType()
export class Cat {
  @Field()
  name: string;

  // friends 字段复杂度 = 10
  @Field(() => [Cat], { complexity: 10 })
  friends: Cat[];

  // 动态复杂度:基于请求的 limit 参数
  @Field(() => [Cat], {
    complexity: (options) => options.args.limit * options.childComplexity,
  })
  relatedCats: Cat[];
}

4.2 Extensions

@Extensions() 为字段添加自定义元数据,配合 Field Middleware 实现细粒度控制:

import { Extensions, Field, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class User {
  @Field()
  name: string;

  @Field()
  @Extensions({ role: Role.ADMIN })  // 只有 ADMIN 可见
  secretInfo: string;

  @Field()
  @Extensions({ deprecated: true, replacedBy: 'fullAddress' })
  address: string;
}

配合 Field Middleware 读取并使用这些元数据:

const roleCheckMiddleware: FieldMiddleware = async (ctx, next) => {
  const { extensions } = ctx.info.parentType.getFields()[ctx.info.fieldName];

  if (extensions?.role) {
    const user = ctx.context.req.user;
    if (user?.role !== extensions.role) {
      return null;
    }
  }
  return next();
};

4.3 Plugins(Apollo 插件)

实现 ApolloServerPlugin 接口,在 Apollo 请求生命周期中插入自定义逻辑:

import { Plugin } from '@nestjs/apollo';
import {
  ApolloServerPlugin,
  GraphQLRequestListener,
} from '@apollo/server';

@Plugin()
export class LoggingPlugin implements ApolloServerPlugin {
  async requestDidStart(requestContext): Promise<GraphQLRequestListener<any>> {
    const start = Date.now();
    console.log('Request started');

    return {
      async willSendResponse() {
        console.log(`Request completed in ${Date.now() - start}ms`);
      },
      async didEncounterErrors({ errors }) {
        console.error('GraphQL errors:', errors);
      },
    };
  }
}

@Plugin() 标记的类会被自动注册到 Apollo Server。

4.4 CLI Plugin

CLI Plugin 通过编译时自动添加 @Field() 装饰器,减少大量样板代码:

// nest-cli.json
{
  "compilerOptions": {
    "plugins": [
      {
        "name": "@nestjs/graphql",
        "options": {
          "typeFileNameSuffix": [".input.ts", ".args.ts", ".entity.ts", ".model.ts"],
          "introspectComments": true
        }
      }
    ]
  }
}

使用前(手动添加所有 @Field()):

@ObjectType()
export class Cat {
  @Field(() => ID)
  id: string;

  @Field()
  name: string;

  @Field(() => Int)
  age: number;

  @Field({ nullable: true })
  breed?: string;
}

使用后(Plugin 自动推断):

@ObjectType()
export class Cat {
  id: string;         // 自动推断为 String
  name: string;       // 自动推断为 String
  age: number;        // 自动推断为 Float(注意:不会自动推断 Int)
  breed?: string;     // 自动推断为 nullable String
}

注意:CLI Plugin 只能推断 TypeScript 类型到 GraphQL 类型,对于 Int / ID 等无法从 TypeScript 类型区分的标量仍需手动标注。

4.5 前后端模型共享

在前端项目中复用后端的 @ObjectType 类时,避免引入 @nestjs/graphql 整个包:

// webpack.config.js(前端项目)
module.exports = {
  resolve: {
    alias: {
      '@nestjs/graphql': '@nestjs/graphql/dist/extra/graphql-model-shim',
    },
  },
};

graphql-model-shim 将所有装饰器替换为空操作(no-op),避免前端项目报错。

4.6 SDL 生成

离线生成 GraphQL Schema 文件(用于工具链或文档):

import { GraphQLSchemaBuilderModule, GraphQLSchemaFactory } from '@nestjs/graphql';
import { NestFactory } from '@nestjs/core';

async function generateSchema() {
  const app = await NestFactory.create(GraphQLSchemaBuilderModule);
  await app.init();

  const gqlSchemaFactory = app.get(GraphQLSchemaFactory);
  const schema = await gqlSchemaFactory.create([CatsResolver]);

  // 使用 graphql 的 printSchema 输出 SDL
  const { printSchema } = require('graphql');
  console.log(printSchema(schema));

  await app.close();
}
generateSchema();

五、Federation 微服务联邦

[高阶] 本节面向需要构建分布式 GraphQL 架构的开发者。

5.1 什么是 Apollo Federation

Apollo Federation 允许将 GraphQL schema 拆分到多个独立服务(Subgraph),通过 Gateway 统一对外暴露:

┌─────────────────────────────────────┐
│          Apollo Gateway             │
│      (统一 GraphQL 端点)             │
├──────────┬───────────┬──────────────┤
│          │           │              │
▼          ▼           ▼              ▼
┌──────┐  ┌──────┐  ┌──────┐  ┌──────────┐
│Users │  │Posts │  │Comments│ │Products  │
│Subgraph│ │Subgraph│ │Subgraph│ │Subgraph │
└──────┘  └──────┘  └──────┘  └──────────┘

5.2 Subgraph 服务

npm i @nestjs/apollo @apollo/subgraph
// users.module.ts — Users Subgraph
import { ApolloFederationDriver, ApolloFederationDriverConfig } from '@nestjs/apollo';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloFederationDriverConfig>(
      ApolloFederationDriver,
      {
        autoSchemaFile: {
          federation: 2,  // Federation v2
        },
      },
    ),
  ],
})
export class AppModule {}
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';

// @key 指令标记实体主键(Federation 核心概念)
@ObjectType()
@Directive('@key(fields: "id")')
export class User {
  @Field(() => ID)
  id: string;

  @Field()
  name: string;

  @Field()
  email: string;
}
import { Resolver, Query, ResolveReference } from '@nestjs/graphql';

@Resolver(() => User)
export class UsersResolver {
  constructor(private readonly usersService: UsersService) {}

  @Query(() => [User])
  users() {
    return this.usersService.findAll();
  }

  // 当其他 Subgraph 引用 User 实体时,通过此方法解析完整对象
  @ResolveReference()
  resolveReference(reference: { __typename: string; id: string }) {
    return this.usersService.findOne(reference.id);
  }
}

跨服务引用

在 Posts Subgraph 中引用 Users 的 User 实体:

// Posts Subgraph 中的 User(只声明 @key 字段)
@ObjectType()
@Directive('@key(fields: "id")')
export class User {
  @Field(() => ID)
  id: string;
}

@ObjectType()
@Directive('@key(fields: "id")')
export class Post {
  @Field(() => ID)
  id: string;

  @Field()
  title: string;

  @Field(() => User)
  author: User;    // 引用 Users Subgraph 的实体
}

5.3 Gateway 服务

npm i @nestjs/apollo @apollo/gateway @apollo/server
import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from '@nestjs/apollo';
import { IntrospectAndCompose } from '@apollo/gateway';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloGatewayDriverConfig>(ApolloGatewayDriver, {
      gateway: {
        supergraphSdl: new IntrospectAndCompose({
          subgraphs: [
            { name: 'users', url: 'http://localhost:3001/graphql' },
            { name: 'posts', url: 'http://localhost:3002/graphql' },
          ],
        }),
      },
    }),
  ],
})
export class AppModule {}

5.4 Federation 限制

特性支持情况
Query完全支持
Mutation完全支持
Subscription不支持(Federation 架构限制)
跨服务实体引用支持(@key + @ResolveReference
接口/联合类型支持(需在 Subgraph 中完整定义)

六、源码解读:GraphQL 模块

[资深] 本节面向希望深入理解 GraphQL 模块内部机制的读者。

6.1 forRoot() 动态模块

GraphQLModule.forRoot() 是一个动态模块,负责:

// @nestjs/graphql 内部流程
GraphQLModule.forRoot(ApolloDriver, options)
       │
       ├─→ 1. 创建 DynamicModule
       │      注册 ApolloDriverGQL_DRIVER provider
       │
       ├─→ 2. DiscoveryService 扫描所有 @Resolver() 类
       │      收集 @Query / @Mutation / @Subscription / @ResolveField
       │
       ├─→ 3. 根据 Code First / Schema First 生成 GraphQLSchema
       │      - Code FirstTypeScript 装饰器 → SDLSchema
       │      - Schema First:.graphql 文件 → Schema
       │
       ├─→ 4.Schema 传给 DriverApollo / Mercurius)
       │
       └─→ 5. Driver 创建 HTTP 中间件并挂载到 NestJS

6.2 Resolver 发现机制

NestJS 使用 DiscoveryService(来自 @nestjs/core)扫描所有被 @Resolver() 装饰的类:

@Resolver(() => Cat) ← Metadata: GQL_RESOLVER_METADATA
  │
  ├── @Query(() => [Cat])         ← Metadata: GQL_QUERY_METADATA
  ├── @Mutation(() => Cat)        ← Metadata: GQL_MUTATION_METADATA
  ├── @Subscription(() => Cat)    ← Metadata: GQL_SUBSCRIPTION_METADATA
  └── @ResolveField('fullName')   ← Metadata: GQL_RESOLVE_FIELD_METADATA

扫描过程:

  1. ResolversExplorerService 通过 DiscoveryService 找到所有标记了 @Resolver() 的 provider
  2. 使用 MetadataScanner 扫描每个 resolver 类的方法
  3. 读取方法上的装饰器元数据,构建 resolver map
  4. 将 resolver map 传给 schema 生成器

6.3 GraphQLSchemaHost

GraphQLSchemaHost 提供运行时访问当前 GraphQL schema 的能力:

import { GraphQLSchemaHost } from '@nestjs/graphql';

@Injectable()
export class SchemaInspector {
  constructor(private readonly schemaHost: GraphQLSchemaHost) {}

  inspectSchema() {
    const { schema } = this.schemaHost;

    // 获取所有类型
    const typeMap = schema.getTypeMap();

    // 获取 Query 类型
    const queryType = schema.getQueryType();

    // 获取所有字段
    const fields = queryType.getFields();
  }
}

GraphQLSchemaHost 内部很简单,就是持有一个 GraphQLSchema 实例的引用,在模块初始化完成后由 Driver 设置。


七、GraphQL 架构策略

[架构] 本节面向技术负责人和架构师。

7.1 REST vs GraphQL 选型

维度RESTGraphQL
数据获取固定端点,固定返回结构客户端按需查询
Over-fetching常见(返回不需要的字段)无(只返回请求的字段)
Under-fetching需多次请求拼装单次请求获取所有需要的数据
缓存HTTP 缓存(URL 级别)简单需要专门的缓存策略
文件上传原生支持(multipart)需要额外处理
版本管理URL 版本(/v1/v2Schema 演进(@deprecated)
监控按 URL 区分,直观所有请求走同一端点,需按 operation 区分
适用场景简单 CRUD、开放 API、移动端复杂前端、BFF 层、多客户端差异查询

7.2 Schema 设计原则

  1. 客户端驱动:Schema 应面向客户端需求设计,而非直接映射数据库表结构
  2. 避免过度嵌套:关联深度控制在 3-4 层以内
  3. 使用 Connection 分页:Relay-style cursor 分页优于 offset 分页
  4. 合理使用 nullable:只有真正可能为 null 的字段才标记为 nullable
  5. Mutation 返回完整对象:方便客户端缓存更新
  6. Input 类型与 Object 类型分离:不要复用
// 推荐的分页设计
@ObjectType()
export class CatConnection {
  @Field(() => [CatEdge])
  edges: CatEdge[];

  @Field(() => PageInfo)
  pageInfo: PageInfo;

  @Field(() => Int)
  totalCount: number;
}

@ObjectType()
export class CatEdge {
  @Field(() => Cat)
  node: Cat;

  @Field()
  cursor: string;
}

@ObjectType()
export class PageInfo {
  @Field()
  hasNextPage: boolean;

  @Field()
  hasPreviousPage: boolean;

  @Field({ nullable: true })
  startCursor?: string;

  @Field({ nullable: true })
  endCursor?: string;
}

7.3 N+1 问题与 DataLoader

GraphQL 最常见的性能问题——N+1 查询:

# 查询 10 只猫和它们的主人
query {
  cats {          # 1 次查询:获取 10 只猫
    name
    owner {       # 10 次查询:每只猫查一次 owner
      name
    }
  }
}
# 总计 11 次数据库查询!

解决方案——DataLoader 批量加载:

npm i dataloader
import DataLoader from 'dataloader';

@Injectable({ scope: Scope.REQUEST })  // 必须 REQUEST 作用域
export class OwnersLoader {
  constructor(private readonly ownersService: OwnersService) {}

  // batchLoadFn:一次性加载多个 owner
  public readonly batchOwners = new DataLoader<string, Owner>(
    async (ownerIds: readonly string[]) => {
      const owners = await this.ownersService.findByIds([...ownerIds]);
      // 保证返回顺序与输入 id 顺序一致
      const ownersMap = new Map(owners.map((o) => [o.id, o]));
      return ownerIds.map((id) => ownersMap.get(id));
    },
  );
}

@Resolver(() => Cat)
export class CatsResolver {
  constructor(private readonly ownersLoader: OwnersLoader) {}

  @ResolveField('owner', () => Owner)
  getOwner(@Parent() cat: Cat) {
    // DataLoader 自动批量化:10 次 load 合并为 1 次 batchLoadFn
    return this.ownersLoader.batchOwners.load(cat.ownerId);
  }
}

结果:11 次查询 → 2 次查询(1 次猫 + 1 次批量 owner)。

7.4 Federation vs Schema Stitching

维度Apollo FederationSchema Stitching
维护方Apollo(官方推荐)社区(graphql-tools)
架构Subgraph + Gateway多个 schema 拼接
实体引用@key + @ResolveReference自定义合并逻辑
类型扩展原生支持(@extends需手动处理
生态工具Apollo Studio、Rover CLI较少
推荐度首选仅在特殊场景使用

7.5 GraphQL 安全检查清单

检查项方案
查询深度限制depthLimit 插件
查询复杂度限制graphql-query-complexity
速率限制@nestjs/throttler(配合 GqlThrottlerGuard)
认证授权Guard + @Extensions({ role })
字段级权限Field Middleware + Extensions
禁用 Introspection生产环境 introspection: false
请求大小限制Apollo Server csrfPrevention + body parser limit

八、课后实践

练习 1:GraphQL Code First 入门(基础)

为 CatsModule 实现 GraphQL 版本:

// 1. 安装依赖
// npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql

// 2. 创建 Cat ObjectType
@ObjectType()
export class Cat {
  @Field(() => ID)
  id: string;

  @Field()
  name: string;

  @Field(() => Int)
  age: number;
}

// 3. 创建 CreateCatInput
@InputType()
export class CreateCatInput {
  @Field()
  name: string;

  @Field(() => Int)
  age: number;
}

// 4. 创建 CatsResolver,实现 cats Query 和 createCat Mutation
// 5. 访问 http://localhost:3000/graphql 在 Playground 中测试

练习 2:实现 Subscription(中阶)

// 1. 安装 graphql-ws
// npm i graphql-ws

// 2. 配置 subscriptions: { 'graphql-ws': true }

// 3. 创建 PubSub 实例

// 4. 在 createCat Mutation 中 pubSub.publish('catAdded', ...)

// 5. 创建 @Subscription catAdded

// 6. 在 Playground 中打开两个标签页:
//    标签 1:subscription { catAdded { id name } }
//    标签 2:mutation { createCat(input: { name: "Tom", age: 3 }) { id } }

练习 3:查询复杂度限制(高阶)

// 1. 安装 graphql-query-complexity
// npm i graphql-query-complexity

// 2. 实现 ComplexityPlugin

// 3. 为 friends 字段设置 complexity: 10

// 4. 发送一个超过复杂度阈值的查询,验证被拦截

练习 4:阅读 @nestjs/graphql 源码(资深)

克隆 @nestjs/graphql 仓库,阅读以下文件:

  1. packages/graphql/lib/graphql.module.tsforRoot() 动态模块
  2. packages/graphql/lib/services/resolvers-explorer.service.ts — Resolver 发现
  3. packages/graphql/lib/graphql-schema.host.ts — SchemaHost 实现

回答:

  1. autoSchemaFile 是如何触发 schema 自动生成的?
  2. @Resolver() 装饰器注册了什么元数据?
  3. Schema First 模式下,类型定义文件是怎么生成的?

九、本课知识点总结

知识点要点
两种模式Code First(装饰器生成 SDL,推荐)vs Schema First(先写 .graphql 文件)
两种驱动Apollo(主流)vs Mercurius(Fastify 原生)
核心装饰器@ObjectType + @Field@Resolver + @Query + @Mutation + @ResolveField@Args + @InputType
类型系统@Scalar(自定义标量)、@InterfaceTypecreateUnionTyperegisterEnumType、映射类型(Partial/Pick/Omit/Intersection)
订阅PubSub + @Subscription + asyncIterableIterator,生产用 Redis PubSub
字段中间件@Field({ middleware }) 实现字段级 AOP
复杂度控制graphql-query-complexity 防止恶意深查询
CLI Plugin编译时自动添加 @Field(),减少 80% 样板代码
FederationApolloFederationDriver(Subgraph)+ ApolloGatewayDriver(Gateway),不支持 Subscription
N+1 问题DataLoader 批量加载,11 次查询 → 2 次
源码入口GraphQLModule.forRoot()DiscoveryService 扫描 Resolver → Driver 创建中间件

下一课预告:第十九课将学习 Standalone Application 和 CRON 任务,掌握 NestJS 在非 HTTP 场景下的应用,包括命令行工具、定时任务、Worker 进程等。