Elysia.js is a Bun-native HTTP framework with end-to-end type safety — new Elysia() creates the app. .get("/path", handler, { beforeHandle, schema: { query, params, response } }) registers a typed route. Schema validation uses Elysia’s t type builder (TypeBox-based): t.Object({ id: t.String(), page: t.Optional(t.Numeric()) }). .derive(({ request }) => ({ userId: getUserId(request) })) adds request-scoped context. .guard({ beforeHandle: [isAuthenticated] }) creates a protected scope. .use(swagger()) adds OpenAPI docs at /swagger. .use(bearer()) extracts Bearer tokens. Eden Treaty: const api = treaty<typeof app>("http://localhost:3000") creates a fully typed client — api.posts.get({ query: { page: 1 } }) returns typed data. Plugins: .use(cors()), .use(staticPlugin()), .use(jwt({ name: "jwt", secret })). .listen(3000) starts the server. Claude Code generates Elysia.js APIs, Eden Treaty clients, JWT auth plugins, and OpenAPI-documented route groups.
CLAUDE.md for Elysia.js
## Elysia.js Stack
- Version: elysia >= 1.1, @elysiajs/eden >= 1.1, @elysiajs/swagger >= 1.1
- Init: const app = new Elysia().use(swagger()).use(cors()).listen(3000)
- Route: app.get("/users/:id", ({ params: { id } }) => getUser(id), { params: t.Object({ id: t.String() }) })
- Body: app.post("/users", ({ body }) => createUser(body), { body: t.Object({ name: t.String(), email: t.String() }) })
- Group: app.group("/api", app => app.get("/health", () => ({ ok: true })))
- Guard: app.guard({ beforeHandle: [isAuthenticated] }, app => app.get("/me", ({ user }) => user))
- Derive: app.derive(({ request }) => ({ token: request.headers.get("authorization") }))
- Eden client: const api = treaty<typeof app>("http://localhost:3000")
Elysia App Setup
// src/index.ts — Elysia app with plugins and routes
import { Elysia, t } from "elysia"
import { swagger } from "@elysiajs/swagger"
import { cors } from "@elysiajs/cors"
import { bearer } from "@elysiajs/bearer"
import { jwt } from "@elysiajs/jwt"
import { db } from "./db"
import { users, posts } from "./db/schema"
import { eq, desc, ilike, count } from "drizzle-orm"
import { hash, verify } from "@node-rs/argon2"
// ── JWT plugin ─────────────────────────────────────────────────────────────
const jwtPlugin = jwt({
name: "jwt",
secret: process.env.JWT_SECRET!,
exp: "7d",
})
// ── Auth middleware ────────────────────────────────────────────────────────
const authMiddleware = new Elysia({ name: "auth-middleware" })
.use(bearer())
.use(jwtPlugin)
.derive({ as: "scoped" }, async ({ bearer, jwt, set }) => {
if (!bearer) {
set.status = 401
throw new Error("Unauthorized")
}
const payload = await jwt.verify(bearer)
if (!payload || typeof payload.sub !== "string") {
set.status = 401
throw new Error("Invalid token")
}
const user = await db.query.users.findFirst({
where: eq(users.id, payload.sub),
})
if (!user) {
set.status = 401
throw new Error("User not found")
}
return { currentUser: user }
})
// ── Posts routes ───────────────────────────────────────────────────────────
const postsRoutes = new Elysia({ prefix: "/posts" })
.get(
"/",
async ({ query }) => {
const { page = 1, pageSize = 10, search } = query
const offset = (page - 1) * pageSize
const where = search ? ilike(posts.title, `%${search}%`) : undefined
const [result, totalResult] = await Promise.all([
db.query.posts.findMany({
where,
limit: pageSize,
offset,
orderBy: desc(posts.createdAt),
}),
db.select({ count: count() }).from(posts),
])
return {
posts: result,
total: totalResult[0]?.count ?? 0,
page,
pageSize,
totalPages: Math.ceil((totalResult[0]?.count ?? 0) / pageSize),
}
},
{
query: t.Object({
page: t.Optional(t.Numeric()),
pageSize: t.Optional(t.Numeric()),
search: t.Optional(t.String()),
}),
},
)
.get("/:id", async ({ params: { id }, set }) => {
const post = await db.query.posts.findFirst({ where: eq(posts.id, id) })
if (!post) {
set.status = 404
return { message: "Post not found" }
}
return post
}, {
params: t.Object({ id: t.String() }),
})
.use(authMiddleware)
.post(
"/",
async ({ body, currentUser, set }) => {
const slug = body.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "")
const [post] = await db.insert(posts).values({
...body,
slug,
authorId: currentUser.id,
}).returning()
set.status = 201
return post
},
{
body: t.Object({
title: t.String({ minLength: 5, maxLength: 200 }),
content: t.String({ minLength: 20 }),
excerpt: t.Optional(t.String({ maxLength: 500 })),
published: t.Optional(t.Boolean()),
}),
},
)
.patch(
"/:id",
async ({ params: { id }, body, currentUser, set }) => {
const existing = await db.query.posts.findFirst({ where: eq(posts.id, id) })
if (!existing) return (set.status = 404, { message: "Not found" })
if (existing.authorId !== currentUser.id) return (set.status = 403, { message: "Forbidden" })
const [updated] = await db.update(posts).set(body).where(eq(posts.id, id)).returning()
return updated
},
{
params: t.Object({ id: t.String() }),
body: t.Partial(t.Object({
title: t.String({ minLength: 5 }),
content: t.String({ minLength: 20 }),
excerpt: t.String({ maxLength: 500 }),
published: t.Boolean(),
})),
},
)
.delete(
"/:id",
async ({ params: { id }, currentUser, set }) => {
const existing = await db.query.posts.findFirst({ where: eq(posts.id, id) })
if (!existing) return (set.status = 404, { message: "Not found" })
if (existing.authorId !== currentUser.id) return (set.status = 403, { message: "Forbidden" })
await db.delete(posts).where(eq(posts.id, id))
set.status = 204
return null
},
{ params: t.Object({ id: t.String() }) },
)
// ── Auth routes ────────────────────────────────────────────────────────────
const authRoutes = new Elysia({ prefix: "/auth" })
.use(jwtPlugin)
.post(
"/register",
async ({ body, set }) => {
const existing = await db.query.users.findFirst({ where: eq(users.email, body.email) })
if (existing) {
set.status = 409
return { message: "Email already in use" }
}
const hashedPassword = await hash(body.password)
const [user] = await db.insert(users).values({
email: body.email,
name: body.name,
hashedPassword,
}).returning({
id: users.id,
email: users.email,
name: users.name,
role: users.role,
createdAt: users.createdAt,
})
const token = await jwt.sign({ sub: user.id, email: user.email })
set.status = 201
return { user, token }
},
{
body: t.Object({
email: t.String({ format: "email" }),
name: t.String({ minLength: 2, maxLength: 100 }),
password: t.String({ minLength: 8 }),
}),
},
)
.post(
"/login",
async ({ body, set }) => {
const user = await db.query.users.findFirst({ where: eq(users.email, body.email) })
if (!user) {
set.status = 401
return { message: "Invalid credentials" }
}
const valid = await verify(user.hashedPassword ?? "", body.password)
if (!valid) {
set.status = 401
return { message: "Invalid credentials" }
}
const token = await jwt.sign({ sub: user.id, email: user.email })
return {
user: { id: user.id, email: user.email, name: user.name, role: user.role },
token,
}
},
{
body: t.Object({
email: t.String(),
password: t.String(),
}),
},
)
// ── Main app ───────────────────────────────────────────────────────────────
export const app = new Elysia()
.use(swagger({
documentation: {
info: { title: "My API", version: "1.0.0" },
tags: [
{ name: "posts", description: "Post management" },
{ name: "auth", description: "Authentication" },
],
},
}))
.use(cors({
origin: process.env.APP_URL ?? "http://localhost:3000",
credentials: true,
}))
.onError(({ code, error, set }) => {
if (code === "VALIDATION") {
set.status = 400
return { message: "Validation error", issues: (error as any).all }
}
if (code === "NOT_FOUND") {
set.status = 404
return { message: "Route not found" }
}
console.error("[Elysia error]", error)
set.status = 500
return { message: "Internal server error" }
})
.get("/health", () => ({
status: "ok",
timestamp: new Date().toISOString(),
uptime: process.uptime(),
}))
.use(postsRoutes)
.use(authRoutes)
.listen(process.env.PORT ?? 3000)
export type App = typeof app
Eden Treaty Client
// lib/api/client.ts — Eden Treaty typed client
import { treaty } from "@elysiajs/eden"
import type { App } from "../../server/src/index"
// Fully type-safe — inferred from server App type
export const api = treaty<App>(
process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000",
{
headers() {
const token = typeof window !== "undefined"
? localStorage.getItem("access_token")
: null
return token ? { Authorization: `Bearer ${token}` } : {}
},
},
)
// Example usage (fully typed):
// const { data, error } = await api.posts.get({ query: { page: 1 } })
// data?.posts ← Post[]
//
// const { data, error } = await api.auth.login.post({ email, password })
// data?.token ← string
//
// const { data } = await api.posts({ id }).delete() // DELETE /posts/:id
For the Hono alternative when deploying to Cloudflare Workers, Deno Deploy, or other edge runtimes beyond Bun is needed — Hono runs everywhere (Node.js, Bun, Cloudflare Workers, Deno) while Elysia is specifically optimized for Bun’s runtime and achieves higher throughput on Bun, see the Hono guide. For the Fastify alternative when a mature Node.js ecosystem with a large plugin registry, JSON Schema validation, and proven production use at scale (millions of requests per day) is preferred over bleeding-edge Bun performance — Fastify has a much larger community and plugin ecosystem while Elysia has a more ergonomic TypeScript API, see the Fastify guide. The Claude Skills 360 bundle includes Elysia.js skill sets covering APIs, Eden Treaty, and JWT auth. Start with the free tier to try Bun-native API generation.