main content

Cómo crear una arquitectura decoupled Drupal con GraphQL para servir contenido a múltiples canales digitales

Voy a ser honesto desde el principio: no todo proyecto Drupal necesita irse a decoupled. La mayoría de webs corporativas viven perfectamente con Twig, render arrays y un buen tema base. Pero cuando un cliente aparece con una web, una app nativa, un kiosco en sucursales y, encima, un asistente de voz que también tiene que servir el mismo contenido, ahí Twig se queda corto. Y la arquitectura decoupled (lo que muchos llaman headless) deja de ser opcional.

GraphQL ha terminado siendo, para mí, la interfaz que mejor encaja entre un Drupal headless y sus consumidores. Frente a REST, deja que cada cliente pida exactamente los campos que necesita en una sola petición, te ahorra el típico baile de over-fetching y under-fetching, y trae tipado fuerte con introspección. Esto último, aunque suene a detalle menor, marca una diferencia enorme en la productividad del equipo frontend, sobre todo si trabajan con TypeScript.

En esta guía cuento cómo configuro un Drupal como CMS headless que expone contenido vía GraphQL, cómo diseño los schemas para que aguanten múltiples consumidores sin convertirse en un infierno, y qué decisiones suelen volver para morderte si no las piensas al principio.

Cuándo Drupal decoupled tiene sentido frente a un headless puro

Antes de abrir Composer, conviene preguntarse si Drupal es la herramienta correcta. He visto equipos elegir Drupal decoupled por inercia (porque ya lo conocían) y acabar usando un 20% de sus capacidades. También he visto lo contrario: equipos que se fueron a Contentful y, dos años después, querían volver porque el modelado se les había quedado pequeño.

Modelado sin techo. Drupal te deja crear tipos de contenido con referencias entre entidades, campos condicionales, taxonomías jerárquicas, media con varios formatos, párrafos anidados con Paragraphs y workflows editoriales a medida. Contentful, Sanity y compañía tienen límites de profundidad y complejidad que en proyectos enterprise se notan. Si tu modelo de contenido es plano (un blog, un catálogo simple), un headless puro probablemente te sobra. Si tienes entidades cruzadas, traducciones complicadas y editores que necesitan flujos serios, Drupal sigue ganando.

Control de la infraestructura. Despliegas en tu propio hosting, en Platform.sh, en Pantheon o donde quieras. Controlas latencia, localización de datos para RGPD, costes a escala y no dependes de un vendor que decida cambiar el pricing el próximo trimestre. Es una de las razones por las que en sector público y banca el headless puro suele quedarse fuera.

Ecosistema maduro. Más de 50.000 módulos contribuidos. Multiidioma serio, búsqueda con Search API y Elasticsearch, control de acceso granular, workflows de aprobación, single sign-on con cualquier IdP razonable. No es marketing: es funcionalidad probada que en una plataforma SaaS reciente puede no existir o estar a medio cocer.

Longevidad. Drupal lleva más de veinte años, una comunidad activa y un roadmap claro liderado por Dries. Para un proyecto que va a vivir una década, esto pesa más de lo que parece, especialmente frente a startups de CMS con tres años de vida y rondas de financiación pendientes.

Configuración del módulo GraphQL en Drupal

Drupal expone GraphQL a través del módulo graphql (4.x para Drupal 10+). Aquí hay un detalle importante que descoloca a quien viene de Hasura o PostGraphile: el módulo no autogenera un schema completo desde tu modelo. Te da un sistema de plugins para mapear resolvers a datos de Drupal, y tú decides qué exponer y cómo.

Instalación y arranque:

Lo instalas con Composer (composer require drupal/graphql), lo habilitas y creas un servidor GraphQL en /admin/config/graphql eligiendo el schema a exponer.

Si quieres ir rápido (o si estás haciendo un prototipo para convencer a un cliente), el submódulo graphql_compose genera automáticamente un schema a partir de tus tipos de contenido existentes y expone nodos, taxonomías, media y menús sin escribir resolvers a mano. Yo lo uso mucho en fase de validación. Luego, en producción, casi siempre acabo añadiendo resolvers personalizados para los casos que se salen del molde.

Estructura típica del schema:

Un schema GraphQL para Drupal suele exponer queries para obtener nodos individuales por ID o por ruta, listados con filtros y paginación, taxonomías y vocabularios, menús con su jerarquía, y bloques o configuración del sitio.

