Astro Server Endpoints
Build REST API routes, webhooks, and form handlers inside your Astro project using
.tsendpoint files and the middleware API.
When to Use
- You need a GET or POST handler in the same project as your Astro pages (form submissions, data APIs, webhooks)
- You want to keep backend logic co-located with your frontend without spinning up a separate server
- You are building an authenticated route that reads cookies or headers on each request
- You need middleware to run before every request (auth checks, logging, locale detection)
- You are in SSR or hybrid mode and need dynamic data endpoints
Instructions
Create endpoint files in
src/pages/with a.tsor.jsextension. The file path maps to the URL, same as page routing:src/pages/api/hello.ts→GET /api/hellosrc/pages/api/posts/[id].ts→GET/POST /api/posts/:id
Export named functions for each HTTP method you want to handle. Function names must be uppercase:
// src/pages/api/posts.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ request, url, cookies, locals }) => {
const tag = url.searchParams.get('tag');
const posts = await fetchPosts(tag);
return new Response(JSON.stringify(posts), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
};
export const POST: APIRoute = async ({ request }) => {
const body = await request.json();
if (!body.title) {
return new Response(JSON.stringify({ error: 'title is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
const post = await createPost(body);
return new Response(JSON.stringify(post), { status: 201 });
};
- Use
context.requestto read the rawRequestobject. Parse the body based on content type:
// JSON body
const data = await request.json();
// Form data
const formData = await request.formData();
const email = formData.get('email') as string;
// Raw text
const text = await request.text();
// Request headers
const auth = request.headers.get('authorization');
- Read and set cookies via
context.cookies:
export const GET: APIRoute = async ({ cookies }) => {
const token = cookies.get('session')?.value;
if (!token) return new Response(null, { status: 401 });
cookies.set('last-visit', new Date().toISOString(), {
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 24 * 7, // 1 week
path: '/',
});
return new Response('OK');
};
- Create middleware at
src/middleware.tsto run logic before every request. UsedefineMiddlewareand callnext()to continue the chain:
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const (context, next) => {
const token = context.cookies.get('session')?.value;
context.locals.user = token ? await validateToken(token) : null;
// Block unauthenticated access to /dashboard
if (context.url.pathname.startsWith('/dashboard') && !context.locals.user) {
return context.redirect('/login');
}
return next(); // proceed to page/endpoint handler
});
- Chain multiple middleware functions with
sequence()fromastro:middleware:
import { defineMiddleware, sequence } from 'astro:middleware';
const auth = defineMiddleware(async (ctx, next) => {
/* ... */ return next();
});
const logging = defineMiddleware(async (ctx, next) => {
console.log(`${ctx.request.method} ${ctx.url.pathname}`);
return next();
});
export const auth);
- Add types for
localsto get IntelliSense in middleware and pages. Augment theApp.Localsinterface inenv.d.ts:
// src/env.d.ts
/// <reference types="astro/client" />
interface Locals {
user: { id: string; email: string } | null;
}
- In SSG mode, endpoints with
GEThandlers can generate static files (JSON, XML, RSS). ExportgetStaticPaths()from a dynamic endpoint file to generate multiple static output files.
Details
Astro endpoints use the standard Web Request/Response API — the same API available in Cloudflare Workers, Deno, and modern Node.js. This means your endpoint logic is portable across runtimes without an adapter-specific API.
SSG vs. SSR endpoints:
In SSG mode, only GET handlers are useful. The build calls each GET handler and writes the response body to a static file. A src/pages/feed.xml.ts with export const GET produces a static dist/feed.xml.
In SSR mode, all HTTP methods work and endpoints are invoked on every request. This is where POST, PUT, DELETE, and PATCH handlers are useful.
prerender per endpoint:
In output: 'hybrid' mode, endpoints are server-rendered by default. Add export const prerender = true to opt a specific endpoint into static generation. In output: 'server' mode, endpoints are server-rendered by default; add export const prerender = true to force static output.
Error handling:
Always return a Response — never throw from an endpoint. If you need to return an error, construct a Response with the appropriate status code. Unhandled throws will produce a 500 with an Astro error page.
locals — the middleware/page contract:
context.locals is a mutable plain object scoped to the current request. Set values in middleware, read them in pages and endpoints. This is the correct way to pass auth state, tenant context, or feature flags from middleware to handlers.
Rate limiting and edge cases:
Astro has no built-in rate limiting. Implement rate limiting in middleware using a memory store (for single-instance) or an external store (Redis, KV for edge). Check context.clientAddress for the caller's IP — this is populated correctly when behind a reverse proxy if the adapter is configured for it.
Source
https://docs.astro.build/en/guides/endpoints
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.