Headless Drupal with GraphQL

Headless Drupal with GraphQL gives a decoupled front end a typed, query-shaped view of your content. This page explains when the GraphQL module is worth its complexity, when JSON:API in core is enough, and what the build involves.

On this page

When headless Drupal with GraphQL is the right build

A team that wants a separate front end, such as a JavaScript framework, to read content from Drupal has to decide how that front end will query Drupal. The two realistic options are the JSON:API module, which ships in Drupal core, and the contributed GraphQL module, which exposes a typed GraphQL schema over the same content. The decision is a trade-off, not a default. Choose GraphQL when the front end needs to fetch exactly the fields it renders across several content types in one request, and when the team is comfortable owning a schema as a versioned contract. Choose core JSON:API when the front end's needs are closer to standard resource reads, when fewer custom data shapes are required, and when you want fewer contributed modules to review at each Drupal upgrade.

The contributed GraphQL module maps Drupal entities, fields, views and menus into a schema that clients query with one endpoint. It does not replace Drupal's permission model: queries run against the same access checks as the rest of the site, so an anonymous or authenticated client sees only what its Drupal role allows. Who needs it is usually a team with a React, Vue or similar front end, a product owner who wants page-level data shaped for specific components, and a platform owner who will maintain the schema. Before committing, confirm with the module's Drupal.org project page which Drupal core versions and PHP versions your target release supports, and check how the schema is extended for your custom content types. The Drupal core release schedule sets the timing you will need to plan around.

Ask three questions before the build starts. First, how many distinct page templates will the front end render, and do they need nested references across content types that would take several JSON:API requests? Second, who will own the schema after launch, and how will schema changes be reviewed against front-end deployments? Third, which caching layers will sit in front of Drupal, since GraphQL responses need cache metadata that matches Drupal's cache tags to stay correct. A short proof of concept against one real template, measured with the content volumes you expect, will show whether the added schema work pays off. If the answers point to simple resource reads, plan the build on JSON:API and keep GraphQL out of scope.

GraphQL or JSON:API in core: which fits your front end

When the front end mostly lists and detail-views content types, with predictable fields and few cross-entity joins, core JSON:API is usually the lower-effort starting point. It ships with Drupal core, exposes every entity type as a resource with a consistent URL structure, and needs no additional schema layer to maintain. The work is mainly deciding which resources and fields are exposed, how access follows Drupal's permissions, and how includes are limited so responses stay small.

  • Fewer moving parts: JSON:API is part of core, so there is no separate schema generation step to test after each content model change.
  • Resource configuration: the team decides which entity types, bundles and fields are published, and reviews them whenever a content type changes.
  • Includes and filters: clients can request related entities, so the team sets sensible limits on depth and page size to avoid heavy responses.
  • Permissions: access is enforced through Drupal's existing roles and entity access, which keeps the review model familiar to site owners.
  • Customization: changes to resource shape usually mean entity or field configuration, or a contributed module, rather than a typed schema.

Before committing, confirm that your content model is stable enough for resource-based URLs and that the front end does not need to combine many entity types into one response.

Configuration options and when each fits

This table compares the main configuration choices for a GraphQL-backed Drupal site: schema scope, authentication mode, caching and persisted queries, and front-end data fetching. Read each row as a decision with a trade-off, and confirm the exact behavior in the module documentation for your GraphQL version before you commit. The main takeaway is that every wider exposure choice trades convenience for a larger surface to secure, cache and maintain.