type Query {
  nodeArticle(id: ID!): NodeArticle
  nodeArticles(
    filter: NodeArticleFilterInput
    sort: [NodeArticleSortInput]
    first: Int
    after: String
  ): NodeArticleConnection
  route(path: String!): RouteUnion
  menu(name: MenuAvailable!): Menu
}

La query route es especialmente útil. Permite al frontend resolver una URL arbitraria y descubrir qué tipo de contenido le toca renderizar, manteniendo el routing alineado con el alias system de Drupal.

Diseñar schemas que aguanten varios frontends

Aquí es donde se gana o se pierde el proyecto. Un schema mal pensado obliga a los consumidores a hacer queries kilométricas, multiplica resolvers en cascada y mata el rendimiento. Un schema bien pensado es un placer de consumir y se mantiene años.

Pensar en dominio, no en cómo lo guarda Drupal. Tu schema debe hablar el lenguaje del negocio, no el de la base de datos. En vez de exponer fieldBodyValue o fieldImage.entity.fieldMediaImage, prefiero body o heroImage. El consumidor no debería ni saber que detrás hay un campo entity reference apuntando a una media entity con un image field.

Fragments e interfaces para reutilizar. Definir una interfaz ContentNode con campos comunes (title, path, created, author) deja que un componente de listado funcione igual con artículos, páginas y cualquier otro tipo. Los fragments hacen el resto: el equipo frontend declara qué campos necesita un componente y los compone donde haga falta.

Conexiones paginadas con cursor. Para listados, sigo el patrón Relay (edges, nodes, pageInfo con hasNextPage y endCursor). Es lo que esperan Apollo, Relay y urql, y evita reinventar paginaciones a medida que luego nadie quiere mantener. Offset-based parece más simple, pero en listados que cambian de orden o de tamaño da problemas raros.

Union types para contenido polimórfico. Cuando una referencia puede apuntar a varios tipos, Union types resuelve el problema con elegancia. Paragraphs es el caso clásico: un mismo campo puede traer texto, imagen, vídeo, formulario o un CTA, y el consumidor maneja cada variante con un __typename.

union ParagraphUnion = ParagraphText | ParagraphImage | ParagraphVideo | ParagraphCTA

type NodePage {
  title: String!
  path: String!
  content: [ParagraphUnion!]!
}

Un consejo que me ha ahorrado disgustos: versiona el schema desde el día uno y documenta cualquier deprecación con la directiva @deprecated. Los consumidores móviles que no actualizan en años te lo agradecerán.

Cache: el talón de Aquiles si no lo planificas

Cada petición GraphQL ejecuta resolvers que tocan la base de datos de Drupal. Sin estrategia de cache, el backend cae bajo carga moderada. Aquí van las capas que monto casi siempre.

Persisted queries. En vez de mandar el query completo en cada request, lo registras en el servidor (Automatic Persisted Queries) y envías un hash. Reduces el tamaño de las peticiones y, mucho más importante, puedes cachear responses por hash en el edge. También cierra una superficie de ataque interesante, porque los clientes no pueden inventarse queries arbitrarias contra producción.

Cache tags de Drupal. Esto sigue siendo, para mí, el superpoder menos valorado de Drupal. Cada response GraphQL se etiqueta con los cache tags de las entidades que contiene. Cuando un editor edita un artículo, solo se invalidan las responses cacheadas que incluían ese artículo concreto, no todo el cache del sitio. En proyectos con miles de páginas, la diferencia entre tener esto bien montado y no tenerlo es abismal.

CDN delante con Varnish o Fastly. Un proxy de cache delante del endpoint GraphQL. Las persisted queries con GET permiten cachear en el edge. La invalidación se gestiona con purge selectivo por cache tag desde Drupal cuando cambia el contenido. Fastly tiene soporte nativo de purge by surrogate key que encaja como un guante con los cache tags de Drupal.

Stale-while-revalidate. Headers que sirven contenido ligeramente desactualizado mientras se regenera en background. Para contenido editorial que no cambia cada segundo, un SWR de 60 segundos te da rendimiento de edge sin sacrificar frescura perceptible. Para fichas de producto con stock en tiempo real, evidentemente no aplica.

Conectar frontends al backend GraphQL

Una de las cosas que más me gusta de esta arquitectura es poder elegir la tecnología frontend que mejor encaja en cada canal, sin obligar a todos a usar lo mismo.

