Endpoints & API Routes
Not every file in an Astro project needs to render HTML. Alongside your .astro pages, you can drop plain .js or .ts files into src/pages/ that export request-handling functions and return raw data: JSON for an API, a generated image, an application/xml feed, or a redirect. These files are called endpoints (sometimes “API routes”), and they let Astro act as both a content site and a lightweight backend without reaching for a separate server.
What is an endpoint?
An endpoint is any file under src/pages/ whose name ends in a non-page extension and which exports named functions matching HTTP methods (GET, POST, PUT, DELETE, PATCH, etc.). Instead of returning a component, these functions return a standard web Response object. Astro builds on the Web Platform here, so the same Request/Response primitives you’d use in a Service Worker or Cloudflare Worker apply directly.
The file path maps to a URL exactly like a page does. A file at src/pages/api/health.ts is served at /api/health.
// src/pages/api/health.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = () => {
return new Response(
JSON.stringify({ status: 'ok', time: Date.now() }),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
);
};
Output:
$ curl http://localhost:4321/api/health
{"status":"ok","time":1750000000000}
Endpoints carry no client-side JavaScript and no HTML wrapper — they are pure data. This keeps Astro’s zero-JS-by-default philosophy intact even when you expose an API.
The APIContext
Each handler receives a single APIContext argument that mirrors the props available inside an .astro frontmatter. You can destructure exactly what you need:
// src/pages/api/echo.ts
import type { APIRoute } from 'astro';
export const POST: APIRoute = async ({ request, params, url }) => {
const body = await request.json();
return new Response(JSON.stringify({ received: body, path: url.pathname }), {
headers: { 'Content-Type': 'application/json' },
});
};
| Property | Description |
|---|---|
request | The standard Request object (method, headers, body) |
params | Dynamic route segments, e.g. { id: '42' } from [id].ts |
url | A parsed URL instance, useful for query strings |
cookies | Astro’s cookie helper for get / set / delete |
redirect | Shortcut that returns a redirect Response |
locals | Per-request data shared from middleware |
site | Your configured site URL from astro.config.mjs |
Static vs. server endpoints
Endpoints behave differently depending on your output mode. In the default static build, only GET handlers run, and they execute once at build time to produce a file on disk. In server (SSR) mode, handlers run on every request and all HTTP methods are available.
| Mode | When handlers run | Methods | Use case |
|---|---|---|---|
Static (output: 'static') | At build time | GET only | Pre-generated JSON, sitemaps, RSS |
Server (output: 'server') | Per request | All methods | Form submissions, auth, live data |
To enable server endpoints, add an adapter and set the output mode:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
});
Returning files and other content types
Because you control the full Response, an endpoint can emit any media type. Set the Content-Type header and return the appropriate body — a string, a Uint8Array, or a ReadableStream.
// src/pages/robots.txt.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = ({ site }) => {
const body = `User-agent: *
Allow: /
Sitemap: ${new URL('sitemap-index.xml', site).href}`;
return new Response(body, {
headers: { 'Content-Type': 'text/plain' },
});
};
Dynamic routes and getStaticPaths
Endpoints support dynamic params with bracket filenames. In static mode you must also export getStaticPaths() to tell Astro which paths to pre-build, just as you would for a page.
// src/pages/products/[id].json.ts
import type { APIRoute, GetStaticPaths } from 'astro';
const products = [
{ id: '1', name: 'Keyboard' },
{ id: '2', name: 'Mouse' },
];
export const getStaticPaths: GetStaticPaths = () =>
products.map((p) => ({ params: { id: p.id } }));
export const GET: APIRoute = ({ params }) => {
const product = products.find((p) => p.id === params.id);
if (!product) {
return new Response('Not found', { status: 404 });
}
return new Response(JSON.stringify(product), {
headers: { 'Content-Type': 'application/json' },
});
};
In static mode, requesting an
idnot returned bygetStaticPaths()yields a 404 because no file was generated. In server mode, the handler runs for anyidand you do the lookup at request time.
Best practices
- Always set an explicit
Content-Typeheader; browsers and fetch clients depend on it to parse the body correctly. - Use the
APIRouteandGetStaticPathstypes fromastroso TypeScript validates your handler signatures. - Return meaningful HTTP status codes (
201for creates,400for bad input,404for missing resources) rather than defaulting everything to200. - Keep secrets and database access in server endpoints — never inline credentials into client-rendered pages or static output.
- Name data files with a clear secondary extension (
.json.ts,.xml.ts) so the output URL and its content type are self-documenting. - Validate and parse incoming request bodies before trusting them, especially for
POST/PUThandlers in server mode.