wandres.dev
ADAPTERS · Node, Cloudflare, Vercel

Configurar el adapter y las APIs del runtime

Más allá de instalar el adapter: cómo se combinan output y adapter en astro.config, cómo platformProxy hace que astro dev emule los bindings de Cloudflare en tu máquina, por qué la optimización de imágenes cambia en el edge cuando Sharp no puede correr, y cómo alcanzar las APIs específicas del runtime —KV, D1, geolocalización, trabajo diferido y sesiones— a través de Astro.locals.runtime.

⏱ 16 min

Instalar un adapter es una línea, pero conectarlo bien es donde asoma el poder de cada plataforma. Esta lección baja a astro.config: cómo se combinan output y adapter con las opciones propias del adapter, cómo platformProxy permite que astro dev emule en tu máquina los bindings de Cloudflare —una base de datos D1, un almacén KV—, por qué la optimización de imágenes cambia en el edge cuando Sharp no puede correr, y cómo se alcanzan las APIs específicas del runtime a través de Astro.locals.runtime. Configurar el adapter no es rellenar opciones al azar: es negociar, opción por opción, cuánto poder de la plataforma tomas y cuánto acoplamiento aceptas a cambio.

🎯 Al terminar esta lección sabrás
  • Combinar output y adapter en astro.config con las opciones del adapter.
  • Activar platformProxy para emular el runtime de Cloudflare en astro dev.
  • Ajustar el servicio de imágenes cuando el runtime no soporta Sharp.
  • Leer bindings, geolocalización, trabajo diferido y sesiones desde Astro.locals.runtime.

adapter en astro.config, con sus opciones

La clave adapter recibe la función del adapter, y esa función acepta un objeto de opciones que ajusta su comportamiento. El output decide la política de renderizado —static con rutas dinámicas puntuales, o server con todo bajo demanda por defecto—; el adapter decide el destino. Juntos definen cómo y dónde corre tu sitio.

import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';

export default defineConfig({
  output: 'server',
  adapter: cloudflare({
    platformProxy: { enabled: true },
    imageService: 'cloudflare',
  }),
});

astro add deja esta clave con valores por defecto sensatos; tu trabajo posterior es afinar las opciones según lo que la plataforma ofrezca. Cada adapter documenta las suyas, pero dos familias de opciones aparecen una y otra vez: las que emulan el runtime en desarrollo y las que redirigen servicios —imágenes, sesiones— hacia lo que el destino sí soporta.

📝
output y adapter responden a preguntas distintas

Es fácil confundirlos porque suelen cambiarse juntos, pero deciden cosas independientes. output fija la política por defecto —cuántas rutas se hornean y cuántas corren por petición—; adapter fija el destino donde corren las que sí son dinámicas. Puedes tener output: 'static' con un adapter instalado para unas pocas rutas bajo demanda, o output: 'server' con ese mismo adapter para servirlo casi todo en vivo. Uno habla de política de renderizado, el otro de plataforma de ejecución.

platformProxy: el runtime de la plataforma en tu máquina

Cloudflare expone recursos —KV, R2, D1, secretos, la información geográfica de la petición— a través de bindings que solo existen dentro de su runtime. Esos bindings se declaran en el fichero de Wrangler, y de ahí los toma tanto la plataforma real como la emulación local.

// wrangler.jsonc
{
  "kv_namespaces": [{ "binding": "MI_KV", "id": "..." }],
  "d1_databases": [{ "binding": "DB", "database_name": "prod" }]
}

En astro dev, sin más, esos bindings no están, y Astro.locals.runtime llega vacío. La opción platformProxy: { enabled: true } resuelve el desfase: usa Wrangler por debajo para levantar emulaciones locales de esos recursos y poblar Astro.locals.runtime como lo haría la plataforma real.

---
// una pagina bajo demanda en Cloudflare
const { env, cf, ctx } = Astro.locals.runtime;

const guardado = await env.MI_KV.get('clave');   // binding KV
ctx.waitUntil(registrarVisita(cf.country));       // trabajo diferido
---
<p>Servido desde {cf.city}</p>

Con platformProxy activo, ese código funciona igual en tu portátil que en producción: lees de un KV emulado, consultas el país de la petición, programas trabajo diferido con ctx.waitUntil. Sin él, tendrías que desplegar para probar cualquier cosa que toque un binding —un ciclo de desarrollo lento y a ciegas—.

Para que Astro.locals.runtime deje de ser any, su tipo se declara una vez en src/env.d.ts. El adapter de Cloudflare exporta un tipo Runtime parametrizado con tu Env —el que genera Wrangler a partir de tus bindings—, y lo enlazas a App.Locals:

// src/env.d.ts
type Runtime = import('@astrojs/cloudflare').Runtime<Env>;
declare namespace App {
  interface Locals extends Runtime {}
}
ℹ️
Tipar el runtime se paga en autocompletado

Con ese env.d.ts en su sitio, env, cf y ctx dejan de ser opacos: llegan tipados, con autocompletado de cada binding que hayas declarado y con errores en tiempo de edición si accedes a uno que no existe. Es la diferencia entre tantear la API del runtime a ciegas y programarla con el editor guiándote paso a paso.

Imágenes y servicios que dependen del runtime

