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, notversion.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.
| Aspect | Static (output: 'static') | Server (SSR) |
|---|---|---|
When GET runs | Once, at build time | On every request |
| Output | Physical file in dist/ | Streamed response |
| Request body / search params | Not available | Fully available |
getStaticPaths | Required for dynamic routes | Not used |
| HTTP methods | GET only | GET, POST, PUT, etc. |
Static endpoints only support
GET. If you exportPOSTor readrequest.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-Typeheader — browsers and crawlers rely on it for JSON, XML, and text feeds. - Type your handler with
APIRoute(andGetStaticPaths) for autocomplete and compile-time safety. - Filter out drafts and unpublished entries before serializing, so build-time files match your published pages.
- Reach for
getStaticPathsto fan out per-category or per-tag files instead of one giant payload clients must parse. - Use
@astrojs/rssand@astrojs/sitemapfor feeds and sitemaps rather than assembling XML strings by hand. - Remember static endpoints emit only
GET; move anything dynamic or method-specific to SSR.