• Docs
  • Remote Schemas

Remote Schemas

It can be valuable to be able to treat remote GraphQL endpoints as if they were local executable schemas. This is especially useful for schema stitching, but there may be other use cases.

💡

Watch Episode #12 of graphql.wtf for a quick introduction to remote schemas:

There are two ways to create remote schemas 👇:

Use Loaders to Load Schemas Easily

Check out Schema Loading to load schemas from an URL and/or different sources easily without implementing an executor.

Create a Remote Executable Schema with Custom Executor Methods

Generally, to create a remote schema, you generally need just three steps:

  1. Create an executor that can retrieve results from that schema
  2. Use introspectSchema to get the non-executable schema of the remote server
  3. Use wrapSchema to create a schema that uses the executor to delegate requests to the underlying service

Creating an Executor

You can use an executor with an HTTP Client implementation (like cross-fetch). An executor is a function capable of retrieving GraphQL results. It is the same way that a GraphQL Client handles fetching data and is used by several graphql-tools features to do introspection or fetch results during execution.

We've chosen to split this functionality up to give you the flexibility to choose when to do the introspection step. For example, you might already have the remote schema information, allowing you to skip the introspectSchema step entirely. Here's a complete example:

type Executor = (request: Request) => Promise<ExecutionResult>
 
type Request = {
  document: DocumentNode
  variables?: Object
  context?: Object
  info?: GraphQLResolveInfo
}

GraphQL over HTTP Executor with @whatwg-node/fetch

Basic usage

import { fetch } from '@whatwg-node/fetch'
import { print } from 'graphql'
import { introspectSchema, wrapSchema } from '@graphql-tools/wrap'
import { AsyncExecutor } from '@graphql-tools/utils'
 
const executor: AsyncExecutor = async ({ document, variables }) => {
  const query = print(document)
  const fetchResult = await fetch('http://example.com/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ query, variables })
  })
  return fetchResult.json()
}
 
export default async () => {
  const schema = wrapSchema({
    schema: await introspectSchema(executor),
    executor
  })
  return schema
}

Authentication headers from the context

import { fetch } from '@whatwg-node/fetch'
import { print } from 'graphql'
import { introspectSchema, wrapSchema } from '@graphql-tools/wrap'
import { AsyncExecutor } from '@graphql-tools/utils'
 
const executor: AsyncExecutor = async ({ document, variables, context }) => {
  const query = print(document)
  const fetchResult = await fetch('http://example.com/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${context.authKey}`
    },
    body: JSON.stringify({ query, variables })
  })
  return fetchResult.json()
}
 
export default async () => {
  const schema = wrapSchema({
    schema: await introspectSchema(executor),
    executor
  })
 
  return schema
}

Executor for Subscriptions and More

Currently, the GraphQL over HTTP specification does specify how subscription operations can be executed. However, other protocols such as graphql-ws and graphql-sse, filled in the gap and allow the execution of query, mutation and subscription operations. We can define our custom executor for those protocols. It is also possible to define a hybrid executor that executes query and mutation operations over HTTP and subscription operations over a different protocol (WebSocket or SSE).

GraphQL over WebSocket with graphql-ws

For the following example to work, the server must implement the library's transport protocol. Learn more about graphql-ws.

With this executor, all operations (query, mutation and subscription) will be executed over graphql-ws.

import { wrapSchema, introspectSchema } from '@graphql-tools/wrap'
import { observableToAsyncIterable, AsyncExecutor } from '@graphql-tools/utils'
import { createClient } from 'graphql-ws'
import { print } from 'graphql'
 
const WS_GRAPHQL_ENDPOINT = 'ws://localhost:3000/graphql'
 
const subscriptionClient = createClient({
  url: WS_GRAPHQL_ENDPOINT
})
 
const executor: AsyncExecutor = async ({ document, variables }) =>
  observableToAsyncIterable({
    subscribe: observer => ({
      unsubscribe: subscriptionClient.subscribe(
        {
          query: print(document),
          variables: variables as Record<string, any>
        },
        {
          next: data => observer.next && observer.next(data as unknown),
          error(err) {
            if (!observer.error) return
            if (err instanceof Error) {
              observer.error(err)
            } else if (err instanceof CloseEvent) {
              observer.error(new Error(`Socket closed with event ${err.code}`))
            } else if (Array.isArray(err)) {
              // GraphQLError[]
              observer.error(new Error(err.map(({ message }) => message).join(', ')))
            }
          },
          complete: () => observer.complete?.()
        }
      )
    })
  })
 
