Harnessing the Power of Decoupled Architecture with Next.js and Drupal
In today's digital ecosystem, the choice of technology stack is crucial to the success of any project, particularly when developing large-scale web applications. A trend gaining momentum is the decoupling of the frontend and backend, which enhances flexibility, scalability, and the overall user experience. This architectural choice is brilliantly exemplified by the integration of Next.js and Drupal, where Drupal's robust content management capabilities are combined with the modern frontend framework of Next.js.
Recognizing the limitations of its traditional Twig-based frontend, Drupal has embraced a more flexible approach known as "Decoupled Drupal." This blog post delves into projects that we’ve worked on: Novozymes and Novonesis that leveraged Next.js for the frontend and Drupal as the headless CMS backend, offering valuable insights for digital solution leads and developers keen on exploring this technology stack.
Table of Contents
- Embracing Flexibility: Drupal as a Headless CMS
- Next.js: Elevating Frontend Development">
- Integrating Next.js with Drupal in Our Projects: Key features
- Routing system
- Retrieving Data from Drupal
- Preview Mode
- Incremental Static Regeneration
- Time-based Incremental Static Regeneration
- On-demand Revalidation
- Authentication Layer for Securing APIs from Drupal to NextJS
- Pros and Cons of Using Next.js with Drupal
- Tips and Strategies for Building a Website with Decoupled Drupal and Next.js
- The Good and the Tough
- Final Thoughts
Embracing Flexibility: Drupal as a Headless CMS
Drupal, widely recognized for its strong content management features, also excels in a headless configuration where it operates solely as a content management backend, exposing its data through APIs for external applications to consume. This approach, known as "Decoupled Drupal," enables developers to pair Drupal's powerful backend with modern frontend technologies such as Next.js, enhancing flexibility and user experience.
The Drupal community has put significant effort into making Drupal an API-first CMS, ensuring that its content is easily accessible to other applications. Drupal's core already includes support for REST API and JSON API, offering seamless integration capabilities. Additionally, for projects requiring more complex data interactions, Drupal can be extended with GraphQL through contributed modules. While this article focuses on leveraging the JSON API for integration with a Next.js site, utilizing GraphQL is also a feasible option depending on specific project requirements. This flexibility makes Drupal an excellent choice for developers looking to harness the power of a headless CMS.
Next.js: Elevating Frontend Development
Next.js stands at the forefront of modern web development frameworks, offering developers an array of powerful tools designed to build fast, scalable, and interactive user experiences. As a React-based framework, Next.js simplifies the creation of complex applications with its efficient data fetching mechanisms and pre-rendering capabilities—both server-side and static generation. This flexibility allows developers to optimize performance and enhance SEO without compromising on user experience or development speed.
Key features like automatic code splitting ensure that applications load quickly, while built-in support for CSS-in-JS libraries like styled-components and Sass enhances the styling process. Next.js also supports API routes, which allow developers to build API endpoints within Next.js apps to handle backend logic or database operations seamlessly. This integration of both frontend and server-side logic within a single framework makes Next.js a highly versatile tool, adept at handling a range of web development challenges.
Integrating Next.js with Drupal in Our Projects: Key features
In the Novozymes project, we utilized a specific open-source toolkit known as "Next.js for Drupal," (https://next-drupal.org/) pivotal for bridging Drupal with Next.js. This toolkit comprises a Drupal module and a Node.js package, which includes a DrupalClient class designed to streamline interactions between Next.js and Drupal.
The project also comes with comprehensive documentation and examples that cover various scenarios, supporting developers through the integration process. Sponsored by Chapter Three, this initiative has seen significant contributions from both the sponsoring organization and the broader community including myself, with sponsorship from BRAINSUM (next revalidate options, updated docs). This ensures that developers have access to the necessary code and guidance to develop Next.js applications using Drupal as the content management backbone.
At the time of our project implementation, we chose to use Next.js version 13 along with its pages router, as it was the most recent version available. Since then, Next.js has released newer versions that include advancements such as the app router. However, the "Next.js for Drupal" package has not kept pace with the recent updates to Next.js, potentially requiring alternative approaches for those looking to utilize the latest features.
While the "Next.js for Drupal" package significantly simplifies the process of integration by providing ready-to-use tools, it is not strictly necessary for bridging Next.js with Drupal.
For those looking to adopt the latest versions of Next.js and utilize features like the app router, developing a custom solution might be required. This custom solution would manage communications with Drupal, replacing the need for any third-party module. Alternatively, you might consider monitoring for any updates or new releases of the mentioned package (https://github.com/chapter-three/next-drupal/issues/692) that could include support for the latest Next.js versions, ensuring compatibility and ongoing effectiveness in integrating these advanced technologies.
The Drupal module provided by the project Next.js for Drupal enables features such as Incremental Static Regeneration and Preview mode of drafts and content (not yet published on the Next.js site).
The Next.js for Drupal project documentation explains how to do this in detail in its Quick Start Guide.
Contrary to the Novozymes website, a couple of months later when we developed the Novonesis project, the stable version of the App Router from Next.js was released. Opting to leverage this in our new project, we faced the challenge of the existing next-drupal package not yet being compatible with these advancements. To address this, we created custom code that facilitated direct communication with Drupal. This was achieved by developing custom classes on the Frontend side equipped with several methods designed to handle the necessary API requests to Drupal. This bespoke integration approach not only allowed us to utilize the latest features of Next.js but also ensured our system was robust and tailored to our specific project needs.
Routing system
Next.js offers a sophisticated routing system that is both intuitive and powerful, setting it apart from traditional React router alternatives. By employing a file-based routing approach, Next.js simplifies the creation and management of routes; developers can define routes based on the file structure within the pages or app directory, where each file automatically corresponds to a specific route. This convention-over-configuration model eliminates the need to explicitly declare routes within the application, streamlining development and reducing the potential for errors. Moreover, Next.js supports dynamic routing, allowing for the creation of complex applications with parameters and custom slugs without additional configuration.
Next.js Pages Router
The Pages Router has a file-system based router built on the concept of pages. When a file is added to the pages directory, it's automatically available as a route. For example, creating a file at `pages/about.js` that exports a React component will make it accessible at `/about`. Additionally, the router will automatically route files named `index` to the root of the directory, such as `pages/news/index.js` mapping to `/news`.
Regarding dynamic routing in a news section scenario, you might use a single React component across multiple pages. In such cases, dynamic routes in Next.js are defined using square brackets. For example, in the file `pages/news/[slug].tsx`, [slug] represents the dynamic segment that changes based on the specific article.
Next.js App Router
Routing with the App Router differs as it is controlled via folders inside the app directory. The UI for a particular route is defined by a page.js file inside of the folder. In this way, every folder becomes a route segment.
Therefore in a folder structure like the one presented on the image above, the `app/page.js` represents the root page of the application, while the `dashboard/page.js` represents the `/dashboard page`. Routes can also be nested following the same approach.
In this example, the `/dashboard/analytics` URL path is not publicly accessible because it lacks a corresponding `page.js` file. Similarly, generating dynamic routes is also possible, such as `app/news/[slug]/page.js` where [slug] is the dynamic segment for news articles.
Retrieving Data from Drupal
Next.js Pages Router
In Next.js (pages router version), data fetching from Drupal utilizes typical methods provided by the framework. Pages in Next.js contain an exported function that delivers all the necessary data for rendering. Next.js offers two primary functions for this: getStaticProps and getServerSideProps. While both functions serve similar purposes, getServerSideProps is specifically designed for server-side rendering, whereas getStaticProps facilitates static site generation, including incremental static regeneration.
The "Next.js for Drupal" project includes a beneficial tool called DrupalClient, which provides utility functions for retrieving JSON data from Drupal.
// Create a new DrupalClient.
const drupal = new DrupalClient("https://example.com")
Numerous functions are available to access Drupal resources; here are some common examples:
To fetch a specific resource by its ID or path, you can use the getResource function if you know the resource type and its UUID:
const article = await drupal.getResource(
"node--article",
"dad82fe9-f2b7-463e-8c5f-02f73018d6cb" );
Both type and UUID are mandatory parameters, but you can use optional parameters, such as locale, to retrieve the resource in a specific language.
Alternatively, if you know the path to a Drupal resource, you can use the getResourceByPath function:
const node = await
drupal.getResourceByPath("/news/slug-for-article");
For static site generation, Next.js needs to identify which routes are part of the application to pre-render them during the build process. This requires defining a getStaticPaths function alongside getStaticProps in your page files.
Fortunately, "Next.js for Drupal" provides a method called getStaticPathsFromContext to retrieve all paths for a specific Drupal content type:
export async function getStaticPaths(context): Promise<GetStaticPathsResult> {
return {
paths: await drupal.getStaticPathsFromContext("node--article", context),
fallback: "blocking",
};
}
This function returns paths for all the article nodes in your Drupal backend. If necessary, the params parameter can be used to refine these paths using any JSON parameters, such as filters.
To fetch data for each page without knowing each resource's UUID, you can use the path obtained from the context of the getStaticProps or getServerSideProps functions. "Next.js for Drupal" offers two helpful functions: translatePathFromContext, which provides information about a Drupal path from the context, and getResourceFromContext, which retrieves the Drupal resource for the current context:
export async function getStaticProps(context) {
const path = await drupal.translatePathFromContext(context);
const node = await drupal.getResourceFromContext(path, context);
return {
props: {
node,
},
};
}
Next.js App Router
In Next.js app router version, the introduction of the /app directory standardizes server-side rendering using React Server Components (RSC). This setup defaults all components within the App directory to server components, enabling data fetching directly on the server within layouts, pages, and individual components. This method not only enhances security by keeping sensitive environment variables off the client side but also streamlines the rendering process by reducing data transfers between server and client.
Data fetching in this newer version leverages an extended instance of the native fetch() Web API, employing async/await within server components to efficiently handle multiple concurrent data requests. It introduces fetch options to manage caching and revalidation rules such as {next: revalidate} for refreshing static data at predetermined intervals or in response to backend changes. For highly dynamic or user-specific data, the {cache: no-store} option ensures that data is not cached, maintaining data integrity and timeliness.
This replaces the reliance on APIs like getServerSideProps or getStaticProps.
For example, fetching data on a page might look like this:
const authInstance = new DrupalAuthentication();
const pathTranslatorApi = new PathTranslatorService(authInstance);
const landingPageApi = new LandingPageApiService(authInstance);
const paragraphsApi = new SectionsApiService(authInstance);
export async function generateStaticParams() {
return [];
}
export default async function LandingPage({ params }: LandingPageProps) {
const { slug } = params;
const translatedPath = await pathTranslatorApi.getTranslatedPath(slug);
if (!translatedPath) {
notFound();
}
const pageData = await landingPageApi.getLandingPage(translatedPath);
if (!pageData) {
//if status is unpublished and not in preview mode
notFound();
}
const paragraphs = (await paragraphsApi.getSections(
pageData
)) as SectionsDTO[];
return <NovonesisLandingPage pageData={pageData} paragraphs={paragraphs} />;
}
In this setup, authInstance is an instance of the custom class created to handle authentication for frontend requests to Drupal. The pathTranslatorApi, landingPageApi and paragraphsApi are custom classes built to manage specific data interactions with Drupal. This approach allows all necessary requests to be made at the page level without the need for the traditional methods like getStaticProps or getServerSideProps.
Similar to the Pages Router, if you have a dynamic route as such as app/[slug]/page.tsx, and wish to pre-render some pages, you can use the generateStaticParams in place of the traditional generateStaticPaths equivalent from Next.js Pages Router. This function can return an array with the different pages’ paths you would like to pre-render at build time or, as shown in the example, it can return an empty array. This means that on the first time the user visits a page within this route, it will be generated at request time and then automatically cached for any subsequent visits.
Preview Mode
Static rendering is effective for pages that retrieve data from a headless CMS. However, this method falls short when you are editing drafts in your headless CMS and need to preview changes instantly on your page. In such scenarios, it's preferable for Next.js to render pages on-demand, pulling draft content rather than published content. You would specifically want Next.js to employ dynamic rendering for these instances to ensure real-time content updates.
Next.js has a feature called Draft Mode (previously called Preview Mode in the Pages Router) which solves this problem.
The implementation was similar across both versions of Next.js we used, with minor differences. Essentially, it involved setting up a preview/draft API route within our Next.js application and developing a function to handle the request. This function triggers a built-in mechanism that sets a cookie in the browser, signaling the preview mode. Here is an example for both cases:
// Pages Router
// pages/api/preview.ts
export default function handler(
req: NextApiRequest,
res: NextApiResponse
) {
// ...
res.setPreviewData({})
// ...
}
// App Router
// app/api/draft/route.ts
import { draftMode } from 'next/headers';
export async function GET(request: Request) {
// ...
// Enable Draft Mode by setting the cookie
draftMode().enable();
// ...
return new Response('Draft mode is enabled');
}
It might also be necessary to adjust data fetching functions on your pages to accommodate preview mode, allowing users to view unpublished pages if the preview mode cookie is set.
Additionally, the Drupal module can integrate site previews using an iFrame on the entity page. Alternatively, you can provide a link to the Next.js site, allowing previews to be viewed in a separate tab or window. This functionality also supports previews of revisions, draft content, and content under moderation.
Incremental Static Regeneration
Traditionally, static site generation involves creating all your site's pages as static entities during the build process. While this enhances performance, it has the drawback that any new pages added or updates to existing pages require a full site rebuild.
Next.js introduces a feature known as Incremental Static Regeneration, which enables updates to statically generated pages post-build. This can be achieved in two ways.
Time-based Incremental Static Regeneration
Time-based revalidation is a simple method for determining cache lifetimes in your Next.js application. By setting an interval (measured in milliseconds), you establish the duration for which the data remains valid in the cache.
Next.js Pages Router
The simpler method involves using the revalidate
prop within getStaticProps
. This prop, set as a numeric value, specifies how many seconds must pass before a new request can trigger a regeneration of the page. Although the user will see the cached version of the page initially, Next.js will work to rebuild it in the background, and the updated version will be served on subsequent requests.
For example, setting the revalidate
prop to 60 in our article page’s getStaticProps
means Next.js will attempt to regenerate each article page every minute.
export async function getStaticProps(context) {
const path = await drupal.translatePathFromContext(context)
if (!path) {
return {
notFound: true,
}
}
const article = await drupal.getResourceFromContext(path, context)
return {
props: {
article,
},
revalidate: 60 // every 60 seconds, the page will be rebuilt
}
}
Next.js App Router
As already mentioned, because on this version the built-in method getStaticProps
is not supported, you can implement time-based revalidation in two ways:
- On a per fetch request basis using the custom Next.js fetch api.
fetch('https://...', { next: { revalidate: 3600 } }) // cache lifetime in seconds
- Revalidate all fetch requests in a route segment using Segment Config Options which allows you to configure the behaviour of a page, layout or route handler by exporting certain constants.
// page.tsx
export const revalidate = 3600 // cache lifetime in seconds
On-demand Revalidation
The alternative is On-Demand Incremental Static Regeneration. This method, which is supported by the Next Drupal module, allows for page regeneration only when changes are made to content in Drupal, such as updates, creations, or deletions.
Next.js Pages Router
To facilitate this, a revalidate API endpoint is required within the Next.js application to process the revalidation request and trigger the regeneration of the relevant page. This endpoint accepts two parameters: the slug for revalidation and a secret to ensure the request is from a reliable source.
// Pages Router
// pages/api/revalidate.ts
export default async function handler(
request: NextApiRequest,
response: NextApiResponse
) {
const slug = request.query.slug as string;
const secret = request.query.secret as string;
// Validate secret.
if (secret !== PREVIEW_SECRET) {
return response.status(401).json({ message: 'Invalid secret.' });
}
// Validate slug.
if (!slug) {
return response.status(400).json({ message: 'Invalid slug.' });
}
try {
await response.revalidate(slug);
return response.json({});
} catch (error) {
return response.status(404).json({
message: error.message,
});
}
}
Next.js App Router
Data can be revalidated on-demand by path (revalidatePath) or by cache tag (revalidateTag) inside a Server Action or Route Handler.
Next.js has a cache tagging system for invalidating fetch requests across routes.
- When using fetch, you have the option to tag cache entries with one or more tags.
- Then, you can call revalidateTag to revalidate all entries associated with that tag.
For example, the following fetch request adds the cache tag collection:
// app/page.tsx
export default async function Page() {
const res = await fetch('https://...',
{ next: { tags: ['collection'] } })
const data = await res.json()
// ...
}
You can then revalidate this fetch call tagged with collection by calling revalidateTag in a Server Action or a Route Handler:
// app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export default async function action() {
revalidateTag('collection')
}
The Drupal module provides configuration options for On-Demand Revalidation by entity types, offering a plugin to trigger revalidation by path. Additionally, it allows manual specification of extra paths for revalidation; for instance, you might want to revalidate the news landing page (/news) whenever an article node is modified, added, or removed.
Authentication Layer for Securing APIs from Drupal to NextJS
The Drupal REST & JSON: API Authentication module enhances security by limiting unauthorized access to your Drupal site's APIs using various authentication methods, such as bearer tokens, basic authentication, and external/third-party provider authentication.
In both projects we used bearer Token Authentication: This method, part of the HTTP authentication scheme, originated with OAuth 2.0 but is now frequently utilized independently.
The DrupalClient offered by the “Next.js for Drupal” package is responsible for managing bearer tokens, including their renewal upon expiration.
// Using Bearer Token for Authentication
const article = drupal.getResource(
"node--article",
"293202c9-f603-444e-a426-a5637e2ade2e",
{
withAuth: {
clientId: process.env.DRUPAL_CLIENT_ID,
clientSecret: process.env.DRUPAL_CLIENT_SECRET,
},
}
);
Alternatively, for Novonesis, we developed a custom class to manage authentication with Drupal. We then passed instances of this class to each custom service, enabling them to fetch specific resources:
export class DrupalAuthentication implements AuthClassInterface {
async getToken() {
if (!CLIENT_ID || !CLIENT_SECRET) {
throw new Error('Client id or client secrets are not defined');
}
const basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString(
'base64'
);
const response = await fetch(
`${DRUPAL_BASE_URL}/oauth/token?scope=frontend_site`,
{
method: 'POST',
headers: {
Authorization: `Basic ${basic}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'client_credentials',
}),
next: { revalidate: 3600 },
}
);
const responseData = await response.json();
if (response.status !== 200) {
throw new Error(response.statusText);
}
const tokenObj: DrupalAccessToken = responseData;
return tokenObj.access_token;
}
}
// app/page.tsx
const authInstance = new DrupalAuthentication();
const landingPageApi = new LandingPageApiService(authInstance);
const paragraphsApi = new SectionsApiService(authInstance);
Pros and Cons of Using Next.js with Drupal
Pros:
- Enhanced Performance.
Next.js provides server-side rendering and static site generation, which, when combined with Drupal's robust backend, results in blazing fast websites that are also SEO-friendly. - Flexibility and Scalability.
The decoupled architecture allows teams to scale the frontend and backend independently. You can take advantage of Drupal's powerful content management capabilities while leveraging the rich ecosystem of React and Next.js for the frontend. Integrating new services can be also easier with options to integrate multiple backend services to the frontend via APIs. - Improved Development Efficiency.
Developers can work on the frontend and backend separately, reducing dependencies and speeding up development cycles. - Strong Community Support.
Both Next.js and Drupal have large, active communities. This translates into a wealth of plugins, packages, and third-party integrations, as well as community support for troubleshooting.
Cons:
- Complexity in Setup and Maintenance.
The initial setup for a decoupled architecture can be complex. Managing two separate systems requires additional coordination and can increase maintenance overhead. - Increased Hosting Costs.
Running separate systems potentially increases hosting costs, as you need different environments for the frontend and backend. - Learning Curve.
For teams unfamiliar with React or Drupal, there is a steep learning curve associated with using these technologies effectively, as well as integrating them appropriately. - Higher development and maintenance cost.
Separate applications have to be built and maintained for the frontend and backend requiring different skills and expertise.
Tips and Strategies for Building a Website with Decoupled Drupal and Next.js
- Adopt TypeScript for Robust Code: Incorporate TypeScript to enhance code reliability and maintainability in your Next.js and Drupal setup. TypeScript’s static typing catches errors early, simplifies debugging, and improves autocomplete features, leading to more efficient development and a stable, scalable application.
- Comprehensive Automated Testing: Implement thorough automated testing for both the backend and frontend. For Drupal, leverage its configuration management system to automate backend deployments. On the frontend, utilize tools like visual regression and unitary tests (like Jest and Testing Library) to ensure that Next.js changes are robust and performant.
- Streamlining API Requests: If you intend to use JSON API for handling data transfer between your Front End application and Backend one, consider utilizing the DrupalJsonApiParams package to simplify and streamline how you construct query parameters in your requests to do filtering, sorting, fields restriction, pagination and so on, following he JSON:API specifications. This package can significantly reduce the effort required to format and manage API calls, saving time and reducing errors.
- Development Environments: Set up dedicated development environments for both frontend (Next.js) and backend (Drupal). This separation aids in isolating issues, testing new features safely, and ensuring that integrations work seamlessly before going live.
- Utilizing Next-Drupal Package: If the compatibility with the specific Next.js version you’re trying to implement permits, consider using the Next-Drupal package for easier integration between Next.js and Drupal. This package provides pre-configured tools and functions facilitating quicker setup. However, if that’s not your case, think about developing a custom Drupal client. While this may increase development time, it ensures tailored functionality that meets specific project requirements.
- DevOps: Automating deployment processes and setting up a more robust CI/CD pipeline would reduce the overhead of managing two systems.
The Good and the Tough
The standout advantage of integrating Next.js with Drupal is the significant enhancement in performance and user experience. Utilizing Next.js for server-side rendering (SSR) optimizes web content delivery, dramatically speeding up the initial page load times and improving the site's responsiveness. This boost in performance is due to Next.js's ability to pre-render pages on the server, ensuring that users receive fully formed HTML pages that are immediately consumable. Additionally, the integration leverages Drupal’s strength as a content management system with extensive content governance tools, workflow capabilities, and a robust security framework. This synergy results in a dynamic web experience where high performance meets rich, easily manageable content. As a member of the frontend team, this project offered a highly enriching learning experience. The decoupled approach allowed us to utilize modern JavaScript frameworks, moving away from traditional Twig templates to embrace the innovative, React-based framework of Next.js. With its frequent updates, comprehensive documentation, and vibrant developer community, Next.js supports and enhances our development journey, allowing us to focus on creating dynamic, efficient user interfaces backed by a solid backend.
However, the integration of Next.js with Drupal does come with its set of challenges, primarily during the initial setup and ongoing integration phases. The complexity lies in configuring two distinct technologies to work harmoniously—a modern JavaScript framework with a traditional, PHP-based CMS. Developers often face hurdles in aligning the server-side capabilities of Next.js with the API-driven approach of headless Drupal, necessitating a deep understanding of both platforms. Security and performance optimizations also add layers of complexity, as ensuring that the systems communicate effectively without exposing vulnerabilities requires meticulous attention to detail. The learning curve can be steep, especially for teams less familiar with either React (on which Next.js is based) or Drupal's extensive backend functionalities. Furthermore, maintaining the integration over time as both platforms evolve involves continuous monitoring and updates, which can strain resources and extend development cycles. Despite these challenges, once set up, the benefits in terms of scalability, speed, and user engagement often outweigh the initial difficulties, positioning this tech stack as a formidable choice for enterprises looking to innovate their web presence.
Final Thoughts
Embracing a decoupled architecture with Headless Drupal and Next.js represents a forward-thinking choice for any organization aiming to enhance its digital strategy towards composable architectures. This combination not only aligns with modern development practices but also prepares businesses for future technological advancements and changing user expectations. By adopting this approach, companies can ensure greater agility in their development cycles and a robust platform that scales seamlessly with their growth. The strategic advantages of this integration—like increased development efficiency and the potential to deliver exceptional user experiences—position organizations well in a competitive digital market. While challenges such as the complexity of initial setup and ongoing maintenance are part of the journey, the payoff in terms of flexibility, scalability, and performance makes a compelling case for considering this powerful duo.