Saltar al contenido

Escribir un loader inline: fetch a una API

El primer loader propio es una simple función asíncrona que devuelve un array de entradas con id. Cómo traer datos de una API remota con fetch, transformarlos con map, dejar que el esquema los valide y reconocer los límites que empujan hacia el loader como objeto en Astro 7.

El loader inline: una función que devuelve entradas

En su forma más simple, un loader es una función asíncrona sin argumentos que devuelve un array. Cada elemento del array es una entrada, y la única regla es que cada uno traiga un campo id único. Astro toma ese array, valida cada elemento contra el esquema y lo deposita en el store. No hay que aprender ninguna API nueva: si sabes escribir una función que devuelve datos, sabes escribir un loader inline.

TypeScript
import { defineCollection, z } from 'astro:content';const productos = defineCollection({  loader: async () => {    return [      { id: 'teclado', nombre: 'Teclado', precio: 80 },      { id: 'raton', nombre: 'Raton', precio: 30 },    ];  },  schema: z.object({    id: z.string(),    nombre: z.string(),    precio: z.number(),  }),});export const collections = { productos };

Ese array escrito a mano ilustra la forma, pero desaprovecha el poder: si los datos van a estar en el código, mejor un file. El loader inline brilla cuando el array no lo escribes tú, sino que lo traes de una fuente que ninguno de los loaders de fábrica sabe leer. La función es el hueco donde cabe cualquier lógica —una llamada de red, una consulta, una transformación— con tal de que termine devolviendo entradas con id.

Astro también acepta que, en vez de un array, devuelvas un objeto cuyas claves sean los id y cuyos valores sean los datos. Las dos formas son equivalentes; el array es más natural cuando la fuente ya te da una lista, y el mapa lo es cuando trabajas con pares clave-valor. Elijas la que elijas, el resultado en el store es el mismo: un conjunto de entradas identificadas por su id.

Devolver un mapa tiene, además, una comodidad: el id es la clave, así que no hace falta repetirlo dentro de cada objeto. Cuando la fuente ya te da un identificador natural, construir ese mapa es directo.

TypeScript
loader: async () => {  const res = await fetch('https://api.ejemplo.com/autores');  const lista = await res.json();  return Object.fromEntries(    lista.map((autor) => [autor.slug, { nombre: autor.nombre }]),  );}

Fetch a una API remota

El caso canónico del loader inline es traer datos de una API. Dentro de la función asíncrona, un fetch normal descarga los datos; después, un map los transforma en la forma que tu esquema espera, cuidando de asignar a cada uno un id. Como la función corre en el build, dentro de Node, dispones de fetch y de todo el entorno del servidor sin restricciones de navegador.

TypeScript
const paises = defineCollection({  loader: async () => {    const res = await fetch('https://restcountries.com/v3.1/all');    if (!res.ok) throw new Error(`La API respondio ${res.status}`);    const datos = await res.json();    return datos.map((pais) => ({      id: pais.cca3,      nombre: pais.name.common,      region: pais.region,      poblacion: pais.population,    }));  },  schema: z.object({    id: z.string(),    nombre: z.string(),    region: z.string(),    poblacion: z.number(),  }),});

Hay tres detalles que separan un loader inline de juguete de uno serio. El primero es el id: la fuente casi nunca lo llama así, de modo que tu map debe elegir qué campo hace de identidad —aquí cca3, el código de tres letras del país— y exponerlo como id. El segundo es el error: si la respuesta no es correcta, lanzar una excepción detiene el build, que es justo lo que quieres —mejor un build roto que una colección medio vacía y silenciosa—. El tercero es la forma: lo que devuelves debe encajar con el esquema, así que el map es también el lugar donde adaptas los nombres y los tipos de la API a los tuyos.

Qué hace Astro con lo que devuelves

Entender el ciclo completo ayuda a no tratar el loader inline como una caja negra. Cuando arranca el dev server o el build, Astro invoca tu función, espera a que la promesa resuelva y recibe tu array. Antes de guardarlo, valida cada entrada contra el schema: si una incumple —falta un campo, un tipo no cuadra—, el build se detiene señalando la entrada culpable por su id. Solo las entradas que pasan la validación llegan al store, ya tipadas.

Ese orden —traer, transformar, validar, guardar— es siempre el mismo, y merece grabarse porque reaparecerá con variantes en todos los loaders del nivel. El loader inline lo recorre entero dentro de una sola función anónima; los loaders más avanzados lo despliegan en piezas con más control, pero la coreografía no cambia. Si tienes clara esta secuencia, ya entiendes el esqueleto de cualquier loader, por sofisticado que se vuelva después.

Hay una característica del loader inline que conviene tener muy presente: reemplaza el store entero en cada ejecución. La función no sabe qué había antes ni compara con lo previo; simplemente devuelve la lista completa, y Astro la toma como la nueva verdad. Eso es una virtud —es simple y predecible— y una limitación: si la API tiene diez mil entradas y solo cambian tres, el loader inline las trae y las revalida las diez mil, cada build, sin aprovechar nada del trabajo anterior.

Esa limitación es exactamente la frontera donde el loader inline se queda corto y pide convertirse en algo más. Cuando necesites no rehacerlo todo cada vez —consultar qué había, actualizar solo lo que cambió, recordar entre builds un token de sincronización— la función pura ya no basta: hará falta el loader como objeto, con acceso directo al store y a la memoria del build, que es lo que aborda la próxima lección. El loader inline es el escalón perfecto para empezar y para fuentes pequeñas; el objeto es el escalón para la escala y la eficiencia.