Core Concepts
A whole Heximon app comes down to six terms. A module groups a feature. Providers are the plain classes inside it. Constructor injection wires those classes together. A controller turns HTTP requests into method calls.
A build step — the heximon() Vite plugin — reads all of that and generates the wiring before your
app runs. And one config line registers that plugin. This page walks each term once; every section
links to the page that goes deep.
Group a feature with a module
A module is a class that extends Module({...}). It draws the boundary of a feature — what it owns, what
it pulls in from other modules — and it's the only place the compiler looks to find your classes.
import { Module } from "@heximon/runtime";
import { UsersController } from "./users.controller";
import { UsersRepository } from "./users.repository";
export class UsersModule extends Module({
providers: [UsersRepository],
http: { controllers: [UsersController] },
exports: [UsersRepository],
}) {}
providers and exports are structural fields the kernel always understands; http is a plugin's
namespace for the concept classes it owns — here, a controller. That keeps registration explicit: a class
is wired because it's listed here, never because it was scanned off disk.
→ Modules & DI
Inject providers by their constructor
A provider is any class the graph can build. List it in providers and it's injectable — no decorator
needed. Its dependencies are its constructor parameters: declare one typed by the class you want,
and the compiler resolves it by identity and passes the instance in.
export class UsersController implements Controller<"/users"> {
// resolved by class identity — no token, no decorator
constructor(private readonly users: UsersRepository) {}
}
That's constructor injection: no token to declare, no string key to drift, just the type in the signature. → Modules & DI
Turn requests into method calls with a controller
A controller declares implements Controller<"/prefix"> and is listed under http: { controllers }.
Each route is declared by its handler's action parameter type — Get<"/:id"> is the route, so
there's no separate config to keep in sync with the method.
import type { Controller, Get } from "@heximon/http";
import { type User, UsersRepository } from "./users.repository";
export class UsersController implements Controller<"/users"> {
constructor(private readonly users: UsersRepository) {}
public async get(action: Get<"/:id">): Promise<User | undefined> {
return this.users.findById(action.request.pathParams.id); // pathParams.id typed from "/:id"
}
}
Let the build step write the wiring
The defining move: dependency injection happens at build time, not at runtime. The heximon() Vite
plugin reads your modules and constructors and emits plain JavaScript that constructs everything directly
(new UsersController(new UsersRepository())) into .heximon/.
There's no container, no reflection, and no entry file to write — the plugin generates the fetch boot,
and pnpm dev serves it. Because the compiler reads source rather than running it, your providers and
imports lists must be static (no computed or conditional imports) — and in exchange the wiring is
ordinary code you can open and read.
pnpm dev
# compiles src/ to wiring, serves it, and recompiles on every change
npm run dev
# compiles src/ to wiring, serves it, and recompiles on every change
yarn run dev
# compiles src/ to wiring, serves it, and recompiles on every change
bun run dev
# compiles src/ to wiring, serves it, and recompiles on every change
→ Context & Lifecycle covers what runs once the wiring boots
(request context, OnInit, graceful shutdown).
Register it with one config line
The build step is a plugin you install like any other Vite plugin — it takes no arguments of its own; the
heximon.config.ts config file lists which Heximon plugins are active (here, just HTTP):
import { defineHeximonConfig } from "@heximon/build";
import { HttpPlugin } from "@heximon/http/compiler";
export default defineHeximonConfig({ plugins: [new HttpPlugin()] });
import heximon from "@heximon/build/vite";
import { defineConfig } from "vite-plus";
export default defineConfig({
plugins: [heximon()], // takes no arguments — plugins live in heximon.config.ts above
});
That's the whole setup: one config file, one line in vite.config.ts. Every capability past this —
validation, contracts, a database, background jobs, events, CQRS — is the same move, repeated: add a
package, add its namespace key to a module, register its plugin. See
Where next? for the full map.
heximon.config.ts, read through their own module instead of the
heximon() Vite plugin — so moving from the dev server to a deployment is a config swap, not a rewrite.
See Deploy targets.Next steps
- Project Structure — turn these concepts into a directory layout that scales.
- Modules & DI — the DI graph in full, including factories and abstract-class seams.
- Controllers — both binding modes, middleware, and status-keyed responses in depth.
- Where next? — the goal-to-package map for everything past this page.
- Examples — the tiered example set; the ladder's L01 — Minimal is exactly the app this page describes.
Quick Start
Build your first Heximon app in three files — a provider, a controller, and a module, wired by the compiler and served by Vite.
Examples
Runnable examples in three tiers — a progressive learning ladder (L01–L08), the event-ticketing flagship monorepo, and thematic gap examples for platform and feature one-offs.