From harness-claude
Builds REST API routes, webhooks, and form handlers in Astro projects using .ts endpoint files in src/pages/ and middleware for auth, cookies, logging. For SSR/hybrid mode with co-located backend.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Build REST API routes, webhooks, and form handlers inside your Astro project using `.ts` endpoint files and the middleware API.
Guides Astro file-based routing via src/pages/ for static, dynamic ([slug]), and rest-parameter ([...slug]) routes. Covers getStaticPaths, pagination, redirects, 404s, and SSR changes.
Provides Astro framework patterns for islands architecture, content collections, rendering strategies (SSG, SSR, hybrid), view transitions, partial hydration, and Cloudflare deployment.
Provides expertise in Astro for islands architecture, selective hydration (client:load/idle/visible/media), content layer API, server islands, SSR adapters, view transitions, i18n, actions, and React/Vue/Svelte/Solid integrations.
Share bugs, ideas, or general feedback.
Build REST API routes, webhooks, and form handlers inside your Astro project using
.tsendpoint files and the middleware API.
Create endpoint files in src/pages/ with a .ts or .js extension. 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/:idExport 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 });
};
context.request to read the raw Request object. 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');
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');
};
src/middleware.ts to run logic before every request. Use defineMiddleware and call next() to continue the chain:// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (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
});
sequence() from astro: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 onRequest = sequence(logging, auth);
locals to get IntelliSense in middleware and pages. Augment the App.Locals interface in env.d.ts:// src/env.d.ts
/// <reference types="astro/client" />
interface Locals {
user: { id: string; email: string } | null;
}
GET handlers can generate static files (JSON, XML, RSS). Export getStaticPaths() from a dynamic endpoint file to generate multiple static output files.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.
https://docs.astro.build/en/guides/endpoints