Bun-native HTTP framework — routing, TypeBox validation, Eden Treaty, plugins, lifecycle hooks
Scanned 9/5/2026
Install to Claude Code
npx -y skills add agents-inc/skills --skill api-framework-elysia --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Api Framework Elysia?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/agents-inc-api-framework-elysia-skills)More formats (shields.io, HTML) on the badges page.
---
name: api-framework-elysia
description: Bun-native HTTP framework — routing, TypeBox validation, Eden Treaty, plugins, lifecycle hooks
---
# API Development with Elysia
> **Quick Guide:** Elysia is a Bun-native HTTP framework with end-to-end type safety. Use method chaining (not separate statements) so TypeScript infers the full route tree. Import `t` from `elysia` for TypeBox validation. Export the app type (`export type App = typeof app`) for Eden Treaty clients. Use `status()` (not the deprecated `error()` function) for error responses with type narrowing.
---
<critical_requirements>
## CRITICAL: Before Using This Skill
> **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
**(You MUST use method chaining on the Elysia instance -- separate `.get()` calls break type inference for Eden Treaty)**
**(You MUST use `status()` for error responses -- `error()` is deprecated since 1.3, prefer `status()`)**
**(You MUST export the app type (`export type App = typeof app`) for Eden Treaty client generation)**
</critical_requirements>
---
**Auto-detection:** Elysia, elysia, ElysiaJS, Eden Treaty, @elysiajs/eden, @elysiajs/openapi, t.Object, t.String, t.Number, t.File, TypeBox, .derive(), .decorate(), .guard(), .macro(), .ws(), onBeforeHandle, onAfterHandle, onRequest, treaty, bun:test
**When to use:**
- Building APIs on Bun runtime with end-to-end type safety
- Need RPC-style client with zero code generation (Eden Treaty)
- TypeBox validation with AOT compilation (~18x faster than Zod on Bun)
- Plugin-based architecture with automatic type propagation
- WebSocket support with schema validation
**When NOT to use:**
- Deploying to Node.js-only environments without Bun (use a Node-first framework)
- Need OpenAPI-first design with `createRoute` patterns (other frameworks with Zod-OpenAPI integration are more mature for this)
- Team already committed to Express/Fastify ecosystem
**Key patterns covered:**
- Route definitions with method chaining and TypeBox validation
- Plugin architecture with `.use()`, `.derive()`, `.decorate()`, `.macro()`
- Scoping rules (local, scoped, global) and `.guard()`
- End-to-end type safety with Eden Treaty
- Lifecycle hooks (onRequest, onBeforeHandle, onAfterHandle, onError)
- Error handling with custom error classes and `status()`
- WebSocket with schema validation
- Testing with `bun:test` and `.handle()` or Eden Treaty
**Detailed Resources:**
- [examples/core.md](examples/core.md) - Route setup, method chaining, validation, plugins
- [examples/eden-treaty.md](examples/eden-treaty.md) - End-to-end type-safe client
- [examples/lifecycle-errors.md](examples/lifecycle-errors.md) - Lifecycle hooks, error handling, custom errors
- [examples/websocket-testing.md](examples/websocket-testing.md) - WebSocket patterns and unit testing
- [reference.md](reference.md) - Decision frameworks, anti-patterns, production checklist
---
<philosophy>
## Philosophy
**Method chaining IS the type system.** Elysia infers the entire route tree through chained calls. Breaking the chain (separate `app.get()` statements) loses type information for Eden Treaty clients. This is the single most important architectural constraint.
**TypeBox over Zod for Bun.** While Elysia 1.4+ supports Standard Schema (Zod, Valibot, etc.), TypeBox (`t` from `elysia`) uses AOT compilation inside Bun for ~18x faster validation. Use TypeBox as default; use Zod only when sharing schemas with a non-Bun codebase.
**Plugins are Elysia instances.** Every `new Elysia()` is a plugin. There is no separate plugin API -- you compose by chaining `.use()`. The `name` property deduplicates plugins across the tree.
</philosophy>
---
<patterns>
## Core Patterns
### Pattern 1: Route Setup with Method Chaining
Chain route definitions on the Elysia instance. Each `.get()`, `.post()`, etc. returns the instance with updated type information.
```typescript
import { Elysia, t } from "elysia";
const app = new Elysia()
.get("/", () => "hello")
.get("/user/:id", ({ params: { id } }) => id, {
params: t.Object({
id: t.Number(),
}),
})
.post("/user", ({ body }) => body, {
body: t.Object({
name: t.String(),
email: t.String({ format: "email" }),
}),
})
.listen(3000);
export type App = typeof app;
```
**Why good:** method chaining preserves type inference across the entire route tree, `export type App` enables Eden Treaty, TypeBox validates at runtime with AOT compilation
See [examples/core.md](examples/core.md) for complete route setup with modular plugins.
---
### Pattern 2: Plugin Architecture
Every Elysia instance is a plugin. Use `.use()` to compose, `name` to deduplicate.
```typescript
import { Elysia } from "elysia";
const userPlugin = new Elysia({ name: "user", prefix: "/user" })
.get("/", () => "list users")
.get("/:id", ({ params: { id } }) => `user ${id}`);
const app = new Elysia().use(userPlugin).listen(3000);
```
**Why good:** `name` prevents duplicate registration when a plugin is `.use()`-d multiple times, `prefix` scopes routes cleanly
See [examples/core.md](examples/core.md) for `.derive()`, `.decorate()`, and `.macro()` patterns.
---
### Pattern 3: Scoping with Guard
Apply validation schemas and lifecycle hooks to groups of routes.
```typescript
import { Elysia, t } from "elysia";
const app = new Elysia()
.guard(
{
headers: t.Object({
authorization: t.String(),
}),
},
(app) =>
app
.get("/protected", ({ headers }) => headers.authorization)
.post("/admin", ({ body }) => body, {
body: t.Object({ action: t.String() }),
}),
)
.get("/public", () => "no auth needed");
```
**Why good:** guard encapsulates validation for route groups without repeating schema definitions, public routes outside the guard are unaffected
---
### Pattern 4: Error Handling with status()
Use `status()` for typed error responses. Register custom error classes with `.error()`.
```typescript
import { Elysia } from "elysia";
const HTTP_UNAUTHORIZED = 401;
const HTTP_NOT_FOUND = 404;
class NotFoundError extends Error {
status = HTTP_NOT_FOUND;
constructor(public resource: string) {
super(`${resource} not found`);
}
}
const app = new Elysia()
.error({ NotFoundError })
.onError(({ code, error, status }) => {
if (code === "NotFoundError") {
return status(HTTP_NOT_FOUND, { error: error.message });
}
if (code === "VALIDATION") {
return status(HTTP_UNAUTHORIZED, { error: "Validation failed" });
}
})
.get("/user/:id", ({ params: { id }, status }) => {
const user = findUser(id);
if (!user) throw new NotFoundError("User");
return user;
});
```
**Why good:** `status()` provides type narrowing for error responses, custom error classes with `.error()` enable `code`-based type narrowing in `onError`, named constants avoid magic status numbers
See [examples/lifecycle-errors.md](examples/lifecycle-errors.md) for all lifecycle hooks and error patterns.
---
### Pattern 5: Eden Treaty Client
Export the app type and use `treaty()` for a fully type-safe client with no code generation.
```typescript
// server.ts
import { Elysia, t } from "elysia";
const app = new Elysia()
.get("/user/:id", ({ params: { id } }) => ({ id, name: "Alice" }), {
params: t.Object({ id: t.Number() }),
})
.post("/user", ({ body }) => body, {
body: t.Object({ name: t.String() }),
});
export type App = typeof app;
```
```typescript
// client.ts
import { treaty } from "@elysiajs/eden";
import type { App } from "./server";
const api = treaty<App>("localhost:3000");
const { data, error } = await api.user({ id: 1 }).get();
// data is typed as { id: number; name: string } | null
// error is typed based on error responses
```
**Why good:** zero code generation, full autocomplete on paths and methods, error/data destructuring with type narrowing
See [examples/eden-treaty.md](examples/eden-treaty.md) for response handling, file uploads, and WebSocket via Treaty.
</patterns>
---
<red_flags>
## RED FLAGS
**High Priority:**
- Separate `app.get()` / `app.post()` calls instead of chaining -- breaks Eden Treaty type inference entirely
- Using deprecated `error()` function instead of `status()` -- deprecated since Elysia 1.3
- Not exporting `type App = typeof app` -- Eden Treaty client has no type information
- Using `as('plugin')` -- removed in 1.3+, use `as('scoped')` instead
**Medium Priority:**
- Importing patterns from other HTTP frameworks -- Elysia has its own routing API and conventions
- Using `t.Object()` without named constants for limits/lengths -- magic numbers in validation schemas
- Not providing `name` on plugin instances -- causes duplicate registration in complex app trees
- `.derive()` or lifecycle hooks without `{ as: 'scoped' }` or `{ as: 'global' }` when parent routes need them -- hooks are local-scoped by default
**Gotchas & Edge Cases:**
- `params` are strings by default -- use `t.Number()` in the schema to coerce path params to numbers
- `.handle()` in tests requires a fully qualified URL (`http://localhost/path`), NOT a path fragment (`/path`)
- `.guard()` group standalone mode is default in 1.4+ -- guard schemas merge with route schemas instead of overwriting
- TypeBox `t.File()` auto-detects `multipart/form-data` content type -- no need to set headers manually
- `t.Files()` (plural) for multiple file uploads, `t.File()` for single
- Eden Treaty dynamic path params use function syntax: `api.user({ id: 1 }).get()` not `api.user[1].get()`
- When Eden Treaty response has status >= 300, `data` is always `null` and `error` has the value
- Lifecycle hooks only apply to routes registered AFTER the hook -- order of `.on*()` and route definitions matters
- `onError` receives a `code` string, not a status number -- switch on `code` for type narrowing
- Cookies parse as JSON automatically if the value looks like JSON (1.3+ behavior)
- `await app.modules` is required in tests when using lazy-loaded plugins (`import('./plugin')`)
</red_flags>
---
<critical_reminders>
## CRITICAL REMINDERS
> **All code must follow project conventions in CLAUDE.md**
**(You MUST use method chaining on the Elysia instance -- separate `.get()` calls break type inference for Eden Treaty)**
**(You MUST use `status()` for error responses -- `error()` is deprecated since 1.3, prefer `status()`)**
**(You MUST export the app type (`export type App = typeof app`) for Eden Treaty client generation)**
**Failure to follow these rules will break end-to-end type safety and Eden Treaty client generation.**
</critical_reminders>
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!