wandres.dev
ESCRIBIR INTEGRACIONES · Integration API

La Integration API: escribir un AstroIntegration

El otro lado del ecosistema: dejar de consumir integraciones y empezar a escribirlas. Qué es exactamente un AstroIntegration, por qué se reduce a un objeto con un nombre y un mapa de hooks, por qué casi todas se distribuyen como una función fábrica que devuelve ese objeto, cómo los hooks enganchan en momentos precisos del ciclo de vida del framework, y el criterio para decidir cuándo un problema pide una integración y cuándo basta con un módulo corriente.

⏱ 16 min

Llevas todo el track consumiendo integraciones: astro add react registró un renderer, un adapter enseñó a Astro a hablar con un runtime, un sitemap apareció en tu build sin que escribieras una línea. Todas esas piezas comparten una misma naturaleza humilde y exacta —son objetos con un nombre y un puñado de funciones enganchadas al ciclo de vida del framework— y ninguna es un caso especial cableado en el núcleo. Este nivel cruza al otro lado del mostrador: dejas de enrolar integraciones ajenas y empiezas a construir las tuyas. Y el primer descubrimiento es lo pequeño que es el contrato que tendrás que satisfacer.

🎯 Al terminar esta lección sabrás
  • Definir qué es un AstroIntegration y reconocer su forma { name, hooks }.
  • Distinguir la integración-objeto del patrón fábrica que la construye.
  • Situar los hooks como enganches nombrados en el ciclo de vida de Astro.
  • Decidir cuándo un problema pide una integración y cuándo basta un módulo.

La forma mínima: un nombre y un mapa de hooks

Una integración de Astro no es una clase ni un plugin con una API rebuscada: es un objeto plano con exactamente dos campos. El primero, name, es una cadena que la identifica en los mensajes y los diagnósticos. El segundo, hooks, es un mapa cuyas claves son nombres de momentos del ciclo de vida y cuyos valores son las funciones que quieres correr en cada uno. Nada más forma el contrato.

import type { AstroIntegration } from 'astro';

const saludo: AstroIntegration = {
  name: 'saludo',
  hooks: {
    'astro:config:setup': ({ logger }) => {
      logger.info('enganchada al ciclo de vida de Astro');
    },
  },
};

export default saludo;

Ese objeto ya es una integración completa y válida: puedes enrolarlo en el array integrations de tu configuración y Astro llamará a su hook en el momento oportuno. El tipo AstroIntegration, importado de astro, no cambia el comportamiento —solo tipa el objeto para que el editor conozca los nombres de hook legales y la forma de las utilidades que cada uno recibe—. La lección de partida es esa desnudez: donde esperabas un framework de plugins, hay un objeto literal.

El patrón fábrica: por qué casi todas son funciones

Si el corazón es un objeto, ¿por qué las enrolas llamándolas, como en react() o sitemap(), con paréntesis? Porque una integración casi siempre admite opciones, y el idioma para parametrizar un objeto es envolver su construcción en una función. Esa función —la fábrica— recibe las opciones del usuario y devuelve el AstroIntegration ya configurado.

import type { AstroIntegration } from 'astro';

interface Opciones {
  saludo?: string;
}

export default function saludador(opciones: Opciones = {}): AstroIntegration {
  const texto = opciones.saludo ?? 'hola';
  return {
    name: 'saludador',
    hooks: {
      'astro:config:setup': ({ logger }) => logger.info(texto),
    },
  };
}

Conviene separar los dos planos con nitidez. El objeto es la integración; la función es solo la comodidad que la construye a partir de unas opciones. Cuando escribes saludador({ saludo: 'hey' }) en el array, no corres ningún hook: ejecutas la fábrica, que devuelve el objeto y lo deja enrolado. Los hooks no se disparan hasta que Astro alcanza cada fase de su ciclo. Registrar es una cosa; ejecutar, otra que llega después y la decide el framework.

