Start of Main Content

Decoupled Drupal applications have a basic problem that becomes more noticeable as a project grows: Drupal knows what its data looks like, but the frontend often has to describe that data all over again, and in ways that may not always work. Consider a Drupal site used as the content source for a Next.js application. Drupal may define an Article content type with a title, summary, image, author, tags, and other fields. The Next.js application then needs TypeScript definitions describing those fields before developers can safely work with the data.

A developer might start with something like this:

interface Article { 
  id: string; 
  title: string; 
  summary: string | null; 
  image: Image | null; 
  tags: Tag[]; 
} 

There is nothing wrong with this interface. The problem is where it came from. A developer looked at Drupal and wrote a TypeScript representation of what they believed Drupal would return. From that point forward, Drupal's content model and the TypeScript interface are two separate things that have to be kept synchronized.

If field_summary changes, a relationship is added, or a new component type becomes available, someone must remember to update the frontend. The TypeScript Definition Generator module takes a different approach. It reads Drupal's Entity, Field, and Typed Data APIs and generates TypeScript definitions from the Drupal site itself. Instead of maintaining two descriptions of the same system, Drupal becomes the source of truth.

Generating TypeScript from Drupal

After selecting the entity types that should be exposed to the frontend, definitions can be generated with Drush:

drush tsg --dir=../next-app/src/definitions

The generated definitions describe the actual Drupal entities and fields configured on the site. If Drupal changes, the definitions can simply be regenerated. This becomes especially useful when Drupal and Next.js are maintained as separate applications. The generated files can be committed to the frontend repository and used like any other TypeScript definitions.

There is no requirement for the generator itself to run in the Next.js application. The frontend receives a contract generated from the CMS rather than maintaining its own approximation of one.

Working with JSON:API

JSON:API is a common integration point between Drupal and Next.js.

TypeScript Definition Generator can generate JSON:API definitions with:

drush tsg \ 
  --dir=../next-app/src/definitions \ 
  --jsonapi

The generated definitions understand Drupal bundles, attributes, and relationships. One important detail is that JSON:API does not guarantee that every response contains data. It can return an error document instead. The generated document types account for this. This is useful because TypeScript is checking the actual behavior of the API rather than assuming every request succeeds.

Relationships are also generated based on what Drupal says a field can reference. An entity reference to Media does not need to become a generic unknown object or a manually maintained frontend type. The relationship can be described using the resource types Drupal actually allows.

Removing Drupal's Field Wrappers

There are situations where a frontend needs to work more directly with Drupal's serialized entity data.

Drupal's internal field representation is useful to Drupal, but it is not necessarily the shape a React developer wants to use.

A value may effectively look like this:

title: FieldItemList;

For a React component, the developer probably wants this:

title: string | null;

TypeScript Definition Generator can generate a normalized frontend representation along with parser functions:

drush tsg \ 
  --dir=../next-app/src/definitions \ 
  --normalized \ 
  --parser

The parser converts Drupal's serialized representation into a simpler structure intended for application code.

A component can then work with straightforward values:

import type { NodeArticle } from "@/definitions/normalized"; 
 
interface ArticleProps { 
  article: NodeArticle; 
} 
 
export function Article({ article }: ArticleProps) { 
  return ( 
    <article> 
      <h1>{article.title}</h1> 
 
      {article.field_summary && ( 
        <p>{article.field_summary}</p> 
      )} 
    </article> 
  ); 
}

This keeps Drupal-specific data handling at the boundary of the application instead of spreading it throughout React components. That distinction matters.

The frontend does not need to pretend Drupal's data model is simpler than it is. The generated definitions can accurately describe Drupal, while the generated normalized types provide a more convenient representation for frontend code.

 

Velir x Brooklyn Data Recognized as a Vercel Certified Solution Partner

Building a decoupled Drupal and Next.js application often means deploying on Vercel. Velir x Brooklyn Data is now a Vercel Certified Solution Partner, recognized for delivering fast, reliable, and scalable headless experiences on composable architectures like this one.

Drupal Canvas and Component-Based Frontends

Drupal Canvas makes this particularly interesting.

Canvas allows editors to assemble pages from components. In a traditional Drupal application, those components may ultimately be rendered by Drupal. In a decoupled application, the frontend may instead need to decide which React component renders a piece of content. For example, a project might have components for:

  • Hero
  • Card
  • Call to Action
  • Quote
  • Image Gallery

The corresponding Next.js application might contain:
components/
  Hero.tsx
  Card.tsx
  CallToAction.tsx
  Quote.tsx
  ImageGallery.tsx