export default async () => {
  const schema = wrapSchema({
    schema: await introspectSchema(executor),
    executor
  })
 
  return schema
}

GraphQL over SSE with graphql-sse

For the following example to work, the server must implement the GraphQL over Server-Sent Events Protocol. Learn more about graphql-sse.

With this executor, all operations (query, mutation and subscription) will be executed over graphql-sse.

import { wrapSchema, introspectSchema } from '@graphql-tools/wrap'
import { AsyncExecutor } from '@graphql-tools/utils'
import { fetch, AbortController } from '@whatwg-node/fetch'
import { observableToAsyncIterable } from '@graphql-tools/utils'
import { createClient } from 'graphql-sse'
import { print } from 'graphql'
 
const SSE_GRAPHQL_ENDPOINT = 'http://localhost:3000/graphql/stream'
 
const subscriptionClient = createClient({
  singleConnection: true,
  url: SSE_GRAPHQL_ENDPOINT,
  fetchFn: fetch,
  abortControllerImpl: AbortController
})
 
const executor: AsyncExecutor = async ({ document, variables, operationName, extensions }) =>
  observableToAsyncIterable({
    subscribe: observer => ({
      unsubscribe: subscriptionClient.subscribe(
        {
          query: print(document),
          variables: variables as Record<string, unknown>,
          operationName,
          extensions
        },
        {
          next: data => observer.next?.(data as unknown),
          error(err) {
            if (!observer.error) return
            if (err instanceof Error) {
              observer.error(err)
            } else if (err instanceof CloseEvent) {
              observer.error(new Error(`Socket closed with event ${err.code}`))
            } else if (Array.isArray(err)) {
              // GraphQLError[]
              observer.error(new Error(err.map(({ message }) => message).join(', ')))
            }
          },
          complete: () => observer.complete && observer.complete()
        }
      )
    })
  })
 
export default async () => {
  const schema = wrapSchema({
    schema: await introspectSchema(executor),
    executor
  })
 
  return schema
}

Hybrid GraphQL over HTTP and GraphQL over WebSocket (graphql-ws)

Sometimes you only want to do subscription operations over WebSocket. In that case, you have identified the operation and then call the corresponding executor for the operation type.

With this executor query and mutation operations will be executed over HTTP (using @whatwg-node/fetch) and subscription operations will be executed via WebSocket (using graphql-ws).

import { wrapSchema, introspectSchema } from '@graphql-tools/wrap'
import { fetch } from '@whatwg-node/fetch'
import { print, getOperationAST, OperationTypeNode } from 'graphql'
import { observableToAsyncIterable, AsyncExecutor } from '@graphql-tools/utils'
import { createClient } from 'graphql-ws'
 
const HTTP_GRAPHQL_ENDPOINT = 'http://localhost:3000/graphql'
const WS_GRAPHQL_ENDPOINT = 'ws://localhost:3000/graphql'
 
const subscriptionClient = createClient({
  url: WS_GRAPHQL_ENDPOINT
})
 
const httpExecutor: AsyncExecutor = async ({ document, variables, operationName, extensions }) => {
  const query = print(document)
  const fetchResult = await fetch(HTTP_GRAPHQL_ENDPOINT, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ query, variables, operationName, extensions })
  })
  return fetchResult.json()
}
 
const wsExecutor: AsyncExecutor = async ({ document, variables, operationName, extensions }) =>
  observableToAsyncIterable({
    subscribe: observer => ({
      unsubscribe: subscriptionClient.subscribe(
        {
          query: print(document),
          variables: variables as Record<string, any>,
          operationName,
          extensions
        },
        {
          next: data => observer.next?.(data as unknown),
          error(err) {
            if (!observer.error) return
            if (err instanceof Error) {
              observer.error(err)
            } else if (err instanceof CloseEvent) {
              observer.error(new Error(`Socket closed with event ${err.code}`))
            } else if (Array.isArray(err)) {
              // GraphQLError[]
              observer.error(new Error(err.map(({ message }) => message).join(', ')))
            }
          },
          complete: () => observer.complete?.()
        }
      )
    })
  })
 
