Decoupled Drupal โ using Drupal exclusively as a headless CMS backend with React (or Next.js, Vue, Svelte) handling the frontend โ has gone from experimental to mainstream in enterprise projects. But "decoupled" isn't a single architecture; it's a spectrum with meaningful trade-offs at every point. This guide covers everything from setting up Drupal's JSON:API, through authentication, to live content previews in Next.js.
1. Fully Decoupled vs Progressively Decoupled
Before writing a line of code, you need to make the most important architectural decision:
- Fully Decoupled: React/Next.js handles all rendering. Drupal serves only JSON via JSON:API or GraphQL. No Drupal theming, no Twig. Drupal editors use the standard admin interface; the frontend is entirely separate.
- Progressively Decoupled: Drupal renders the page shell and some components traditionally. React "islands" handle specific interactive components (search, cart, live data). Simpler to implement, easier content preview, lower hosting complexity.
Fully decoupled makes sense when: the frontend team prefers React/Next.js, you need CDN-edge rendering, or you're building multiple frontends (web + mobile + kiosk) from one content backend. Progressively decoupled is often the right call when the editorial experience or content preview matters more than frontend freedom.
2. Setting Up Drupal as a JSON:API Backend
JSON:API ships in Drupal core. Enable it, configure CORS, and your content is immediately available:
# services.yml โ CORS for your React dev server and production domain
cors.config:
enabled: true
allowedHeaders: ['*']
allowedMethods: ['GET','POST','PATCH','DELETE']
allowedOrigins: ['https://yourapp.com','http://localhost:3000']
exposedHeaders: false
maxAge: false
supportsCredentials: true
Fetch nodes from React:
const res = await fetch(
'https://cms.example.com/jsonapi/node/article?
include=field_image,uid&
filter[status]=1&
sort=-created&
page[limit]=10',
{ headers: { Accept: 'application/vnd.api+json' } }
);
const { data, included } = await res.json();
JSON:API supports filtering, sorting, pagination, sparse fieldsets, and relationship includes โ all without writing a custom endpoint.
3. Authentication: OAuth2 and Simple OAuth
Public content needs no authentication. For authenticated requests (personalised content, drafts, protected resources), install the Simple OAuth module:
# Get a token (client credentials flow for server-side Next.js)
curl -X POST https://cms.example.com/oauth/token \
-d 'grant_type=client_credentials' \
-d 'client_id=YOUR_CLIENT_ID' \
-d 'client_secret=YOUR_CLIENT_SECRET'
# Use the token
fetch('/jsonapi/node/article?filter[status]=0', {
headers: { Authorization: `Bearer ${token}` }
})
For user-authenticated frontends (login, personalisation), use the Authorization Code flow. For server-side Next.js, the client credentials flow is simpler and sufficient.
4. Building the React Frontend with Next.js
Next.js App Router with server components is the current best practice for Drupal + React projects. Pages are rendered at the edge, draft content is previewed via Next.js Draft Mode, and ISR (Incremental Static Regeneration) keeps content fresh without full rebuilds:
// app/articles/[slug]/page.tsx
export async function generateStaticParams() {
const { data } = await fetchFromDrupal('/jsonapi/node/article?fields[node--article]=field_slug');
return data.map((node: any) => ({ slug: node.attributes.field_slug }));
}
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const article = await fetchArticleBySlug(params.slug);
return <ArticleTemplate article={article} />;
}
5. Content Preview in Decoupled Setups
Content preview is the hardest part of decoupled Drupal. Editors expect to click "Preview" and see their unpublished changes in the actual frontend. The standard solution uses Next.js Draft Mode:
- Install the
next_drupalmodule (from nextjs.drupal.org) - Configure a Preview URL pointing to your Next.js
/api/previewroute - The preview route authenticates with Drupal, sets a Draft Mode cookie, and redirects to the node page
- Server components detect Draft Mode and fetch unpublished revisions via the authenticated JSON:API
6. GraphQL as an Alternative to JSON:API
JSON:API is great for REST-style data fetching but verbose when you need deeply nested, precisely shaped responses. The GraphQL module for Drupal lets you define a schema and fetch exactly what you need:
query ArticleQuery($slug: String!) {
route(path: $slug) {
... on NodeArticle {
title
body { value processed }
fieldImage { url alt }
fieldTags { name }
}
}
}
We use GraphQL when the frontend team prefers it, or on projects with complex relationship graphs where JSON:API's include chains become unwieldy.
7. When Not to Go Decoupled
Decoupled adds complexity โ two deployment pipelines, two hosting environments, CORS configuration, authentication, preview complexity. Don't choose it by default. Traditional Drupal with a well-built Twig theme is faster to build, easier to deploy, and has better built-in editorial ergonomics for most projects. Go decoupled when:
- You need CDN-edge rendering for global audiences
- The frontend team specifically requires React/Vue/Svelte
- Multiple frontends will consume the same content
- You need offline-capable PWA functionality