Nitro
Host the same Heximon modules inside Nitro and you can deploy them to Node, Cloudflare Workers, Deno, Bun, or AWS Lambda by changing one preset — nothing in your app code changes. Nitro owns the platform (route rules, caching, headers, the live server); Heximon owns the app (the DI graph, validation, error filters). You wire the two together with a single config entry and never write a compile step.
Install the package
pnpm add @heximon/nitro
npm install @heximon/nitro
yarn add @heximon/nitro
bun add @heximon/nitro
nitro is an optional peer dependency — you install it as part of your Nitro app, not through Heximon (the
workspace pins nitro@3.0.260603-beta). The Heximon package only pulls Nitro in when you actually build against
it, so importing the runtime client or the compiler plugin elsewhere costs you nothing. The dev loop below needs
one more dev dependency, vite-plus — the same Vite-based toolchain the Vite host uses.
Register the host module and its dev loop
List the bare heximonNitroModule in Nitro's modules. It takes no options — there is nothing to pass,
because Heximon config lives entirely in heximon.config.ts at the project root, the single
source of truth every host reads identically. Add heximonNitroVitePlugin() in a vite.config.ts next to it,
and nitro dev gains the same dev experience the Vite host gives — a build-error page, a JSON-preview page,
recompile on every source change, and browser auto-reload. All three files are the recommended setup from day
one: a bare nitro dev (module only, no Vite plugin) still serves your app, but without the plugin there is no
dev loop — no recompile-on-change, no build-error page, no auto-reload on a source edit:
import { defineNitroConfig } from "nitro/config";
import { heximonNitroModule } from "@heximon/nitro/module";
export default defineNitroConfig({
compatibilityDate: "2025-07-04",
serverDir: "server",
modules: [heximonNitroModule],
});
import { defineHeximonConfig } from "@heximon/build";
import { HttpPlugin } from "@heximon/http/compiler";
export default defineHeximonConfig({ plugins: [new HttpPlugin()] });
import { heximonNitroVitePlugin } from "@heximon/nitro/vite";
import { nitro } from "nitro/vite";
import { defineConfig } from "vite-plus";
export default defineConfig({ plugins: [nitro(), heximonNitroVitePlugin()] });
That is the whole integration. When you run nitro dev or nitro build, heximonNitroModule loads
heximon.config.ts, runs Heximon's compiler over whatever serverDir Nitro is configured with, and mounts the generated
routes onto Nitro's server at startup — that part runs with or without the Vite plugin, so a bare nitro dev
still serves your app. What the plugin adds is the loop on top: without it, a broken compile serves a stale
response instead of the diagnostic page, and a source change needs a manual restart to pick up. You never run
a compiler yourself, and there is no entry file or alias to maintain either way.
nitro build is unaffected by either.heximon.config.ts is deliberately the same file every host reads — so moving a project from the dev server
to a Nitro deployment is a config swap, not a rewrite. The one difference is the scan root: under Nitro there is
no src/ default, because Nitro already owns the serverDir convention. Your Heximon code lives wherever Nitro
looks for server code.
Plug in the tiers you use
heximon.config.ts's plugins array is the compiler plugin set, identical to the one every other host reads. The
example above is HTTP-only, so it lists just new HttpPlugin(). Add a plugin per tier you want compiled under
Nitro.
import { CqrsPlugin } from "@heximon/cqrs/compiler";
import { EventEmitterPlugin } from "@heximon/events/compiler";
import { HttpPlugin } from "@heximon/http/compiler";
import { defineHeximonConfig } from "@heximon/build";
export default defineHeximonConfig({
plugins: [new HttpPlugin(), new EventEmitterPlugin(), new CqrsPlugin()],
});
You never list a Nitro plugin yourself — heximonNitroModule requires HTTP and adds itself, so a user-listed
HttpPlugin collapses with the one Nitro pulls in. List only the extra tiers your app uses; the auto-compile
honors the whole set.
Write a host-agnostic controller
Nothing in your app code knows it's running under Nitro. Routes are declared by each handler's action parameter
type, and CounterService is injected from the constructor signature — the same controller runs unchanged under
the Vite dev server or inside Nitro's request pipeline.
import type { Controller, Get, Post } from "@heximon/http";
import { CounterService } from "./counter.service";
export class CounterController implements Controller<"/"> {
constructor(private readonly counter: CounterService) {}
public async index(_action: Get<"/">): Promise<{ message: string; hints: string[] }> {
return {
message: "Heximon is running inside Nitro.",
hints: ['POST /track with {"page": "/home"}', "GET /stats"],
};
}
public async track(action: Post<"/track">): Promise<{ page: string; views: number }> {
const { page } = (await action.request.json()) as { page: string };
return { page, views: this.counter.increment(page) };
}
public async stats(_action: Get<"/stats">): Promise<Record<string, number>> {
return this.counter.snapshot();
}
}
The module declares the controller under the http namespace and the service in providers — exactly as it
would for any Heximon host. There is no host-specific registration:
import { Module } from "@heximon/runtime";
import { CounterController } from "./counter.controller";
import { CounterService } from "./counter.service";
export class CounterModule extends Module({
providers: [CounterService],
http: { controllers: [CounterController] },
exports: [CounterService],
}) {}
The root module composes feature modules with imports, and Nitro compiles that graph as its build step:
import { Module } from "@heximon/runtime";
import { CounterModule } from "./counter/counter.module";
export class AppModule extends Module({
imports: [CounterModule],
}) {}
The base path is owned by your app code, not the host. A Controller<"/"> serves at the root; a
Controller<"/api"> mounts every route under /api. There is no Nitro mount prefix to configure — routes land
at exactly the paths your controllers declare, so Nitro's per-route rules and OpenAPI scan see each one.
Deploy anywhere Nitro deploys
The same server/ directory targets every Nitro runtime; you pick the runtime by switching the preset at build
time. The compiler emits one createApp plus a route table, and Nitro bundles that for whichever target you
choose.
| Preset | Runtime |
|---|---|
node-server | Long-running Node.js process |
cloudflare_module | Cloudflare Workers / Pages |
deno-server | Deno |
bun | Bun |
aws-lambda | AWS Lambda |
A vendor preset pulls its Heximon platform package on demand — they are optional peer dependencies, so you
install only the one matching your target: @heximon/cloudflare for a cloudflare* preset, @heximon/aws for
aws-lambda, @heximon/vercel for vercel/vercel-edge, @heximon/netlify for netlify/netlify-edge,
@heximon/deno for deno-deploy. A plain Node-class preset (node-server, bun, deno-server, a generic
*-edge) needs none. Build against a vendor preset without its package installed and the build fails loud with
an install hint, rather than dragging every vendor's toolchain into a single-target app.
CI smoke-tests a real nitro build + boot for every preset above (the nitro-preset-smoke job builds
the nitro-presets gap example under each and asserts it
serves): node-server / bun / deno-server boot and answer GET /, aws-lambda's handler is invoked with a
synthetic event, and cloudflare_module runs the emitted Worker on real workerd (miniflare). For a
Cloudflare deployment you can also use @heximon/cloudflare (host-independent
wrangler.json emit, the flagship Cloudflare app)
with a @heximon/build bundle.
Because the wiring is compiled to plain JavaScript with no eval and no JIT, the same build runs on the edge
runtimes that reject reflection-based frameworks — Heximon never needs to inspect a decorator at boot.
The module also reads your preset to set the compiler's build target, so target-aware codegen matches the
runtime you deploy to. A V8-isolate / fetch-only preset (cloudflare-*, *-edge, deno-deploy) compiles for
edge; every long-lived-process preset (node-server, aws-lambda, vercel, netlify, bun, deno-server,
…) compiles for node. The most visible effect is OpenTelemetry: on an edge preset @heximon/otel bakes its
lean Workers tracer leg with a per-request ctx.waitUntil flush instead of the heavier Node SDK — no config
change, just the preset you already pick. The same mapping applies in nitro dev, so the dev compile matches the
deploy runtime.
The Nitro module maps Vercel's and Netlify's presets to the corresponding Heximon strategies automatically: the
vercel preset resolves to VercelFunctionsStrategy, and the netlify preset resolves to
NetlifyFunctionsStrategy. See Deploy overview for the
standalone build path (skipping Nitro) that writes the complete vercel deploy --prebuilt / netlify deploy
output tree directly.
Call your own routes in-process
When a provider injects InternalClient, the Nitro host resolves it to a NitroFetchClient that wraps Nitro's
own fetch — so a service calling another module's endpoint dispatches in-process, with no network hop out and
back. You never wire it: the host supplies it through createApp automatically.
This works for one reason that runs through all of Heximon: class identity is the only DI token. Because
NitroFetchClient extends H3FetchClient extends InternalClient, any constructor that asks for InternalClient
gets the host's in-process dispatch — no token, no registration, no adapter.
Race-free mount and readiness
The Nitro module solves a cold-start problem common in async frameworks: routes are bound to h3
synchronously at plugin startup — before createApp resolves — so a request that arrives while the app is
still booting waits for the boot promise rather than falling through to Nitro's 404. The app is constructed
exactly once, regardless of how many requests race in before it resolves.
The boot promise is exposed through heximonBootPromise from @heximon/nitro/module, which returns the
single retained Promise<AppHandle> for a running Nitro app. A health route can gate on it to guarantee the
app is ready before reporting healthy:
import type { Controller, Get } from "@heximon/http";
import { heximonBootPromise } from "@heximon/nitro/module";
import { useNitroApp } from "nitro/app";
export class HealthController implements Controller<"/"> {
public async health(_action: Get<"/_health">): Promise<{ status: string }> {
await heximonBootPromise(useNitroApp());
return { status: "ok" };
}
}
heximonBootPromise returns undefined when heximon was never mounted onto the app (no module installed), so a
health probe can distinguish a healthy Heximon app from one that never started.
Graceful shutdown
When Nitro receives a shutdown signal (SIGTERM, nitro.close()), the module runs Heximon's fault-tolerant
app.shutdown() via Nitro's close runtime hook. Shutdown drains in-flight requests and tears down constructed
instances in reverse construction order — a pool's end(), a queue's drain, and so on. The shutdown is
AggregateError-collecting: a single teardown failure surfaces as a rejection without leaving the rest of the
graph half-torn. No configuration is needed; the hook is registered by the module automatically.
Run it
Nitro's own dev command drives everything — heximonNitroModule.setup compiles your server/ directory before
Nitro serves it:
nitro dev
curl http://localhost:3000/
# → { "message": "Heximon is running inside Nitro.", "hints": [...] }
curl -X POST http://localhost:3000/track -H "content-type: application/json" -d '{"page":"/home"}'
# → { "page": "/home", "views": 1 }
curl http://localhost:3000/stats
# → { "/home": 1 }
A production build is nitro build against your chosen preset, then run the bundled output the way that preset
expects (node .output/server/index.mjs for node-server). Same source, same routes — only the bundle target
differs.
Because vite.config.ts registered heximonNitroVitePlugin() back when you set up the host, that same
nitro dev already carries the dev loop: edit anything under server/ and the recompiled routes reload on
their own, a broken compile shows the diagnostic codeframe in the browser (and a plain 500 to curl/a JSON
client) instead of a stale response, and a browser navigation to a JSON route renders the syntax-highlighted
preview page.
See also
- Nuxt — serve the same Heximon routes alongside Vue pages on one Nitro server, with SSR cookie forwarding.
- Controllers — the two controller modes, middleware, and typed responses the Nitro host serves unchanged.
- Durable Objects — run Cloudflare stateful actors under compile-time DI once Nitro is your host.
- WebSockets — layer a real-time transport onto the Nitro host's
fetchserver. - The nitro-presets gap example — the
full integration end to end: a bare
heximonNitroModule, a host-agnostic counter app, and a test that drives the auto-compile and serves real requests through Nitro's server.
Deno Deploy
DenoDeployStrategy, dist/deno-worker.js, Deno.cron registrations, deployctl deploy, platform limits.
Nuxt
Run a Heximon API inside a Nuxt 5 app on one Nitro server, with SSR cookie forwarding, a typed Contract client with TanStack Vue Query bindings, heximon.config.ts, HttpPlugin, and a server/ controller.