Data Fetching
Fetching data is just top-level await
Section titled “Fetching data is just top-level await”An .astro component script runs on the server, so you fetch external data the plainest way possible: await fetch() at the top level. No useEffect, no loading state, no client round-trip — the data is resolved before the HTML exists.
---const res = await fetch('https://api.example.com/products');if (!res.ok) throw new Error(`API returned ${res.status}`);const products = await res.json();---<ul> {products.map((p) => <li>{p.name} — ${p.price}</li>)}</ul>The same pattern works against a REST API, a database client, or a headless CMS SDK. Because it runs on the server, secrets (API keys) stay server-side and never ship to the browser.
Build-time vs request-time: the crucial distinction
Section titled “Build-time vs request-time: the crucial distinction”When that fetch runs depends on the page’s rendering mode — and this is the single most important thing to understand about data in Astro:
- Static page (the default). The
fetchruns once, at build time. The response is baked into the HTML file. Every visitor gets the same snapshot until you rebuild. Perfect for a product list, a blog index, docs — data that changes on a deploy cadence. - On-demand page (
prerender = false). Thefetchruns per request, on the server. Each visitor can get fresh, personalized data — the current user, live inventory, a search result. Requires an adapter (see the rendering module).
---// This page renders per request, so the fetch is live on every visit.export const prerender = false;
const res = await fetch('https://api.example.com/inventory');const inventory = await res.json();---<p>{inventory.inStock} in stock right now</p>The mental test: does this data need to be fresh per visitor, or is a build-time snapshot fine? Snapshot → leave it static (fast, cacheable). Fresh → opt the route into on-demand rendering. Reaching for on-demand when a static snapshot would do is the most common way people give up Astro’s speed for no reason.
flowchart TB q["Does data change per request?"] q -->|no| static["Static: fetch runs once at build, baked into HTML"] q -->|yes| ondemand["On-demand: prerender = false, fetch runs per request"] static --> cdn["served from CDN, same for everyone"] ondemand --> server["rendered on the server per visitor"]
Headless CMS
Section titled “Headless CMS”A headless CMS (Contentful, Sanity, Storyblok, a WordPress API) is just another data source. Fetch it in the component script — often via the CMS’s SDK — and map the response into your template. For content you own and edit as files, prefer a content collection with a loader; for content managed in an external system, fetch it (or use a community loader that wraps the CMS behind the collections API). Either way the page code looks the same: get data on the server, render HTML.
What about Astro.glob()?
Section titled “What about Astro.glob()?”Older Astro code used Astro.glob() to pull in many local files (all Markdown in a folder) at once. It still exists but is superseded by content collections, which give you the same “load many files” power plus schema validation and generated types. For local content, define a collection with a glob loader instead of reaching for Astro.glob(); reserve raw fetch for genuinely external data.