One core, many runtimes
createHandler returns a web-standard (request: Request) => Promise<Response>. Nothing about it is tied to Node, so the same app runs anywhere that speaks Request → Response.
Deno:
const handler = await createHandler({ routes: manifest });
Deno.serve(handler);
Bun:
const handler = await createHandler({ routes: manifest });
Bun.serve({ fetch: handler });
Cloudflare Workers (needs the nodejs_compat flag):
const handler = await createHandler({ routes: manifest });
export default { fetch: handler };
The handler includes the per-request essentials: a correlation id, the request logger, the Origin CSRF check (production only), and the error floor. It does not serve static files or run the dev tooling. Those belong to the host.
Node
On Node, mount the handler on Express with toExpress from the camelon/express subpath. Serve public/ yourself.
import express from 'express';
import { createHandler } from 'camelon';
import { toExpress } from 'camelon/express';
import { manifest } from './gen/manifest.generated';
const app = express();
app.use(express.static('public', { index: false }));
app.use(toExpress(await createHandler({ routes: manifest })));
app.listen(3000);
camelon/express also exports listen(app), a dev helper that finds a free port from 3333 (pin with PORT). Plain app.listen is all the framework needs.
Build-time manifest
A non-Node host has no filesystem route scan, so pass the build-time routes manifest from camelon build. With it there is no fs scan and no runtime dynamic import, and the app bundles cleanly.
Browser service worker
The core is just Request → Response, so you can run the whole framework inside a browser service worker. The worker intercepts the page's fetches and answers them with camelon — no server at all.
import { createHandler } from 'camelon';
import { manifest } from './gen/manifest.generated';
const handler = await createHandler({ routes: manifest, dev: true });
const sw = self as unknown as ServiceWorkerGlobalScope;
sw.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.origin === sw.location.origin) {
event.respondWith(handler(event.request));
}
});<!doctype html>
<title>booting…</title>
<script type="module">
// The only page the static host serves. Register the worker, then reload —
// after that camelon (in the worker) answers every request.
await navigator.serviceWorker.register('/sw.js', { type: 'module' });
await navigator.serviceWorker.ready;
location.reload();
</script>A bootstrap page registers the worker and reloads. After that, camelon controls the origin: /, your routes, everything. The worker bundle is built on Node (esbuild stubs the node:* builtins the graph touches, since the browser has none); the browser only runs it. See examples/browser-counter for the full setup.
This is a demonstration of how portable the core is, not a production target.