dist/plugins/api-framework-elysia/skills/api-framework-elysia/SKILL.md
Bun-native HTTP framework — routing, TypeBox validation, Eden Treaty, plugins, lifecycle hooks
npx skillsauth add agents-inc/skills api-framework-elysiaInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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
tfromelysiafor TypeBox validation. Export the app type (export type App = typeof app) for Eden Treaty clients. Usestatus()(not the deprecatederror()function) for error responses with type narrowing.
<critical_requirements>
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:
When NOT to use:
createRoute patterns (other frameworks with Zod-OpenAPI integration are more mature for this)Key patterns covered:
.use(), .derive(), .decorate(), .macro().guard()status()bun:test and .handle() or Eden TreatyDetailed Resources:
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.
Chain route definitions on the Elysia instance. Each .get(), .post(), etc. returns the instance with updated type information.
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 for complete route setup with modular plugins.
Every Elysia instance is a plugin. Use .use() to compose, name to deduplicate.
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 for .derive(), .decorate(), and .macro() patterns.
Apply validation schemas and lifecycle hooks to groups of routes.
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
Use status() for typed error responses. Register custom error classes with .error().
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 for all lifecycle hooks and error patterns.
Export the app type and use treaty() for a fully type-safe client with no code generation.
// 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;
// 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 for response handling, file uploads, and WebSocket via Treaty.
</patterns><red_flags>
High Priority:
app.get() / app.post() calls instead of chaining -- breaks Eden Treaty type inference entirelyerror() function instead of status() -- deprecated since Elysia 1.3type App = typeof app -- Eden Treaty client has no type informationas('plugin') -- removed in 1.3+, use as('scoped') insteadMedium Priority:
t.Object() without named constants for limits/lengths -- magic numbers in validation schemasname 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 defaultGotchas & 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 overwritingt.File() auto-detects multipart/form-data content type -- no need to set headers manuallyt.Files() (plural) for multiple file uploads, t.File() for singleapi.user({ id: 1 }).get() not api.user[1].get()data is always null and error has the value.on*() and route definitions mattersonError receives a code string, not a status number -- switch on code for type narrowingawait app.modules is required in tests when using lazy-loaded plugins (import('./plugin'))</red_flags>
<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>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety