Back to Graphql Js

GraphQL Harness

website/pages/docs/graphql-harness.mdx

17.0.14.4 KB
Original Source

import { Callout } from 'nextra/components';

GraphQL Harness

<Callout type="info"> `GraphQLHarness` is new in GraphQL.js v17. It customizes the phases used by `graphql()` and `graphqlSync()`. </Callout>

graphql() is the convenience entry point that parses, validates, and executes a GraphQL operation. In v17, those phases are represented by a harness:

ts
type GraphQLHarness = {
  parse: GraphQLParseFn;
  validate: GraphQLValidateFn;
  execute: GraphQLExecuteFn;
  subscribe: GraphQLSubscribeFn;
};

defaultHarness is the built-in harness used by graphql() and graphqlSync().

js
import { defaultHarness, graphql } from 'graphql';

const result = await graphql({
  schema,
  source,
  harness: defaultHarness,
});

Why this exists

The harness is a host integration API modeled after Envelop, The Guild's GraphQL plugin system. Envelop showed that many servers need to customize the same request phases: parsing, validation, execution, subscription execution, and the cross-cutting behavior around those phases.

GraphQL.js remains a reference implementation, not a full plugin framework. The harness brings the broader phase types used by that ecosystem closer to the reference implementation so frameworks and plugin systems can share a common shape. For example, GraphQLParseFn can return a DocumentNode or a promise for a DocumentNode, even though the built-in GraphQL.js parse() function is synchronous.

For application servers, prefer Envelop or a framework built on Envelop over using a raw GraphQLHarness directly. The goal is that frameworks can accept a custom harness, and plugin systems that customize these phases can interoperate without each framework inventing a different integration surface.

What can be customized

Each harness function receives the same arguments as the corresponding GraphQL.js phase. The difference is that a harness phase may finish immediately or by returning a promise:

ts
type MaybePromise<T> = T | Promise<T>;

type GraphQLParseFn = (
  source: string | Source,
  options?: ParseOptions,
) => MaybePromise<DocumentNode>;

type GraphQLValidateFn = (
  schema: GraphQLSchema,
  documentAST: DocumentNode,
  rules?: readonly ValidationRule[],
  options?: ValidationOptions,
) => MaybePromise<readonly GraphQLError[]>;

type GraphQLExecuteFn = (args: ExecutionArgs) => MaybePromise<ExecutionResult>;

type GraphQLSubscribeFn = (
  args: ExecutionArgs,
) => MaybePromise<
  ExecutionResult | AsyncGenerator<ExecutionResult, void, void>
>;

Any harness phase may return synchronously or asynchronously. graphqlSync() still requires every phase and resolver it reaches to complete synchronously. GraphQLExecuteFn deliberately returns only ExecutionResult; it does not include the experimental incremental delivery result type.

Cached documents

A host that has a trusted document cache can replace the parse phase while keeping the default validation and execution behavior.

js
import { defaultHarness, graphql } from 'graphql';

const harness = {
  ...defaultHarness,
  parse(source, options) {
    const cached = documents.get(String(source));
    return cached ?? defaultHarness.parse(source, options);
  },
};

const result = await graphql({
  schema,
  source,
  variableValues,
  operationName,
  harness,
});

External validation

A host can also replace validation. This is useful for persisted operation registries that validate at build time and return stored validation results at runtime.

js
import { defaultHarness, graphql } from 'graphql';

const harness = {
  ...defaultHarness,
  async validate(schema, document, rules, options) {
    const cached = await registry.getValidationResult(document, schema);
    return cached ?? defaultHarness.validate(schema, document, rules, options);
  },
};

const result = await graphql({
  schema,
  source,
  harness,
});

Relationship to incremental delivery

graphql() remains a single-result operation pipeline. A harness does not make graphql() return incremental delivery payloads, and the harness execute function has the same single-result contract.

Operations that use @defer or @stream should use experimentalExecuteIncrementally() after parsing and validation. See Advanced Execution Pipelines for the lower-level execution APIs and Defer and Stream for the incremental result shape.