const executor: AsyncExecutor = async args => {
  // get the operation node of from the document that should be executed
  const operation = getOperationAST(args.document, args.operationName)
  // subscription operations should be handled by the wsExecutor
  if (operation?.operation === OperationTypeNode.SUBSCRIPTION) {
    return wsExecutor(args)
  }
  // all other operations should be handles by the httpExecutor
  return httpExecutor(args)
}
 
export default async () => {
  const schema = wrapSchema({
    schema: await introspectSchema(executor),
    executor
  })
 
  return schema
}

API

introspectSchema(executor, [context])

Use executor to obtain a non-executable client schema from a remote schema using a full introspection query. introspectSchema is used to acquire the non-executable form of a remote schema that must be passed to wrapSchema. It returns a promise to a non-executable GraphQL.js schema object. Accepts optional second argument context, which is passed to the executor; see the docs about executors above for more details.

import { introspectSchema } from '@graphql-tools/wrap'
 
introspectSchema(executor).then(schema => {
  // use the schema
})
 
// or with async/await:
const schema = await introspectSchema(executor)

wrapSchema(schemaConfig)

wrapSchema comes most in handy when wrapping a remote schema. When using the function to wrap a remote schema, it takes a single object: an subschema configuration object with properties describing how the schema should be accessed and wrapped. The schema and executor options are required.

import { wrapSchema } from '@graphql-tools/wrap'
 
const schema = wrapSchema({ schema, executor, transforms })

Transforms are further described within the general schema wrapping section. When using a schema configuration object, transforms should be placed as a property within the configuration, rather than as a separate argument to wrapSchema.

Batching and caching can be accomplished by specifying customized executors that manage this for you. We export a linkToExecutor function in @graphql-tools/links package that can be used to transform the HTTPLinkDataloader Apollo-style link (created by Prisma) that will batch and cache all requests. Per request caching is a simple add-on, as the executor function is provided in the context, so a global executor specified by wrapSchema can simply forward all arguments to a request-specific executor provided on the context.

For users who need to customize the root proxying resolvers at the time that the wrapping schema is generated, you can also specify a custom createProxyingResolver function that will create your own root resolvers for the new outer, wrapping schema. This function has the following signature:

export type CreateProxyingResolverFn = (options: ICreateProxyingResolverOptions) => GraphQLFieldResolver<any, any>
 
export interface ICreateProxyingResolverOptions {
  subschemaConfig: SubschemaConfig // target schema config for delegation
  transformedSchema?: GraphQLSchema // pre-processed result of applying any transforms to the target schema
  operation?: Operation // target operation type = 'query' | 'mutation' | 'subscription'
  fieldName?: string // target root field name
}

You may not need all the options to accomplish what you need. For example, the default proxying resolver creator just uses a subset of the passed arguments, with the fieldName inferred:

import { delegateToSchema } from '@graphql-tools/delegate'
 
export function defaultCreateProxyingResolver({
  subschemaConfig,
  operation
}: ICreateProxyingResolverOptions): GraphQLFieldResolver<any, any> {
  return (_parent, _args, context, info) =>
    delegateToSchema({
      schema: subschemaConfig,
      operation,
      context,
      info
    })
}

Parenthetically, note that args from the root field resolver are not directly passed to the target schema. These arguments have already been parsed into their corresponding internal values by the GraphQL execution algorithm. The correct, serialized form of the arguments is available within the info object, ready for proxying. Specifying the args property for delegateToSchema allows one to pass additional arguments to the target schema, which is not necessary when creating a simple proxying schema.

The above can all be put together like this:

import { wrapSchema } from '@graphql-tools/wrap'
 
const schema = wrapSchema({
  schema,
  executor: myCustomExecutor,
  createProxyingResolver: myCustomCreateProxyingResolverFn
})

Note that within the defaultCreateProxyingResolver function, delegateToSchema receives executor function stored on the subschema config object originally passed to wrapSchema. As above, use of the the createProxyingResolver option is helpful when you want to customize additional functionality at resolver creation time. If you just want to customize how things are proxied at the time that they are proxied, you can make do just with custom executors.

Last updated on October 4, 2022