Tuesday, 23 June 2020

Introspecting a remote GraphQL schema

  • typescript
  • graphql
  • apollo

I recently built an application at Deliveroo using Next.js / TypeScript and GraphQL (Apollo specifically). It was pretty cool. But it was lacking some decent end to end tests. My framework of choice was of course going to be Cypress.io but then I ran into some issues..

An issue arises

My client-side application was all setup to talk to my backend Ruby on Rails application via GraphQL. I started setting up Cypress in my client app and hooking it up in my CI config. But then I noticed that there wasn't a reliable way of stubbing out GraphQL requests to my remote GraphQL API ☚ī¸ (or at least a solution didn't present itself to me at that exact moment in time).

So I settled on something half baked.. I decided to stub out any fetch calls on the client-side so that any HTTP requests matching the path /graphql were intercepted and then I created some JSON fixture files to return dependent on the GraphQL operationName in the request body. It worked, but it was a bit shit.

Why was it shit you ask? Because:

  • The mocking out of fetch just returned static JSON fixtures from the file system in the repository, which meant that any time I made tiny changes to the GraphQL schema on the Rails side, I would have to dig into a GraphQL response payload in development / production and copy the new payload structure into my fixtures.
  • Given that Next.js has server side rendering (SSR) built into it by default, in order for my window.fetch orientated mocking strategy to work, I had to disable SSR in my end to end testing environment, which in turn sometimes caused some unexpected environmental differences between the SSR-rendered pages and the client side rendered ones.

I was aware that the graphql-tools library provided the ability to introspect on a remote GraphQL schema and make it executable, but previous attempts at introspecting failed due to a variety of mundane reasons (auth issues, other projects taking precedence). This time I finally muddled through and got it working 👍

First I started creating a new mock-graphql-server service in my client side application's docker-compose.yml file:

version: '3'

    command: npm run start
      - "3003:3003"
      context: ./mockserver

My mock server (which will be built in TypeScript) has a very simple Dockerfile (your bog standard node one really):

FROM node:12-alpine

COPY . /app

RUN npm install --ignore-scripts
RUN npm run build

CMD ["npm", "run", "start"]

The Dockerfile above is fairly simple. All it does is builds my simple TypeScript / Node.js app, and defines the start command. The corresponding package.json commands are as follows:

  "name": "mockserver",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "tsc --project tsconfig.json",
    "start": "node /app/index.js"
  "author": "Adam Bull",
  "license": "ISC",
  "dependencies": {
    "@graphql-tools/mock": "^6.0.10",
    "@graphql-tools/schema": "^6.0.10",
    "apollo-link-http": "^1.5.17",
    "cross-fetch": "^3.0.5",
    "express": "^4.17.1",
    "express-graphql": "^0.9.0",
    "graphql": "^15.1.0",
    "graphql-tools": "^6.0.10",
    "i": "^0.3.6",
    "node-fetch": "^2.6.0",
    "npm": "^6.14.5",
    "typescript": "^3.9.5"
  "devDependencies": {
    "@deliveroo/tsconfig": "^1.0.0",
    "@types/express": "^4.17.6",
    "@types/express-graphql": "^0.9.0",
    "@types/node-fetch": "^2.5.7"

Now, all that remains is to implement the mock GraphQL server.

Creating the GraphQL mock server

npm start just boots up a very simple Express server:

import graphqlHTTP from 'express-graphql';

import express from 'express';

import schema from './schema';

const app = express();

schema().then((sc) => {
      schema: sc,
      graphiql: true,
      pretty: true,

app.listen(3003, () => {
  console.log('Running a GraphQL API server at http://localhost:3003/graphql');

I have opted to use the very lightweight express-graphql server for the purposes of creating the mock gql server. I guess you could use Apollo Server too.

As you can see, all of the remote schema introspection + mocking magic happens in the schema module...

import { fetch } from 'cross-fetch';
import { graphql, print } from 'graphql';
import {
} from 'graphql-tools';

interface ExecutorData {
  document: any;
  variables: any;
  context: any;

const executor = async ({ document, variables, context }: ExecutorData) => {
  const query = print(document);
  const fetchResult = await fetch('http://host.docker.internal:3000/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer banana`,
    body: JSON.stringify({ query, variables }),
  return fetchResult.json();

export default async () => {
  const remote = await introspectSchema(executor as AsyncExecutor);
  return addMocksToSchema({ schema: remote, preserveResolvers: false });

In recent versions of graphql-tools the API of introspectSchema has changed to receive an executor function which lets introspectSchema know where to send its introspection queries.

Given that this is just an MVP, and that I didn't have the ability to auth with my production backend GraphQL API (I didn't have a bearer token to hand!), I just configured the introspection query to point at my backend application locally. This app was not defined in the same docker-compose configuration as it is a separate repository, so I decided to make use of Docker's host.docker.internal keyword to automatically interpolate the IP of the Docker host machine.

The express-graphql library provides a graphiql playground out of the box which allowed me to go and experiment with some GraphQL queries to see if mocked data was being returned.


We have lift off!

I took some random GraphQL queries from my client app, and stuck them into the graphiql playground which is located at http://localhost:3003/graphql, and hey presto, mocked data is returned!

Interestingly, it looks like addMocksToSchema automatically stubs out the values returned based on data type (you can see that I've misconfigured the types for some of my ids as strings, hence why "Hello world" is returned for some instances of originId).