Web (Next.js o Nuxt.js). Para la web principal, un framework con SSR/SSG y soporte nativo de GraphQL es la opción más completa. Next.js con Apollo Client o urql te permite pre-renderizar páginas en build time (SSG) o en cada request (SSR), y combinar ambos según el caso. Las páginas de landing van SSG, el área de usuario logado va SSR, los listados con filtros se quedan en cliente. La granularidad es enorme.

App móvil (React Native o Flutter). La app consume el mismo endpoint que la web, pero pide solo los campos que su pantalla necesita: imágenes en resolución móvil, textos truncados, datos preparados para modo offline. GraphQL evita que la app baje datos que no va a usar, lo que en redes móviles es ancho de banda y batería que el usuario nota.

Partners y terceros. Aquí monto un schema público con autenticación por API key (o mejor, OAuth2 si el partner lo soporta). El tipado fuerte de GraphQL funciona como documentación auto-generada y reduce muchísimo el soporte técnico al integrar.

Previsualizaciones editoriales. Este es el problema UX más feo del decoupled, y depende mucho del equipo frontend para resolverlo bien. Los editores estaban acostumbrados a darle a "previsualizar" y ver la página real en Drupal. Ahora hay que montar un modo preview en el frontend que conecte con un endpoint de draft content, con su propia autenticación, y reproduzca el render real. Si lo descuidas, los editores te lo recuerdan cada semana.

Media y assets sin matar el servidor

Los archivos multimedia merecen un capítulo propio. El frontend necesita acceder a las imágenes y archivos almacenados en Drupal de forma eficiente, y aquí se cometen errores caros.

Image styles bajo demanda. Drupal genera derivados de imagen (thumbnails, crops, webp) con Image Styles. En decoupled, expongo en GraphQL las URLs de los derivados ya generados, en varios tamaños, para que el frontend implemente srcset y responsive images sin tener que pedirle al backend que decida por él.

CDN dedicado para media. Configura un CDN para los archivos de /sites/default/files. Imágenes y documentos deben servirse desde edge con headers de cache agresivos (idealmente inmutables con hash en el nombre, o max-age largo con purge por path). Servir media desde el origen de Drupal es un anti-patrón que se nota la primera vez que un artículo se viraliza.

Servicios externos para volumen alto. En proyectos con catálogos grandes de imágenes, integrar Drupal con Cloudinary, Imgix o Fastly Image Optimizer permite generar derivados on-the-fly sin tocar el servidor. El coste mensual se compensa con creces frente a tener que escalar PHP-FPM solo para procesar imágenes.

Workflows editoriales: que el equipo de contenido no sufra

La experiencia editorial no debería degradarse por irte a decoupled. Drupal tiene piezas que mantienen la productividad si las configuras bien.

Content Moderation define estados (borrador, en revisión, publicado, archivado) con transiciones controladas por roles. Solo el contenido en estado publicado se expone vía GraphQL al frontend público. Los estados intermedios se sirven en el endpoint preview con autenticación.

El sistema de revisiones mantiene historial completo de cada cambio, con comparación entre versiones y rollback. Cuando varios canales consumen el mismo contenido, esto es crítico: un cambio impacta a todos a la vez y necesitas poder volver atrás rápido si algo se rompe.

Scheduled publishing con el módulo Scheduler programa publicación y despublicación en fechas futuras, útil para campañas coordinadas entre canales.

Si estás valorando si Drupal decoupled con GraphQL encaja en tu proyecto multicanal, o si necesitas una segunda opinión para diseñar el schema, montar la capa de cache o migrar desde un Drupal monolítico sin romper SEO, puedes hablar con nuestro equipo de arquitectura Drupal y plantear tu caso concreto.

Decoupled como decisión arquitectónica, no como moda

Cerrando con honestidad: el decoupled no es una bala de plata. Si tu proyecto es una web corporativa con un blog y un formulario, montar Drupal headless con Next.js delante es overengineering, vas a complicar el deploy, multiplicar costes de hosting y frustrar a un equipo de contenido acostumbrado al WYSIWYG completo. Para esos casos, un Drupal monolítico bien hecho sigue siendo la mejor decisión técnica.

Pero cuando aparece la combinación de varios canales sirviendo el mismo contenido, equipos frontend con preferencias tecnológicas distintas, picos de tráfico que exigen edge caching agresivo y un modelo de contenido que va a crecer en complejidad, Drupal con GraphQL ofrece una base difícil de superar: potencia editorial, libertad de modelado y eficiencia de entrega. La clave está en saber en qué cuadrante estás antes de empezar a escribir resolvers.