wandres.dev
ESCRIBIR INTEGRACIONES · Integration API

Middleware, endpoints y plugins de Vite en una integración

Cómo una integración añade comportamiento de servidor y de bundler: addMiddleware para inyectar un onRequest antes o después del middleware del usuario, injectRoute para servir endpoints y páginas que viven dentro del paquete, y updateConfig con vite para colar plugins de Vite. El patrón de los módulos virtuales —el prefijo virtual, el byte nulo y el par resolveId y load— para exponer datos generados como si fueran un import normal.

⏱ 18 min

Una integración madura rara vez se limita a tocar la configuración: quiere correr lógica en cada petición, servir sus propias páginas y endpoints, y a veces bajar hasta el bundler para transformar módulos o inventarlos de la nada. Astro expone justo esas tres capas —el middleware, el enrutado y Vite— a través de utilidades que ya conoces de otros niveles, ahora accionadas desde dentro de una integración. Y corona el conjunto con uno de los patrones más elegantes del ecosistema: los módulos virtuales, ficheros que no existen en disco pero que tu código importa como si existieran.

🎯 Al terminar esta lección sabrás
  • Inyectar un middleware desde una integración con addMiddleware y su order.
  • Servir endpoints y páginas que viven dentro del paquete con injectRoute.
  • Colar plugins de Vite en el bundle con updateConfig y la clave vite.
  • Exponer datos generados como un módulo virtual con el par resolveId y load.

addMiddleware: correr lógica en cada petición

Ya sabes que src/middleware.ts intercepta toda petición del sitio. Una integración puede aportar su propio middleware sin que el usuario cree ese fichero, usando addMiddleware dentro de astro:config:setup. Le das un entrypoint —un módulo que exporta un onRequest— y un order que decide si tu middleware corre antes o después del que el usuario haya escrito.

'astro:config:setup': ({ addMiddleware }) => {
  addMiddleware({
    order: 'pre',
    entrypoint: 'mi-integracion/middleware.js',
  });
},

El order tiene dos valores y la elección importa. Con 'pre', tu middleware corre antes que el del usuario, útil para preparar locals o cabeceras que sus rutas darán por sentadas. Con 'post', corre después, útil para rematar la respuesta ya construida. Varias integraciones pueden inyectar middleware a la vez; todos se encadenan con el del usuario en una sola tubería, exactamente como los sequence que estudiaste en el nivel de middleware.

Endpoints y páginas dentro del paquete

injectRoute no sirve solo para páginas: su entrypoint puede ser un endpoint —un módulo que exporta GET, POST y demás verbos— y así tu integración expone una API sin que el usuario escriba un fichero en src/pages. Es el mecanismo con el que una integración regala un webhook, un endpoint de estado o un feed.

// mi-integracion/endpoints/salud.ts  (dentro del paquete)
import type { APIRoute } from 'astro';

export const GET: APIRoute = () =>
  Response.json({ estado: 'ok', hora: Date.now() });
'astro:config:setup': ({ injectRoute }) => {
  injectRoute({
    pattern: '/api/salud',
    entrypoint: 'mi-integracion/endpoints/salud.ts',
    prerender: false,
  });
},

La ruta inyectada es indistinguible de una escrita a mano: participa del enrutado, del middleware y del render como cualquier otra. La diferencia es de procedencia —vive en tu paquete, viaja con él y aparece en cada proyecto que instale la integración— sin pedirle nada al usuario más que enrolarla.

Plugins de Vite desde la integración

Cuando necesitas transformar módulos, resolver imports especiales o participar del empaquetado, la herramienta es un plugin de Vite, y lo enchufas fundiéndolo en la configuración con updateConfig. Como Astro corre sobre Vite, todo el poder del bundler queda a tu alcance desde la integración.

'astro:config:setup': ({ updateConfig }) => {
  updateConfig({
    vite: {
      plugins: [
        {
          name: 'mi-plugin-vite',
          transform(code, id) {
            // transforma modulos que coincidan con un criterio
            return null;
          },
        },
      ],
    },
  });
},

Como updateConfig funde en profundidad, añadir tu plugin no borra los que el usuario u otras integraciones ya pusieron: se suma al array. Esta es la puerta trasera controlada por la que una integración accede al pipeline de compilación —la misma capa que en su día tocaste a mano en la clave vite de la configuración, ahora accionada por código—.

Módulos virtuales: importar lo que no existe

El patrón que corona esta lección resuelve un problema recurrente: quieres que el código del usuario importe datos que tu integración genera —opciones resueltas, una lista construida en el build—, pero esos datos no viven en ningún fichero. La solución es un módulo virtual: un plugin de Vite intercepta un especificador imaginario y devuelve su contenido al vuelo.

function moduloVirtual(saludo: string) {
  const id = 'virtual:mi-integracion';
  const resuelto = '\0' + id;
  return {
    name: 'mi-integracion:virtual',
    resolveId(spec: string) {
      if (spec === id) return resuelto;
    },
    load(spec: string) {
      if (spec === resuelto) return `export const saludo = ${JSON.stringify(saludo)};`;
    },
  };
}

