«

Setting up GraphQL's findBreakingChanges and eslint-plugin-graphql

When most folks talk about GraphQL features, a few things tend to surface commonly. Things like GraphiQL, declarative data, and consolidated network calls are distinguishing factors that I'm sure you've heard of.

Today, I'd like to talk about implementing a few other features that will make love GraphQL even more, and those are findBreakingChanges and eslint-plugin-graphql. Both are derived from having a type-schematic around your data, and because of that can be setup simultaneously with automation, which will immediately boost developer productivity.

Before we dive too far into this, let's go over a few requirements 😊

  1. npm in some fashion or another.
  2. eslint already being used in your application.
  3. Your GraphQL server must support introspection, which most do.

For my examples below, I'm using the FaceBook reference implementation of GraphQL.

Let's get started

1. Serialize your introspection

The first step in starting this is to serialize your GraphQL's introspection query into some sort of an artifact, in this case a JSON file. There's a few ways you can automate this in a CI environment (I'll get to that), so if this feels odd it'll become more clear why we're doing it this way in a few minutes.

For my use case, I have an npm script called snapshot that captures this, and a corresponding scripts/snapshot.js file:

#!/usr/bin/env node
const path = require('path');  
const fs = require('fs-extra');

// Pull in some helper utilities to isolate ourselves
// from crafting/copying the introspection query
const {  
  introspectionQuery,
  graphql,
} = require('graphql');

// Load the schema defined by our server
const schema = require('../src/server/schemas');

// Set a place in your app where this can be saved
const snapshotLocation = path.join(__dirname, '../integration', '__schema_snapshot__.json');

// Execute the introspection query on the schema and save the json
graphql(schema, introspectionQuery)  
  .then((res) => fs.writeJSONSync(snapshotLocation, res.data))
  .then(() => {
    console.log('Successfully snapshotted the schema!');
    process.exit(0);
  })
  .catch((e) => {
    console.log('Snapshotting was unsuccessful: ', e);
    process.exit(1);
  });

It's important to note that you should keep this file in version control so that both CI environments and other developers can use it to ensure their changes happen without unintentional breakages.

Once this is done, we can surface this script in package.json:

{
  "scripts": {
    ...
    "snapshot": "./scripts/snapshot.js",
  }
  ...
}

2. Add a test for breaking changes

Now that we have a JSON artifact of our application's schema, we can write a test assertion that prevents unwanted breaking changes from accidentally finding their way into our app. You can use whatever test framework or assertion library (I like jest).

Unsurprisingly, the test itself is fairly small:

const {  
  findBreakingChanges,
  buildClientSchema,
} = require('graphql');

const oldIntrospection = require('./__schema_snapshot__.json');

const oldSchema = buildClientSchema(oldIntrospection);  
const newSchema = require('../src/server/schemas');

describe('Breaking Changes', () => {  
  it('there should be no breaking changes', () => {
    const breaking = findBreakingChanges(oldSchema, newSchema);

    // `breaking` here will be an array of breakages if they exist, if not it's empty
    expect(breaking).toEqual([]);
  });
});

Nice! So now we have a cache of the previous declaration of our GraphQL schema, and way to assert that nothing breaks there, but there's a problem: developers will have manually update this JSON file whenever there's changes, and failures to do so could result in merge conflicts or, even worse, a breaking change!

To get around this, we can lean on our CI environment to keep this up-to-date for us automatically.

3. Automate snapshotting

Before I dive into this, just know that this can be accomplished in varying ways depending on what you use for a CI environment, so I'll conceptually go over what we're trying to achieve, and give you some code for the Jenkins case.

At a high-level, we want to do the following when the master branch changes:

Here's how this looks in Jenkins:

  1. In the Build section of config, add a conditional step.
  2. Run should be Strings match.
  3. String 1 is $GIT_BRANCH.
  4. String 2 is origin/master.
  5. Builder is Execute Shell.

Here's the script to execute:

# Take a snapshot of the schema definition
# so future schema changes can be vetted against
git checkout master  
git reset origin/master --hard  
npm run snapshot  
if [[ -n $(git status --porcelain) ]]; then  
  echo "Schema changes found, saving to snapshot"
  git commit -m 'JENKINS: Updating schema snapshot on master' ./integration/__schema_snapshot__.json
  git push origin master
fi  

Whew! This is the hardest part at this point, and everything is downhill from here!

4. Publish your snapshot to npm

You can likely automate this part of the process as well, but I'm old-fashioned in the fact that I like to publish packages manually. Sadly, this part really isn't optional as the eslint-plugin-graphql needs some sort of artifact in order to run lint rules against, so having that artifact published someplace is a requirement.

An alternative approach to this is to force consumers to query and maintain their own schema-snapshot to lint against, but this puts a pretty significant burden on potentially many application vs maintaining it at the source.

I'll also throw out that a lot of internal CI environments don't have access to the outside internet, so forcing consumers down this path is a non-starter in many cases. This, plus the fact that it will also add a lot of HTTP load on your back-end infrastructure since developers will be pulling down introspection queries numerous times versus referencing it through npm.

5. Install the eslint plugin into your app

eslint-plugin-graphql operates just like any other eslint plugin, which one exception: your .eslintrc file now becomes a .js file.

This is due to the fact that you must load the schema JSON via a require call. While this might sound less than ideal, remember that you now will have your API queries linted at developer time, which is a very small trade-off for a lot of upside.

All of that, run the following command:

npm install --save-dev eslint-plugin-graphql your-schema-snapshot-package

Then, add the following to your .eslint.js file:

module.exports = {  
  ...
  rules: {
    ...
    "graphql/template-strings": [
      "error",
      {
        env: "apollo",
        schemaJson: require(
          "your-npm-package/integration/__schema_snapshot__.json"
        )
      }
    ]
  },
  plugins: ["graphql"]
};

There's a lot more options and configurations you can make, documented in depth here.

Summary

With all of that accomplished, you're left with the following:

  1. An automated way to ensure your GraphQL server never breaks.
  2. Automation around keeping that snapshot up-to-date.
  3. Linting and automation around GraphQL queries.

Once this was plugged into the application I help maintain, we surfaced two bugs in consuming code that could have potentially cost us real time and money. I also feel much better merging in pull-requests as the automation around breaking changes guards us against unwanted regressions. So much winning!

I hope you've enjoyed this and found it useful! Please share your comments and thoughts below, and thank you!

Share Comment on Twitter