API Routes

Create type-safe API endpoints with Axi

Creating an API Route

Create a route.ts file in the app/api/ directory:

// app/api/hello/route.ts import { route } from "@axi-js/core"; export const hello = route.get().handle(async (ctx) => { return { message: "Hello, World!" }; });

This creates a GET endpoint at /api/hello.

Note: API handlers must be created with the route.<method>().handle() builder. Axi discovers handlers by the HTTP method metadata attached by the builder, so plain functions like export function GET() are not registered.

HTTP Methods

Use method builders for different HTTP methods:

// app/api/users/route.ts import { route } from "@axi-js/core"; export const listUsers = route.get().handle(async (ctx) => { return { users: [] }; }); export const createUser = route.post().handle(async (ctx) => { const user = ctx.body; return { user, created: true }; }); export const deleteUser = route.delete().handle(async (ctx) => { return { deleted: true }; });

Available methods: .get(), .post(), .put(), .patch(), .delete()

Request Context

The handler receives a context object with useful properties:

export const getUser = route.get().handle(async (ctx) => { // Access request properties const { params, query, body, headers, url, method } = ctx; // Return JSON response return ctx.json({ message: "Hello" }); });

Dynamic Routes

Use parameters in your API routes:

// app/api/users/[id]/route.ts import { route } from "@axi-js/core"; export const getUser = route.get().handle(async (ctx) => { const userId = ctx.params.id; return { userId, name: "John Doe" }; });

Catch-All Routes

Use [...param] to match one or more path segments:

// app/api/files/[...path]/route.ts import { route } from "@axi-js/core"; import { z } from "zod"; const Params = z.object({ path: z.array(z.string()) }); export const getFile = route .get() .params(Params) .handle(({ params }) => ({ path: params.path, // string[] }));

[[...param]] matches zero or more segments. The generated client accepts an array for the catch-all param, e.g. api.files.getFile({ path: ["assets", "logo.png"] }).

Validation

Add validation with Zod schemas:

import { route } from "@axi-js/core"; import { z } from "zod"; const UserSchema = z.object({ name: z.string(), email: z.string().email(), }); export const createUser = route .post() .body(UserSchema) .handle(async (ctx) => { // ctx.body is typed and validated return { user: ctx.body }; });

Middleware

Add middleware to routes:

import { route, defineMiddleware } from "@axi-js/core"; const auth = defineMiddleware(async (ctx) => { const token = ctx.headers.get("authorization"); if (!token) { return new Response("Unauthorized", { status: 401 }); } return { user: { id: "123" } }; }); export const getProfile = route .get() .use(auth) .handle(async (ctx) => { // ctx.user is available from middleware return { user: ctx.user }; });

Error Handling

Errors are automatically handled:

import { route, error } from "@axi-js/core"; export const getUser = route.get().handle(async (ctx) => { if (!ctx.query.id) { return error("ID is required", 400); } return { id: ctx.query.id }; });

Response Headers

Set custom response headers:

export const download = route.get().handle(async (ctx) => { return new Response(JSON.stringify({ data: "hello" }), { headers: { "Content-Type": "application/json", "X-Custom-Header": "value", }, }); });

OpenAPI Documentation

Add metadata for auto-generated API docs:

export const createUser = route .post() .meta({ summary: "Create a new user", description: "Creates a user account", tags: ["users"], }) .body(UserSchema) .handle(async (ctx) => { return { user: ctx.body }; });

See OpenAPI & Swagger for full documentation.

For streaming responses and Server-Sent Events, see the Streaming guide.