Wiring up subscriptions for GraphQL remote schema stitching
2019
GraphQL / 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}`);
})
Creating links to our remote schemas
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 connectionsapollo-link-ws
- allows us to send GraphQL requests over WebSocketsapollo-utilities
- utilities for dealing with Apollosubscriptions-transport-ws
- a GraphQL WebSocket clientws
- 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.