dunx
Dependency injection for Bun. Everything a service needs, one version.
Controllers, dependency injection, validation, OpenAPI, WebSockets, queues, an ORM, auth, a test harness and an ops dashboard. Released together, tested together, on Bun's own primitives.
bunx @dunx/create-app my-api
What you get
Elysia and Hono hand you a router, and everything above it is yours to choose and keep in step. This is the other trade: one dependency, one release train.
Every one of those is a Bun primitive or a best-in-class library wired to one,
never a reimplementation. Each is opt-in: @dunx/core has zero dependencies,
and an app that imports no queue installs no queue.
What an app looks like
import { Logger, Module } from '@dunx/core';
import {
Controller,
Get,
HttpFactory,
type Input,
type RouteSchemas,
} from '@dunx/http';
export class GreetingsService {
constructor(private readonly logger: Logger) {}
greet(name: string) {
this.logger.info('greeting', { name });
return { greeting: `hello, ${name}` };
}
}
@Controller('greetings')
export class GreetingsController {
constructor(private readonly greetings: GreetingsService) {}
@Get('/:name')
one(input: Input<RouteSchemas>) {
return this.greetings.greet(input.req.params['name'] ?? 'world');
}
}
@Module({
controllers: [GreetingsController],
providers: [GreetingsService],
})
export class AppModule {}
const app = await HttpFactory.create(AppModule);
await app.listen(3000);
There is no @Injectable(), no @Inject(), no reflect-metadata import and no
experimentalDecorators, and a handler returns a plain value instead of calling
Response.json(). Two lines in bunfig.toml turn the constructor types into
wiring:
preload = ["@dunx/transform/preload"]
[test]
preload = ["@dunx/transform/preload"]
Bun's test runner reads its own preload, so bun test needs the [test]
entry. Without it the app runs, but the tests fail at the first provider with a
constructor parameter.
How the DI differs
The constructor injection above reads like NestJS. The mechanism resolving it underneath is different, in four places.
No reflect-metadata, and no experimentalDecorators. dunx uses standard
TC39 decorators. @dunx/transform reads each class's constructor parameter
types at load time with oxc-parser, a
Rust parser over N-API, and records them on the class. There are no parameter
decorators in the TC39 proposal, so @Inject() does not exist and never will.
A type that does not exist at runtime fails boot. If a parameter's type is
an interface, a primitive or a type-only import, dunx fails at boot and names
that parameter. emitDecoratorMetadata gives you undefined and a stack trace
three frames away from the mistake.
No forwardRef. The dependency record is a thunk, evaluated at resolution
rather than at class-definition time, so a circular import resolves on its own.
The container is scoped. Every module reference is a scope holding what it
declares, exports is its public surface, and an absent exports exports
nothing. A module that takes no options is a decorated class rather than a
forRoot(), because each forRoot() call returns a fresh object and so a fresh
scope.
One thing to add rather than delete: every relative import ends in .js,
because moduleResolution: nodenext is what the scaffold sets. An extensionless
specifier is a compile error rather than a runtime surprise.
Documentation
- The live demo -
examples/fullrunning on a Raspberry Pi 5: the API explorer, the ops dashboard, bull-board, and a walkthrough that drives twenty-five capabilities from a browser, including a transaction rolling back, a CSRF refusal, an idempotent retry, an order crossing a RabbitMQ topic exchange, and the process's own request, query and event-loop timings - The guide - thirty-five pages, introduction through cookies
- Migrating from NestJS - what maps across and what does not
- ARCHITECTURE.md - what was measured, what was rejected, and why
- ROADMAP.md - what is built and what is next
- setup.md - the same setup for
an agent to fetch and follow: "set up my project using
https://dunx.win/setup.md". Every scaffolded app also gets an
AGENTS.mdand aCLAUDE.md - llms.txt - every document above, linked as raw markdown
Performance
Structure on Bun does not have to cost throughput. @dunx/http is a layer over
Bun.serve({ routes }). It lands at 88-99% of the raw server's throughput,
within noise of Elysia, and 3.8-5.5x NestJS on Fastify. Boot is 43 ms against
294 ms.
Median req/s, 64 connections, 5 runs:
The harness also runs Go, Rust and JVM subjects alongside these. The NestJS
subject is real NestJS with real reflect-metadata. It states what it cannot
measure, too: the load generator shares a machine with the subject, and the
closed-loop design is subject to coordinated omission.
Reproduce it with bun run --filter '@dunx/bench' start. The methodology is in
internal/bench/README.md.
When not to use it
- You do not want DI. Elysia and Hono own that space, they are mature, and they are faster to learn.
- Boot time is your critical number. 43 ms is fine for a service that starts once and stays up, and wrong for a per-invocation serverless function.
- You need request-scoped or transient providers. Not supported, rejected with measurements.
- You are not certain you are on Bun. No CommonJS build, no Node
compatibility layer.
Bun.serve,bun:sqlite,Bun.RedisClientandBun.passwordare load-bearing throughout. - You want a mature third-party ecosystem. There is not one yet.
Built with dunx
Firecracker - live
A provably-fair crash game: bet during the window, watch the rocket climb, cash out before it explodes. Every round is verifiable after the fact.
21 modules, 8 controllers, a WebSocket gateway and 4 job handlers across 165
backend files, on @dunx/core, @dunx/http, @dunx/infra, @dunx/auth,
@dunx/openapi, @dunx/testing, @dunx/transform.
Examples
Each is kept alive by CI. Each also exits 0 with no database, Redis or S3 installed.
bun install
bun run --filter '@dunx/example-minimal' start
Packages
Contributing
Pull requests are welcome. CONTRIBUTING.md is the full guide: how to get set up, which checks CI runs, the repo's rules on native implementations and third-party dependencies, and the house style a review will hold you to.
The short version: Bun only, no npm/npx/yarn/pnpm; bun install then
bun run build before anything else; run bun run ci, every gate CI runs, before
you push; conventional commits; a bug fix comes with the test
that would have caught it; and a claim about performance comes with the numbers.