Arquitectura headless

Drupal Headless con Next.js y Express.js: 3 Portales Universitarios en Producción

Guía práctica de arquitectura headless Drupal: cómo desacoplar Drupal con Next.js y Express.js como BFF. Caso real de 3 portales universitarios con SSR, endpoints JSON custom y agregación multi-fuente.

por Santi López ·

El monolito ya no escala

En resumen: Arquitectura headless real con Drupal + Next.js SSR + Express.js como BFF para tres portales universitarios en producción. Endpoints JSON custom, agregación multi-fuente, caché con cache bins y mapeo de entidades a React.

  • Por qué usar Express.js como BFF para agregar datos de múltiples fuentes de Drupal
  • Endpoints JSON custom con normalizador-serializador vs JSON:API estándar
  • Caché en Drupal con cache bins sincronizados por cache tags (no en Node.js)
  • Lecciones reales de tres portales con miles de usuarios simultáneos

Drupal es un CMS excelente para la gestión de contenidos. De hecho, tras probar opciones estandarizadas como WordPress o Joomla, no he encontrado nada comparable en cuanto a flexibilidad y potencia.

Sin embargo, cuando necesitas servir ese contenido a través de un frontend rápido, moderno y escalable, el monolito tradicional empieza a mostrar sus límites. Con esto no quiero decir que el enfoque monolítico de Drupal sea malo, sino que resulta poco ágil en un entorno digital que exige velocidad. Las plantillas de Twig, el sistema de renderizado tradicional y la carga completa de páginas desde PHP terminan penalizando la experiencia de usuario.

Además, al tratarse de una solución monolítica, Drupal a menudo dificulta que los desarrolladores backend y frontend trabajen con total independencia, ya que sus responsabilidades tienden a solaparse.

La solución no es abandonar Drupal, sino desacoplarlo: trasladar el frontend a una tecnología moderna mientras Drupal continúa operando como el gestor de contenidos y motor backend. Esta guía detalla la arquitectura real que utilicé para llevar a producción tres portales universitarios desacoplados, capaces de soportar miles de usuarios simultáneos y un volumen elevado de contenido multimedia.

La arquitectura: Next.js + Express.js + Drupal

La arquitectura que implementamos no sigue el patrón típico de “Drupal JSON:API + Next.js” que ves en tutoriales. En nuestro caso, los requerimientos institucionales nos llevaron a un diseño con tres capas:

[Usuario] → [Next.js SSR] → [Express.js BFF] → [Endpoints JSON custom de Drupal]

¿Por qué tres capas?

La respuesta corta: agregación de datos. Un portal universitario no es un blog; la página de inicio puede requerir información de cinco o seis fuentes distintas (noticias destacadas, eventos del calendario, avisos administrativos, enlaces rápidos y contenido editorial). Si Next.js realizara una llamada HTTP independiente por cada una de estas fuentes, el renderizado del lado del servidor (SSR) se volvería inviable.

Para solucionar esto, Express.js actúa como un BFF (Backend for Frontend): recibe una única petición de Next.js, consulta todos los endpoints necesarios de Drupal en paralelo, consolida los resultados en una sola respuesta JSON y se la devuelve al frontend. El resultado: una única petición SSR en lugar de seis.

Ahora bien, esto es en la teoría; en la práctica, las cosas no son tan sencillas.

Para empezar, cuando la lógica se delega a un entorno como Express.js —que no impone una estructura estricta ni un estándar predefinido sobre cómo hacer las cosas—, es muy fácil caer en el temido código espagueti. Esto contrasta fuertemente con Drupal: cuando creas un módulo allí, existe un consenso claro sobre las convenciones de la arquitectura. Todos sabemos, o deberíamos saber, que la lógica de negocio debe residir en un servicio, dónde localizar dichos servicios, en qué archivo declarar una ruta o dónde ubicar un comando de Drush. Express.js ofrece una flexibilidad total, pero carece de esa estructura guiada por defecto.

Además, cometimos el error de intentar gestionar la caché en la capa de Node.js con Express, descubriendo que este framework no dispone de un sistema de control de caché tan maduro, robusto y sofisticado como el que ofrece Drupal de forma nativa.

Endpoints JSON custom en Drupal

En lugar de utilizar el estándar de JSON:API, optamos por crear endpoints JSON a medida. La decisión no fue arbitraria; cuando evaluamos cómo obtener un nodo con todas sus entidades relacionadas, no vimos claras las ventajas del modelo estándar, parecía muy complicado extraer datos de forma sencilla a través de JSON:API. Preferíamos un endpoint al que simplemente se le pasara el alias de un nodo y devolviera la información ya serializada mediante un enfoque personalizado, en lugar de lidiar con la serialización por defecto de JSON:API.

