Steve Holgado

Wiring up subscriptions for GraphQL remote schema stitching

2019GraphQL / Apollo / Node / Express

Stitching remote GraphQL schemas can be tricky, especially when you need to delegate subscriptions.

To keep this article brief, I will focus only on setting up the gateway server to handle subscriptions with remote GraphQL APIs.

Therefore, I’m going to assume that you already have remote GraphQL APIs running on other servers (even if just on localhost).

Given the topic of this article, I will also assume that your remote GraphQL servers accept subscriptions.

For simplicity in this article, I will refer to 3 fictional remote GraphQL endpoints:

  • remote-one.com/graphql
  • remote-two.com/graphql
  • remote-three.com/graphql

You can change these to the locations of your own remote APIs.

Table of contents

Setting up our gateway server with Express

We’re going to set up a basic Apollo server for our gateway. Let’s install it as a dependency, along with graphql:

npm install apollo-server graphql

Now we can set up a simple Apollo server:

const { ApolloServer, gql } = require('apollo-server')

// We can add types defs and resolvers for stitching logic here
const typeDefs = gql``
const resolvers = {}

const server = new ApolloServer({ typeDefs, resolvers })

server.listen().then(({ url, subscriptionsUrl }) => {
  console.log(`🚀 Server ready at ${url}`);
  console.log(`🚀 Subscriptions ready at ${subscriptionsUrl}`);
})

So, now we need to fetch the schemas from our remote GraphQL services.

To create an executable schema from a remote server we need to create an Apollo Link. Apollo Link is a standard interface for modifying control flow of GraphQL requests and fetching GraphQL results.

We’re going to need to install the apollo-link-http package for this.

Also, as Node.js does not have a native implementation of fetch, let’s also install node-fetch:

npm install apollo-link-http node-fetch

Let’s now create a new async function called getRemoteSchema where we can create our new http link, passing along our fetch implementation. We can then use it to create an executable schema which we can delegate to:

const { ApolloServer, gql, mergeSchemas, introspectSchema, makeRemoteExecutableSchema } = require('apollo-server')
const { HttpLink } = require('apollo-link-http')
const fetch = require('node-fetch')

async function getRemoteSchema({ uri }) {
  const link = new HttpLink({ uri, fetch })

  const schema = await introspectSchema(link)
  const executableSchema = makeRemoteExecutableSchema({ schema, link })

  return executableSchema
}

// We can add types defs and resolvers for stitching logic here
const typeDefs = gql``
const resolvers = {}

const server = new ApolloServer({ typeDefs, resolvers })

server.listen().then(({ url, subscriptionsUrl }) => {
  console.log(`🚀 Server ready at ${url}`);
  console.log(`🚀 Subscriptions ready at ${subscriptionsUrl}`);
})

Creating a merged schema

We can now use our new getRemoteSchema function to fetch and introspect the schemas from our remote services and merge them into a single schema for our gateway server.

We also need to wrap the server logic in an async function because we now need to await the fetching of our remote schemas:

const { ApolloServer, mergeSchemas, introspectSchema, makeRemoteExecutableSchema } = require('apollo-server')
const { HttpLink } = require('apollo-link-http')
const fetch = require('node-fetch')

async function getRemoteSchema({ uri }) {
  const link = new HttpLink({ uri, fetch })

  const schema = await introspectSchema(link)
  const executableSchema = makeRemoteExecutableSchema({ schema, link })

  return executableSchema
}

async function run() {
  const remoteSchema1 = await getRemoteSchema({ uri: 'http://remote-one.com/graphql' })
  const remoteSchema2 = await getRemoteSchema({ uri: 'http://remote-two.com/graphql' })
  const remoteSchema3 = await getRemoteSchema({ uri: 'http://remote-three.com/graphql' })

  // We can add types defs and resolvers for stitching logic here
  const typeDefs = gql``
  const resolvers = {}

  const schema = mergeSchemas({
    schemas: [
      remoteSchema1,
      remoteSchema2,
      remoteSchema3,
      typeDefs // Include type defs for stitching
    ],
    resolvers: resolvers, // Include resolvers for stitching
  })

  const server = new ApolloServer({ schema })

  server.listen().then(({ url, subscriptionsUrl }) => {
    console.log(`🚀 Server ready at ${url}`);
    console.log(`🚀 Subscriptions ready at ${subscriptionsUrl}`);
  })
}

