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