Plugin modules
Install native Elysia plugins through module imports and dependency injection.
Every Elysia plugin works inside an AponiaJS application unchanged. A plugin
becomes a module import, the platform passes it to Elysia's own .use(), and
what it adds to the request context reaches every controller. Plugins are
installed in module dependency order, before controllers are mounted.
Define a plugin as a module import
defineElysiaPlugin converts a native plugin into a module import that also
carries the plugin's type:
import { defineElysiaPlugin } from "@aponiajs/platform-elysia";
import { Elysia } from "elysia";
export const clock = defineElysiaPlugin(
new Elysia({ name: "clock" })
.decorate("now", () => new Date().toISOString())
.state("requests", 0)
.derive({ as: "global" }, () => ({ traceId: crypto.randomUUID() })),
{ key: "clock" },
);
export type clock = typeof clock;import { Module } from "@aponiajs/common";
import { clock } from "./clock.plugin.ts";
@Module({ imports: [clock] })
export class AppModule {}The same value is also a complete low-level module import, which preserves its type in a statically declared graph:
import { defineModule } from "@aponiajs/common";
export const AppModule = defineModule({
id: "AppModule",
imports: [clock],
});A published plugin is wrapped the same way:
bun add @elysiajs/corsimport { cors } from "@elysiajs/cors";
import { defineElysiaPlugin } from "@aponiajs/platform-elysia";
export const corsPlugin = defineElysiaPlugin(cors(), { key: "cors" });
export type corsPlugin = typeof corsPlugin;Register a plugin value directly
ElysiaPluginModule.register(plugin, { key }) performs the same registration
without attaching the plugin type. It stays supported; prefer
defineElysiaPlugin unless no handler ever needs the plugin's types.
import { Module } from "@aponiajs/common";
import { ElysiaPluginModule } from "@aponiajs/platform-elysia";
import { cors } from "@elysiajs/cors";
@Module({
imports: [
ElysiaPluginModule.register(cors(), {
key: "cors",
}),
],
})
export class AppModule {}The plugin value is passed unchanged to Elysia's .use().
Configure a plugin with DI
ElysiaPluginModule.registerAsync builds the plugin from the container, so it
can read configuration a provider owns:
import { Module } from "@aponiajs/common";
import { ElysiaPluginModule } from "@aponiajs/platform-elysia";
import { jwt } from "@elysiajs/jwt";
import { ConfigModule, ConfigService } from "./config/config.module.ts";
@Module({
imports: [
ElysiaPluginModule.registerAsync({
key: "jwt",
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) =>
jwt({ name: "jwt", secret: config.get("JWT_SECRET") }),
}),
],
})
export class AuthModule {}imports lists the modules whose exports the factory resolves against. A token
that no imported module exports fails with MISSING_PROVIDER at compile time.
registerAsync is not an async provider
Despite its name, useFactory must return the plugin synchronously.
Promise-based provider initialization is not implemented.
Keys and duplicate registration
- A non-empty
keycreates a stable module identity (Symbol.for-backed). - Diamond imports of the same keyed plugin install it once.
- Two different plugin definitions sharing one key fail with
DUPLICATE_MODULEbefore the server listens. - Omitting a key creates a distinct identity for each registration.
- An empty or whitespace-only key throws
TypeError.
Use stable keys for shared infrastructure plugins and distinct keys when the same plugin intentionally needs multiple configurations.
Typing what a plugin adds
Everything a plugin decorates, stores, or globally derives is present in every handler at runtime. Typing it requires naming the plugin — see typed plugin context.
When static route types should flow into Eden Treaty as well, return the
composed application with AponiaFactory.createNative(); see
Eden Treaty.