Skip to content
C Codeloom
Astro

Astro Server Endpoints Tutorial

Build JSON APIs and dynamic responses directly in Astro using server endpoints. Learn the file conventions, request and response shapes, dynamic params, and how to mix endpoints with static and SSR pages.

·4 min read · By Codeloom
Intermediate 8 min read

What you'll learn

  • What an Astro server endpoint is and where it lives
  • How to handle GET, POST, and other methods
  • How dynamic route params flow into endpoints
  • How endpoints behave in static vs server output mode
  • Pitfalls around CORS, caching, and request bodies

Prerequisites

  • An Astro project at 4.x or later

What and Why

Astro started as a static site framework but it has grown into a full hybrid runtime. Server endpoints are the way you expose plain HTTP routes from your Astro project: JSON APIs, form handlers, webhook receivers, RSS feeds, image transformers. Anything that returns a non-HTML response or needs to read a request belongs in an endpoint.

The why is simplicity. Instead of running a separate backend for a contact form or a search index, you keep your routes inside the same project, deploy together, and share the same components and config.

Mental Model

An endpoint is a file in src/pages/ whose name does not end in .astro. It exports one async function per HTTP method: GET, POST, PUT, DELETE, PATCH, OPTIONS. Each function receives a context object containing request, params, url, cookies, and redirect, and returns a standard Response.

The file path maps to a URL the same way pages do. src/pages/api/hello.ts answers requests to /api/hello. Dynamic segments like src/pages/api/posts/[id].ts parse the URL into context.params.id.

In static output the endpoint runs at build time and writes a file. In server or hybrid output it runs per request.

Hands-on Example

Create src/pages/api/posts/[id].ts:

import type { APIRoute } from 'astro';

const db = new Map([
  ['1', { id: '1', title: 'Hello', body: 'World' }],
]);

export const GET: APIRoute = ({ params }) => {
  const post = db.get(params.id!);
  if (!post) {
    return new Response('Not found', { status: 404 });
  }
  return new Response(JSON.stringify(post), {
    headers: { 'content-type': 'application/json' },
  });
};

export const POST: APIRoute = async ({ request, params }) => {
  const body = await request.json();
  db.set(params.id!, { id: params.id!, ...body });
  return new Response(null, { status: 204 });
};
HTTP request: GET /api/posts/1
      |
      v
Astro router matches src/pages/api/posts/[id].ts
      |
      v
context = { params: { id: "1" }, request, url, cookies }
      |
      v
Exported GET(context) runs -> returns Response
      |
      v
Response sent to client (JSON, status 200)
Request lifecycle for an Astro endpoint

Call it from a page or another client:

const res = await fetch('/api/posts/1');
const post = await res.json();

For static builds, the function runs once during astro build per known param, and the result is written as a static file. Mark the file export const prerender = false to force per-request execution in hybrid mode.

Common Pitfalls

The first pitfall is returning a plain object. Endpoints must return a Response. Returning { status: 200, body: ... } does not work in current Astro versions.

The second pitfall is forgetting await request.json() on POST handlers. The body is a stream; reading it twice will throw.

The third is CORS. Endpoints do not add CORS headers by default. If a different origin will call them, return access-control-allow-origin and handle OPTIONS preflights explicitly.

The fourth is caching. In static mode the result is frozen at build time. If your data changes, you need server or hybrid output, and you should set cache-control headers thoughtfully.

Finally, dynamic params in static mode require getStaticPaths() so Astro knows which files to generate.

Best Practices

Keep endpoints small and route-shaped. If business logic grows, extract it into src/lib/ and let the endpoint stay a thin adapter.

Validate inputs at the boundary. Parse request.json() through Zod or Valibot and return 400 with a clear message on failure. Never trust params either; they are strings from the URL.

Set explicit content-type and cache-control headers. Defaults vary between adapters and you do not want to debug a stale CDN at 2 a.m.

Co-locate tests next to the endpoint and run them with the same Vitest setup you use for components. Endpoints are pure functions of their context, which makes them easy to test.

Wrap-up

Astro server endpoints turn your content site into a small full-stack app without extra infrastructure. Drop a .ts file in src/pages/, export a method, return a Response. Mind the output mode, validate inputs, and set headers explicitly, and you have a clean, deployable API alongside your pages.