Skip to content

Architecture

How Drupal, JSON:API and Nuxt fit together in a fully decoupled Druxt site.

Druxt is a framework for fully decoupled Drupal and Nuxt.js sites. In a traditional (coupled) Drupal site, Drupal both manages and renders content. In a Druxt site, the two jobs are split:

  • Drupal models content, configuration and business logic, and exposes it as JSON:API.
  • Nuxt handles the URL, the request lifecycle and the rendering.
  • Druxt is the contract between them: clients, stores, schemas and components that turn JSON:API data into a rendered page.

Understanding the split explains most of Druxt's design decisions:

  • Why the frontend asks Drupal to translate paths.
  • Why display modes drive field rendering.
  • Why every component can be themed.

The request lifecycle

When a browser requests a page from a Druxt site, the work happens in this order:

  1. Nuxt receives the request and matches it against its own Vue Router routes. In a typical site, a wildcard route hands the path to the router module.
  2. Path translation: Druxt asks Drupal (via the decoupled_router module) what entity serves this path. The answer identifies the entity type, bundle (Drupal's word for a content type, like article), UUID and view mode.
  3. Data fetching: the DruxtStore checks its cache, and asks Drupal's JSON:API for anything missing. Display-mode schemas can narrow the query so only rendered fields travel the wire.
  4. Rendering: entity, field and block components render the data. Each delegates its markup to a theme component chosen by the suggestion system.
  5. Response: Nuxt returns fully server-rendered HTML, then hydrates it in the browser: Vue takes over the already-rendered markup and makes it interactive without re-fetching the page.

The same five steps, as a picture:

%% One page request through the decoupled stack, from browser to hydration
sequenceDiagram
  participant B as Browser
  participant N as Nuxt + Druxt
  participant DR as decoupled_router
  participant JA as JSON:API
  B->>N: GET /article-path
  N->>DR: translate-path?path=/article-path
  DR-->>N: entity type, bundle, UUID
  N->>JA: fetch resource (schema-narrowed)
  JA-->>N: JSON:API resource + includes
  N-->>B: server-rendered HTML + serialized store
  Note over B: Hydration. Vue takes over the markup.

What lives where

LayerPackage(s)Responsibility
Core clientdruxtDruxtClient: JSON:API communication, authentication, caching hints
Core storedruxtDruxtStore: resource/collection cache shared by all modules
Base componentdruxtDruxtModule: the druxt() options contract every module builds on
Routingdruxt-routerPath translation, redirects, metadata
Schemasdruxt-schemaDisplay modes as query-filtering and render configuration
Contentdruxt-entityEntity and field components
Site furnituredruxt-blocks, druxt-menu, druxt-views, druxt-breadcrumbBlocks, menus, views, breadcrumbs
Everything at oncedruxt-siteOpinionated bundle: a working site layout out of the box

On the Drupal side, the druxt module is the only piece to install (it adds the permissions and path translators). It depends on decoupled_router, jsonapi_menu_items and jsonapi_views, so composer and Drupal bring those in with it. See Prepare the Drupal backend.

Why this shape

  • JSON:API as the contract (not custom REST endpoints) keeps the frontend compatible with Drupal core's supported web services, including its filtering, inclusion and pagination semantics.
  • A shared store (rather than per-component fetching) means two components asking for the same article hit Drupal once, and includes (an article's image, the image's file) are stored once no matter which query brought them in.
  • Display modes as configuration source means site builders (not frontend developers) decide which fields appear where, using a UI they already know.
  • Wrapper components everywhere keep the frontend from becoming a fork: any component's rendering can be replaced per-site without patching the framework.

Trade-offs

The architecture is opinionated about Drupal: it assumes JSON:API, display modes and decoupled routing are available and authoritative. It is not a general-purpose headless-CMS toolkit: the tight coupling is what makes the out-of-the-box experience possible.

The current major line targets Nuxt 2 / Vue 2.7; the rendering layer is designed to be rebuilt (Nuxt 4 / Vue 3) without changing the Drupal-side contract.

Where to go next