Concepts
API Routes
Serve a JSON API alongside your pages. Define endpoints with apiRoute and createApi — the same :param and :rest* matcher as page routes, dependency-free — or delegate the prefix to any router with a Web Standards fetch method.
Defining routes
apiRoute(method, path, handler)
creates one endpoint;
createApi(routes, options)
assembles them into a Fetch-compatible router. Handlers receive
{ request, env, context, url, params, query, signal }. A
returned Response passes through; any other value becomes
Response.json(value).
// site/api.js
import { apiRoute, createApi } from "@nativefragments/core/server";
export const api = createApi([
apiRoute("GET", "/api/todos", ({ query }) => listTodos(query.get("filter"))),
apiRoute("POST", "/api/todos", async ({ request }) =>
Response.json(await createTodo(await request.json()), { status: 201 }),
),
apiRoute("DELETE", "/api/todos/:id", ({ params }) => removeTodo(params.id)),
]);
API paths use the same matcher as pages: :id captures one
segment onto params, and a trailing :rest*
captures the remainder.
Mounting an API
Pass the api to
createCloudflareHandler.
Requests under apiPrefix (default /api) go to
the API; everything else renders pages. The adapter also accepts an
array of apiRoute() items directly and calls
createApi for you.
// worker.js
import { createCloudflareHandler } from "@nativefragments/core/cloudflare";
import { api } from "./site/api.js";
import { routes } from "./site/routes.js";
import { shell } from "./site/shell.js";
export default createCloudflareHandler({ routes, shell, api });
Dispatch semantics
createApi resolves each
request against the matched path:
- No path match returns JSON
404. - A path match with an unsupported method returns
405with anAllowheader listing the methods. HEADfalls back to theGEThandler when noHEADhandler exists.- A handler that throws calls
onErrorand returns JSON500without leaking the message.
export const api = createApi(routes, {
onError: ({ error, request, route }) =>
console.error(request.url, route?.path, error),
});
Any Web Standards router
You are not limited to createApi. api only
needs a fetch(request, env, context) method, so anything
that speaks the Workers fetch contract works — a Hono app, or a plain
object.
import { Hono } from "hono";
const app = new Hono();
app.get("/api/health", (c) => c.json({ ok: true }));
export default createCloudflareHandler({ routes, shell, api: app });
Changing the prefix
export default createCloudflareHandler({
routes,
shell,
api,
apiPrefix: "/rpc", // /rpc and /rpc/* now go to the API
});
Content Security Policy
The Cloudflare adapter passes a per-request nonce to the
shell and to framework streaming scripts. Keep the default compatible
policy, or opt into a strict nonce-based policy with
contentSecurityPolicy.
import { attrs, html, raw } from "@nativefragments/core/server";
export const shell = ({ body, meta, nonce }) => html`<!doctype html>
<html>
<head>
<title>${meta.title}</title>
<script${attrs({ nonce })}>
document.documentElement.classList.add("js");
</script>
</head>
<body>${body}</body>
</html>`;
export default createCloudflareHandler({
routes,
shell,
contentSecurityPolicy: ({ nonce }) =>
[
"default-src 'self'",
"base-uri 'none'",
"object-src 'none'",
"frame-ancestors 'none'",
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'nonce-${nonce}'`
].join("; ")
});
See also
- Routing — page routes, and
actionfor form mutations. - Workers — offload client-side work instead.
- Reference:
apiRoute,createApi,createCloudflareHandler.