Esto nos aportó beneficios clave:

  • Control fino de la respuesta: transmitíamos únicamente los campos necesarios para el frontend, devolviendo los campos del display de cada una de las entidades de Drupal, evitando los metadatos innecesarios que añade JSON:API y que incrementan el peso de la carga.
  • Formato predecible: el equipo de frontend no necesitó aprender la especificación de JSON:API; recibían JSON plano y podían trabajar directamente con los datos de forma intuitiva.

Evidentemente, el endpoint para obtener nodos mediante su alias no fue el único que desarrollamos. También creamos endpoints específicos para recuperar vistas, menús, entidades por id y tipo. Incluso habilitamos la opción de gestionar formularios: el frontend se encargaba de renderizarlos y, al enviarlos, el formulario se procesaba directamente en Drupal Webform.

Este enfoque nos permitió construir la API exactamente como el frontend la necesitaba, sin pagar el coste de serialización de JSON:API para datos que no se iban a usar.

Next.js con SSR puro: la decisión de renderizado

Elegimos SSR puro (Server-Side Rendering) frente a ISR o SSG por una razón: los portales universitarios tenían contenido que cambiaba constantemente —avisos administrativos urgentes, cambios de aula, noticias de última hora— y el retraso de una revalidación ISR de incluso 30 segundos no era aceptable.

La implementación en Next.js seguía este patrón, donde toda petición a Next.js iba al mismo componente Slug que simplemente trasladaba ese slug al backend:

import { NextResponse } from 'next/server';

const BACKEND_URL = 'https://tu-api-externo.com/api';

async function handleRequest(request, { params }) {
  try {
    const resolvedParams = await params;
    const slugArray = resolvedParams.slug || [];
    const path = slugArray.join('/');

    const targetUrl = `${BACKEND_URL}/${path}`;

    const headers = new Headers(request.headers);
    headers.delete('host');

    let body = undefined;
    if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method)) {
      body = await request.text();
    }

    const response = await fetch(targetUrl, {
      method: request.method,
      headers: headers,
      body: body,
    });

    const data = await response.text();

    return new NextResponse(data, {
      status: response.status,
      headers: response.headers,
    });

  } catch (error) {
    console.error('Error en el proxy:', error);
    return NextResponse.json(
      { error: 'Error interno en el servidor proxy' },
      { status: 500 }
    );
  }
}

export { 
  handleRequest as GET, 
  handleRequest as POST, 
  handleRequest as PUT, 
  handleRequest as DELETE, 
  handleRequest as PATCH 
};

El problema de rendimiento y cómo lo resolvimos

Con el tráfico real de miles de usuarios simultáneos, nos encontramos dos problemas:

1. SSR lento

El Next.js a veces era lento: toda petición pasaba por Express.js y después a Drupal. Implementamos una caché en la capa Node.js, esto funcionaba pero no había un refresco automático cuando un contenido se actualizaba en Drupal. Lo que se hizo entonces fue quitar la caché de Node.js y pasarla a Drupal mediante un cache bin nuevo, que se encargaba de cachear las respuestas y refrescar esa caché siempre que hubiera una actualización de contenido utilizando las cache tags.

Conseguimos por un lado tener una caché funcionando, mejorando notablemente los tiempos de carga, y además sincronizada con el backend.

Mapeo de datos: de entidades Drupal a componentes React

El mayor desafío de desarrollo fue el mapeo de las relaciones entre entidades de Drupal al frontend. Un contenido editorial típico no era solo un nodo con campos planos: era un nodo con referencias a taxonomías (categorías), referencias a archivos (imágenes, PDFs), referencias a otros nodos (contenido relacionado) y paragraphs anidados (bloques de contenido flexible).

La estrategia fue normalizar en Drupal: creamos un normalizador-serializador personalizado que tuviera en cuenta entidades relacionadas, paragraphs, media, etc., para que de una llamada a una entidad devolviera todo lo necesario conforme al display de cada entidad.

Lecciones de 3 portales en producción