El servicio de imágenes de Astro usa por defecto Sharp, un módulo nativo de Node. En el edge no hay módulos nativos, así que optimizar imágenes bajo demanda ahí exige otro camino. El adapter de Cloudflare expone imageService para elegirlo, y ese menú resume el patrón general: cuando el runtime no soporta la implementación por defecto, el adapter ofrece una específica de la plataforma.

🛠️

compile

Optimiza en el build solo lo prerenderizado, con Sharp. Nada nativo corre en producción.

☁️

cloudflare

Delega el redimensionado bajo demanda en el servicio de imágenes de Cloudflare, sin Sharp.

➡️

passthrough

Sirve el original sin transformar. Útil cuando optimizas fuera o no lo necesitas.

adapter: cloudflare({
  imageService: 'cloudflare', // redimensionado en el edge, sin Sharp
}),

La lección general trasciende las imágenes: cualquier capacidad que en Node dabas por sentada —procesar un PDF, comprimir con una librería nativa, leer una fuente del disco— hay que replantearla en el edge como un servicio de la plataforma o una llamada a una API externa. El adapter te dice qué tienes disponible; el runtime, qué te falta y por dónde suplirlo.

Trabajo diferido y sesiones en el runtime

El runtime no solo se lee: también difiere trabajo y guarda estado. ctx.waitUntil acepta una promesa que la plataforma mantiene viva después de enviar la respuesta —registrar una analítica, calentar un caché, escribir un log— sin retrasar al usuario ni un milisegundo. Y el almacenamiento del runtime puede respaldar a Astro: en Cloudflare, un binding KV sirve de trastienda al sistema de sesiones, de modo que Astro.session persiste entre peticiones usando la infraestructura de la propia plataforma.

---
const { ctx, env } = Astro.locals.runtime;
// responde ya y registra la visita despues, sin bloquear
ctx.waitUntil(env.LOGS.put(crypto.randomUUID(), JSON.stringify({ t: Date.now() })));
---

Estas APIs del runtime —los bindings, la geolocalización, el trabajo diferido, el almacenamiento— son puertas de poder, pero también son puntos de acoplamiento. Astro.locals.runtime.env no existe en Node ni en Vercel; cf.country es vocabulario de Cloudflare. Usarlas es legítimo y a menudo la razón misma de elegir esa plataforma, pero conviene saber que cada una es un hilo que te ata al destino.

flowchart TD
CFG[astro config con adapter y opciones] --> PP[platformProxy en dev]
CFG --> IMG[imageService del runtime]
CFG --> LOC[Astro locals runtime]
PP --> DEV[bindings emulados en tu maquina]
LOC --> ENV[env KV D1 R2 y secretos]
LOC --> CFO[cf geolocalizacion de la peticion]
LOC --> CTX[ctx waitUntil trabajo diferido]
style CFG fill:#89b4fa,color:#11111b
style LOC fill:#a6e3a1,color:#11111b
Cada opción que activas es una capacidad comprada con acoplamiento

La configuración del adapter es el lugar donde se negocia, de forma explícita y medible, el precio de la portabilidad. Vista superficialmente parece una lista de ajustes; vista con hondura es un balance contable entre dos monedas enfrentadas: poder de la plataforma y libertad de movimiento. Cada opción que enciendes te da algo real —bindings que evitan montar tu propia infraestructura, un servicio de imágenes que no tienes que operar, geolocalización sin una base de datos de IPs, trabajo diferido sin una cola aparte— y te cobra en la misma medida un acoplamiento al destino que la ofrece. Astro.locals.runtime.env es maravilloso en Cloudflare y sencillamente no existe fuera de él; imageService: 'cloudflare' resuelve un problema que en Node ni se plantea. No hay opción gratis: la única que no cuesta acoplamiento es la que no usa nada propio de la plataforma. Lo que distingue al ingeniero maduro no es evitar ese acoplamiento —a veces es justo lo que quieres, porque la plataforma resuelve un problema difícil mejor que tú— sino saber que lo está contrayendo y confinarlo. La disciplina consiste en que todo lo específico del runtime entre por una única puerta nombrada —Astro.locals.runtime, un módulo de acceso a datos, una función de imágenes— en lugar de esparcirse por medio proyecto. Cuando el acoplamiento vive concentrado en una frontera visible, sigue siendo una decisión reversible: el día que quieras portar, sabes exactamente qué ficheros tocar. Cuando se esparce, deja de ser una decisión y se vuelve un destino. Configurar bien un adapter, entonces, no es maximizar las capacidades que enciendes ni minimizarlas por miedo, sino elegirlas a conciencia y mantener cada dependencia del proveedor donde puedas verla y, si hace falta, arrancarla.

⚔️ Conecta y confina el poder del runtime
  1. Configura el adapter de Cloudflare con platformProxy: { enabled: true } y lee un valor de un binding KV desde una página en astro dev.
  2. Añade el env.d.ts con el tipo Runtime y comprueba que env, cf y ctx llegan tipados.
  3. Cambia imageService a 'cloudflare', difiere una escritura de log con ctx.waitUntil y razona por qué Sharp no serviría en ese runtime.
  4. Reúne todos los accesos a Astro.locals.runtime en un único módulo de tu proyecto y explica cómo eso facilitaría portar a otro adapter.