[]previous and next page

Route handlers to API routes

app/api/**/route.ts becomes a file under routes/api with method exports and no default export.

For agentsUpdated

app/api/**/route.ts becomes a file under routes/api with method exports and no default export. The handler receives the request event instead of NextRequest, and returns a Response, or any value: an object becomes JSON, a string becomes text, null becomes 204.

Next.js route handler Dusk API route
export async function GET(request, { params }) export const GET = (event) => ...; params in event.params (always an object, no await)
NextResponse.json(data, { status }) json(data, { status }) from @shadow-js/dusk/server, or Response.json
request.json() await readJson(event.request): checks the content type, rejects __proto__ keys and nesting deeper than 64
request.nextUrl.searchParams event.url.searchParams
cookies().get("x") event.cookies.get("x"); event.cookies.set("x", value, options) queues Set-Cookie with HttpOnly; Secure; SameSite=Lax by default
headers() event.request.headers; response headers on the Response or event.responseHeaders
export const dynamic = "force-dynamic" Not needed: responses are cache-control: private, no-store unless you set one
export const runtime = "edge" None. The handler runs on Node
OPTIONS handled by Next HEAD is derived from GET; OPTIONS answers 204 with Allow unless you export it
CSRF Checked for every state-changing method before your code runs; export const csrf = false opts a webhook out
jsx
// app/api/posts/[id]/route.ts (Next.js)
export async function GET(request, { params }) {
  const { id } = await params;
  const post = await db.post(id);
  if (!post) return NextResponse.json({ error: "Not found" }, { status: 404 });
  return NextResponse.json(post);
}
src/routes/api/posts/[id].tsts
import { apiRoute, HttpError, json, readJson } from "@shadow-js/dusk/server";
import { getPost } from "../../../server/blog";

export const { GET, POST } = apiRoute({
  GET: async (event) => {
    const post = await getPost({ id: event.params["id"] ?? "" });
    if (post === undefined) throw new HttpError(404, "Not found");
    return json(post);
  },
  POST: async (event) => json({ received: await readJson(event.request) }, { status: 201 }),
});

An error thrown from an API route becomes a JSON body { "error": { name, message, status, requestId } }: an HttpError shows its message for 4xx, anything else is Internal Server Error in production.