🎉 Explore GraphQLConf 2026 • May 19-21 • Fremont, CA • View the schedule
DocumentationGraphQL Harness

GraphQL Harness

GraphQLHarness is new in GraphQL.js v17. It customizes the phases used by graphql() and graphqlSync().

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

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

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

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:

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.

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.

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.