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 😊
npm
in some fashion or another.eslint
already being used in your application.For my examples below, I’m using the FaceBook reference implementation of GraphQL.
Let’s get started
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",
}
...
}
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.
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:
npm run snapshot
.Here’s how this looks in Jenkins:
Build
section of config, add a conditional step.Run
should be Strings match
.$GIT_BRANCH
.origin/master
.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!
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
.
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.
With all of that accomplished, you’re left with the following:
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!