AponiaJS vs Elysia
Elysia is the runtime AponiaJS is built on. This page is about what the module graph adds, what it costs, and when the plain chained application is still the better answer.
Elysia is not a competitor. It is the server AponiaJS runs on, and every AponiaJS application is an Elysia application underneath.
The question is whether the layer on top earns its place.
At a glance
Composition
Module graph with imports and exports
Method chaining and plugins
Dependency injection
Yes. Constructor injection with a container
Partial. decorate, state, derive, resolve
Type inference
Partial. Explicit annotations on handlers
Yes. Inferred through the chain
Eden Treaty
Partial. Statically composed native routes
Yes. Inferred directly
Startup validation
Yes. Graph diagnostics before listening
No. None; mistakes surface per request
Maturity
No. Alpha
Yes. Stable
The same endpoint, twice
Elysia:
import { Elysia, t } from "elysia";
const users = new Elysia({ prefix: "/users" })
.decorate("repository", new UserRepository())
.get("/:id", ({ params, repository }) => repository.find(params.id), {
params: t.Object({ id: t.String() }),
});
new Elysia().use(users).listen(3000);AponiaJS:
import { Controller, Get, Injectable, Module, Param } from "@aponiajs/common";
import { AponiaFactory } from "@aponiajs/platform-elysia";
@Injectable()
class UserRepository {
find(id: string) {
return { id };
}
}
@Controller("users")
class UserController {
constructor(private readonly repository: UserRepository) {}
@Get(":id")
find(@Param("id") id: string) {
return this.repository.find(id);
}
}
@Module({ controllers: [UserController], providers: [UserRepository] })
class AppModule {}
await (await AponiaFactory.create(AppModule)).listen(3000);At this size Elysia wins on every axis that matters: fewer lines, fewer concepts, and full type inference without a single annotation.
What changes at scale
The chained form couples three things that grow at different rates: the dependency wiring, the URL surface, and the handler bodies. Past a certain number of features, teams end up inventing a convention for splitting them. AponiaJS is that convention, made explicit.
- Dependencies are declared, not threaded.
decorateputs a value on the context for everything downstream. A module declares what it provides and what it exports, so a dependency reaching the wrong file is a startup error rather than a merge conflict. - Construction happens once. Providers are singletons built during bootstrap. See providers.
- The graph is validated. Missing providers, cycles, and unexported dependencies are reported at startup with diagnostics, not discovered at request time.
The cost is real: more files, explicit handler parameter types instead of inference through the chain, and an alpha framework between you and a stable one.
Nothing is hidden
The escape hatches are the point of the Elysia platform section:
- native access hands you the underlying application to configure or extend;
- plugin modules mount Elysia plugins through the module graph, with DI-configured variants;
- typed plugin context types a handler against the plugins it actually reads;
- low-level descriptors register routes directly on the native application when a decorator cannot express them;
- Eden Treaty exports the composed route types to a client.
Which one to pick
Choose plain Elysia when
- the service is small, or one team owns all of it;
- inference through the chain is worth more to you than declared boundaries;
- you want the stable, documented, widely used option;
- you use Elysia features that AponiaJS has not surfaced yet.
Choose AponiaJS when
- the application has enough features that wiring has become the hard part;
- you want module boundaries and constructor injection without leaving Bun;
- coming from NestJS, the structure is what you miss;
- alpha software is acceptable for this codebase.
Adopt it gradually
From Elysia keeps the native application and introduces modules a route at a time.
Frequently asked questions
- Does AponiaJS replace Elysia?
- No. AponiaJS compiles a decorated module graph into a single Elysia application, and the native instance stays reachable. Elysia plugins, hooks, and types keep working, which is why the Elysia platform section of these docs exists.
- Is AponiaJS slower than plain Elysia?
- The structural work happens at startup, not per request. Modules are validated and providers constructed once during bootstrap, then routes are registered on one Elysia instance, so the request path is Elysia's. Startup does more work than a chained application does.
- Can I add AponiaJS to an existing Elysia application?
- Yes, incrementally. Keep the native application, mount decorated modules alongside it, and move routes across when it pays off. The from-Elysia migration page walks through it.
AponiaJS vs AdonisJS
AdonisJS is a batteries-included full-stack Node framework with an ORM, auth, and validation. AponiaJS is an HTTP and dependency injection layer on Bun, and nothing more.
AponiaJS vs Express
Express decides almost nothing and has the largest middleware ecosystem in Node. AponiaJS decides how the application is structured and runs only on Bun.