Gatsby GraphQL Schema Customization for Beginners

Gatsby has one big difference from other frameworks: it comes with a GraphQL data layer. That layer opens up a lot of possibilities and is a key piece of its extensive plugin ecosystem. With GraphQL you can pull data from a CMS, query images, or query markdown content thanks to a wide range of plugins.
All of this is possible because those plugins fetch data from their sources and turn it into GraphQL nodes during the build. In most cases that's more than enough, and you don't even need to master GraphQL to use Gatsby and build interesting things.
But today I want to talk about Gatsby's Schema Customization API, because in certain cases it can massively improve the quality of a project.
What Is Gatsby's Schema Customization API?
This API lets you interact with the GraphQL data layer and customize it however you like. It's used to extend existing GraphQL types or create entirely new ones.
Use Cases
If you're just starting out, you'll probably find these common scenarios useful:
- Overriding data in an existing GraphQL field
- Adding a new field to an existing GraphQL type and populating it with data
- Defining relationships between GraphQL types
Overriding Data in an Existing GraphQL Field
This technique is very useful when you want to define a default value for an existing field. For example, you have a blog with markdown articles, and some of them carry the draft: true flag while others don't. To filter better, you want to make sure every post has a draft field in GraphQL with a value of false or true.
There are two ways to achieve this. The first is to use the onCreateNode API (docs) that Gatsby provides. Just go to gatsby-node.js and add these lines.
exports.onCreateNode = ({ node, actions }) => { const { createNodeField } = actions; if (node.frontmatter) { createNodeField({ node, name: 'isDraft', value: node.frontmatter.isDraft || false, }); }};
This code runs every time a node is created, so it's a good idea to filter nodes by certain parameters so the field is only added where it belongs. It's a quick fix, but a bit crude. As you can see, the original isDraft field from the markdown frontmatter stays as it was, and we create a new one on that node, inside fields.
For better performance, we can use a more granular approach: overriding the original field with a custom resolver. This method requires a bit more knowledge. We'll need to know the exact GraphQL type we want to modify. To find out its name, we can start Gatsby with a special flag: GATSBY_GRAPHQL_IDE=playground npm start. That way we'll see GraphQL Playground instead of the GraphiQL IDE at http://localhost/__graphql, and we can get the information we need there.
Open the Schema panel and search for the type with Ctrl+F. Once you find it, you'll see the type we're looking for is MdxFrontmatter and that the isDraft field is of type Boolean. So, with the help of the createResolvers API (docs) declared in gatsby-node.js, we can define a default value for the field.
exports.createResolvers = ({ createResolvers }) => { createResolvers({ MdxFrontmatter: { isDraft: { type: 'Boolean', resolve: ({ isDraft }) => isDraft || false, }, }, });};
As you can see, the isDraft field no longer has any null values: only false or true.
Adding a New Field to an Existing GraphQL Type and Populating It with Data
We just saw how to modify existing data; now let's take it a step further and add a completely new field. Let's look at an example: imagine a website with open source project case studies stored in markdown, where you want to fetch each project's GitHub star count at build time. This is a perfect task for Gatsby's GraphQL schema customization.
For each case study we want to declare a githubStars field and tell Gatsby to fetch the data from the GitHub API. We've already used the createResolvers function to modify a field, but it also works for adding a new one.
From GraphQL Playground we can get the type used for markdown files, which will be Mdx, and add the new field like this.
exports.createResolvers = ({ createResolvers }) => { createResolvers({ Mdx: { githubStars: { type: 'String', resolve: async ({ frontmatter, fileAbsolutePath }) => { const { name, githubUsername, githubRepoName, isOpenSource } = frontmatter; if ( fileAbsolutePath.includes('/case-studies/') && isOpenSource && githubUsername && githubRepoName ) { try { const response = await fetch( `https://api.github.com/repos/${githubUsername}/${githubRepoName}` ); const { stargazers_count } = await response.json(); return new Intl.NumberFormat('en-US').format(stargazers_count); } catch (e) { throw new Error(`Failed to fetch GitHub stars for case study "${name}"`); } } return null; }, }, }, });};
The resolve function receives all the node's data, so it's easy to filter mdx files by certain rules, like the folder they're in. The resolver can be an asynchronous function, so you can also make external calls from there without any issue.
Defining Relationships Between GraphQL Types
Another common use case. You have a markdown blog and a list of authors in JSON. You want to pull all the data from the GraphQL layer, so the author appears nested inside the post's data. You can achieve this with the createSchemaCustomization hook in gatsby-node.js using the createTypes API (docs).
The structure of the author JSON object could look like this:
/// content/posts/post-authors.json[ { "name": "Jane Doe", "photo": "../../src/images/post-authors/jane-doe.jpg", "description": "Software engineer with 10+ years of web development experience. Currently focused on React, Next.js, Gatsby." }, ... more authors]
And the post's content might look something like this:
// content/posts/example.md---title: 'Taking automated web page screenshots with Puppeteer and Sharp'summary: A step-by-step tutorial on how to create perfect high-resolution web page screenshots automatically. No fuss, it just works.author: Jane Doecover: cover.jpgcategory: Development---... Blog post content
As you can see, we have the author's name in the post's frontmatter and that same name in authors.json. Now the goal is for the author to be accessible from GraphQL as a nested object when fetching the post's data.
Note: for blog posts we use gatsby-plugin-mdx, and to load the JSON data into GraphQL, gatsby-transformer-json.
Here's the gatsby-config.js configuration that makes that data loading possible:
{ resolve: 'gatsby-source-filesystem', options: { name: 'posts', path: `${__dirname}/content/posts`, }, }, { resolve: 'gatsby-source-filesystem', options: { name: 'post-authors', path: `${__dirname}/content/posts/post-authors.json`, }, },
We'll follow the same strategy as before: open GraphQL Playground. We already know the GraphQL type for markdown files, but we need to find the type for the JSON-based nodes. In our case it will be PostAuthorsJson.
With that information, we can add createSchemaCustomization to gatsby-node.js and use a function called createTypes. This technique lets us modify the GraphQL schema.
Note: if you want to fully override an existing GraphQL type, use the @dontInfer directive alongside the type definition (docs).
There are several options for manually defining nested relationships. But the simplest is to use the built-in @link directive. Think of it as a helper that maps two GraphQL types based on the foreign key relationships you give it.
Let's give it a try. Add this code to gatsby-node.js and check out the result.
exports.createSchemaCustomization = ({ actions }) => { const { createTypes } = actions; createTypes(` type Mdx implements Node { author: PostAuthorsJson @link(by: "name", from: "frontmatter.author") } `);};
Now the authors are linked to the markdown nodes. This way you can get nested data and even filter results by the author's information.
Summary
Now you know how to customize Gatsby's GraphQL schema. I hope you find it useful in your project.
