Heximon Logo
Compile-time framework for TypeScript backends

Backends, compiled.

Heximon reads your plain TypeScript classes and wires the whole application before it boots — routing, dependency injection, validation, typed clients. No decorators. No runtime container. Nothing left to guess at runtime.

0 decorators0 runtime reflection100% readable output
orders.module.tsinput
export class OrdersModule extends Module({  providers: [OrdersService, OrdersRepository],  http: { controllers: [OrdersController] },  events: { handlers: [OrderPlacedHandler] },}) {}
Vite build
GET /orders
OrdersController
Controller<"/orders">
OrdersService
Provider
OrdersRepository
Provider
OrderPlacedHandler
EventHandler
.heximon/OrdersModule.wiring.js emitted plain new calls — no container boots at runtime

No magic — provably

Read what it writes.

Every framework claims "no magic". Heximon lets you cat the proof: the compiler emits plain JavaScript into .heximon/ — constructors called in dependency order, cross-module dependencies passed in as plain arguments, nothing else. Read it in review. Diff it. Set a breakpoint in it.
orders.module.tsyou write
import { Module } from "@heximon/runtime";

export class OrdersModule extends Module({
  imports: [PersistenceModule],
  providers: [
    OrdersService,
    OrdersRepository,
    PaymentGateway,
  ],
  http: { controllers: [OrdersController] },
  events: { handlers: [OrderPlacedHandler] },
}) {}
orders.service.tsyou write
export class OrdersService {
  // The constructor is the dependency declaration.
  constructor(
    private readonly orders: OrdersRepository,
    private readonly payments: PaymentGateway,
  ) {}
}
.heximon/modules/OrdersModule.wiring.jsthe compiler emits
// GENERATED by Heximon — do not edit.
import { PaymentGateway } from "../src/payment.gateway.js";
import { OrdersRepository } from "../src/orders.repository.js";
import { OrdersService } from "../src/orders.service.js";
import { OrdersController } from "../src/orders.controller.js";
import { OrderPlacedHandler } from "../src/order-placed.handler.js";

export async function initOrdersModule(inputs, disposables) {  const PaymentGateway_ = new PaymentGateway();  const OrdersRepository_ = new OrdersRepository(inputs.Database);  const OrdersService_ = new OrdersService(OrdersRepository_, PaymentGateway_);  const OrdersController_ = new OrdersController(OrdersService_);  const OrderPlacedHandler_ = new OrderPlacedHandler(OrdersService_);  return {
    PaymentGateway: PaymentGateway_,
    OrdersRepository: OrdersRepository_,
    OrdersService: OrdersService_,
    OrdersController: OrdersController_,
    OrderPlacedHandler: OrderPlacedHandler_,
  };
}

Hover a class to trace it from source to emitted wiring.

inputs.Database comes from the imported module.

Wiring mistakes are build errors

A missing provider or an unsatisfied import is a named diagnostic on the exact module line — at compile time, not a 500 at 3 a.m.

Boot is just function calls

Construction order was solved at build time, so startup is executing a handful of new expressions — re-boot from the memoized app is measured under 220 ns.

Runs where reflection can’t

The output contains no eval and no runtime scanning, so the same app deploys to JIT-less edge runtimes like Cloudflare Workers unchanged.

Measured, not promised

A framework you can weigh.

No runtime container means the costs are small enough to publish. These are the in-repo benchmark baselines — methodology, hardware, and caveats included.

0 KB

gzipped HTTP floor on Cloudflare Workers

~47 KB for the Node build

+0 ms

cold-start overhead on Workers

54.6 ms empty worker → 60.0 ms full app

up to 0k

requests/s on one core

40 modules · 400 providers · 440 routes

<0 ns

re-boot from the memoized app

boot wiring is precomputed at build time

Measured in-repo on the benchmark fixtures — read the baselines.

Fails loud

Broken wiring never boots.

The same mistake is caught twice: in your editor as you type, and by the compiler as a named diagnostic with a codeframe. Here, WelcomeEmailSender injects a MailerService its module never provides — production sees neither error.

Errors as you type

Module(...)'s generic typing runs entirely in your editor — a constructor dependency nothing visible provides is flagged on the exact providers entry, before any build runs.

Plain TypeScript, no plugin

It's just types: any editor with a TypeScript language server shows it, and tsc/tsgo in CI sees exactly the same error.

What it catches

Invisible constructor dependencies, exports nothing backs, an imported module's unmet requires, a missing eager: true, a misspelled config key.

notifications.module.ts1 problem
1import { Module } from "@heximon/runtime";
2import { WelcomeEmailSender } from "./welcome-email.sender";
3
4export class NotificationsModule extends Module({
5  providers: [WelcomeEmailSender],
6}) {}

