Elysia
"Elysia: Bun-native web framework with type-safe routing, TypeBox validation, plugins, Eden treaty client, lifecycle hooks, and Swagger documentation"
Elysia is a Bun-native web framework designed for ergonomics and performance. It achieves end-to-end type safety through TypeBox validation schemas that simultaneously validate runtime data and infer TypeScript types — no separate type definitions needed. Every route's input and output types propagate to the Eden treaty client, giving you tRPC-like developer experience with standard HTTP semantics. ## Key Points - Use TypeBox (`t.Object`, `t.String`, etc.) for all validation — it provides runtime validation and compile-time type inference simultaneously. - Structure the application with plugins: one plugin per domain (auth, users, posts) composed in the main entry point. - Export the app type for the Eden treaty client to get full end-to-end type safety without codegen. - Use `derive` to compute and inject values (current user, permissions) into the handler context. - Define `response` schemas on routes to enable automatic Swagger documentation and response validation. - Use `guard` to scope authentication and authorization to groups of routes without repeating middleware. - Handle errors globally with `onError` and use typed error codes (`VALIDATION`, `NOT_FOUND`) for consistent responses. - **Not using TypeBox for validation.** Skipping validation loses Elysia's core advantage of automatic type inference from runtime schemas. - **Putting all routes in a single file.** Use plugins and `group` to separate concerns; a monolithic app file is hard to navigate and test. - **Running Elysia on Node.js.** Elysia is designed for Bun; running it on Node forfeits performance benefits and may cause compatibility issues. - **Ignoring the `set` object for status codes.** Returning data without setting `set.status` defaults to 200, even for creation or deletion. - **Deeply nested plugin dependencies.** Keep the plugin graph shallow; deep nesting creates hard-to-debug type inference chains.
skilldb get api-frameworks-skills/ElysiaFull skill: 365 linesElysia
Core Philosophy
Elysia is a Bun-native web framework designed for ergonomics and performance. It achieves end-to-end type safety through TypeBox validation schemas that simultaneously validate runtime data and infer TypeScript types — no separate type definitions needed. Every route's input and output types propagate to the Eden treaty client, giving you tRPC-like developer experience with standard HTTP semantics.
Elysia's architecture is built around a plugin system and lifecycle hooks. Plugins compose functionality (auth, CORS, Swagger) while hooks provide fine-grained control over the request/response pipeline. The framework consistently benchmarks as one of the fastest, leveraging Bun's optimized HTTP server and its own compile-time route optimization.
Setup
Basic Application
// Install: bun add elysia
// src/index.ts
import { Elysia } from "elysia";
const app = new Elysia()
.get("/", () => "Hello Elysia!")
.get("/json", () => ({ message: "typed response", timestamp: Date.now() }))
.listen(3000);
console.log(`Server running at ${app.server?.hostname}:${app.server?.port}`);
Application with Configuration
import { Elysia } from "elysia";
import { cors } from "@elysiajs/cors";
import { swagger } from "@elysiajs/swagger";
const app = new Elysia({ prefix: "/api" })
.use(cors({
origin: ["https://example.com"],
methods: ["GET", "POST", "PUT", "DELETE"],
}))
.use(swagger({
documentation: {
info: { title: "My API", version: "1.0.0" },
tags: [
{ name: "Users", description: "User management" },
{ name: "Posts", description: "Post operations" },
],
},
}))
.listen(3000);
Key Techniques
Type-Safe Routing with Validation
import { Elysia, t } from "elysia";
const app = new Elysia()
// Path parameters with validation
.get("/users/:id", ({ params: { id } }) => {
return db.user.findUnique({ where: { id } });
}, {
params: t.Object({
id: t.String({ format: "uuid" }),
}),
})
// Query parameters
.get("/posts", ({ query }) => {
return db.post.findMany({
take: query.limit,
skip: (query.page - 1) * query.limit,
where: query.published !== undefined ? { published: query.published } : undefined,
orderBy: { createdAt: query.sort },
});
}, {
query: t.Object({
page: t.Number({ default: 1, minimum: 1 }),
limit: t.Number({ default: 20, minimum: 1, maximum: 100 }),
sort: t.Optional(t.Union([t.Literal("asc"), t.Literal("desc")])),
published: t.Optional(t.Boolean()),
}),
})
// Request body validation
.post("/posts", async ({ body, set }) => {
const post = await db.post.create({ data: body });
set.status = 201;
return post;
}, {
body: t.Object({
title: t.String({ minLength: 1, maxLength: 200 }),
content: t.String({ minLength: 10 }),
tags: t.Array(t.String(), { default: [], maxItems: 5 }),
published: t.Boolean({ default: false }),
}),
})
// Response schema for documentation and validation
.get("/users/:id/profile", ({ params: { id } }) => {
return getProfile(id);
}, {
params: t.Object({ id: t.String() }),
response: t.Object({
id: t.String(),
name: t.String(),
email: t.String({ format: "email" }),
bio: t.Optional(t.String()),
postCount: t.Number(),
}),
});
Plugins and Modular Architecture
// src/plugins/auth.ts
import { Elysia, t } from "elysia";
import { jwt } from "@elysiajs/jwt";
import { bearer } from "@elysiajs/bearer";
export const authPlugin = new Elysia({ name: "auth" })
.use(jwt({ name: "jwt", secret: process.env.JWT_SECRET! }))
.use(bearer())
.derive(async ({ jwt, bearer, set }) => {
if (!bearer) {
set.status = 401;
throw new Error("Authentication required");
}
const payload = await jwt.verify(bearer);
if (!payload) {
set.status = 401;
throw new Error("Invalid token");
}
return {
user: { id: payload.sub as string, role: payload.role as string },
};
});
// src/plugins/database.ts
export const dbPlugin = new Elysia({ name: "database" })
.decorate("db", prisma)
.onStop(async () => {
await prisma.$disconnect();
});
// src/routes/users.ts
export const userRoutes = new Elysia({ prefix: "/users" })
.use(dbPlugin)
.get("/", async ({ db }) => {
return db.user.findMany({
select: { id: true, name: true, email: true },
});
})
.use(authPlugin)
.get("/me", async ({ user, db }) => {
return db.user.findUnique({ where: { id: user.id } });
})
.put("/me", async ({ user, body, db }) => {
return db.user.update({ where: { id: user.id }, data: body });
}, {
body: t.Object({
name: t.Optional(t.String({ minLength: 1 })),
bio: t.Optional(t.String({ maxLength: 500 })),
}),
});
// src/index.ts — Compose everything
const app = new Elysia()
.use(userRoutes)
.use(postRoutes)
.listen(3000);
Lifecycle Hooks
const app = new Elysia()
// Runs before route matching — use for logging, rate limiting
.onRequest(({ request }) => {
console.log(`${request.method} ${new URL(request.url).pathname}`);
})
// Runs before handler — use for auth, validation preprocessing
.onBeforeHandle(({ set, request }) => {
const apiKey = request.headers.get("x-api-key");
if (apiKey && !isValidApiKey(apiKey)) {
set.status = 403;
return { error: "Invalid API key" };
}
})
// Runs after handler — use for response transformation
.onAfterHandle(({ response, set }) => {
if (typeof response === "object" && response !== null) {
return { data: response, timestamp: new Date().toISOString() };
}
})
// Error handler
.onError(({ code, error, set }) => {
switch (code) {
case "VALIDATION":
set.status = 400;
return { error: "Validation failed", details: error.message };
case "NOT_FOUND":
set.status = 404;
return { error: "Route not found" };
default:
console.error("Unhandled error:", error);
set.status = 500;
return { error: "Internal server error" };
}
});
Eden Treaty Client
// Install: bun add @elysiajs/eden
// server.ts — Export the app type
const app = new Elysia()
.get("/users", () => db.user.findMany(), {
response: t.Array(t.Object({
id: t.String(),
name: t.String(),
email: t.String(),
})),
})
.post("/users", ({ body }) => db.user.create({ data: body }), {
body: t.Object({
name: t.String(),
email: t.String({ format: "email" }),
}),
})
.get("/users/:id", ({ params: { id } }) => db.user.findUnique({ where: { id } }), {
params: t.Object({ id: t.String() }),
})
.listen(3000);
export type App = typeof app;
// client.ts — Fully typed HTTP client
import { treaty } from "@elysiajs/eden";
import type { App } from "./server";
const api = treaty<App>("localhost:3000");
// Full autocompletion and type inference
const { data: users, error } = await api.users.get();
const { data: newUser } = await api.users.post({
name: "Alice",
email: "alice@example.com",
});
const { data: user } = await api.users({ id: "abc-123" }).get();
// Error handling is typed
if (error) {
switch (error.status) {
case 400: console.error("Validation:", error.value); break;
case 401: console.error("Unauthorized"); break;
}
}
Guards and Scoped Middleware
const app = new Elysia()
// Public routes
.get("/health", () => ({ status: "ok" }))
.get("/posts", () => db.post.findMany({ where: { published: true } }))
// Guard scopes all subsequent routes with auth requirement
.guard(
{
beforeHandle: async ({ bearer, jwt, set }) => {
const payload = await jwt.verify(bearer);
if (!payload) {
set.status = 401;
return { error: "Unauthorized" };
}
},
},
(app) =>
app
.get("/me", ({ user }) => user)
.post("/posts", ({ body, user }) =>
db.post.create({ data: { ...body, authorId: user.id } })
)
.delete("/posts/:id", async ({ params: { id }, user, set }) => {
const post = await db.post.findUnique({ where: { id } });
if (post?.authorId !== user.id) {
set.status = 403;
return { error: "Forbidden" };
}
await db.post.delete({ where: { id } });
set.status = 204;
})
);
WebSocket Support
const app = new Elysia()
.ws("/chat/:room", {
body: t.Object({
message: t.String({ minLength: 1 }),
}),
params: t.Object({
room: t.String(),
}),
open(ws) {
ws.subscribe(`room:${ws.data.params.room}`);
ws.publish(`room:${ws.data.params.room}`, {
system: true,
message: "User joined",
});
},
message(ws, { message }) {
ws.publish(`room:${ws.data.params.room}`, {
author: ws.id,
message,
timestamp: Date.now(),
});
},
close(ws) {
ws.publish(`room:${ws.data.params.room}`, {
system: true,
message: "User left",
});
},
});
Best Practices
- Use TypeBox (
t.Object,t.String, etc.) for all validation — it provides runtime validation and compile-time type inference simultaneously. - Structure the application with plugins: one plugin per domain (auth, users, posts) composed in the main entry point.
- Export the app type for the Eden treaty client to get full end-to-end type safety without codegen.
- Use
deriveto compute and inject values (current user, permissions) into the handler context. - Define
responseschemas on routes to enable automatic Swagger documentation and response validation. - Use
guardto scope authentication and authorization to groups of routes without repeating middleware. - Handle errors globally with
onErrorand use typed error codes (VALIDATION,NOT_FOUND) for consistent responses.
Anti-Patterns
- Not using TypeBox for validation. Skipping validation loses Elysia's core advantage of automatic type inference from runtime schemas.
- Putting all routes in a single file. Use plugins and
groupto separate concerns; a monolithic app file is hard to navigate and test. - Running Elysia on Node.js. Elysia is designed for Bun; running it on Node forfeits performance benefits and may cause compatibility issues.
- Ignoring the
setobject for status codes. Returning data without settingset.statusdefaults to 200, even for creation or deletion. - Deeply nested plugin dependencies. Keep the plugin graph shallow; deep nesting creates hard-to-debug type inference chains.
- Using
derivefor expensive operations without caching. Derived values run on every request in scope; cache database lookups or token verifications when appropriate.
Install this skill directly: skilldb add api-frameworks-skills
Related Skills
Express.js
"Express.js with TypeScript: routing, middleware patterns, error handling, validation, authentication, static files, CORS, and production-ready configuration"
Fastify
Fastify: high-performance Node.js web framework with schema-based validation, logging, plugin architecture, and TypeScript support
Apollo GraphQL
"Apollo GraphQL: schema design, resolvers, Apollo Server, Apollo Client with React, useQuery/useMutation, caching strategies, subscriptions, and codegen"
Hono
"Hono: ultra-fast web framework for edge, serverless, and Node.js — middleware, routing, Zod validation, JWT, CORS, RPC client, and JSX support"
Koa
Koa: lightweight Node.js framework by the Express team with async/await middleware, context object, and composable architecture
NestJS
NestJS: progressive Node.js framework with decorators, dependency injection, modules, guards, pipes, and interceptors for scalable APIs