How the Gatsby Build Process Works: A Deep Dive into createPages, sourceNodes, and onCreateNode
During the Gatsby build process, the framework executes a deterministic sequence of lifecycle APIs—sourceNodes to fetch data, onCreateNode to enrich nodes, and createPages to generate routes—coordinated by the api-runner-node.js orchestrator to produce static HTML and JavaScript bundles.
The Gatsby build pipeline transforms raw content into optimized static assets through a plugin-driven architecture. According to the gatsbyjs/gatsby source code, this process relies on a central runner that invokes specific APIs at precise phases, ensuring data is sourced, transformed, and ultimately rendered into pages. Understanding these three core APIs is essential for customizing how Gatsby handles content from filesystems, CMSs, or external APIs.
The Seven Phases of the Gatsby Build Pipeline
When you execute gatsby build or gatsby develop, Gatsby progresses through distinct stages defined in packages/gatsby/src/bootstrap/ and packages/gatsby/src/utils/api-runner-node.js:
- Bootstrap — Loads
gatsby-config.js, resolves plugins, and initializes the Redux store. - Source & Transform — Executes
sourceNodesacross all plugins to create raw data nodes. - Node Enrichment — Runs
onCreateNodefor every created node to add derived fields or child nodes. - Schema Generation — Builds the GraphQL schema from all node types in
packages/gatsby/src/schema/schema.js. - Page Creation — Invokes
createPagesandcreatePagesStatefullyso plugins can declare routes usingactions.createPage. - HTML Rendering — Renders React trees to static HTML during the
build-htmlstage (seecase \build-html`at line 151 of [packages/gatsby/src/utils/webpack.config.js`](https://github.com/gatsbyjs/gatsby/blob/main/packages/gatsby/src/utils/webpack.config.js)). - JS Bundling — Compiles client-side bundles during the
build-javascriptstage (line 164 of the same webpack config).
The api-runner-node.js file serves as the execution engine, binding plugin actions, injecting tracing spans, and managing the lifecycle transitions between these phases.
How sourceNodes Creates the Data Graph
The sourceNodes API is the entry point for populating Gatsby's internal data layer. Plugins implement this async function to fetch remote or local data and create nodes using actions.createNode.
In packages/gatsby/src/utils/api-runner-node.js, the runner invokes sourceNodes at lines 528–538, creating a bound actions object for each plugin. Here is a typical implementation pattern:
exports.sourceNodes = async ({ actions, createNodeId, createContentDigest }) => {
const data = await fetchRemoteData(); // Async I/O operation
const node = {
id: createNodeId(`my-data-${data.id}`),
parent: null,
children: [],
internal: {
type: `MyData`,
contentDigest: createContentDigest(data),
},
...data,
};
actions.createNode(node); // Registers node in the Redux store
};
Nodes created during this phase become available in the GraphQL schema. The runner specifically checks for sourceNodes at line 528 and optionally defers certain actions to optimize performance during bulk node creation.
Enriching Nodes with onCreateNode
After sourceNodes creates a node, Gatsby immediately runs onCreateNode for every plugin that exports it. This synchronous API allows transformation of nodes before the schema is generated.
The implementation in api-runner-node.js (lines 566–640) detects this API and wraps it with node-mutation tracking (enabled via shouldDetectNodeMutations at lines 437–444). Common use cases include:
- Adding custom fields using
actions.createNodeField - Creating child nodes (e.g., transforming Markdown into
MarkdownRemarknodes) - Modifying internal metadata like node types
exports.onCreateNode = ({ node, actions, getNode }) => {
if (node.internal.type === `MarkdownRemark`) {
const fileNode = getNode(node.parent);
actions.createNodeField({
node,
name: `slug`,
value: `/blog/${fileNode.name}/`,
});
}
};
This two-step approach—sourceNodes for async I/O and onCreateNode for synchronous enrichment—ensures all derived data exists before GraphQL schema compilation begins.
Generating Pages with createPages and createPagesStatefully
Once the schema is ready, Gatsby executes page creation APIs to transform nodes into routable pages.
createPages
The standard createPages API receives graphql and actions objects, allowing plugins to query the node graph and call actions.createPage for each route. The runner implements special safeguards in api-runner-node.js (lines 81–94), wrapping createPage to warn if called after the API promise resolves:
exports.createPages = async ({ graphql, actions, reporter }) => {
const { data } = await graphql(`
{
allMarkdownRemark {
nodes {
id
fields { slug }
}
}
}
`);
data.allMarkdownRemark.nodes.forEach(node => {
actions.createPage({
path: node.fields.slug,
component: require.resolve(`./src/templates/blog-post.js`),
context: { id: node.id }, // Passed as props to the page component
});
});
};
The wrapper tracks apiFinished (line 100) to prevent asynchronous race conditions where pages might be created after the phase officially ends.
createPagesStatefully
Plugins needing to create pages after the initial creation phase export createPagesStatefully. This API receives the Redux store and is invoked once the page list is fully populated.
The internal dev-404-page plugin (packages/gatsby/src/internal-plugins/dev-404-page/gatsby-node.js, line 5) demonstrates this pattern:
exports.createPagesStatefully = async ({ store, actions }) => {
const { createPage } = actions;
createPage({
path: `/404.html`,
component: require.resolve(`./src/pages/404.js`),
matchPath: `/*`, // Catch-all pattern
});
};
Gatsby triggers this specifically via public.js (line 421) using the traceId initial-createPagesStatefully, ensuring it runs after normal createPages completes.
The api-runner-node.js Orchestration Layer
All these APIs converge in packages/gatsby/src/utils/api-runner-node.js, which implements the apiRunnerNode function (line 16). For each lifecycle API, the runner:
- Creates a tracing span (
pluginSpan) for performance monitoring - Binds plugin-specific action creators using
bindActionCreators - Constructs a rich context object (lines 470–522) containing:
actions(wrapped with safeguards for certain APIs)- Node accessors:
getNode,getNodes,getNodesByType,loadNodeContent - Helpers:
createNodeId,createContentDigest - GraphQL schema builders
- Captures errors with friendly code frames (lines 560–607) if plugins throw exceptions
- Aggregates results into an array resolved at lines 250–260
This architecture guarantees that sourceNodes completes before onCreateNode fires, and that createPages runs only after the GraphQL schema is stable.
Complete Implementation Example
Here is a comprehensive gatsby-node.js demonstrating all three APIs working together:
// gatsby-node.js
exports.sourceNodes = async ({ actions, createNodeId, createContentDigest }) => {
const posts = await fetchPostsFromCMS();
posts.forEach(post => {
actions.createNode({
id: createNodeId(`post-${post.id}`),
parent: null,
children: [],
internal: {
type: `CMSPost`,
contentDigest: createContentDigest(post),
},
title: post.title,
rawContent: post.body,
});
});
};
exports.onCreateNode = ({ node, actions, getNode }) => {
if (node.internal.type === `CMSPost`) {
// Create child Markdown node for remark processing
const markdownNode = {
id: `${node.id} >>> MarkdownRemark`,
parent: node.id,
children: [],
internal: {
type: `MarkdownRemark`,
mediaType: `text/markdown`,
contentDigest: node.internal.contentDigest,
},
rawMarkdownBody: node.rawContent,
};
actions.createNode(markdownNode);
actions.createParentChildLink({
parent: node.id,
child: markdownNode.id
});
}
};
exports.createPages = async ({ graphql, actions, reporter }) => {
const result = await graphql(`
{
allMarkdownRemark {
nodes {
id
parent {
... on CMSPost {
title
}
}
}
}
}
`);
if (result.errors) {
reporter.panicOnBuild(`GraphQL error`, result.errors);
return;
}
result.data.allMarkdownRemark.nodes.forEach(node => {
actions.createPage({
path: `/posts/${node.id}/`,
component: require.resolve(`./src/templates/post.js`),
context: {
id: node.id,
title: node.parent.title
},
});
});
};
Summary
- Bootstrap and Source: Gatsby loads configurations and executes
sourceNodesto populate the data layer with raw nodes viaactions.createNode. - Node Enrichment: The
onCreateNodeAPI runs synchronously for every node, allowing plugins to add fields or create child nodes before schema generation. - Page Generation:
createPagesqueries the finalized GraphQL schema to generate routes, whilecreatePagesStatefullyhandles post-creation logic like 404 pages. - Orchestration: The
api-runner-node.jsfile coordinates execution order, binds actions, and provides error handling across all lifecycle phases. - Output: Webpack configurations in
packages/gatsby/src/utils/webpack.config.jshandle the finalbuild-htmlandbuild-javascriptstages to write thepublic/directory.
Frequently Asked Questions
What is the difference between createPages and createPagesStatefully in Gatsby?
createPages is the standard API for generating pages during the initial build phase, receiving graphql and actions to query data and create routes. createPagesStatefully runs after the initial page creation is complete and receives the Redux store, allowing plugins to inspect the full page list before adding supplemental pages like 404 catch-alls.
When should I use onCreateNode versus sourceNodes in a Gatsby plugin?
Use sourceNodes for asynchronous data fetching operations that create initial nodes from external sources like APIs or filesystems. Use onCreateNode for synchronous transformations of existing nodes, such as adding derived fields, creating child nodes from parent content, or modifying node metadata before the GraphQL schema is compiled.
How does Gatsby prevent plugins from calling createPage after the createPages API finishes?
In packages/gatsby/src/utils/api-runner-node.js (lines 81–94), Gatsby wraps the createPage action with a checker that tracks when the createPages promise resolves. If a plugin attempts to call createPage after apiFinished becomes true (line 100), the runner emits a warning to prevent race conditions and ensure build determinism.
Where does Gatsby build the final HTML and JavaScript bundles?
The build stages are implemented in packages/gatsby/src/utils/webpack.config.js, which defines separate webpack configurations for build-html (line 151) and build-javascript (line 164). These stages render React components to static HTML and bundle client-side JavaScript, respectively, before writing all assets to the public/ directory.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →