Route handlers to API routes
app/api/**/route.ts becomes a file under routes/api with method exports and no default export.
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.