DecisionWhen it fitsTrade-off to weigh
Schema scope (entity types, fields, bundles)Expose only the bundles and fields that a specific front end renders, such as articles and their media and taxonomy referencesA narrow schema is easier to secure and cache, but every new front-end need becomes a schema change that needs review and a release
Schema scope (broad exposure)Early prototypes or internal tools where the content model is still changing and speed matters more than a tight surfaceFields that are exposed by default can include unpublished or sensitive data unless access rules are checked for each one
Anonymous accessPublic content that is already published and carries no personal or restricted dataAnonymous queries are the easiest to cache at the edge, but any field left open is readable by anyone who finds the endpoint
Session-based accessFront ends served from the same domain as Drupal, where users already sign in to DrupalRequests depend on cookies, which complicates separate-domain front ends and makes cross-origin setup harder to test
Token or OAuth-based accessSeparate front ends, mobile apps or partner clients that must read restricted content or act for a signed-in userAdds token issuance, expiry, revocation and scope design, and the cache key must account for who is asking
Caching and persisted queriesRead-heavy public front ends where the same query shapes repeat and cache hits matterPersisted queries reduce request size and limit arbitrary queries, but they require a build step that registers each query
Uncached or per-user responsesPersonalized or permission-sensitive pages where a shared cache would leak data between usersLower cache hit rates raise load on Drupal and the database, so the capacity plan must assume more uncached traffic
Front-end data fetching (one query per page)Pages with a stable layout where the framework can request all needed fields in one call at build or request timeLarge queries can over-fetch and slow rendering, and changes to one component can ripple into the page query
Front-end data fetching (component-level queries)Component-based front ends that need each block to load its own data and be reused across pagesMore requests and more cache keys to manage, so the team must confirm how the framework batches and caches them

How to design the schema around content types and editorial needs

The GraphQL schema should follow the Drupal content model, not the page layouts your front end happens to need today. Map each content type to a GraphQL type, each field to a field, and each entity reference to a typed connection, then decide deliberately which entities are exposed at all. Menus, taxonomy terms and paragraph or component types need the same treatment: a taxonomy vocabulary usually becomes its own type that content references, while paragraph types are better exposed as a union or interface so front ends can switch on the component name without parsing markup. Editors own the content model, so a schema that mirrors it lets them add a field in Drupal and see a predictable result in the API. Confirm with your GraphQL module version how it handles entity references, unions and interfaces before you commit to a pattern.

Expose only what the front end needs. Internal fields such as revision metadata, moderation state of unpublished drafts, author email addresses, field-level computed values that reveal access logic, and raw uid or nid values used by Drupal internals should stay out of the schema or sit behind explicit access checks. Review each exposed field against the roles that can view it, because a field that is hidden in the Drupal UI can still be returned through the API if the resolver does not check access. Keep names stable: once a front end ships a query, renaming title to headline breaks every consumer. Use the schema as a contract, add new fields alongside old ones, and mark retired fields as deprecated with a removal date agreed with the front end team.

Plan deprecation around the Drupal change cycle. A field rename in the content model, a paragraph type split, or a change to a reference cardinality will change the schema, so put content model changes and schema changes through the same review and release. Test queries in a staging environment that holds a copy of production content before any upgrade, including a Drupal core upgrade that moves to a new major version. Record the schema version alongside each front end release so you can tell which queries were valid when a regression appears. Ask your partner or internal team to confirm how the chosen GraphQL module version is supported on the Drupal version you plan to run, since module compatibility can differ from core compatibility.

How the GraphQL build sequence runs

The sequence moves from confirming the Drupal and PHP baseline to a tested, cached GraphQL endpoint that a decoupled front end and its editors can rely on. Each step closes with a check so problems surface before the schema and access rules are built on top of them.

  1. Confirm the Drupal and PHP baseline

    Before anything is installed, the team confirms which Drupal major version the site runs or will run, and checks the PHP version against that release's requirement. Drupal 10 requires PHP 8.1 or later, Drupal 11 requires PHP 8.3 or later, and Drupal 12 requires PHP 8.5 or later, according to the Drupal.org PHP requirements page. Confirm the web server and database against the Drupal system requirements as well. Done means a written version matrix, signed off by the platform owner, that the contributed GraphQL module release supports the chosen core version.

  2. Install and configure the GraphQL module

    A Drupal developer adds the GraphQL module through Composer in a development environment, enables it, and sets the endpoint path and the default server configuration. Dependencies are pinned in the lock file so that the build can be reproduced in staging and production. Done means the endpoint answers an introspection request on a development build and the configuration is exported to version control.

  3. Define the exposed schema scope

    The team, with content owners and the front-end lead, decides which content types, fields, views and mutations the schema exposes. Each type is mapped to a documented consumer need, and anything without a consumer stays out. Done means an approved schema inventory that lists every exposed type and field, and a review that confirms no internal-only fields, such as moderation notes, appear in the public schema.

  4. Set access and authentication rules

    Developers configure which roles and anonymous users can run each query, and choose how authenticated requests are made, such as token or session-based access for the front end. Field-level access from Drupal entity permissions is tested with real test accounts, because GraphQL responses must respect the same rules as the Drupal pages. Done means an access test matrix in which each role's query results match what that role can see in the Drupal interface.

  5. Configure caching and query limits

    Developers set the Drupal cache contexts and tags that GraphQL responses depend on, and decide which responses can be cached by a reverse proxy or CDN. Query depth or complexity limits are set where the chosen configuration supports them, and the team confirms this with the module documentation for the release in use. Done means a cache test showing that editing a node invalidates the responses that include it, and that a deliberately expensive query is rejected or limited.

  6. Validate queries against the front end

    The front-end team runs its actual component queries against a staging endpoint, fixing mismatches in field names, nullability and nested relationships before the front end is considered connected. Generated types, if the front end uses them, are regenerated from the staging schema. Done means every production query in the front end passes against staging with no schema errors, and the query files are stored with the front-end code.

  7. Test previews and performance

    The team tests editorial preview, where an unpublished or draft revision must render in the front end for an authorized editor, and measures response times for the heaviest pages under realistic concurrent load. Results are compared against the targets set by the business owner. Done means a preview test passed by an editor on staging, and a load test report that lists slow queries and the caching or query changes made in response.

  8. Hand over and document the endpoint

    The team publishes a short runbook covering the endpoint location, how schema changes are reviewed, how cache invalidation is verified, and who receives security advisories for the GraphQL module and Drupal core. Support arrangements for later Drupal upgrades are agreed in writing. Done means the runbook is accepted by the platform owner and the front-end team, and a first schema change has been run through the documented review process.

What drives query performance and caching decisions

A GraphQL endpoint in front of Drupal inherits Drupal's entity access, cache metadata and rendering costs, so most performance problems trace back to a few design choices. The cards below cover the drivers that move page load and the ongoing maintenance work, and what to check for each before the schema is frozen.

Query depth and breadth

Each nested relationship in a query, such as an article that loads its author, tags, media and related articles, multiplies the entity loads Drupal performs for one request. Breadth matters too, because a list query that asks for many fields per item loads more data than a card view needs. Review the front-end component inventory against the schema and set depth limits or query cost rules in the GraphQL server. Expect the schema reviews to recur whenever a new front-end view is added.

Entity access checks per field

Drupal checks access on each entity and, for many fields, on each field value before returning it. Anonymous and authenticated requests can therefore cost different amounts, and a field-level permission rule can add work to every item in a list. Ask the team to list which fields sit behind custom access logic and to test list queries as an anonymous user and as an editor. Access logic written in custom code also needs tests, because it is easy to change during an upgrade without noticing the cost.

Drupal cache tags and contexts

Drupal decides how long a response can be cached using cache tags for the entities it used and cache contexts for variables such as user role, language or query arguments. Every GraphQL response should carry this metadata, or the caches above it will serve stale or wrongly scoped content. Overly broad contexts reduce the cache hit rate, while missing tags cause stale content after edits. Plan a test that edits a node and confirms the next query returns the new value, and keep that test in the release checklist.

Reverse proxy and CDN caching

A CDN or reverse proxy can serve GraphQL responses without reaching Drupal, but GraphQL usually sends queries as POST requests with bodies that caches do not key on by default. Caching at the edge therefore depends on using GET requests, a deliberate cache key that includes the query and variables, and purge hooks tied to Drupal cache tags. Confirm with the CDN vendor how it handles POST bodies and authorization headers, and decide which responses are safe to share between visitors before enabling it.