El convenio tiene dos mitades. El especificador público lleva el prefijo virtual:, la señal idiomática de que no es un fichero real. Y el identificador interno que devuelve resolveId lleva por delante un byte nulo —el prefijo que le dice a Vite y a Rollup que no busquen eso en disco porque es suyo—. La función load reconoce ese identificador interno y devuelve el código como una cadena. A partir de ahí, el usuario escribe import { saludo } from 'virtual:mi-integracion' y recibe tus datos como si viniesen de un fichero.

🚪

addMiddleware

Inyecta un onRequest desde el paquete, con order pre o post respecto al middleware del usuario.

🛰️

injectRoute

Sirve endpoints y páginas que viven dentro de la integración, indistinguibles de las de src pages.

⚙️

vite.plugins

Funde plugins de Vite con updateConfig para transformar módulos y participar del empaquetado.

👻

módulo virtual

El par resolveId y load expone datos generados bajo un especificador virtual que el usuario importa.

flowchart TD
INT[integracion] --> MW[addMiddleware onRequest en cada peticion]
INT --> RT[injectRoute endpoints y paginas del paquete]
INT --> VP[updateConfig con vite plugins]
VP --> RES[resolveId reconoce el especificador virtual]
RES --> LOAD[load devuelve el codigo al vuelo]
LOAD --> IMP[el usuario importa el modulo virtual]
style INT fill:#89b4fa,color:#11111b
style IMP fill:#a6e3a1,color:#11111b
ℹ️
El byte nulo marca la propiedad del módulo

El prefijo de byte nulo delante del identificador resuelto es una convención de Rollup que Vite hereda: señala que ese módulo es virtual, propiedad de un plugin, y que ninguna otra parte del sistema debe intentar leerlo del sistema de ficheros. Sin ese prefijo, Vite buscaría un fichero con ese nombre, no lo encontraría y fallaría. Es un detalle diminuto y fácil de olvidar, y su ausencia es la causa número uno de que un módulo virtual no funcione.

💡
Acompaña cada módulo virtual de sus tipos

Un módulo virtual que el usuario importa debería estar tipado, o su editor lo verá como un valor sin forma. Empareja el plugin de Vite con un injectTypes en astro:config:done que declare el módulo —declare module 'virtual:mi-integracion'— con las exportaciones que ofreces. Así el import virtual no solo funciona en runtime, también autocompleta y se comprueba, como cualquier import de un fichero real.

⚠️
El order del middleware cambia el resultado

Elegir pre o post en addMiddleware no es indiferente. Un middleware que prepara locals para las rutas debe correr en pre, antes que el del usuario, o las rutas no encontrarán lo que esperan. Uno que ajusta la respuesta final debe correr en post, cuando ya hay respuesta que tocar. Poner el order equivocado produce fallos sutiles —datos ausentes, cabeceras que no cuajan— que no lanzan error pero rompen la lógica.

El módulo virtual borra la frontera entre configurar y programar

Merece la pena mirar de cerca el módulo virtual, porque es más profundo de lo que su tamaño sugiere. En el desarrollo corriente hay una línea tácita entre dos mundos: el código, que se escribe y vive en ficheros, y la configuración, que se declara y se pasa como datos. El usuario configura tu integración con opciones; tu integración corre código en los hooks. Esos dos mundos parecen no tocarse —¿cómo haría el código del usuario, escrito antes de que su configuración se resuelva, para leer un valor que solo existe después?—. El módulo virtual es el puente que une esos dos tiempos. Tu integración toma un dato que solo conoce en tiempo de build —las opciones ya fundidas, una lista que acabas de construir— y lo materializa como un módulo, un artefacto que el sistema de imports trata igual que cualquier fichero. De pronto, algo que era configuración se puede importar como si fuera código; el resultado de una decisión tomada en un hook se vuelve un valor que una plantilla escrita meses antes puede consumir con un import normal. Esta es una idea con siglos de linaje en la informática: la que borra la distinción entre programa y dato, entre lo que se ejecuta y lo que se lee. Un módulo virtual es datos vestidos de código, generación de código sin la fealdad de escribir ficheros a mano, metaprogramación domesticada a la ergonomía de un import. Y su elegancia está en lo poco que pide creer: no hay un compilador especial ni una sintaxis nueva, solo dos funciones —resolveId, que reconoce un nombre, y load, que devuelve una cadena— y un byte nulo que marca la propiedad. Con ese aparato mínimo, tu integración deja de estar confinada a los hooks y empieza a hablarle directamente al código del usuario, poniéndole en las manos, bajo la forma más familiar posible, aquello que solo ella podía calcular.

⚔️ Da a tu integración voz en cada capa
  1. Inyecta un middleware con addMiddleware en order: 'pre' que rellene un valor en locals, y léelo desde una página para confirmar que llegó antes que ella.
  2. Sirve un endpoint con injectRoute que devuelva JSON desde un módulo de tu paquete y visítalo sin haber creado nada en src/pages.
  3. Funde un plugin de Vite con updateConfig que registre en consola cada id que transforma, y observa el pipeline en marcha.
  4. Expón un módulo virtual virtual:mi-integracion con resolveId y load, impórtalo desde una página y añade su injectTypes para que el editor lo conozca.