Skip to content
Astro as endpoints 4 min read

Static File Endpoints

Astro endpoints let you generate arbitrary files at build time, not just HTML pages. By exporting a GET function from a .js or .ts file inside src/pages/, you can emit JSON APIs, RSS feeds, sitemaps, plain-text robots files, or anything else your site needs as a static asset. Because these files are produced during astro build, they ship zero JavaScript and are served straight from your host’s CDN — fast, cacheable, and free of runtime cost.

How static endpoints work

A static endpoint is a file in src/pages/ whose name ends in a non-.astro extension, exporting an async GET function. The function receives a context object and returns a standard Response. The file name (minus the trailing .js/.ts) becomes the output path: src/pages/data.json.ts builds to /data.json.

This works only in Astro’s default static (output: 'static') mode, where every route is pre-rendered at build time. In server (SSR) mode the same handler runs per request instead — see Server Endpoints.

// src/pages/api/version.json.ts
import type { APIRoute } from 'astro';

export const GET: APIRoute = () => {
  return new Response(
    JSON.stringify({ version: '5.0.0', updated: '2026-06-14' }),
    {
      headers: { 'Content-Type': 'application/json' },
    }
  );
};

After astro build, this produces a physical file at dist/api/version.json.

Output:

{"version":"5.0.0","updated":"2026-06-14"}

The double extension matters. Name the file version.json.ts, not version.ts. The first extension (.json) is preserved in the build output; the second (.ts) tells Astro it is a module to execute, not a static asset to copy.

Pulling data from content collections

The real power of static endpoints is combining build-time data — usually a content collection — with a generated file. Because the code runs at build, you can freely await getCollection() and shape the result however you like.

// src/pages/posts.json.ts
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';

export const GET: APIRoute = async () => {
  const posts = await getCollection('blog');

  const payload = posts
    .filter((post) => !post.data.draft)
    .map((post) => ({
      slug: post.id,
      title: post.data.title,
      date: post.data.pubDate.toISOString(),
    }));

  return new Response(JSON.stringify(payload), {
    headers: { 'Content-Type': 'application/json' },
  });
};

Generating many files with getStaticPaths

A single endpoint file can emit multiple output files by exporting getStaticPaths, exactly like a dynamic page route. Each returned param becomes its own generated file, and the matched values arrive on params.

// src/pages/categories/[category].json.ts
import type { APIRoute, GetStaticPaths } from 'astro';
import { getCollection } from 'astro:content';

export const getStaticPaths: GetStaticPaths = async () => {
  const posts = await getCollection('blog');
  const categories = [...new Set(posts.map((p) => p.data.category))];
  return categories.map((category) => ({ params: { category } }));
};

export const GET: APIRoute = async ({ params }) => {
  const posts = await getCollection('blog');
  const matches = posts.filter((p) => p.data.category === params.category);

  return new Response(JSON.stringify(matches.map((p) => p.data.title)), {
    headers: { 'Content-Type': 'application/json' },
  });
};

With three categories, this produces dist/categories/tech.json, dist/categories/news.json, and so on.

Emitting non-JSON files

The Response body is just bytes, so endpoints can produce any text or binary format. Set the appropriate Content-Type and name the file accordingly.

// src/pages/robots.txt.ts
import type { APIRoute } from 'astro';

const site = 'https://example.com';

export const GET: APIRoute = () => {
  const body = [
    'User-agent: *',
    'Allow: /',
    `Sitemap: ${site}/sitemap-index.xml`,
  ].join('\n');

  return new Response(body, {
    headers: { 'Content-Type': 'text/plain' },
  });
};

For RSS specifically, prefer the official @astrojs/rss integration rather than hand-building XML — it handles escaping and the feed envelope for you. See RSS Feed.

Static vs. server endpoints

The same handler behaves differently depending on your rendering mode.

AspectStatic (output: 'static')Server (SSR)
When GET runsOnce, at build timeOn every request
OutputPhysical file in dist/Streamed response
Request body / search paramsNot availableFully available
getStaticPathsRequired for dynamic routesNot used
HTTP methodsGET onlyGET, POST, PUT, etc.

Static endpoints only support GET. If you export POST or read request.json() in static mode, that code never runs because there is no live request. You need SSR for request handling.

Best Practices

  • Always use the double extension (name.json.ts) so the output path keeps the correct file type.
  • Set an explicit Content-Type header — browsers and crawlers rely on it for JSON, XML, and text feeds.
  • Type your handler with APIRoute (and GetStaticPaths) for autocomplete and compile-time safety.
  • Filter out drafts and unpublished entries before serializing, so build-time files match your published pages.
  • Reach for getStaticPaths to fan out per-category or per-tag files instead of one giant payload clients must parse.
  • Use @astrojs/rss and @astrojs/sitemap for feeds and sitemaps rather than assembling XML strings by hand.
  • Remember static endpoints emit only GET; move anything dynamic or method-specific to SSR.
Last updated June 14, 2026
Was this helpful?