Los hooks: enganches nombrados en el ciclo de vida

Cada clave de hooks es el nombre de un instante concreto del proceso de Astro, y la función asociada corre cuando el framework llega a ese instante —una sola vez, con unas utilidades hechas a medida de la fase—. No eliges cuándo corren tus funciones; eliges en qué momento del ciclo las cuelgas, y Astro las invoca al pasar por él.

🏷️

name

La cadena que identifica la integración. Debe ser única y estable; aparece en los logs y en los errores que la mencionan.

🪝

hooks

El mapa de momento del ciclo a función. Solo declaras los que te interesan; los demás no existen y no cuestan nada.

🏭

fábrica

La función que recibe opciones y devuelve el objeto. Es ergonomía y configuración, no parte del contrato del framework.

🔌

utilidades

Cada hook recibe funciones a medida como updateConfig, injectRoute o injectScript, con las que actúa sobre esa fase.

flowchart TD
REG[integracion enrolada en el array] --> CFG[astro config setup]
CFG --> DONE[astro config done]
DONE --> DEV[astro server setup en modo dev]
DONE --> BUILD[astro build start en modo build]
BUILD --> SSR[astro build ssr]
SSR --> BDONE[astro build done]
style CFG fill:#89b4fa,color:#11111b
style BDONE fill:#a6e3a1,color:#11111b

El diagrama ordena los hooks que recorrerás en las próximas lecciones. astro:config:setup abre el ciclo: corre antes que nada, cuando la configuración aún se puede modificar. astro:config:done la sella. A partir de ahí el camino se bifurca según el comando: en dev se prepara el servidor, en build arranca la construcción, que pasa por la fase de servidor y termina cuando todo está en disco. Conocer ese orden es saber dónde puedes hacer cada cosa.

Cuándo escribir una integración

No todo problema pide una integración, y confundirlas con módulos corrientes lleva a sobre-ingeniería. La pregunta que decide es una sola: ¿necesito engancharme al ciclo de vida de Astro? Si tu código solo exporta un componente, una función de utilidad o un dato, no hay nada que enganchar —es un módulo que importas y ya está—. La integración se justifica cuando debes actuar sobre el framework mismo: modificar la configuración, inyectar rutas o scripts, añadir un middleware, tocar el build o el servidor de desarrollo.

// NO necesita integracion: es solo un modulo que se importa
export function formatearFecha(d: Date): string {
  return d.toISOString().slice(0, 10);
}

El segundo criterio es la reutilización. Una configuración que ajustas a mano en un proyecto no merece empaquetarse; pero si quieres que ese mismo comportamiento —un conjunto de rutas, un plugin de Vite, unos tipos generados— viaje intacto a varios proyectos o al ecosistema, la integración es el envase. Un adapter, que viste en el nivel de SSR, es exactamente esto: una integración con un rol reservado. La regla mental es limpia —si el problema vive dentro de tu app, escribe un módulo; si vive en el borde entre tu app y el framework, escribe una integración.

Queda un matiz que gobierna la convivencia: el orden en el array integrations importa. Cuando varias integraciones declaran el mismo hook, Astro las invoca en el orden en que aparecen, así que una que lee la configuración verá los parches de las que van antes y no los de las que van después. La mayoría son indiferentes al orden, pero las que dependen de lo que otra dejó —un adapter que reacciona a un renderer, por ejemplo— asumen esa secuencia. Escribir integraciones robustas es, en parte, no dar por sentado un orden que el usuario podría alterar.

💡
Empieza por el objeto, envuélvelo después

Al prototipar una integración, escribe primero el objeto literal { name, hooks } y enrólalo directamente en el array para verlo funcionar. Cuando el comportamiento sea el que quieres, envuélvelo en una función fábrica para admitir opciones. Ese orden —objeto primero, fábrica después— evita el error de diseñar la configuración antes de saber qué vas a configurar.

ℹ️
El nombre viaja en los diagnósticos

