Customizing the GraphQL Schema
One of Gatsby’s main strengths is the ability to query data from a variety of sources in a uniform way with GraphQL. For this to work, a GraphQL Schema must be generated that defines the shape of the data.
Gatsby is able to automatically infer a GraphQL Schema from your data, and in many cases this is really all you need. There are however situations when you either want to explicitly define the data shape, or add custom functionality to the query layer - this is what Gatsby’s Schema Customization API provides.
The following guide walks through some examples to showcase the API.
This guide is aimed at plugin authors, users trying to fix GraphQL schemas created by automatic type inference, developers optimising builds for larger sites, and anyone interested in customizing Gatsby’s schema generation. As such, the guide assumes that you’re somewhat familiar with GraphQL types and with using Gatsby’s Node APIs.
Explicitly defining data types
Our example project is a blog that gets its data from local Markdown files which provide the post contents, as well as author information in JSON format. We also have occasional guest contributors whose info we keep in a separate JSON file.
---
title: Sample Post
publishedAt: 2019-04-01
author: jane@example.com
tags:
- wow
---
# Heading
Text[
{
"name": "Doe",
"firstName": "Jane",
"email": "jane@example.com",
"joinedAt": "2018-01-01"
}
][
{
"name": "Doe",
"firstName": "Zoe",
"email": "zoe@example.com",
"receivedSwag": true
}
]To be able to query the contents of these files with GraphQL, we need to first
load them into Gatsby’s internal data store. This is what source and transformer
plugin accomplish - in this case gatsby-source-filesystem and
gatsby-transformer-remark plus gatsby-transformer-json. Every markdown post
file is hereby transformed into a “node” object in the internal data store with
a unique id and a type MarkdownRemark. Similarly, an author will be
represented by a node object of type AuthorJson, and contributor info will be
transformed into node objects of type ContributorJson.
The Node interface
This data structure is represented in Gatsby’s GraphQL schema with the Node
interface, which describes the set of fields common to node objects created by
source and transformer plugins (id, parent, children, as well as a couple
of internal fields like type). In GraphQL Schema Definition Language (SDL),
it looks like this:
interface Node {
id: ID!
parent: Node!
children: [Node!]!
internal: Internal!
}
type Internal {
type: String!
}Types created by source and transformer plugins implement this interface. For
example, the node type created by gatsby-transformer-json for authors.json
will be represented in the GraphQL schema as:
type AuthorJson implements Node {
id: ID!
parent: Node!
children: [Node!]!
internal: Internal!
name: String
firstName: String
email: String
joinedAt: Date
}A quick way to inspect the schema generated by Gatsby is the GraphQL Playground. Start your project with
GATSBY_GRAPHQL_IDE=playground gatsby develop, open the playground athttp://localhost:8000/___graphqland inspect theSchematab on the right.
Automatic type inference
It’s important to note that the data in author.json does not provide type
information of the Author fields by itself. In order to translate the data
shape into GraphQL type definitions, Gatsby has to inspect the contents of
every field and check its type. In many cases this works very well and it is
still the default mechanism for creating a GraphQL schema.
There are however two problems with this approach: (1) it is quite time-consuming and therefore does not scale very well and (2) if the values on a field are of different types Gatsby cannot decide which one is the correct one. A consequence of this is that if your data sources change, type inference could suddenly fail.
Both problems can be solved by providing explicit type definitions for Gatsby’s GraphQL schema.
Creating type definitions
Let’s take the latter case first. Assume a new author joins the team, but in the
new author entry there is a typo on the joinedAt field: “201-04-02” which is
not a valid Date.
+ {
+ "name": "Doe",
+ "firstName": "John",
+ "email": "john@example.com",
+ "joinedAt": "201-04-02"
+ }
]This will confuse Gatsby’s type inference since the joinedAt
field will now have both Date and String values.
Fixing field types
To ensure that the field will always be of Date type, you can provide explicit
type definitions to Gatsby with the createTypes action.
It accepts type definitions in GraphQL Schema Definition Language:
exports.sourceNodes = ({ actions }) => {
const { createTypes } = actions
const typeDefs = `
type AuthorJson implements Node {
joinedAt: Date
}
`
createTypes(typeDefs)
}Note that the rest of the fields (name, firstName etc.) don’t have to be
provided, they will still be handled by Gatsby’s type inference.
Although the
createTypesaction is passed to allgatsby-nodeAPIs, it has to be called before schema generation. We recommend to use thesourceNodesAPI.
Opting out of type inference
There are however advantages to providing full definitions for a node type, and bypassing the type inference mechanism altogether. With smaller scale projects inference is usually not a performance problem, but as projects grow the performance penalty of having to check each field type will become noticable.
Gatsby allows to opt out of inference with the @dontInfer type directive - which
in turn requires that you explicitly provide type definitions for all fields
that should be available for querying:
exports.sourceNodes = ({ actions }) => {
const { createTypes } = actions
const typeDefs = `
type AuthorJson implements Node @dontInfer {
name: String!
firstName: String!
email: String!
joinedAt: Date
}
`
createTypes(typeDefs)
}Note that you don’t need to explicitly provide the Node interface fields (id,
parent, etc.), Gatsby will automatically add them for you.
If you wonder about the exclamation marks - those allow specifying nullability in GraphQL, i.e. if a field value is allowed to be
nullor not.
Nested types
So far we have only been dealing with scalar values (String and Date;
GraphQL also knows ID, Int, Float, Boolean and JSON). Fields can
however also contain complex object values. To target those fields in GraphQL SDL, you
can provide a full type definition for the nested type, which can be arbitrarily
named (as long as the name is unique in the schema). In our example project, the
frontmatter field on the MarkdownRemark node type is a good example. Say we
want to ensure that frontmatter.tags will always be an array of strings.
exports.sourceNodes = ({ actions }) => {
const { createTypes } = actions
const typeDefs = `
type MarkdownRemark implements Node {
frontmatter: Frontmatter
}
type Frontmatter {
tags: [String!]!
}
`
createTypes(typeDefs)
}Note that with createTypes we cannot directly target a Frontmatter type
without also specifying that this is the type of the frontmatter field on the
MarkdownRemark type, The following would fail because Gatsby would have no way
of knowing which field the Frontmatter type should be applied to:
exports.sourceNodes = ({ actions }) => {
const { createTypes } = actions
const typeDefs = `
# This will fail!!!
type Frontmatter {
tags: [String]!
}
`
createTypes(typeDefs)
}It is useful to think about your data, and the corresponding GraphQL schema, by always starting from the Node types created by source and transformer plugins.
Note that the
Frontmattertype must not implement the Node interface since it is not a top-level type created by source or transformer plugins: it has noidfield, and is just there to describe the data shape on a nested field.
Gatsby Type Builders
In many cases, GraphQL SDL provides a succinct way to provide type definitions
for your schema. If however you need more flexibility, createTypes also
accepts type definitions provided with the help of Gatsby Type Builders, which
are more flexible than SDL syntax but less verbose than graphql-js. They are
accessible on the schema argument passed to Node APIs.
exports.sourceNodes = ({ actions, schema }) => {
const { createTypes } = actions
const typeDefs = [
schema.buildObjectType({
name: "ContributorJson",
fields: {
name: "String!",
firstName: "String!",
email: "String!",
receivedSwag: {
type: "Boolean",
resolve: source => source.receivedSwag || false,
},
},
interfaces: ["Node"],
}),
]
createTypes(typeDefs)
}Gatsby Type Builders allow referencing types as simple strings, and accept full
field configs (type, args, resolve). When defining top-level types, don’t forget
to pass interfaces: ['Node'], which does the same for Type Builders as adding
implements Node does for SDL-defined types. It is also possible to opt out of type
inference with Type Builders by setting the infer type extension to false:
schema.buildObjectType({
name: "ContributorJson",
fields: {
name: "String!",
},
interfaces: ["Node"],
+ extensions: {
+ // While in SDL we have two different directives, @infer and @dontInfer to
+ // control inference behavior, Gatsby Type Builders take a single `infer`
+ // extension which accepts a Boolean
+ infer: false
+ },
}),Type Builders also exist for Input, Interface and Union types:
buildInputType,buildInterfaceType, andbuildUnionType. Note that thecreateTypesaction also acceptsgraphql-jstypes directly, but usually either SDL or Type Builders are the better alternatives.
Foreign-key fields
Gatsby’s automatic type inference has one trick up its sleeve: for every field
that ends in ___NODE it will interpret the field value as an id and create a
foreign-key relation.
Creating foreign-key relations with the createTypes action,
i.e. without relying on type inference and the ___NODE field naming
convention, requires a bit of manual setup.
In our example project, we want the frontmatter.author field on
MarkdownRemark nodes to expand the provided field value to a full AuthorJson node.
For this to work, we have to provide a custom field resolver. (see below for
more info on context.nodeModel)
exports.sourceNodes = ({ action, schema }) => {
const { createTypes } = actions
const typeDefs = [
"type MarkdownRemark implements Node { frontmatter: Frontmatter }",
schema.buildObjectType({
name: "Frontmatter",
fields: {
author: {
type: "AuthorJson",
resolve: (source, args, context, info) => {
// If we were linking by ID, we could use `getNodeById` to
// find the correct author:
// return context.nodeModel.getNodeById({
// id: source.author,
// type: "AuthorJson",
// })
// But since we are using the author email as foreign key,
// we can use `runQuery`, or simply get all author nodes
// with `getAllNodes` and manually find the linked author
// node:
return context.nodeModel
.getAllNodes({ type: "AuthorJson" })
.find(author => author.email === source.author)
},
},
},
}),
]
createTypes(typeDefs)
}What is happening here is that we provide a custom field resolver that asks
Gatsby’s internal data store for the the full node object with the specified
id and type.
Because creating foreign-key relations is such a common usecase, Gatsby luckily also provides a much easier way to do this — with the help of extensions or directives. It looks like this:
type MarkdownRemark implements Node {
frontmatter: Frontmatter
}
type Frontmatter {
author: AuthorJson @link # default foreign-key relation by `id`
reviewers: [AuthorJson] @link(by: "email") # foreign-key relation by custom field
}
type AuthorJson implements Node {
posts: [MarkdownRemark] @link(by: "frontmatter.author", from: "email") # easy back-ref
}You simply provide a @link directive on a field and Gatbsy will internally
add a resolver that is quite similar to the one we wrote manually above. If no
argument is provided, Gatsby will use the id field as the foreign-key,
otherwise the foreign-key has to be provided with the by argument. The
optional from argument allows getting the foreign-keys from the specified
field, which is especially helpful when adding a field for back-linking.
Note that when using
createTypesto fix type inference for a foreign-key field created by a plugin, the underlying data will probably live on a field with a___NODEsuffix. Use thefromargument to point thelinkextension to the correct field name. For example:author: [AuthorJson] @link(from: "author___NODE").
Extensions and directives
Out of the box, Gatsby provides four extensions
that allow adding custom functionality to fields without having to manually
write field resolvers: the link extension has already been discussed above,
dateformat allows adding date formatting options, fileByRelativePath is
similar to link but will resolve relative paths when linking to File nodes,
and proxy is helpful when dealing with data that contains field names with
characteres that are invalid in GraphQL.
To add an extension to a field you can either use a directive in SDL, or the
extensions property when using Gatsby Type Builders:
exports.sourceNodes = ({ action, schema }) => {
const { createTypes } = actions
const typeDefs = [
"type MarkdownRemark implements Node { frontmatter: Frontmatter }",
`type Frontmatter {
publishedAt: Date @dateformat(formatString: "DD-MM-YYYY")
}`
schema.buildObjectType({
name: 'AuthorJson',
fields: {
joinedAt: {
type: 'Date',
extensions: {
dateformat: {}
}
}
}
})
]
createTypes(typeDefs)
}The above example adds date formatting options
to the AuthorJson.joinedAt and the MarkdownRemark.frontmatter.publishedAt
fields. Those options are available as field arguments when querying those fields:
query {
allAuthorJson {
joinedAt(fromNow: true)
}
}For publishedAt we also provide a default formatString which will be used
when no explicit formatting options are provided in the query.
Setting default field values
For setting default field values, Gatsby currently does not (yet) provide an
out-of-the-box extension, so resolving a field to a default value (instead of
null) requires manually adding a field resolver. For example, to add a default
tag to every blog post:
exports.sourceNodes = ({ action, schema }) => {
const { createTypes } = actions
const typeDefs = [
"type MarkdownRemark implements Node { frontmatter: Frontmatter }",
schema.buildObjectType({
name: "Frontmatter",
fields: {
tags: {
type: "[String!]",
resolve(source, args, context, info) {
// For a more generic solution, we could pick the field value from
// `source[info.fieldName]`
const { tags } = source
if (source.tags == null || (Array.isArray(tags) && !tags.length)) {
return ["uncategorized"]
}
return tags
},
},
},
}),
]
createTypes(typeDefs)
}createResolvers API
While it is possible to directly pass args and resolvers along the type
definitions using Gatsby Type Builders, an alternative approach specifically
tailored towards adding custom resolvers to fields is the createResolvers Node
API.
exports.createResolvers = ({ createResolvers }) => {
const resolvers = {
Frontmatter: {
author: {
resolve(source, args, context, info) {
return context.nodeModel.getNodeById({
id: source.author,
type: "AuthorJson",
})
},
},
},
}
createResolvers(resolvers)
}Note that createResolvers allows adding new fields to types, modifying args
and resolver — but not overriding the field type. This is because
createResolvers is run last in schema generation, and modifying a field type
would mean having to regenerate corresponding input types (filter, sort),
which we want to avoid. If possible, specifying field types should be done with
the createTypes action.
Accessing Gatsby’s data store from field resolvers
As mentioned above, Gatsby’s internal data store and query capabilities are
available to custom field resolvers on the context.nodeModel argument passed
to every resolver. Accessing node(s) by id (and optional type) is possible
with getNodeById and
getNodesByIds. To get all nodes, or all
nodes of a certain type, use getAllNodes.
And running a query from inside your resolver functions can be accomplished
with runQuery, which accepts filter, sort,
limit and skip query arguments.
We could for example add a field to the AuthorJson type that lists all recent
posts by an author:
exports.createResolvers = ({ createResolvers }) => {
const resolvers = {
AuthorJson: {
recentPosts: {
type: ["MarkdownRemark"],
resolve(source, args, context, info) {
return context.nodeModel.runQuery({
query: {
filter: {
frontmatter: {
author: { eq: source.email },
date: { gt: "2019-01-01" },
},
},
},
type: "MarkdownRemark",
firstOnly: false,
})
},
},
},
}
createResolvers(resolvers)
}Custom query fields
One powerful approach enabled by createResolvers is adding custom root query
fields. While the default root query fields added by Gatsby (e.g.
markdownRemark and allMarkdownRemark) provide the whole range of query
options, query fields designed specifically for your project can be useful. For
example, we can add a query field for all external contributors to our example blog
who have received their swag:
exports.createResolvers = ({ createResolvers }) => {
const resolvers = {
Query: {
contributorsWithSwag: {
type: ["ContributorJson"],
resolve(source, args, context, info) {
return context.nodeModel.runQuery({
query: {
filter: {
receivedSwag: { eq: true },
},
},
type: "ContributorJson",
firstOnly: false,
})
},
},
},
}
createResolvers(resolvers)
}Because we might also be interested in the reverse - which contributors haven’t received their swag yet - why not add a (required) custom query arg?
exports.createResolvers = ({ createResolvers }) => {
const resolvers = {
Query: {
contributors: {
type: ["ContributorJson"],
args: {
receivedSwag: "Boolean!",
},
resolve(source, args, context, info) {
return context.nodeModel.runQuery({
query: {
filter: {
receivedSwag: { eq: args.receivedSwag },
},
},
type: "ContributorJson",
firstOnly: false,
})
},
},
},
}
createResolvers(resolvers)
}It is also possible to provide more complex custom input types which can be defined
directly inline in SDL. We could for example add a field to the ContributorJson
type that counts the number of posts by a contributor, and then add a custom root
query field contributors which accepts min or max arguments to only return
contributors who have written at least min, or at most max number of posts:
exports.createResolvers = ({ createResolvers }) => {
const resolvers = {
Query: {
contributors: {
type: ["ContributorJson"],
args: {
postsCount: "input PostsCountInput { min: Int, max: Int }",
},
resolve(source, args, context, info) {
const { max, min = 0 } = args.postsCount || {}
const operator = max != null ? { lte: max } : { gte: min }
return context.nodeModel.runQuery({
query: {
filter: {
posts: operator,
},
},
type: "ContributorJson",
firstOnly: false,
})
},
},
},
ContributorJson: {
posts: {
type: `Int`,
resolve: (source, args, context, info) => {
return context.nodeModel
.getAllNodes({ type: "MarkdownRemark" })
.filter(post => post.frontmatter.author === source.email).length
},
},
},
}
createResolvers(resolvers)
}Taking care of hot reloading
When creating custom field resolvers, it is important to ensure that Gatsby
knows about the data a page depends on for hot reloading to work properly. When
you retrieve nodes from the store with context.nodeModel methods,
it is usually not necessary to do anything manually, because Gatsby will register
dependencies for the query results automatically. The exception is getAllNodes
which will not register data dependencies by default. This is because
requesting re-running of queries when any node of a certain type changes is
potentially a very expensive operation. If you are sure you really need this,
you can add a page data dependency either programmatically with
context.nodeModel.trackPageDependencies, or with:
context.nodeModel.getAllNodes(
{ type: "MarkdownRemark" },
{ connectionType: "MarkdownRemark" }
)Custom Interfaces and Unions
Finally, let’s say we want to have a page on our example blog that lists all
team members (authors and contributors). What we could do is have two queries,
one for allAuthorJson and one for allContributorJson and manually merge
those. GraphQL however provides a more elegant solution to these kinds of
problems with “abstract types” (Interfaces and Unions). Since authors and
contributors actually share most of the fields, we can abstract those up into
a TeamMember interface and add a custom query field for all team members
(as well as a custom resolver for full names):
exports.sourceNodes = ({ actions }) => {
const { createTypes } = actions
const typeDefs = `
interface TeamMember {
name: String!
firstName: String!
email: String!
}
type AuthorJson implements Node & TeamMember {
name: String!
firstName: String!
email: String!
joinedAt: Date
}
type ContributorJson implements Node & TeamMember {
name: String!
firstName: String!
email: String!
receivedSwag: Boolean
}
`
}
exports.createResolvers = ({ createResolvers }) => {
const fullName = {
type: "String",
resolve(source, args, context, info) {
return source.firstName + " " + source.name
},
}
const resolvers = {
Query: {
allTeamMembers: {
type: ["TeamMember"],
resolve(source, args, context, info) {
return context.nodeModel.getAllNodes({ type: "TeamMember" })
},
},
},
AuthorJson: {
fullName,
},
ContributorJson: {
fullName,
},
}
createResolvers(resolvers)
}To use the newly added root query field in a page query to get the full names of all team members, we can write:
export const query = graphql`
{
allTeamMembers {
... on Author {
fullName
}
... on Contributor {
fullName
}
}
}
`Extending third-party types
So far, the examples have been dealing with types created from locally available data. However, Gatsby also allows to integrate and modify third-party GraphQL schemas.
Usually, those third-party schemas are retrieved from remote sources via introspection
query with Gatsby’s gatsby-source-graphql plugin. To customize types integrated from
a third-party schema, we can use the createResolvers API.
Feeding remote images into gatsby-image
As an example, let’s look at using-gatsby-source-graphql to see how we could use createResolvers to feed images from a CMS into gatsby-image (the assumption is that gatsby-source-graphql was configured
to prefix all types from the third-party schema with CMS):
exports.createResolvers = ({
actions,
cache,
createNodeId,
createResolvers,
store,
}) => {
const { createNode } = actions
createResolvers({
CMS_Asset: {
imageFile: {
type: `File`,
resolve(source, args, context, info) {
return createRemoteFileNode({
url: source.url,
store,
cache,
createNode,
createNodeId,
})
},
},
},
})
}We create a new imageFile field on the CMS_Asset type, which will create File
nodes from every value on the url field. Since File nodes automatically have
childImageSharp convenience fields available, we can feed the images from the CMS
into gatsby-image by simply querying:
query {
cms {
post {
# In this example, the `post.image` field is of type `CMS_Asset`
image {
# It is important to include all fields in the query which we want to
# access in the resolver. In this example we want to make sure to include
# the `url` field. In the future, Gatsby might provide a `@projection`
# extension to automatically include those fields.
url
imageFile {
childImageSharp {
fixed {
...GatsbyImageSharpFixed
}
}
}
}
}
}
}Edit this page on GitHub