ELPOD / DOCS FOUNDATIONS

foundations / reference

Native Elysia Routing and Controllers in Elpod

Elpod controllers register native Elysia routes inside feature pods, preserving Elysia APIs and Eden Treaty type inference.

Elpod controllers register native Elysia routes inside feature pods on Bun. Use a controller when one feature needs an HTTP entrypoint and you want Elysia schemas, hooks, plugins, and Eden Treaty types to remain available.

The idea

Frameworks often make routing feel like filling out paperwork: add metadata here, decorate a method there, and hope another layer generates the route you meant. That can be useful, but it also creates a second language to learn.

How Elpod provides it

Elpod controllers are thin native Elysia route registrars. The routes(app) method receives a grouped Elysia instance and must return it. ElpodElysia adds typed request identity fields while preserving Elysia’s route API. If Elysia can do it, your controller can do it.

import { t } from "elysia";
import type { ElpodElysia } from "@elpod/core";

export class UsersController {
  routes(app: ElpodElysia) {
    return app
      .get("/", () => ({ users: [] }), {
        response: t.Object({ users: t.Array(t.Object({ id: t.String() })) }),
      })
      .post("/", ({ body, requestId }) => ({ id: body.id, requestId }), {
        body: t.Object({ id: t.String({ minLength: 1 }) }),
      });
  }
}

Use Elysia’s t schemas, params, query, headers, cookies, response maps, lifecycle hooks, app.ws, multipart, redirects, streams, and plugins directly. Elpod does not create route decorators or a second router. Native Response values retain status, headers, body, and streaming behavior.

The request context includes requestId and correlationId, and Elpod returns both headers. Request-scoped services can receive the richer REQUEST_CONTEXT token through constructor injection.

Eden Treaty contract

Elpod preserves the native route type assembled from pod controllers. Export the contract from the composition root:

import { application, type ElpodContract } from "@elpod/core";

export const app = application({ features: [users] });
export type Api = ElpodContract<typeof app>;

Clients can consume it from the same workspace or from a published type-only package:

import { treaty } from "@elysiajs/eden";
import type { Api } from "@company/api-contract";

export const api = treaty<Api>("https://api.example.com");

The contract includes controller schemas, dynamic parameters, response types, and pod prefixes. Keep public routes in pods; routes added through untyped configure callbacks or native plugins are intentionally runtime-only escape-hatch routes.

When to use a controller

Use one controller as the HTTP entrypoint for a feature. Keep parsing and transport concerns in the route, then call a service for business logic. Keep route schemas close to routes so Elysia validation and OpenAPI reflect what is actually registered.

Common mistakes

  • Returning a plain object from routes() instead of the Elysia instance.
  • Adding business authorization only in a route hook and forgetting service-level calls from jobs or events.
  • Using a route prefix as a tenancy or permission boundary.
  • Hiding route registration behind metadata that routeManifest() cannot inspect.

Production notes

Validate request input and response output with Elysia schemas. Set a deliberate request-body limit in bootstrap({ maxRequestBodyBytes }), handle native streaming cleanup, and use Native Response only when its headers/status are intentional. See OpenAPI and Errors.

OpenAPI and routes · Elysia plugins · Dependency injection.