Persisted queries

Persisted queries store approved query documents on the server and let the front end send only an identifier. This makes responses easier to cache at the edge, shrinks request size and blocks arbitrary queries from clients you have not reviewed. The cost is a build step that registers each query, a deployment order in which the server knows the query before the front end ships, and a process for retiring old identifiers. Ask whether the front-end framework and the GraphQL module setup support this pattern in your chosen versions before committing to it.

Image derivative generation

Image styles create derivatives the first time they are requested, and a list page that requests many sizes can trigger heavy image processing and slow the first visitors after a deploy. Decide which derivatives the front end needs, generate them ahead of launch or during deployment, and keep the image URL pattern stable so caches are not invalidated by every change. Storage growth from derivatives also recurs, so include cleanup and sizing in the operations plan.

Operating the GraphQL layer after launch

The GraphQL layer needs an owner after launch, and the first recurring task is watching what it does in production. Set up monitoring for query errors, for resolvers that exceed the response times your front end can tolerate, and for queries that fetch far more fields or nested entities than the page uses. Review these signals weekly for the first months, then at a cadence your team can sustain. Confirm with your hosting team which logging and tracing tools already see Drupal requests, because a GraphQL request arrives as a single POST to one endpoint and generic web metrics will not show which field caused a slow response. Ask the module maintainers and your agency or internal team which query-depth or complexity limits are configured, since those limits are the first control to tune when a client sends an expensive query.

Every schema change needs a review step before it reaches the front end. Adding a field is usually safe, while renaming, removing or changing the type of a field breaks any client that depends on it. Treat the schema as a published contract: record each change, check it against the front-end queries in the repository, and agree a deprecation window before removing anything. Caching decisions belong in the same review, because a new field that depends on user-specific data may disable caching for every query that includes it. Plan these reviews as part of the content model change process, not as a separate task, so that editors adding fields in the admin interface also trigger a schema check. Drupal core and contributed module updates change the GraphQL output in ways that are easy to miss. Bugfix releases arrive on the first Wednesday of each month and security releases on the third Wednesday, according to the Drupal core release schedule, so use those dates as fixed points in your planning calendar. Schedule a test pass on a staging copy with the front-end build running against it before each update reaches production, and include the GraphQL module, the authentication module and any caching modules in that pass. Coordinate front-end and Drupal releases so that a new front-end build never requires a schema change that production does not yet have. Ask your team to confirm how emergency security releases will be tested, since a third-Wednesday release may need to go out before a full regression cycle is possible.

Limits to confirm in your environment

GraphQL for Drupal depends on the module release, the Drupal core version, the PHP runtime and the entity types your content model uses. Each of these limits shapes the schema, the auth design and the load you can carry, so confirm them against the module documentation and your own test build before you fix the architecture.

Supported Drupal versions

Check which Drupal core versions each GraphQL module release supports before you pick a release line. Compatibility is set per release, so a module that works on your current core may not work on the version you plan to reach in the next upgrade cycle. Drupal 10 reaches end of life on December 9, 2026, so confirm the release you choose supports Drupal 11 and whether a Drupal 12 release is planned.

PHP version alignment

Confirm the PHP version your hosting stack runs against the minimum each Drupal core line requires. Drupal 11 requires PHP 8.3 or later, and Drupal 12 requires PHP 8.5 or later, so the PHP runtime can become the upgrade blocker before the module does. Test the GraphQL module and its dependencies on the exact PHP version in your container image or server build.

Entity type and field coverage

Verify that the GraphQL module exposes the entity types, bundles, field types and computed or custom fields your content model relies on. Media, paragraphs, taxonomy references, menus and user fields often need extra configuration or custom schema code. List each entity type in your model and test a query for it, rather than assuming that core content types are covered by default.

Authentication method behavior

