Skip to main content
An API route is a file under app/api/ (or pages/api/) that exports named HTTP method handlers. Handlers receive the standard Web Request and return a Response. The router style changes the file location and the handler’s argument shape. The request and response APIs are the same in both.

Prerequisites

  • A project created with veryfront init (see Create project).
  • The dev server running (veryfront dev) or a build target you can hit with HTTP.

Router module shapes

Use app/api/**/route.ts in the app router. Export named HTTP method handlers. Each handler receives the Request directly and receives route params in the second argument.
// app/api/hello/route.ts
export function GET() {
  return Response.json({ message: "Hello, world!" });
}
Use pages/api/** in the pages router. Export named HTTP method handlers or a default fallback handler. Each handler receives an APIContext as ctx; use ctx.request for the raw request, ctx.params for route params, ctx.query for query parameters, and ctx.json() or Response.json() to return JSON.
// pages/api/hello.ts
import type { APIContext } from "veryfront";

export function GET(ctx: APIContext) {
  return ctx.json({ message: "Hello, world!" });
}

Basic route

// app/api/hello/route.ts
export function GET() {
  return Response.json({ message: "Hello, world!" });
}
This creates GET /api/hello. Try it with the dev server running:
curl http://localhost:3000/api/hello
The response should be:
{ "message": "Hello, world!" }

HTTP methods

Export any standard HTTP method:
// app/api/users/route.ts
const users = [{ id: "user_123", name: "Ada Lovelace" }];

export async function GET() {
  return Response.json(users);
}

export async function POST(request: Request) {
  const body = await request.json();
  const user = { id: "user_456", ...body };
  return Response.json(user, { status: 201 });
}

export async function DELETE(request: Request) {
  const { id } = await request.json();
  if (!id) return Response.json({ error: "Missing id" }, { status: 400 });
  return new Response(null, { status: 204 });
}
The same pages router route uses ctx:
// pages/api/users.ts
import type { APIContext } from "veryfront";

const users = [{ id: "user_123", name: "Ada Lovelace" }];

export async function GET(ctx: APIContext) {
  return ctx.json(users);
}

export async function POST(ctx: APIContext) {
  const body = await ctx.request.json();
  const user = { id: "user_456", ...body };
  return ctx.json(user, { status: 201 });
}

export async function DELETE(ctx: APIContext) {
  const { id } = await ctx.request.json();
  if (!id) return ctx.json({ error: "Missing id" }, { status: 400 });
  return new Response(null, { status: 204 });
}

Dynamic parameters

Use brackets in the path, then read params from the route context:
// app/api/users/[id]/route.ts
export async function GET(
  _request: Request,
  { params }: { params: Record<string, string> },
) {
  const id = params.id;

  if (!id) {
    return Response.json({ error: "Not found" }, { status: 404 });
  }

  const user = { id, name: "Ada Lovelace" };
  return Response.json(user);
}
In the pages router, params are available on ctx.params:
// pages/api/users/[id].ts
import type { APIContext } from "veryfront";

export async function GET(ctx: APIContext) {
  const id = String(ctx.params.id ?? "");

  if (!id) {
    return ctx.json({ error: "Not found" }, { status: 404 });
  }

  const user = { id, name: "Ada Lovelace" };
  return ctx.json(user);
}

Request parsing

JSON body

export async function POST(request: Request) {
  const { name, email } = await request.json();
  // ...
}

Form data

export async function POST(request: Request) {
  const form = await request.formData();
  const file = form.get("avatar") as File;
  // ...
}

Query parameters

export async function GET(request: Request) {
  const url = new URL(request.url);
  const page = url.searchParams.get("page") ?? "1";
  // ...
}

Headers and cookies

export async function GET(request: Request) {
  const token = request.headers.get("authorization");
  const cookie = request.headers.get("cookie");
  // ...
}

Streaming responses

Return a ReadableStream for real-time data:
// app/api/stream/route.ts
export function GET() {
  const stream = new ReadableStream({
    start(controller) {
      let i = 0;
      const interval = setInterval(() => {
        controller.enqueue(new TextEncoder().encode(`data: ${i++}\n\n`));
        if (i > 10) {
          clearInterval(interval);
          controller.close();
        }
      }, 100);
    },
  });

  return new Response(stream, {
    headers: { "content-type": "text/event-stream" },
  });
}

Chat endpoint

The most common API route pattern in Veryfront connects a chat UI to an agent:
// app/api/ag-ui/route.ts
import { createAgUiHandler } from "veryfront/agent";

export const POST = createAgUiHandler("assistant");
Messages use Veryfront’s parts-based chat message format with id, role, and parts fields. The route responds with AG-UI SSE and pairs with useChat configured for /api/ag-ui on the client. See the Chat UI guide.

Verify it worked

Call each handler with curl from another terminal while veryfront dev is running:
curl -i http://localhost:3000/api/hello
curl -i -X POST http://localhost:3000/api/echo \
  -H "Content-Type: application/json" \
  -d '{"hello":"world"}'
A working handler returns the expected status code and a JSON or streamed body. A 404 means the file path does not match the URL; a 405 means the HTTP method handler is not exported.