Property '"ERROR — constructor dependency not provided in this module"' is missing in type 'typeof WelcomeEmailSender'.

The required type names the missing token: MailerService.

Real output — the message this repo's type-diagnostics test asserts against a live tsgo run.

Capabilities

Growth is one line at a time.

The config file is the app's capability manifest — the entire HTTP layer is one plugin in a list. Events, CQRS, queues, and realtime are one more line each. Toggle them and watch what they actually cost.

Worker bundle

~26.0 KBgzip

26 KB floor~48.2 KB everything on

Events through Health are the measured ~14 KB gzip stack on the Workers build (per-package split approximate); Durable Objects, Cron, Sagas, and Workflows are in-repo estimates (~). Opt-in weight, not baseline weight.

heximon.config.ts1 plugin
import { defineHeximonConfig } from "@heximon/build";import { HttpPlugin } from "@heximon/http/compiler";export default defineHeximonConfig({  plugins: [    new HttpPlugin(),  ],});

Toggle a capability — the config is the whole registration step.

Contracts

Declare once. Typed everywhere.

A route is declared in one place. The compiler holds your handlers to it, the client infers its calls from it, and OpenAPI docs and MCP tools are generated from the same value — drift is a compile error, not an incident.
orders.api.tsthe one declaration
import { Contract, Route } from "@heximon/contract";

export class OrdersApi extends Contract({
  prefix: "/api/orders",
  routes: {    list: Route.get("/").responses({ 200: orderList }),    create: Route.post("/")
      .body(createOrder)
      .responses({ 201: order }),  },
}) {}

Hover a route to follow it into every consumer.

orders.controller.tschecked by the compiler
export class OrdersController implements Controller<OrdersApi> {  public async list(action: Action<OrdersApi, "list">) {
    return action.json(await this.orders.list());
  }  public async create(action: Action<OrdersApi, "create">) {
    // invalid body → 400 problem+json, before your code runs
    const body = await action.request.readValidatedBody();
    return action.respond(201, await this.orders.create(body));
  }}
anywhere.ts — tests, another service, the browserinferred client
const api = new OrdersApi(ClientTransport.fetch({ baseUrl }));
const created = await api.client.create({
  body: { sku: "HEX-01", qty: 2 },
});// typed from the 201 schema — non-2xx throws ContractError

Typed REST client

the Contract class is the client

Typed WS client

WsClient over a WebSocketContract

Typed SSE client

SseClient over a ServerSentEvents class

OpenAPI docs

generated from the same value

MCP server

tools 1:1 from the route table

Delete the create handler and the controller class fails to compile. Rename a field and every drifted call site turns red — the handler, the REST call, the WS send, the SSE subscription — you fix them in the editor, not in production.

Realtime

Realtime, compiled in.

WebSockets, Server-Sent Events, and Durable Objects are transports, not side quests — declared with the same class-and-type-argument move as everything else, validated by the same schemas, and hosted in-process on Node or on hibernating Durable Objects at the edge.

One contract, both directions

clientToServer and serverToClient schema maps validate every frame both ways — an unknown kind or invalid payload never reaches your handler.

A method per message kind

Bind one handler method per inbound kind; the compiler enforces exhaustiveness at build time, so a new message kind is a build error until you handle it.

Hibernation-proof on the edge

On Cloudflare the connection rides a keyed Durable Object — path params and connection data survive a hibernation wake, byte for byte.

seat-map.contract.tsshared with the frontend
export class SeatMapContract extends WebSocketContract({
  path: "/ws/events/:eventId/seats",
  clientToServer: {
    "seat.hold": SeatSelectionSchema,
    "seat.release": SeatSelectionSchema,
  },
  serverToClient: {
    "seat.map": SeatMapSchema,
    "seat.update": SeatUpdateSchema,
  },
}) {}
browser.tstyped WS client
const conn = new SeatMapContract(
  WsTransport.native({ baseUrl: "wss://api.example.com" }),
).connect({ eventId: "evt-42" });

conn.on("seat.update", (data) => {
  // data is typed: { seat, state: "held" | "released", … }
});
conn.send("seat.hold", { seat: 14 }); // validated before it leaves

Same handler code, two hosts.
In-process broadcast on a Node server; keyed, hibernating Durable Objects on Cloudflare — switching is a deploy target, not a rewrite.

Realtime guide

Get started

Five files from now, it's running.

One command scaffolds a working app — a module, a controller, a provider, and the config that wires them. It stays that small until your domain earns more.
terminal
MIT licensedbuilt on the unjs ecosystemships as a Nuxt module
Copyright © 2026