Data Fetching & API Endpoints

Chapter 9
Data Fetching & API Endpoints
Build-time fetch(), src/pages/api/ endpoints, and client-side fetching in an island

There are three genuinely different ways to get data into an Astro site, each with its own timing and tradeoffs.

Fetching in the Frontmatter: Build Time, Not Request Time

--- const response = await fetch('https://api.example.com/posts'); const posts = await response.json(); --- <ul> {posts.map((post) => <li>{post.title}</li>)} </ul>
A direct consequence of Chapter 2's own finding
Chapter 2 established that the frontmatter fence runs once, not reactively. That's exactly why this fetch() call happens at build time in Astro's default static output mode — the response gets baked directly into the pre-rendered HTML. If the API's own data changes afterward, the site won't reflect it until the next build and deploy. This isn't a bug to work around; it's the same static-by-default model already established, just applied to a network request instead of a plain variable.

API Endpoints

// src/pages/api/posts.json.ts export async function GET() { const posts = [ { id: 1, title: 'First Post' }, { id: 2, title: 'Second Post' }, ]; return new Response(JSON.stringify(posts), { headers: { 'Content-Type': 'application/json' }, }); }

Any file in src/pages/api/ exporting a named GET (or POST, PUT, etc.) function returning a real Response becomes a working API endpoint — here, reachable at /api/posts.json. In static output mode, this endpoint is also generated once at build time, the same constraint as the frontmatter fetch above; Chapter 10's server/hybrid modes are what let an endpoint like this run fresh on every request instead.

Client-Side Fetching Inside an Island

// src/components/LivePosts.jsx import { useEffect, useState } from 'react'; export default function LivePosts() { const [posts, setPosts] = useState([]); useEffect(() => { fetch('/api/posts.json') .then((r) => r.json()) .then(setPosts); }, []); return ( <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul> ); }

A hydrated island (Chapter 7) can fetch data itself, in the browser, at runtime — genuinely fresh every time the component loads, regardless of when the site was last built. The real cost is exactly what Chapter 7 already established: this requires shipping and hydrating real client-side JavaScript, unlike the frontmatter fetch's zero-JS static output.

Three Approaches, Compared

ApproachRuns WhenShips JS?Data Freshness
Frontmatter fetch()Build time, onceNoFrozen until next build
API endpoint (static mode)Build time, onceNo (unless called from an island)Frozen until next build
Client-side fetch in an islandEvery time the component loads, in the browserYesAlways current

Coding Challenges

Challenge 1

Fetch data from a public placeholder API inside a page's frontmatter, render it as a list, then rebuild the site and confirm the rendered output only changes on a fresh build, not on every page load.

📄 View solution
Challenge 2

Build a src/pages/api/posts.json.ts endpoint returning a hard-coded JSON array via a GET function, and confirm it's reachable directly by visiting /api/posts.json.

📄 View solution
Challenge 3

Build a client-side-fetching island (client:load) that fetches from your own /api/posts.json endpoint at runtime, and explain how this differs from Challenge 1's frontmatter-fetch approach.

📄 View solution

Chapter 9 Quick Reference

  • Frontmatter fetch() — runs once at build time, result frozen into static HTML
  • src/pages/api/*.ts — a named GET/POST export returning a Response becomes a real endpoint
  • Both are static in default mode — Chapter 10's server/hybrid modes remove this constraint
  • Client-side fetch in an island — always fresh, but requires shipping real JavaScript
  • Next chapter: Rendering Modes: Static, Server & Hybrid