The rise of modern JavaScript frameworks has pushed many teams to ask whether they still need a traditional CMS. The answer, increasingly, is: yes โ€” but not in the traditional sense. Drupal is emerging as one of the most capable headless backends available, pairing its powerful content modelling and editorial interface with a clean JSON:API (or GraphQL) layer that any frontend can consume.

In this guide we'll build a working mental model of Drupal as a headless backend, walk through connecting a React.js frontend to Drupal's JSON:API, handle authentication, and tackle real-world challenges like content previews and structured data relationships.

1. Why Drupal as a Headless Backend?

Not all headless CMS platforms are equal as backends. Drupal stands out for several reasons that matter at production scale:

  • Mature content modelling: Content types, fields, taxonomies, entity references, paragraphs โ€” Drupal's field API is the most flexible content model available in any CMS.
  • JSON:API is built into core: Since Drupal 8.7, JSON:API ships as a core module. Zero extra cost, fully standards-compliant, actively maintained.
  • Granular permissions: Role-based access control down to individual field level. Critical for editorial workflows.
  • Battle-tested at scale: Drupal powers sites serving hundreds of millions of page views. The backend can handle whatever your React frontend throws at it.

2. Enabling JSON:API in Drupal

Enable the JSON:API core module:

drush en jsonapi
drush cr

Once enabled, every content entity in Drupal is automatically exposed at a predictable URL pattern:

GET /jsonapi/node/{content_type}
GET /jsonapi/node/{content_type}/{uuid}
GET /jsonapi/taxonomy_term/{vocabulary}

For example, to fetch all published blog posts:

GET /jsonapi/node/blog_post?filter[status]=1&sort=-created&page[limit]=10

3. Querying Content from React

In your React application, fetch from Drupal's JSON:API using standard fetch() or your preferred HTTP client:

const DRUPAL_BASE = 'https://cms.diamondtechsoft.com';

async function getBlogPosts() {
  const res = await fetch(
    `${DRUPAL_BASE}/jsonapi/node/blog_post` +
    `?filter[status]=1` +
    `&sort=-created` +
    `&page[limit]=10` +
    `&include=field_featured_image,field_author`
  );
  const data = await res.json();
  return data.data; // array of post nodes
}

// Access field values
posts.forEach(post => {
  const title   = post.attributes.title;
  const body    = post.attributes.body?.processed; // HTML
  const created = post.attributes.created;
});

The include parameter tells JSON:API to embed related entities (images, authors) in the same response, reducing the number of HTTP requests your React app needs to make.

4. Authentication and Protected Routes

For public content, no authentication is needed. For content that requires a logged-in user (drafts, user-specific data), use OAuth2 with the simple_oauth Drupal module:

composer require drupal/simple_oauth
drush en simple_oauth

In React, exchange credentials for a bearer token and attach it to requests:

async function getToken() {
  const res = await fetch(`${DRUPAL_BASE}/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type:    'password',
      client_id:     'YOUR_CLIENT_ID',
      client_secret: 'YOUR_CLIENT_SECRET',
      username:      'editor@example.com',
      password:      'password',
    }),
  });
  return (await res.json()).access_token;
}

// Attach token to protected requests
const token = await getToken();
const res = await fetch(`${DRUPAL_BASE}/jsonapi/node/blog_post`, {
  headers: { Authorization: `Bearer ${token}` },
});

5. Handling Drupal Previews in React

One of the biggest pain points in headless CMS setups is content preview โ€” editors need to see how their draft content looks in the React frontend before publishing. The approach with Drupal + Next.js:

  1. Install drupal/next module on the Drupal side.
  2. Install next-drupal package in your Next.js project.
  3. Configure a preview URL in Drupal that triggers Next.js Draft Mode.

For plain React (non-Next.js), pass a ?preview=true&token={previewToken} query param and use the jsonapi_extras module to expose draft nodes to authenticated requests.

6. GraphQL as an Alternative to JSON:API

For complex data requirements โ€” deeply nested entity graphs, custom queries, or reducing over-fetching โ€” Drupal's graphql module provides a full GraphQL schema auto-generated from your content model:

composer require drupal/graphql
drush en graphql graphql_core

GraphQL is particularly valuable when your React frontend needs data from multiple entity types in a single query, or when mobile performance makes minimising payload size critical.

7. Structuring Your Decoupled Project

A well-structured decoupled project separates concerns cleanly:

  • Drupal (backend): Runs on cms.yourdomain.com or a non-public subdomain. Handles all content management, editorial workflows, and API exposure.
  • React/Next.js (frontend): Runs on www.yourdomain.com. Fetches from Drupal at build time (SSG) or runtime (SSR/CSR) depending on content freshness requirements.
  • Shared API client: Centralise all Drupal API calls in a single lib/drupal.js file to avoid scattered fetch calls across components.

Conclusion

Drupal as a headless backend gives you the editorial power of a mature CMS combined with the complete frontend freedom of React. JSON:API makes integration straightforward from day one, and the ecosystem around decoupled Drupal โ€” particularly with Next.js โ€” has matured to the point where production deployments are routine rather than experimental.

The investment in setting up Drupal as your backend pays off as content requirements grow in complexity โ€” structured relationships, editorial workflows, multilingual support, and access control are all handled by Drupal, leaving your React team free to focus on the user experience.