How to detect unused GraphQL operations

Guillaume Munsch
OVRSEA
Published in
4 min readDec 2, 2022

At Ovrsea, we use GraphQL for our API. After several years, our codebase got bigger and bigger and we ended up encountering an issue: how to detect if queries or mutations are still being used.

In general, we want to remove code that’s not used anymore to avoid maintaining it for nothing. This is costly and useless.

With GraphQL, this is not an easy task because GraphQL queries are not used via direct imports. This leads us to the following question:

How can you know if an operation is still being used or not?

First of all, let’s define that. We’ll call “operation” a query or a mutation.

After refactors, optimizations, product evolutions, … removing the unused operations from your codebase is a good practice. You could do this manually for each of them, digging through the codebase and watching its usage on Apollo Studio, but wouldn’t it be nice to have it done automatically?

Let’s create a script that does exactly this

🙃 TL;DR: The final version of this script is available via an NPM Package and all the code is open sourced HERE on Github.

This process is split in 3 steps:

  1. Find your GraphQL schema’s operations
  2. Find all operations usages inside the codebase
  3. Compare them 👍

This script will be written in TypeScript.

1. Find your GraphQL schema’s operations

Here we’ll first generate a .json version of our schema which is way easier to parse. We’ll do this with the NPM package graphql-codegen. You will need the following devDependencies:

"devDependencies": {
"@graphql-codegen/cli": "^2.12.1",
"@graphql-codegen/introspection": "^2.2.1",
"@graphql-codegen/typescript": "^2.7.3",
"@graphql-codegen/typescript-operations": "^2.5.3",
"@graphql-codegen/typescript-resolvers": "^2.7.3"
}

Now, let’s create graphql-codegen config file and name it config.yml.

#config.yml
overwrite: true

generates:
./graphql.schema.json:
schema: "./tests/schema.graphql"
plugins:
- "introspection"
config:
apolloClientVersion: 3

To generate the graphql.schema.json run the following command.

npx graphql-codegen -config config.yml

Now we can parse this json to extract the list of all available operations.

const schema = require("path/to/my/graphql.schema.json");

const findOperationsInSchemaByType = (
schema: Schema,
type: string
): SchemaField[] => {
const schemaFields = find(
schema.__schema.types,
(schemaType) => schemaType.name === type
);
return schemaFields?.fields ?? [];
};

const parseSchema = (schema: Schema): string[] => {
return [
...findOperationsInSchemaByType(schema, "Query"),
...findOperationsInSchemaByType(schema, "Mutation"),
].map((elem) => elem.name);
};

const schemaOperationsList = parseSchema(schema);

2. Find all operations usages inside the codebase

This isn’t an “easy” thing to achieve, at least manually. We’ll need to define all the files we want to check. For this we used Glob with the following pattern: "**/*.{ts,tsx,js}" .

const files = glob.sync("**/*.{ts,tsx,js}", { cwd: "" });

And now it gets a little bit tricky. To extract the operations from these files, we feared a manual and laborious parsing of each file’s AST. Thankfully, after digging through GraphQL Tools documentation, we found a CodeFileLoader that’ll do all this job for us 🎉. By “job” I mean, opening the file, reading it, parsing its AST, extracting only the GraphQL related elements. Read more about it HERE.

Basically this allows us to create such a function that will extract the operations used in a file.

const loadAndParseFile =
async (filePath: string): Promise<Operations> => {
try {
const sources = await loadDocuments(filePath, {
loaders: [new CodeFileLoader()],
});
const fileOperations = sources
.filter(({ document }) => document !== undefined)
.map(({ document }: Source) => {
// We'll explain this function right after
// "DocumentNode" is a type from "graphql"
return parseDocumentNode(document as DocumentNode);
})
.reduce((acc, currentOperations) => {
// "union" comes from lodash and just removes the duplicates
return union(acc, currentOperations);
}, []);
return fileOperations;
} catch (error) {
return [];
}
};

There we have it. We just need to write the method that parses the DocumentNode to extract the operations.

// DefinitionNode and OperationDefinitionNode come from "graphql"
const isOperationDefinitionNode = (
definitionNode: DefinitionNode
): definitionNode is OperationDefinitionNode =>
definitionNode.kind === "OperationDefinition";

const parseDocumentNode = (
node: DocumentNode
): string[] => {
const resolvedOperations =
node.definitions
.filter(isOperationDefinitionNode)
.map((definition: OperationDefinitionNode) => {
return definition.selectionSet.selections
.filter(isOperationDefinitionNode)
.map((selection) => {
return selection.name.value;
});
}) ?? [];
// "flatten" comes from lodash as well.
return flatten(resolvedOperations)
};

We now have a fully functional method called loadAndParseFile that allows us to extract all the operations used in a file.

We now just need to call this method for each file. We can do it like this:

const getAllOperations = async (
files: string[],
) => {
const parsedFiles = await Promise.all(files.map(loadAndParseFile));

return parsedFiles.reduce(
(acc, currentOperations) => {
return union(acc, currentOperations);
},
[]
);
};

const allUsedOperationList = await getAllOperations(files);

And there you have it, we have all the operations used in our codebase 🎉.

3. Compare them 👍

For this final step, we decided to simply use the difference method from Lodash that detects the differences between two arrays.

const unusedOperations = difference(
schemaOperationsList,
allUsedOperationList
);

Good job, you can now use this tool directly in your CI for example 🙂.

How did we use it?

As explained a little bit earlier in this article, at Ovrsea we worked on this problem and went a bit further. You can read about it on the package’s Github page.

We implemented several other options like the detection of unused fragments, a whitelist of operations that we do not want to remove (for ongoing development for instance) or even patterns to ignore some specific folders. Contributions are more than welcome 🙂.

How it helped us

Adding this package to our CI allowed us to remove 20 unused operations and 4 unused fragments right when we implemented it.

--

--