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.
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.
- Native Elysia
- Class-based BaseApiService
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"] },
}
);
A class extending BaseApiService, written exactly like an Express BaseApiService:
the same per-method convention (_<verb>, _<verb>Swagger, _<verb>Validation,
_<verb>Multer), the same SwaggerOptions schema DSL, the same permission tuple. Only
two things differ: the handler uses method syntax with the native Elysia context (no
req, res), and runtime validation is TypeBox instead of express-validator.
import { BaseApiService, type Context, RouteValidation, SwaggerOptions, t } from "efesto/elysia";
const products = [{ id: 1, name: "Anvil" }];
export default class extends BaseApiService {
_getSwagger: SwaggerOptions = { operationId: "listProducts", permission: ["readAll", "Product"], summary: "List products" };
_get() {
return products;
}
_postSwagger: SwaggerOptions = { operationId: "createProduct", permission: ["create", "Product"] };
_postValidation: RouteValidation = { body: t.Object({ name: t.String() }) };
_post({ body }: Context) {
const product = { id: Math.floor(Math.random() * 1000), name: (body as { name: string }).name };
products.push(product);
return product;
}
}
A named class with constructor() { super(__filename); } is also accepted (for parity with
Express), but it is optional, the mount derives the path from the file location either way.
Per verb you declare:
_<verb>— the handler, method syntax_get(ctx) { ... }, native Elysia context, returns a value._<verb>Validation— TypeBox schemas{ body, query, params, response }(typedRouteValidation); runtime validation compiled by Elysia and added to OpenAPI._<verb>Swagger—SwaggerOptions:operationId,summary,requestBody,responses,parameters(the Efesto Magic Types DSL, expanded into native OpenAPI) pluspermission: [action, model]for ABAC._<verb>Multer/_<verb>MultipleMulter(post/patch/putonly) — accept multipart uploads; the file arrives inctx.body. See File Uploads.
Supported verbs: get, post, put, patch, delete, head, options.
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
| Concern | Express (efesto) | Bun/Elysia (efesto/elysia) |
|---|---|---|
| Route file | class extends BaseApiService | native 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) |
| OpenAPI | generated .yaml from _<verb>Swagger | native @elysiajs/openapi, from TypeBox + the _<verb>Swagger Magic Types DSL |
| Uploads | _<verb>Multer / _<verb>MultipleMulter | same: _<verb>Multer / _<verb>MultipleMulter |
| Auth | global authMiddleware + _<verb>OverrideAuth | native, via setup |
| Caching | built-in Redis (cache/purgeKey) | native Elysia plugins |
| Types | generated .d.ts (+ magic editing) | inferred natively by TypeScript |