Skip to content
Astro as endpoints 4 min read

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' },
  });
};
PropertyDescription
requestThe standard Request object (method, headers, body)
paramsDynamic route segments, e.g. { id: '42' } from [id].ts
urlA parsed URL instance, useful for query strings
cookiesAstro’s cookie helper for get / set / delete
redirectShortcut that returns a redirect Response
localsPer-request data shared from middleware
siteYour 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.

ModeWhen handlers runMethodsUse case
Static (output: 'static')At build timeGET onlyPre-generated JSON, sitemaps, RSS
Server (output: 'server')Per requestAll methodsForm 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 id not returned by getStaticPaths() yields a 404 because no file was generated. In server mode, the handler runs for any id and you do the lookup at request time.

Best practices

  • Always set an explicit Content-Type header; browsers and fetch clients depend on it to parse the body correctly.
  • Use the APIRoute and GetStaticPaths types from astro so TypeScript validates your handler signatures.
  • Return meaningful HTTP status codes (201 for creates, 400 for bad input, 404 for missing resources) rather than defaulting everything to 200.
  • 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/PUT handlers in server mode.
Last updated June 14, 2026
Was this helpful?