API Routes
Standalone server endpoints that live alongside your pages. Export named HTTP method handlers or one default handler, then return Response objects directly.
File Convention
API routes live in src/api/. The file path maps to the URL:
| File | URL |
|---|---|
src/api/health.ts |
/api/health |
src/api/users.ts |
/api/users |
src/api/users/[id].ts |
/api/users/:id |
Method Handlers
Export named functions for each HTTP method you want to handle. Unhandled methods return 405.
import type { ApiRouteArgs } from "@pracht/core";
export function GET({ request }: ApiRouteArgs) {
return Response.json([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
]);
}
export async function POST({ request }: ApiRouteArgs) {
const body = await request.json();
// Create user...
return Response.json({ id: 3, ...body }, { status: 201 });
}You can also export one default handler and branch on request.method yourself:
import type { ApiRouteArgs } from "@pracht/core";
export default async function handler({ request }: ApiRouteArgs) {
if (request.method === "GET") {
return Response.json([{ id: 1, name: "Alice" }]);
}
if (request.method === "POST") {
const body = await request.json();
return Response.json({ id: 2, ...body }, { status: 201 });
}
return new Response("Method not allowed", { status: 405 });
}API Middleware
API routes can have their own middleware chain, separate from page middleware. Configure it in defineApp:
export const app = defineApp({
// Page routes...
api: {
middleware: ["rateLimit"],
},
});API middleware runs before the handler, just like page middleware runs before loaders.
Middleware Without a Manifest (Higher-Order Functions)
When using the pages router or any setup without a routes.ts manifest, you can apply middleware to individual API routes with a plain higher-order function — no framework API required:
import type { ApiRouteArgs, ApiRouteHandler } from "@pracht/core";
export function withAuth(handler: ApiRouteHandler): ApiRouteHandler {
return async (args: ApiRouteArgs) => {
const session = args.request.headers.get("cookie")?.includes("session=");
if (!session) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
return handler(args);
};
}Then wrap any handler export:
import { withAuth } from "../lib/with-auth";
export const GET = withAuth(({ request }) => {
return Response.json({ user: "Alice" });
});You can compose multiple wrappers for stacking:
import { withAuth } from "../lib/with-auth";
import { withRateLimit } from "../lib/with-rate-limit";
export const POST = withAuth(withRateLimit(async ({ request }) => {
const body = await request.json();
return Response.json({ ok: true });
}));This pattern works with both the pages router and the manifest router — it's just JavaScript.
Full Control
API handlers receive the same LoaderArgs context (request, params, context, signal) and return standard Response objects. You have full control over status codes, headers, and body format.
export function GET() {
return new Response("plain text", {
status: 200,
headers: { "content-type": "text/plain" },
});
}