Europe/London
Mar 2026
Career
2 min read

Going headless with Drupal -- what nobody tells you about JSON:API

CORS, preview mode, authentication tokens, and the caching gotchas that will bite you in production.

"Headless Drupal" sounds straightforward — Drupal manages content, Next.js renders it. In practice there are a handful of sharp edges that nobody writes about until they've already cut themselves. Here are the ones that caught me.

CORS on the Drupal side

By default Drupal's JSON:API will block cross-origin requests. You need to configure the cors.config in services.yml explicitly. Don't just set allowedOrigins: ['*'] on production — scope it to your frontend domain.

# web/sites/default/services.yml
cors.config:
  enabled: true
  allowedHeaders: ['Content-Type', 'Authorization']
  allowedMethods: ['GET', 'POST', 'PATCH', 'DELETE']
  allowedOrigins: ['https://your-frontend.vercel.app']
  exposedHeaders: false
  maxAge: false
  supportsCredentials: false

Local development: Add http://localhost:3000 to allowedOrigins in your dev services.local.yml. Never commit it to your main services.yml.

Preview mode

Drupal's JSON:API doesn't expose unpublished content to anonymous requests. For editorial preview you need the jsonapi_extras module plus a custom Next.js preview route that authenticates against Drupal's OAuth token endpoint before fetching draft content.

Caching gotchas

Next.js ISR and Drupal's internal page cache can fight each other. The symptom is stale content on the frontend even after a Drupal save. The fix is a cache tag purge webhook — Drupal sends a purge request to your Next.js revalidation endpoint on node save. Simple, reliable, worth the 30 minutes to set up.

// Next.js API route: /api/revalidate
export async function POST(request: Request) {
  const { tag, secret } = await request.json();
  if (secret !== process.env.REVALIDATE_SECRET) {
    return Response.json({ error: 'Invalid secret' }, { status: 401 });
  }
  revalidateTag(tag);
  return Response.json({ revalidated: true });
}

Image handling

Drupal serves images at their original URL. Next.js Image component needs the domain explicitly whitelisted in next.config.ts. Also set up an image style in Drupal at the exact dimensions your frontend needs — fetching full-resolution originals and resizing client-side is wasteful.