El campo name no es decorativo: Astro lo usa para atribuir avisos, errores y tiempos a tu integración. Un name claro y único —el nombre del paquete es una buena elección— convierte un mensaje anónimo en una pista accionable. Cuando dos integraciones comparten nombre, los diagnósticos se vuelven ambiguos; trátalo como un identificador, no como una etiqueta cualquiera.

⚠️
Enrolar no es ejecutar

Colocar miIntegracion() en el array corre la fábrica, no los hooks. Si pones lógica pesada —leer ficheros, llamar a la red— en el cuerpo de la fábrica en vez de dentro de un hook, esa lógica corre al cargar la configuración, antes y fuera del ciclo de vida, con utilidades que aún no existen. La fábrica solo debe preparar opciones y devolver el objeto; todo el trabajo real vive dentro de los hooks.

📝
Los hooks pueden ser asíncronos

Un hook no tiene por qué ser síncrono: si devuelve una promesa, Astro la espera antes de pasar a la fase siguiente. Eso te deja leer un fichero, consultar la red o generar datos dentro de astro:config:setup o astro:build:done con un await normal, sin trucos. El framework serializa el ciclo —no arranca el build hasta que tu config:setup asíncrono termina—, de modo que puedes apoyarte en operaciones asíncronas con la certeza de que ninguna fase se adelantará a la que aún no ha acabado.

Una integración es la costura por la que el framework se abre sin romperse

Detente en la asimetría que acabas de cruzar. Durante veinte niveles Astro fue un sistema cerrado que tú configurabas desde fuera; ahora resulta que ese mismo Astro se construyó, por dentro, con exactamente las piezas que tú puedes escribir. astro add react no invoca un poder que a ti se te niega: registra un AstroIntegration indistinguible en forma del que compondrás esta tarde. Esa igualdad no es un detalle simpático, es una tesis de diseño —la que en ingeniería se llama el principio abierto-cerrado—: un sistema debe estar cerrado a la modificación de su núcleo y abierto a la extensión por sus bordes, y la integración es justo la costura donde ambas cosas se concilian. Fíjate en la mecánica de esa costura, porque es sutil. Tu integración no importa el núcleo de Astro para manipularlo; al revés, es Astro quien, al llegar a cada fase, llama a tu hook y le entrega las utilidades con las que puede actuar —updateConfig, injectRoute, addMiddleware—. Es una inversión de control en estado puro: no tomas capacidades, las recibes, y solo las que esa fase autoriza. El framework no te abre su tripa entera; te pasa, en cada momento, exactamente el juego de herramientas que ese momento admite, y ni una más. Por eso el contrato puede ser tan minúsculo —un objeto con un nombre y un mapa de funciones— y a la vez tan potente: la potencia no vive en la anchura del contrato sino en la riqueza de las utilidades que cada hook reparte. Aprender a escribir integraciones es, en el fondo, aprender a pensar en términos de capacidades entregadas en el momento justo en lugar de acceso total permanente. Cuando interiorizas eso, dejas de ver a Astro como una caja que usas y empiezas a verlo como un anfitrión que, en instantes precisos, te cede el mando y luego lo recupera. Extender el framework deja de ser hackearlo y pasa a ser lo que siempre fue para sus propios autores: colgar funciones de las costuras que él mismo dejó abiertas.

⚔️ Escribe tu primera integración de la nada
  1. Crea un módulo que exporte por defecto un objeto { name, hooks } con un solo astro:config:setup que use logger.info para anunciarse; enrólalo en integrations y arranca dev.
  2. Confirma en la terminal que tu mensaje aparece al iniciar, y que aparece una sola vez por arranque, no por página.
  3. Convierte el objeto en una función fábrica que reciba un texto por opciones y lo registre; enróllala con y sin argumento y observa el valor por defecto.
  4. Escribe al lado una función de utilidad corriente y razona por qué esa no necesita ser una integración: nada en ella se engancha al ciclo de vida.