Tutorials

Drupal GraphQL: Setup Guide and Best Practices

Jay Callicott··9 min read

Drupal GraphQL: Setup Guide and Best Practices

Drupal's GraphQL API gives frontend developers a typed, queryable interface to content that goes well beyond what REST or JSON:API offer out of the box. You describe the shape of the data you need, and you get exactly that — no over-fetching, no multiple round trips for related entities.

This guide covers how to set up GraphQL in Drupal, write real queries, handle authentication, and avoid the common pitfalls that trip up teams building decoupled Drupal frontends.

Two Approaches: GraphQL Module vs. GraphQL Compose

Drupal has two main GraphQL modules, and understanding the difference matters before you install anything.

GraphQL Module (graphql 4.x)

The original GraphQL module (version 4.x) gives you full control over your schema. You define resolvers, types, and fields manually using PHP plugins. This is powerful for teams that need a custom API surface — for example, combining data from multiple Drupal subsystems into a single query — but it requires significant backend work. Every content type change means updating resolver code.

GraphQL Compose is the newer approach and the one most teams should start with. It auto-generates a GraphQL schema directly from your Drupal content types, fields, and entity references. When you add a field to a content type in Drupal, it appears in the GraphQL schema automatically. No custom resolvers. No PHP code.

GraphQL Compose is built on top of the GraphQL 4.x module, so you still get the full GraphQL server — you just skip the manual schema definition step.

When to choose which:

GraphQL Compose GraphQL 4.x (Custom)
Setup time Minutes Days to weeks
Schema maintenance Automatic Manual PHP resolvers
Best for Standard content APIs Custom data aggregation
Content type changes Schema updates automatically Requires code changes
Learning curve Low (Drupal admin only) High (PHP + GraphQL internals)

For building a headless Drupal frontend with Next.js, React, or any JavaScript framework, GraphQL Compose is the practical choice.

Setting Up GraphQL Compose

Step 1: Install the Modules

composer require drupal/graphql drupal/graphql_compose
drush en graphql graphql_compose -y

This installs both the base GraphQL module and the Compose layer.

Step 2: Create a GraphQL Server

Navigate to Admin > Configuration > Web services > GraphQL (/admin/config/graphql). Click Add server and configure:

  • Label: Compose (or any descriptive name)
  • Schema plugin: Select GraphQL Compose
  • Endpoint: /graphql (the default)
  • Caching: Enable for production

Save the server. Your GraphQL endpoint is now live.

Step 3: Configure Exposed Content Types

Go to Admin > Configuration > Web services > GraphQL Compose (/admin/config/graphql_compose). Here you control which content types, fields, and entity types are exposed in the schema.

For each content type, you can:

  • Enable or disable the type in the schema
  • Choose which fields to expose
  • Configure field-level access

This is where you shape your API surface without writing code. Only expose what your frontend actually needs.

Step 4: Verify with the Explorer

Back on the GraphQL server configuration page, click Explorer. This opens GraphiQL, an in-browser IDE where you can test queries against your schema. Try a simple query:

{
  __schema {
    queryType {
      fields {
        name
      }
    }
  }
}

If you see your content types listed in the response, the setup is working.

Query Examples

The following examples use the schema that GraphQL Compose generates. Field names follow the pattern node{ContentType} for single items and node{ContentType}s for lists.

Fetching a List of Articles