run().catch(console.log)

Now that we have our remote schemas that we can delegate to, we can add any stitching logic that we may need.

I’m have not included any stitching logic as that will depend on the remote schemas that you are delegating to.

There are examples of this in the Apollo docs.

The problem with subscriptions

If you try this out now, your subscriptions that delegate to remote services will not work.

Currently, we can delegate to any of our remote schemas via the http links that we set up when creating the executable schemas. However, if our gateway receives a subscription request, it will require a WebSocket connection rather than a http connection.

So our browser will have a WebSocket connection to our gateway server, but our gateway server will not have a corresponding WebSocket connection to the remote server to which it is delegating.

Handling subscriptions to our remote servers

To fix this, we are going to need to improve our getRemoteSchema function to be able to handle WebSocket connections in order to delegate subscriptions.

We’re going to need to split the connection to our remote services to use the WebSocket link if the operation is a subscription, and the http link otherwise.

First, let’s install some dependencies:

npm install apollo-link apollo-link-ws apollo-utilities subscriptions-transport-ws ws
  • apollo-link - provides function to split connections
  • apollo-link-ws - allows us to send GraphQL requests over WebSockets
  • apollo-utilities - utilities for dealing with Apollo
  • subscriptions-transport-ws - a GraphQL WebSocket client
  • ws - a WebSocket implementation for Node.js

When creating our Apollo WebSocket link, we need to create a subscription client manually and give it a WebSocket implementation. In this case, we have installed the ws package.

We then create a new combined link using the split function from the apollo-link package, and we use that to create our executable schema:

const { ApolloServer, gql, mergeSchemas, introspectSchema, makeRemoteExecutableSchema } = require('apollo-server')
const { HttpLink } = require('apollo-link-http')
const { WebSocketLink } = require('apollo-link-ws')
const { split } = require('apollo-link')
const { getMainDefinition } = require('apollo-utilities')
const { SubscriptionClient } = require('subscriptions-transport-ws')
const fetch = require('node-fetch')
const ws = require('ws')

async function getRemoteSchema({ uri, subscriptionsUri }) {
  const httpLink = new HttpLink({ uri, fetch })
  
  // Create WebSocket link with custom client
  const client = new SubscriptionClient(subscriptionsUri, { reconnect: true }, ws)
  const wsLink = new WebSocketLink(client)

  // Using the ability to split links, we can send data to each link
  // depending on what kind of operation is being sent
  const link = split(
    operation => {
      const definition = getMainDefinition(operation.query)
      return (
        definition.kind === 'OperationDefinition' &&
        definition.operation === 'subscription'
      )
    },
    wsLink,  // <-- Use this if above function returns true
    httpLink // <-- Use this if above function returns false
  )

  const schema = await introspectSchema(httpLink)
  const executableSchema = makeRemoteExecutableSchema({ schema, link })

  return executableSchema
}

// ...

The split function takes three arguments. The first is a test function, which is passed the operation object. If the function returns true, the link passed as the second argument is used. If the function returns false, the link passed as the third argument is used.

If the operation is a subscription then we use the WebSocket connection. Otherwise, we use the regular http connection.

Finally, we need to go back to our run function and pass along the subscriptions uri to our getRemoteSchema calls:

// ...

async function run() {
  const remoteSchema1 = await getRemoteSchema({
    uri: 'http://remote-one.com/graphql',
    subscriptionsUri: 'ws://remote-one.com/graphql'
  })
  const remoteSchema2 = await getRemoteSchema({
    uri: 'http://remote-two.com/graphql',
    subscriptionsUri: 'ws://remote-two.com/graphql'
  })
  const remoteSchema3 = await getRemoteSchema({
    uri: 'http://remote-three.com/graphql',
    subscriptionsUri: 'ws://remote-three.com/graphql'
  })

  // ...
}

run().catch(console.log)

Testing it out

If you now start your server and test out your subscriptions, you should see that the connection is wired up to your remote servers correctly.

Final note: make sure that all of your remote services are running before launching the gateway server as it will need to connect to them to fetch the schemas.


Hi, I'm a Senior Front-End Developer based in London. Feel free to contact me with questions, opportunities, help with open source projects, or anything else :)

You can find me on GitHub, Stack Overflow or email me directly.