AponiaJSDocs
Elysia platform

Typed plugin context

Type what a native Elysia plugin adds to a decorated controller handler with ElysiaRouteContext.

Compiling a decorated controller erases the plugin instances its module imports, so no plugin type reaches a handler on its own. The values are still in the context at runtime — only untyped. Name the plugins in ElysiaRouteContext and the context types them.

import { Controller, Ctx, Get } from "@aponiajs/common";
import { type ElysiaRouteContext } from "@aponiajs/platform-elysia";
import { clock } from "./clock.plugin.ts";

@Controller("health")
export class HealthController {
  @Get()
  read(@Ctx() context: ElysiaRouteContext<clock>) {
    context.store.requests += 1;
    return { now: context.now(), traceId: context.traceId };
  }
}

clock here is the value and same-named type produced by defineElysiaPlugin, which is why no typeof is needed. A plugin exported only as a const is written ElysiaRouteContext<typeof clock>.

Argument forms

The first type argument accepts either the plugins or a route schema, so a route without a schema never writes an empty one:

AnnotationTypes
ElysiaRouteContextneither
ElysiaRouteContext<clock>one plugin
ElysiaRouteContext<[clock, cache]>several plugins
ElysiaRouteContext<typeof createUser>the route schema
ElysiaRouteContext<typeof createUser, [clock, jwt]>the schema and the plugins

Renaming the type on import keeps annotations short:

import { type ElysiaRouteContext as e } from "@aponiajs/platform-elysia";

@Get()
read(@Ctx() context: e<clock>) {
  return { now: context.now() };
}

Declare the pairing once

An application that always mounts the same plugins declares one alias and keeps every handler short:

src/app.context.ts
import { type ElysiaInputSchema, type ElysiaRouteContext } from "@aponiajs/platform-elysia";
import { cache } from "./cache.plugin.ts";
import { clock } from "./clock.plugin.ts";

export type AppContext<TSchema extends ElysiaInputSchema = {}> = ElysiaRouteContext<
  TSchema,
  [clock, cache]
>;
@Get()
read(@Ctx() context: AppContext) {}

@Post("/", createUser)
create(@Ctx() context: AppContext<typeof createUser>) {}

ElysiaInputSchema is Elysia's own route schema shape, re-exported so an application can write this alias without importing from elysia directly.

No ambient plugin registry

AponiaJS deliberately has no framework-level plugin registry. Ambient registration through declaration merging would apply to a whole compilation, including files that never mount the plugin, so the pairing stays an application-owned alias.

What is typed, and what actually arrives

The mapping follows Elysia's own .use() rule, so the type matches runtime exactly:

Declared in the pluginReaches a controllerTyped
.decorate(...)yesyes
.state(...)yesyes
.derive({ as: "global" }, ...)yesyes
.resolve({ as: "global" }, ...)yesyes
.derive({ as: "scoped" }, ...)yesyes
.resolve({ as: "scoped" }, ...)yesyes
.derive(...) without a scopenono

A plugin-local derive stays inside the plugin, because a controller is mounted beside the plugin rather than inside it.

Supporting types

ElysiaPluginSource is one plugin a handler reads from — a native Elysia instance, or the module import defineElysiaPlugin produces for it. ElysiaPluginTypes is one such source or a readonly array of them. Both are exported for applications that build their own context aliases.

See the CORS and configured plugin recipes for complete registrations.

On this page