Elysia Bun
Elysia web framework on Bun for type-safe, high-performance HTTP APIs
You are an expert in the Elysia web framework for building type-safe, high-performance HTTP APIs on the Bun runtime.
## Key Points
- **Define schemas on every route** — they give you validation, types, and automatic documentation in one step.
- **Use `decorate`** for dependency injection (database clients, services) — it integrates with Elysia's type system.
- **Break routes into plugins** with `new Elysia({ prefix })` for clean organization in larger apps.
- **Use `@elysiajs/swagger`** — Elysia generates OpenAPI specs from your schemas automatically.
- **Test with Eden treaty** for end-to-end type-safe client calls against your app instance, no HTTP needed.
- **Use `guard`** to apply shared validation (auth headers, common query params) to groups of routes.
- **Use lifecycle hooks** (`onRequest`, `onBeforeHandle`, `onAfterHandle`, `onError`) for cross-cutting concerns like logging and metrics.
- **Plugin order matters** — middleware registered via `.use()` applies to routes defined after it. Put `cors()` and `swagger()` before your route plugins.
- **Forgetting `.listen()`** — the app does nothing until you call `.listen()`. In tests, pass the app instance directly to Eden instead.
- **TypeBox `t.Numeric`** parses string query params to numbers. Use it for query strings; use `t.Number` or `t.Integer` for JSON bodies.
- **Response schema is optional but recommended** — without it, responses are not validated and documentation is incomplete.
- **Heavy computation blocks the event loop** — Bun is single-threaded for JS. Offload CPU-intensive work to workers or external processes.skilldb get deno-bun-skills/Elysia BunFull skill: 310 linesElysia Framework on Bun — Modern JS Runtimes
You are an expert in the Elysia web framework for building type-safe, high-performance HTTP APIs on the Bun runtime.
Overview
Elysia is a TypeScript-first web framework designed for Bun. It provides end-to-end type safety, a plugin system, schema validation via TypeBox, and automatic OpenAPI documentation. Elysia leverages Bun's fast HTTP server to achieve high request throughput with ergonomic, Express-like routing syntax.
Core Concepts
Type Safety
Elysia infers request and response types from schema definitions. When you define a body schema, the handler's body parameter is fully typed — no manual casting or as assertions needed.
Plugin System
Elysia uses a composable plugin model where each plugin can add routes, middleware, state, and decorators. Plugins compose via .use() and types propagate through the chain.
Schema Validation
Elysia integrates TypeBox (a JSON Schema builder with TypeScript inference) for request validation. Invalid requests receive automatic 400 responses with structured error details.
Implementation Patterns
Basic Application
import { Elysia } from "elysia";
const app = new Elysia()
.get("/", () => "Hello, Elysia!")
.get("/api/health", () => ({ status: "ok", timestamp: Date.now() }))
.listen(3000);
console.log(`Running at http://${app.server?.hostname}:${app.server?.port}`);
Route Parameters and Query Strings
import { Elysia, t } from "elysia";
const app = new Elysia()
.get("/users/:id", ({ params: { id } }) => {
return { userId: id };
})
.get(
"/search",
({ query }) => {
return { term: query.q, page: query.page };
},
{
query: t.Object({
q: t.String(),
page: t.Optional(t.Numeric({ default: 1 })),
}),
}
)
.listen(3000);
Request Body Validation
import { Elysia, t } from "elysia";
const app = new Elysia()
.post(
"/api/users",
({ body }) => {
// body is fully typed as { name: string; email: string; age?: number }
return { created: true, user: body };
},
{
body: t.Object({
name: t.String({ minLength: 1 }),
email: t.String({ format: "email" }),
age: t.Optional(t.Integer({ minimum: 0 })),
}),
response: {
201: t.Object({
created: t.Boolean(),
user: t.Object({
name: t.String(),
email: t.String(),
}),
}),
},
}
)
.listen(3000);
Guards and Middleware
import { Elysia, t } from "elysia";
function authPlugin(app: Elysia) {
return app
.derive(({ headers }) => {
const token = headers.authorization?.replace("Bearer ", "");
return { token };
})
.guard(
{
headers: t.Object({
authorization: t.String(),
}),
},
(app) =>
app
.get("/api/profile", ({ token }) => {
// token is typed and guaranteed present
return { token, profile: "..." };
})
.get("/api/settings", ({ token }) => {
return { token, settings: {} };
})
);
}
const app = new Elysia().use(authPlugin).listen(3000);
Plugins and Composition
import { Elysia, t } from "elysia";
import { cors } from "@elysiajs/cors";
import { swagger } from "@elysiajs/swagger";
// Custom plugin
const dbPlugin = new Elysia({ name: "db" })
.decorate("db", {
async getUser(id: string) {
// database lookup
return { id, name: "Alice" };
},
async createUser(data: { name: string; email: string }) {
const id = crypto.randomUUID();
return { id, ...data };
},
});
// User routes as a plugin
const userRoutes = new Elysia({ prefix: "/api/users" })
.use(dbPlugin)
.get("/:id", async ({ params, db }) => {
return db.getUser(params.id);
})
.post(
"/",
async ({ body, db }) => {
return db.createUser(body);
},
{
body: t.Object({
name: t.String(),
email: t.String({ format: "email" }),
}),
}
);
// Main app
const app = new Elysia()
.use(cors())
.use(swagger({ path: "/docs" }))
.use(userRoutes)
.listen(3000);
WebSocket Routes
import { Elysia, t } from "elysia";
const app = new Elysia()
.ws("/chat", {
body: t.Object({
message: t.String(),
sender: t.String(),
}),
open(ws) {
ws.subscribe("chat-room");
ws.publish("chat-room", JSON.stringify({ type: "joined" }));
},
message(ws, { message, sender }) {
ws.publish(
"chat-room",
JSON.stringify({ type: "message", message, sender })
);
},
close(ws) {
ws.unsubscribe("chat-room");
},
})
.listen(3000);
Error Handling
import { Elysia, t, NotFoundError } from "elysia";
class AuthError extends Error {
constructor(message = "Unauthorized") {
super(message);
}
}
const app = new Elysia()
.error({ AUTH: AuthError })
.onError(({ code, error, set }) => {
switch (code) {
case "AUTH":
set.status = 401;
return { error: error.message };
case "VALIDATION":
set.status = 400;
return { error: "Invalid request", details: error.all };
case "NOT_FOUND":
set.status = 404;
return { error: "Resource not found" };
default:
set.status = 500;
return { error: "Internal server error" };
}
})
.get("/protected", ({ token }) => {
if (!token) throw new AuthError();
return { data: "secret" };
})
.listen(3000);
Testing Elysia Apps
import { describe, it, expect } from "bun:test";
import { Elysia } from "elysia";
import { treaty } from "@elysiajs/eden";
const app = new Elysia()
.get("/api/health", () => ({ status: "ok" }))
.post("/api/echo", ({ body }) => body, {
body: t.Object({ message: t.String() }),
});
// Using Eden treaty for type-safe testing
const client = treaty(app);
describe("API", () => {
it("health check", async () => {
const { data, status } = await client.api.health.get();
expect(status).toBe(200);
expect(data).toEqual({ status: "ok" });
});
it("echoes body", async () => {
const { data } = await client.api.echo.post({ message: "hello" });
expect(data?.message).toBe("hello");
});
});
Core Philosophy
Elysia is built on the premise that type safety should be a compile-time guarantee, not a runtime aspiration. By integrating TypeBox schema validation directly into the routing layer, Elysia ensures that request bodies, query parameters, headers, and responses are all typed and validated in one place. The schema definition simultaneously serves as documentation, validation, and TypeScript inference, eliminating the common problem of these three concerns drifting out of sync.
The plugin and composition model treats application structure as type-safe composition rather than middleware stacking. Each plugin can contribute routes, decorators, state, and middleware, and the types flow through the .use() chain. When you add a database plugin, every route in the chain gains typed access to the database client. This is fundamentally different from Express middleware, where req.db is an untyped convention enforced by documentation rather than the type system.
Elysia leverages Bun's performance characteristics deliberately. The framework is designed to minimize overhead per request, and its schema validation uses pre-compiled TypeBox validators rather than runtime parsing. Combined with Bun's fast HTTP server, this produces a framework that handles high request throughput while still providing full type safety and automatic API documentation. The performance is not incidental but a core design constraint.
Anti-Patterns
-
Omitting schemas on routes. Routes without body, query, or response schemas lose type safety, automatic validation, and OpenAPI documentation. Even simple routes benefit from schema definitions because they make the contract explicit and catch invalid requests automatically.
-
Registering middleware after the routes it should protect. Plugin and middleware order matters in Elysia.
cors()andswagger()must be registered before your route plugins, or they will not apply to those routes. This is a common source of "why is CORS not working" bugs. -
Using
t.Numberfor query string parameters. Query parameters arrive as strings.t.Numericparses string values to numbers, whilet.Numbervalidates the value as already being a number. Usingt.Numberfor query params causes validation failures on every request. -
Building complex client-side state management for data Elysia already validates. Since Elysia guarantees the shape of request and response data through schemas, client-side code consuming Elysia APIs (especially via Eden treaty) does not need additional validation or type assertion layers.
-
Performing CPU-intensive computation in route handlers without workers. Bun is single-threaded for JavaScript execution. A handler that does heavy computation blocks all other requests. Offload CPU-intensive work to worker threads or external processes.
Best Practices
- Define schemas on every route — they give you validation, types, and automatic documentation in one step.
- Use
decoratefor dependency injection (database clients, services) — it integrates with Elysia's type system. - Break routes into plugins with
new Elysia({ prefix })for clean organization in larger apps. - Use
@elysiajs/swagger— Elysia generates OpenAPI specs from your schemas automatically. - Test with Eden treaty for end-to-end type-safe client calls against your app instance, no HTTP needed.
- Use
guardto apply shared validation (auth headers, common query params) to groups of routes. - Use lifecycle hooks (
onRequest,onBeforeHandle,onAfterHandle,onError) for cross-cutting concerns like logging and metrics.
Common Pitfalls
- Plugin order matters — middleware registered via
.use()applies to routes defined after it. Putcors()andswagger()before your route plugins. - Forgetting
.listen()— the app does nothing until you call.listen(). In tests, pass the app instance directly to Eden instead. - TypeBox
t.Numericparses string query params to numbers. Use it for query strings; uset.Numberort.Integerfor JSON bodies. - Response schema is optional but recommended — without it, responses are not validated and documentation is incomplete.
- Heavy computation blocks the event loop — Bun is single-threaded for JS. Offload CPU-intensive work to workers or external processes.
- Eden import confusion —
@elysiajs/edenhas bothtreaty(REST-like) andedenFetch(fetch-like) clients. Usetreatyfor most cases.
Install this skill directly: skilldb add deno-bun-skills
Related Skills
Bun Basics
Bun runtime fundamentals including speed optimizations, built-in APIs, and package management
Bun Bundler
Using Bun as a bundler for frontend assets and as a fast test runner
Compatibility
Node.js compatibility layers in Deno and Bun for running existing npm packages and Node APIs
Deno Basics
Deno runtime fundamentals including permissions, module system, and built-in tooling
Deno Deploy
Deno Deploy edge functions for globally distributed serverless applications
Fresh Framework
Fresh full-stack web framework for Deno with islands architecture and zero client JS by default