query GetArticles {
  nodeArticles(first: 10) {
    nodes {
      id
      title
      body {
        processed
      }
      path
      created {
        timestamp
      }
      author {
        displayName
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

The first argument controls page size. The pageInfo object gives you cursor-based pagination — pass endCursor as the after argument in the next query to get the next page.

Fetching a Single Node by ID

query GetArticle($id: ID!) {
  nodeArticle(id: $id) {
    id
    title
    body {
      processed
    }
    path
    created {
      timestamp
    }
    fieldImage {
      url
      alt
      width
      height
    }
    fieldCategory {
      name
    }
  }
}

Filtering and Sorting

GraphQL Compose supports filtering through condition arguments:

query GetPublishedArticles {
  nodeArticles(
    first: 10
    sortKey: CREATED_AT
    reverse: true
  ) {
    nodes {
      id
      title
      created {
        timestamp
      }
    }
  }
}

For more complex filtering — by taxonomy term, date range, or custom field values — you can use the filter argument when available, or create GraphQL Compose filter plugins for advanced use cases.

Querying Relationships

Entity references resolve as nested objects. If an article references a category taxonomy term and an author entity, you query them as nested fields:

query GetArticleWithRelations($id: ID!) {
  nodeArticle(id: $id) {
    title
    fieldCategory {
      name
      description {
        processed
      }
    }
    fieldTags {
      name
    }
    fieldRelatedArticles {
      title
      path
    }
  }
}

No extra requests. No relationship IDs to resolve manually. The graph structure of GraphQL maps naturally to Drupal's entity reference system.

Pagination with Cursors

query GetArticlesPaginated($cursor: String) {
  nodeArticles(first: 10, after: $cursor) {
    nodes {
      id
      title
      path
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

In your frontend, pass the endCursor from the previous response as the $cursor variable to load the next page.

Authentication

Your Drupal GraphQL API needs authentication when the frontend must access unpublished content, create content, or perform any action beyond anonymous read access. There are three main approaches.

OAuth is the standard for server-to-server and user-delegated authentication with Drupal. Install the Simple OAuth module:

composer require drupal/simple_oauth
drush en simple_oauth -y

Generate keys, create a consumer (client), and request tokens:

// Get an access token
const tokenResponse = await fetch('https://your-site.com/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: process.env.DRUPAL_CLIENT_ID,
    client_secret: process.env.DRUPAL_CLIENT_SECRET,
  }),
})
const { access_token } = await tokenResponse.json()

// Use the token in GraphQL requests
const response = await fetch('https://your-site.com/graphql', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${access_token}`,
  },
  body: JSON.stringify({ query, variables }),
})

Best for: Production decoupled frontends, server-side data fetching in Next.js, CI/CD pipelines.

JWT (JSON Web Tokens)

JWT tokens are stateless and work well for short-lived sessions. Install the JWT Auth module. The workflow is similar to OAuth — you exchange credentials for a token, then include it in the Authorization header.

Best for: Single-page applications, mobile apps, scenarios where you need stateless authentication.

Drupal's default session authentication works with GraphQL if the frontend runs on the same domain. The browser sends the session cookie with each request. This is the simplest approach but limits you to same-origin requests.

const response = await fetch('https://your-site.com/graphql', {
  method: 'POST',
  credentials: 'include', // Send cookies
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ query }),
})

Best for: Admin interfaces, preview environments, same-domain setups.

Which Authentication Method to Use

Scenario Recommended Method
Public content (anonymous) None — use the public endpoint
Server-side rendering (Next.js) OAuth 2.0 (client credentials)
User-facing login (SPA) OAuth 2.0 (authorization code) or JWT
Editorial preview Session-based or OAuth
CI/CD or build-time fetching OAuth 2.0 (client credentials)

Mutations: Creating and Updating Content

GraphQL Compose can expose mutations for creating and updating content. Enable mutations in the module configuration, then use them with proper authentication:

mutation CreateArticle($input: NodeArticleInput!) {
  createNodeArticle(input: $input) {
    node {
      id
      title
      path
    }
    errors
  }
}

With variables:

{
  "input": {
    "title": "New Article Title",
    "body": {
      "value": "<p>Article body content.</p>",
      "format": "full_html"
    },
    "fieldCategory": "category-term-uuid"
  }
}

Updating content follows the same pattern with an update mutation and the node's ID:

mutation UpdateArticle($id: ID!, $input: NodeArticleInput!) {
  updateNodeArticle(id: $id, input: $input) {
    node {
      id
      title
    }
    errors
  }
}

Mutations always require authentication. The authenticated user must have the corresponding Drupal permissions (create, edit) for the content type.

Best Practices

Cache Aggressively

GraphQL responses are highly cacheable because clients request exactly the fields they need. Configure caching at multiple layers:

  • Drupal's internal page cache: Enable the GraphQL server's built-in caching
  • CDN caching: Cache GET-based GraphQL requests (for persisted queries) at the edge
  • Client-side caching: Use a client like Apollo or urql that caches normalized query results

Use Persisted Queries

Persisted queries replace full query strings with short hashes. This improves security (clients can't send arbitrary queries) and performance (smaller request payloads, better CDN cacheability).

// Instead of sending the full query string:
const response = await fetch('/graphql?queryId=abc123&variables={}')

The GraphQL module supports automatic persisted queries out of the box. Enable them in the server configuration.

Keep Your Schema Lean

Expose only the content types and fields your frontend needs. A bloated schema slows down introspection, increases attack surface, and makes the API harder to understand. Review your GraphQL Compose configuration periodically and disable fields that aren't being queried.

Handle Errors Gracefully

GraphQL returns a 200 status code even when queries fail. Always check the errors array in the response:

const { data, errors } = await response.json()

if (errors?.length) {
  console.error('GraphQL errors:', errors)
  // Handle partial data or bail out
}

Secure the Endpoint

  • Disable introspection in production (or restrict it to authenticated users)
  • Set query depth limits to prevent deeply nested malicious queries
  • Use persisted queries to lock down the allowed query set
  • Apply Drupal's permission system — GraphQL respects entity access controls

How Decoupled.io Simplifies This

Setting up Drupal's GraphQL stack from scratch means installing modules, generating keys, configuring servers, managing schema updates, and handling caching — all before you write your first frontend query.

Decoupled.io removes that overhead:

  • GraphQL is pre-configured: Every Decoupled.io site ships with a GraphQL endpoint ready to query. No module installation or server configuration.
  • Schema stays in sync: When you update content types through the admin UI, the GraphQL schema updates automatically. There's no manual step.
  • Authentication is built in: OAuth tokens and API keys work out of the box. No key generation or consumer setup.
  • Caching and persisted queries: Production caching is configured by default, including CDN-level caching for GraphQL responses.
  • Full API docs: The GraphQL API reference and REST API docs document every available query and endpoint.

If you want the power of Drupal's GraphQL API without the DevOps overhead, Decoupled.io gives you a managed GraphQL endpoint backed by Drupal's content modeling — ready to connect to your Next.js, React, or any frontend framework.

Next Steps