Why Gatsby's Unified GraphQL Layer Changes How You Think About Content
Gatsby's unified GraphQL layer shifts all data fetching to build time, turning heterogeneous content sources into a single compile-time validated schema. Here's how it works, where it strains, and a minimal example you can verify today.
14 Nov 2025, 01:24 UTC

The problem: content lives everywhere, but your frontend wants one API
Most teams don't start with a clean content architecture. They inherit Markdown files in a repo, product data in Shopify, blog posts in Contentful, and maybe a headless WordPress instance marketing insisted on. The traditional React answer is to write a bespoke data-fetching layer — useEffect hooks, SWR, React Query — that stitches these sources together at runtime. Every page load becomes a waterfall of network requests, each with its own error handling, caching strategy, and TypeScript interface.
Gatsby takes a different bet: do all the data work at build time, emit static JSON, and let the browser hydrate without a single fetch. The unified GraphQL data layer is the mechanism that makes this possible. It's not just a query language; it's a compile-time contract between your content sources and your components.
Source plugins normalize heterogeneity into one schema
Every Gatsby source plugin implements the Node API: it reaches into a remote CMS, a local filesystem, or a REST endpoint and emits nodes — generic objects with a stable id, a type name, and arbitrary fields. Transformer plugins then enrich those nodes. gatsby-source-filesystem creates File nodes; gatsby-transformer-remark reads the Markdown frontmatter and body, adding MarkdownRemark nodes with html, frontmatter, and fields. gatsby-source-contentful maps Contentful entries to ContentfulBlogPost, ContentfulProduct, etc.
All of these converge into a single GraphQL schema that you query in pageQuery (for pages) or useStaticQuery (for components). The schema is inferred automatically from the first data sample each plugin sees. That means allMarkdownRemark { edges { node { frontmatter { title } } } } works without you writing a single type definition — but it also means the schema shape depends on what content happened to exist during the first build.
Compile-time validation eliminates a whole class of runtime bugs
When you write a page query, Gatsby validates it against the inferred schema during the build. If a CMS editor removes a required field, or a Markdown file misses a frontmatter key, the build fails with a clear GraphQL error — not a cryptic Cannot read property 'title' of undefined in production. This shifts the feedback loop from "user reports broken page" to "CI blocks merge".
The trade-off is schema drift. Optional fields in your CMS that happen to be empty in the first build become nullable in the inferred schema. Later, when content appears, the field becomes non-null and existing queries that didn't guard for null start failing. The mitigation is gatsby-plugin-graphql-snapshot: it writes a schema.gql snapshot on first build, and subsequent builds diff against it. You review the diff like a code change, accept intentional additions, and catch accidental breaking changes before they hit main.
Worked example: Markdown + optimized images in one query
Let's make this concrete. Assume a Gatsby 5 site with gatsby-source-filesystem pointed at content/posts, gatsby-transformer-remark, and gatsby-plugin-image installed.
// gatsby-config.js
module.exports = {
plugins: [
{
resolve: `gatsby-source-filesystem`,
options: { name: `posts`, path: `${__dirname}/content/posts` },
},
`gatsby-transformer-remark`,
`gatsby-plugin-image`,
`gatsby-plugin-graphql-snapshot`,
],
};
A post file content/posts/hello.md:
---
title: "Hello Gatsby"
date: "2026-10-15"
cover: ./hero.png
---
Body content here.
The page query in src/pages/posts/{MarkdownRemark.frontmatter__slug}.js (using File System Route API):
export const pageQuery = graphql`
query($id: String!) {
markdownRemark(id: { eq: $id }) {
html
frontmatter {
title
date(formatString: "MMMM D, YYYY")
cover {
childImageSharp {
gatsbyImageData(width: 800, placeholder: BLURRED)
}
}
}
}
}
`;
At build time, Gatsby:
- Creates a
Filenode forhero.png. gatsby-transformer-sharp(pulled in bygatsby-plugin-image) generates WebP, AVIF, and multiple widths, plus a base64 blur placeholder.- The query resolves
gatsbyImageDatato a JSON object containingsrcSet,sizes,width,height, andplaceholder. - The page component receives fully typed data (with
gatsby-plugin-typegen) and passesgatsbyImageDatato<GatsbyImage>— no client-side image optimization, no layout shift.
Run gatsby build and inspect public/static/: you'll see hero-800w.webp, hero-800w.avif, and a page-data/ JSON file with the query result embedded. The browser never requests an image transformation endpoint.
Where the model strains
- Schema inference is fragile with sparse data. If your Contentful
BlogPosttype has an optionalseoDescriptionthat's empty for the first 50 posts, the inferred schema marks it nullable. Post 51 adds a value — now the field becomes non-null and any query that didn't handle null breaks the build. Snapshot testing catches this, but it's a process change, not a free guarantee. - Remote GraphQL sources hit rate limits.
gatsby-source-graphql(for Shopify, Contentful GraphQL API, etc.) paginates and caches, but a full initial sync on a large catalog can take hours and burn API quotas. Preview webhooks help for incremental updates, but you still need to tunebatchSizeandconcurrentRequests. - Client-only data forces a mental-model split. Auth tokens, user-specific pricing, or real-time comments can't live in the static layer. You fetch those at runtime with standard React patterns (
useEffect, TanStack Query), meaning some components get data via props from page queries and others via hooks. The boundary is explicit but it's still two mental models. - TypeScript without
gatsby-plugin-typegenis painful.useStaticQueryreturnsany; page queries lose autocomplete. The plugin generatesgraphql-types.d.tsand typed hooks (useStaticQuery<MyQuery>), but you must remember to run it after schema changes.
Verify it yourself in five minutes
gatsby new demo https://github.com/gatsbyjs/gatsby-starter-hello-world- Add
gatsby-source-filesystem,gatsby-transformer-remark,gatsby-plugin-image,gatsby-plugin-graphql-snapshottogatsby-config.js. - Create
content/posts/test.mdwith frontmatter and a local image. gatsby develop→ open http://localhost:8000/___graphql and exploreallMarkdownRemark.gatsby build→ checkpublic/page-data/for JSON artifacts andpublic/static/for derived images.- Add a new frontmatter field to the Markdown file, run
gatsby buildagain, and watch the snapshot diff appear in.graphql-snapshot/.
If the build passes and the JSON artifacts contain exactly what your query asked for, you've just replaced a runtime orchestration layer with a compile-time contract. That's the engineering decision Gatsby makes — and once you've felt the difference, it's hard to go back to fetching in useEffect.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.