Test how your chosen authentication method behaves with GraphQL requests: session cookies, token-based access, OAuth-based clients and anonymous reads. Check how each method interacts with caching, CORS settings and per-field access checks, and confirm which mutations the method permits. Your identity provider integration may also limit which methods you can use for editor preview.

Performance under expected query load

Measure query cost and response times with queries that match your front end, including nested relationships, image styles and large collections. Deep nesting and unbounded lists can multiply database work, so confirm how the module handles query depth, complexity limits and cache tags. Run the test against production-like data volumes and hosting, because a development database rarely shows the load pattern.

Module maintenance and security advisories

Check the maintenance status of the GraphQL module and its dependencies, the release cadence, and the open issues affecting your release line. Subscribe to Drupal Security Team advisories for the project, since security releases follow a Wednesday schedule and a module update may be needed between core upgrades. Record who owns the update and how quickly your team can apply it.

Questions teams ask before building a headless Drupal API

What drives the cost of a headless Drupal GraphQL build?

The largest cost drivers are the number of content types, the depth of relationships the front end needs, and how many channels or front ends consume the same schema. Each content type, field, and relationship has to be modeled, exposed, tested, and documented before the front end can rely on it. Authentication design, preview workflows, and caching rules add further work. One-time costs cover schema design, module configuration, custom resolvers, and testing. Recurring costs cover module updates, schema changes as the front end evolves, and monitoring. Ask any provider for a quote that lists these components separately, so you can see which ones scale with your content model.

How long does a headless Drupal API take to build?

Duration depends on the same drivers as cost: the size of the content model, the number of queries the front end needs, the authentication model, and how many environments and previews must work. A small schema for one front end moves much faster than a schema shared by several channels with editorial previews. Your front end team's readiness also matters, because schema decisions often wait on UI requirements. Ask for a timeline tied to written deliverables and identify which parts depend on decisions your team has not yet made, such as the preview model or public versus authenticated access.

What does our team need to provide for a GraphQL project?

Your team needs to supply a clear content model, a list of front end screens or components with the data each one needs, and the access rules that apply to unpublished and restricted content. You also need named owners for the Drupal site, the front end, and the identity provider if users sign in. Environment access, a staging site with realistic content, and a decision on hosting for the GraphQL endpoint are usually required early. Without these inputs, schema changes tend to happen late in the build, which increases rework and testing effort.

Can our in-house team build a GraphQL API for Drupal without outside help?

Yes, if your team has Drupal developers who understand entities, fields, and access control, and front end engineers who can write and maintain GraphQL queries. The GraphQL module is configurable, but production-grade schemas usually need custom resolvers, query complexity controls, and caching decisions that are hard to revisit later. Evaluate your team against those skills, and consider a short design review or an external security review before the schema is exposed publicly. Plan for ongoing ownership of schema changes, module updates, and performance tuning, since these recur after launch.

When should we choose JSON:API in core instead of GraphQL?

Choose JSON:API in core when your front end mostly reads whole entities, your content model is stable, and you want fewer contributed modules to maintain. It is built into Drupal core, so it inherits core's release cycle and has fewer custom components to own. GraphQL is the better fit when front ends need to request exactly the fields they render across several nested relationships, or when one schema must serve multiple consumers with different shapes. Many teams start with JSON:API and move to GraphQL only when query shape becomes the bottleneck, so test both against your real screens before deciding.

How do we secure a GraphQL endpoint on Drupal?

Start by treating the endpoint like any other public API: enforce Drupal's entity access checks, restrict fields that hold unpublished or personal data, and decide how anonymous and authenticated requests differ. Token-based authentication, such as OAuth 2.0 bearer tokens, is common for preview and private content, and you should confirm what each token can read. Add query depth and complexity limits so a single request cannot exhaust the database. Keep the Drupal core and contributed modules current, because security releases follow a published schedule and advisories are published by the Drupal Security Team.

Does a GraphQL layer change how caching and performance work?

