Skip to main content

Bun / Elysia stack

Efesto runs on the Bun / Elysia stack through a separate entrypoint: efesto/elysia. On this stack Efesto's job is narrow: it provides filesystem routing and mounts your modules. Everything else, validation, OpenAPI, hooks, is idiomatic, native Elysia.

Different model from Express

OpenAPI is produced natively by @elysiajs/openapi, not from generated YAML. Routes are plain Elysia, or Efesto's BaseApiService class, which mirrors the Express per-method convention (_<verb>, _<verb>Swagger, _<verb>Validation, _<verb>Multer). Runtime validation comes from TypeBox; there is no swaggerModel auto-generation and no .d.ts type generation (TypeScript infers handler types from your TypeBox schemas).

Install​

Elysia and the OpenAPI plugin are optional peer dependencies:

bun add efesto elysia @elysiajs/openapi

Create the server​

import efesto from "efesto/elysia";

const app = efesto({
isProduction: process.env.NODE_ENV === "production",
routesDir: `${import.meta.dir}/routes`,
prefix: "/api/v1",
swagger: true, // native OpenAPI at /api/v1/openapi
}).listen(2014);

console.log(`http://localhost:${app.server?.port}/api/v1`);

efesto(...) returns a native Elysia instance, so you can also mount it as a plugin: new Elysia().use(efesto(...)). The configuration object is documented in full under Configuration.

Writing routes​

Each route file export defaults one of two forms. They can be mixed freely in the same project, and both compile to native Elysia routes, so they perform identically.

A plain Elysia instance using relative paths. Efesto mounts it at the prefix derived from the file (routes/user/index.ts → /user):

import { Elysia, t } from "efesto/elysia";

const users = [{ id: 1, name: "Ada", surname: "Lovelace" }];

export default new Elysia()
.get("/", () => users, {
detail: { summary: "List users", tags: ["User"] },
})
.put(
"/",
({ body }) => {
const user = { id: Math.floor(Math.random() * 1000), ...body };
users.push(user);
return users;
},
{
body: t.Object({ name: t.String(), surname: t.String() }),
detail: { summary: "Create user", tags: ["User"] },
}
);

Annotate _<verb>Swagger: SwaggerOptions and _<verb>Validation: RouteValidation as in the example, that keeps permission inferred as a tuple without casts. The handler's ctx is the native Elysia context, so read validated input from ctx.body/ctx.query/ ctx.params (cast or annotate the shape you declared in _<verb>Validation).

Shared models​

Declare reusable schemas once via the models option; @Model references in any route's _<verb>Swagger then resolve against them (rewritten to native #/components/schemas/Model refs and registered as OpenAPI components). Values may use the Efesto DSL or be plain/TypeBox schemas:

efesto({
isProduction: false,
routesDir: `${import.meta.dir}/routes`,
models: {
User: { properties: { name: "string", email: "string::email", friend: "@User?" } },
},
// swaggerConfig: { customTypes, customFormats } extends the DSL with your own types
});

Cross-cutting concerns​

Compose auth, ABAC, caching, and CORS natively through setup, which runs on the root instance before routes mount, so it applies to every route. For class-based permission rules, provide the CASL ability in the context here:

import { defineAbility } from "@casl/ability";

efesto({
isProduction: false,
routesDir: `${import.meta.dir}/routes`,
setup: (root) => {
root
.derive(() => ({ ability: defineAbility((can) => can("readAll", "User")) }))
.onBeforeHandle(({ ability }) => {
// authenticate / check permissions, or throw to reject
});
},
});

See Authentication and ABAC Permissions for the details, both have a Bun/Elysia tab.

Differences from the Express stack​

ConcernExpress (efesto)Bun/Elysia (efesto/elysia)
Route fileclass extends BaseApiServicenative new Elysia()... or class extends BaseApiService
Handler_get(req, res, next) + res.json(...)native _get(ctx) { return value }
Validation_<verb>Validation (express-validator)_<verb>Validation (per-route TypeBox schema)
OpenAPIgenerated .yaml from _<verb>Swaggernative @elysiajs/openapi, from TypeBox + the _<verb>Swagger Magic Types DSL
Uploads_<verb>Multer / _<verb>MultipleMultersame: _<verb>Multer / _<verb>MultipleMulter
Authglobal authMiddleware + _<verb>OverrideAuthnative, via setup
Cachingbuilt-in Redis (cache/purgeKey)native Elysia plugins
Typesgenerated .d.ts (+ magic editing)inferred natively by TypeScript