The challenge is making sure the two applications continue to agree about which components exist.

TypeScript Definition Generator can generate a bundle-to-component contract:

drush tsg \ 
  --dir=../next-app/src/definitions \ 
  --components

The Next.js application can then define its component mapping against that generated contract:

import Hero from "@/components/Hero"; 
import Card from "@/components/Card"; 
import CallToAction from "@/components/CallToAction"; 
import Quote from "@/components/Quote"; 
import ImageGallery from "@/components/ImageGallery"; 
 
import type { 
  DrupalComponentMap, 
} from "@/definitions/components"; 
 
export const componentMap = { 
  hero: Hero, 
  card: Card, 
  call_to_action: CallToAction, 
  quote: Quote, 
  image_gallery: ImageGallery, 
} satisfies DrupalComponentMap;

Now consider what happens when someone adds a new Video component in Drupal. Drupal now knows about:

  • Hero
  • Card
  • Call to Action
  • Quote
  • Image Gallery
  • Video

The definitions are regenerated, but the Next.js application still only has mappings for the original five components. TypeScript can report that the Video component is missing. That is much better than discovering the problem because an editor builds a page containing a Video component and the frontend renders an empty space.

The content model has changed, so the frontend build tells the developer that frontend work is required. This approach is not limited to Canvas. The generator works with Drupal content entities and can be useful with component models built using Canvas, Paragraphs, Layout Builder, or custom entity types.

A Manifest for Uses Beyond TypeScript

Not every integration needs TypeScript output.

The module can also generate a language-independent schema manifest:

drush tsg \ 
  --dir=../next-app/src/definitions \ 
  --manifest

This produces a schema.json representation containing information such as field types, cardinality, allowed values, and storage classifications. That provides an integration point for tools that do not consume TypeScript directly.

For example, a project could use the manifest to generate Zod validation schemas. TypeScript checks code while it is being developed and built. Zod can validate the data that actually arrives over the network. The same manifest could also be used to generate JSON Schema, API documentation, test data, or other project-specific outputs.

The generator does not need to directly support every possible target. The manifest provides the Drupal schema as structured data that other tools can consume.

Catching Drupal Changes in CI

Generation becomes even more useful when it is part of the development process. The --check option compares the definitions that currently exist with what Drupal would generate:

drush tsg \ 
  --dir=../next-app/src/definitions \ 
  --parser \ 
  --check

It does not overwrite the files. If Drupal's current schema would produce different definitions, the command exits with an error.

This means it can run in CI. A project can install Drupal from committed configuration, run the generator in check mode, and then run the frontend type check and build.

Suppose a Drupal change adds field_related_articles to the Article content type, but the generated definitions were not updated. The check will fail.

Suppose a new Canvas component is introduced. The definitions are updated, but nobody creates the corresponding React component. The TypeScript build can fail.

These are useful failures. They happen during development instead of becoming missing content or unexpected data in production.

Keeping Runtime Applications Separate

It is also important to understand what the module does not do. It does not create a runtime dependency between Next.js and the generator. The generator runs against Drupal and produces ordinary TypeScript files. Those generated files do not require Drupal or the generator to execute inside Next.js.

JSON:API, or another chosen API, remains responsible for delivering the actual content.

The generated definitions simply make sure the frontend has an accurate description of the Drupal application it is communicating with.

Drupal Already Knows the Answer

The larger idea behind TypeScript Definition Generator is fairly simple. Drupal already has a detailed description of its data.

It knows which entity types exist. It knows their bundles. It knows their fields. It knows field cardinality. It knows which entity types a reference can point to. Drupal Canvas and other component systems add another layer of structured information that a decoupled frontend may need to understand. A TypeScript application needs much of the same information.

For a Next.js application, that provides useful integration at several levels:

  • JSON:API responses can have generated TypeScript definitions.
  • Drupal serialized entities can be converted into simpler frontend types.
  • Canvas and other component-based implementations can have checked component mappings.
  • A schema manifest can feed runtime validators and other tools.
  • CI can detect when Drupal configuration and frontend definitions no longer agree.

None of these change how Drupal content is delivered. They change how confidently the frontend can work with it.

As decoupled Drupal applications become larger, that distinction becomes increasingly valuable. The goal is not to hide Drupal from TypeScript. It is to let TypeScript understand Drupal without requiring developers to continuously describe the same content model by hand.

Published:

Latest Ideas

Like our ideas? Leverage our expertise for your next project.