Yes. GraphQL usually sends many entity reads in one request, so the cost of a query depends on the fields and relationships it asks for. Drupal's cache tags still apply to the underlying entities, which lets reverse proxies and CDNs purge correctly, but you must confirm that resolvers and custom code attach the right cache metadata. Persisted queries, which store approved query text on the server, are a common way to control load and improve cache hit rates. Ask your provider to load test representative queries and show how cache invalidation behaves when editors publish content.

How DeployOpen scopes a headless Drupal build

A headless Drupal build is scoped as a fixed-scope engagement, with the written scope agreed before any schema or code work starts. That document should name every content type and field exposed through GraphQL, each consuming application (the Next.js or other front end, any mobile client, and any internal service), and the authentication model for each consumer: anonymous reads for public content, token-based access for previews, and any role-based access for authenticated data. It should also state the caching targets, such as which queries must be served from a cache, which cache tags invalidate them, and what an acceptable response time looks like under expected load. Ask for these to be written as acceptance tests, so the team can confirm that a query returns the same fields, permissions and cache behavior in staging that the schema promises.

Several decisions belong in that scope before the build begins. Confirm whether the project needs GraphQL at all, or whether core JSON:API covers the consumers with less custom schema to maintain. Decide which entity access rules the GraphQL schema must respect, since an unpublished node or a restricted field must not appear in a response because a query was written in a new way. Record who owns schema changes after launch, because every new field the front end needs becomes a change to a public contract. The scope should also name the Drupal and PHP versions it targets. The Drupal core release schedule and the Drupal.org PHP requirements page are the natural places to check current support windows before those versions are fixed in the document, since they change on published dates.

Ongoing work is priced and planned separately from the build. Once the headless site is live, schema changes, Drupal core and contributed module updates, security releases and cache tuning recur, and these can be covered by a DeployOpen retainer for support and upgrades instead of being re-scoped each time. Ask for the retainer terms in writing, including which Drupal versions, modules and environments it covers and how requests are prioritized. DeployOpen does not publish a price list, so the quote for both the build and any retainer follows from the written scope and the exposed surface area described above.

How DeployOpen works on this

DeployOpen plans, builds, documents and supports Headless Drupal with GraphQL inside your own infrastructure, whether that is your cloud accounts, data centers or air-gapped networks. Engagements are fixed-scope, so the schema, query limits, authentication model and deliverables are agreed in writing before build work starts.

How we scope

  • Inventory the content types, fields and relationships that front ends will actually query, and agree which ones stay out of the schema.
  • Define the authentication model for anonymous reads, authenticated previews and editor-only data before any schema work begins.
  • Record the written scope, deliverables and timeline with your team, including the decision on GraphQL versus JSON:API for each endpoint.

How we build

  • Design the GraphQL schema around front-end query patterns, then configure fields, types and resolvers in Drupal with custom code kept in version control.
  • Set query depth and complexity limits, persisted queries where appropriate, and cache metadata so responses invalidate correctly when content changes.
  • Test queries, permissions and cache behavior in a staging environment that mirrors your production web server, database and reverse proxy setup.

How we hand over

  • Deliver the schema reference, query examples, configuration exports and custom module documentation so your engineers can change them independently.
  • Walk your front-end and Drupal teams through the caching model, preview flow and how to add a field without breaking existing clients.
  • Provide a runbook covering deployment, cache clearing, credential rotation and the checks to run after each Drupal core or GraphQL module update.

How we support

  • Support and maintenance can be arranged as a retainer covering module updates, security patches and schema changes after launch.
  • Review query performance and cache hit rates periodically, and adjust limits or caching rules when front-end usage changes.
  • Ask us to confirm response times, update windows and escalation paths in the retainer terms before signing.

Talk to our team.

Tell us what you're working on, whether it's a deployment, an audit, a security test or a cyber range. You'll speak with an engineer who can help you scope it.

  • 30-minute call: free, with no obligation.
  • NDA on request: we can sign before you share details.
  • Clear next steps: a scope and plan after the call.