API Routes
createApiHandler() ports route.ts handlers: the same path syntax as page routes, one handler per HTTP method, on web-standard Request/Response. It runs anywhere those classes exist — Node 18+, Bun, Deno, edge runtimes.
Defining Handlers
import { createApiHandler, json, notFound } from "@domphy/app"
const handler = createApiHandler([
{
path: "/api/users",
GET: () => json(listUsers()),
POST: async (request) => {
const body = await request.json()
return json(createUser(body), { status: 201 })
},
},
{
path: "/api/users/[id]",
GET: (_request, { params }) => {
const user = findUser(params.id as string)
if (!user) notFound()
return json(user)
},
DELETE: (_request, { params }) => {
removeUser(params.id as string)
return new Response(null, { status: 204 })
},
},
])
// handler: (request: Request) => Promise<Response>
Built-In Behavior
- 404 for unmatched paths, 405 with an
Allowheader for unsupported methods - HEAD falls back to
GETwith the body stripped - OPTIONS answers automatically with the allowed methods
redirect()thrown inside a handler becomes a307/308response,notFound()a404, other errors a500json(data, init?)is theNextResponse.json()equivalent
Serving from Node
import http from "node:http"
http.createServer(async (request, response) => {
const webRequest = new Request(`http://localhost${request.url}`, {
method: request.method,
headers: request.headers as HeadersInit,
body: ["GET", "HEAD"].includes(request.method!) ? undefined : request,
duplex: "half",
} as RequestInit)
const webResponse = await handler(webRequest)
response.writeHead(webResponse.status, Object.fromEntries(webResponse.headers))
response.end(Buffer.from(await webResponse.arrayBuffer()))
}).listen(3000)
Combine with renderToString in one server: route /api/* to the API handler, everything else to page rendering.