Skip to main content
Technology & EngineeringDeno Bun310 lines

Elysia Bun

Elysia web framework on Bun for type-safe, high-performance HTTP APIs

Quick Summary18 lines
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 lines
Paste into your CLAUDE.md or agent config

Elysia 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() and swagger() 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.Number for query string parameters. Query parameters arrive as strings. t.Numeric parses string values to numbers, while t.Number validates the value as already being a number. Using t.Number for 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 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.

Common Pitfalls

  • 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.
  • Eden import confusion@elysiajs/eden has both treaty (REST-like) and edenFetch (fetch-like) clients. Use treaty for most cases.

Install this skill directly: skilldb add deno-bun-skills

Get CLI access →