Tras poner en producción los tres portales universitarios con esta arquitectura, estas son las conclusiones:

  • Express.js puede ser necesario o no serlo. Cuando tu frontend necesita datos de múltiples fuentes de Drupal, agregar en el servidor intermedio puede ayudar, pero si la fuente principal de información es Drupal, quizás no sea necesaria esa capa adicional. Bajo mi punto de vista cada capa adicional debe estar justificada porque añade complejidad a la arquitectura: sus pros deben superar a sus contras.

  • Controla tu serialización. JSON:API es un estándar, pero si tu equipo de frontend no lo conoce o tus datos son complejos, unos endpoints JSON custom bien documentados ahorran más tiempo del que cuesta implementarlos.

  • La caché en capas es imprescindible. Sin una estrategia de caché, cada petición SSR genera una cascada de llamadas que destruye el rendimiento. En nuestro caso la mejor solución fue centralizarla en Drupal mediante un cache bin propio con invalidación por tags.

  • Normaliza en el servidor, no en el cliente. Las relaciones de entidades de Drupal son el punto donde más proyectos headless tropiezan. Si el frontend recibe datos anidados con relaciones sin resolver, el código se vuelve frágil. Resuelve las relaciones en el backend (Drupal o BFF) y entrega objetos planos.

  • SSR puro no es para todos. Si tu contenido no cambia varias veces al día, ISR o SSG te darán mejor rendimiento y menor coste de servidor. SSR solo tiene sentido cuando la frescura del contenido es crítica, como en nuestros portales universitarios.


Desacoplar Drupal no es solo cambiar Twig por React. Es repensar cómo fluyen los datos desde el CMS hasta el usuario, dónde pones la caché y cómo resuelves las relaciones entre entidades. Si tu organización gestiona contenido complejo y necesita un frontend moderno sin perder la potencia de Drupal como backend, una arquitectura BFF + SSR es una opción probada en producción.

Si quieres entender cómo cachear los datos que mueve esta arquitectura, tienes la guía de cache bins. Y si necesitas configurar Redis como backend de caché para el CMS, la guía de Redis en Drupal cubre la integración paso a paso. Y si el reto es mantener la sincronización entre Drupal y el frontend, la guía de Queue API cubre exactamente ese caso. Y si necesitas importar contenido desde un sistema externo hacia Drupal, la guía de Migrate API cubre la importación completa con JSON.

Preguntas frecuentes

¿Cómo funciona una arquitectura Drupal headless con Next.js?

En esta arquitectura Drupal actúa como gestor de contenidos y motor de API, sin renderizar la página visible. El frontend en Next.js renderiza en servidor (SSR) y se comunica con Drupal, normalmente a través de un Backend for Frontend (BFF) en Express.js. El usuario ve el HTML generado por Next.js; Drupal solo entrega los datos. Así se llevaron a producción tres portales universitarios capaces de soportar miles de usuarios simultáneos.

¿Qué es un BFF y para qué se usa en una arquitectura Drupal headless?

Un BFF (Backend for Frontend) es una capa intermedia, típicamente Express.js, que recibe una única petición del frontend y consulta todos los endpoints necesarios de Drupal en paralelo, consolida los resultados en una sola respuesta JSON y se la devuelve. Su función principal es la agregación de datos: evita que Next.js haga seis llamadas HTTP por página y las convierte en una sola petición SSR.

¿Por qué usar endpoints JSON custom en lugar de JSON:API en Drupal?

Los endpoints JSON a medida dan control fino de la respuesta: devuelven solo los campos que el frontend necesita, según el display de cada entidad, evitando los metadatos que añade JSON:API. Además ofrecen un formato predecible que el equipo de frontend consume sin aprender la especificación JSON:API. Fue la decisión tomada para los portales universitarios, donde se necesitaban vistas, menús y entidades por id y tipo de forma sencilla.

¿SSR, ISR o SSG con Next.js para Drupal headless?

Para los tres portales se eligió SSR puro porque el contenido cambiaba constantemente: avisos administrativos urgentes, cambios de aula y noticias de última hora no admitían el retraso de una revalidación ISR de incluso 30 segundos. El SSR garantiza datos siempre actuales a costa de más trabajo en servidor. Elige ISR o SSG cuando el contenido cambie con menos frecuencia y quieras maximizar el rendimiento de entrega.

¿Cómo se resuelve el rendimiento en una arquitectura headless?

El mayor problema detectado fue el SSR lento, porque cada petición pasaba por Express.js y después a Drupal. La solución fue quitar la caché de la capa de Node.js y moverla a Drupal con un cache bin personalizado que cachea las respuestas y se refresca automáticamente con las cache tags cuando el contenido se actualiza. El resultado fue una caché sincronizada con el backend y tiempos de carga notablemente mejores.

¿Cómo mapear entidades de Drupal a componentes de React?

El mayor desafío fue mapear las relaciones entre entidades: taxonomías, archivos, nodos relacionados y paragraphs anidados. La estrategia fue normalizar en Drupal: crear un normalizador-serializador personalizado que, en una sola llamada a una entidad, devolviera todas sus entidades relacionadas y media conforme al display de cada entidad. Así el frontend recibe JSON plano y listo para consumir en componentes React.