
elysiajs-expert
Expert guidance for ElysiaJS web framework development. Use when building REST APIs, GraphQL services, or WebSocket applications with Elysia on Bun. Covers routing, lifecycle hooks, TypeBox validation, Eden type-safe clients, authentication with JWT/Bearer, all official plugins (OpenAPI, CORS, JWT, static, cron, GraphQL, tRPC), testing patterns, and production deployment. Assumes bun-expert skill is active for Bun runtime expertise.
Expert guidance for ElysiaJS web framework development. Use when building REST APIs, GraphQL services, or WebSocket applications with Elysia on Bun. Covers routing, lifecycle hooks, TypeBox validation, Eden type-safe clients, authentication with JWT/Bearer, all official plugins (OpenAPI, CORS, JWT, static, cron, GraphQL, tRPC), testing patterns, and production deployment. Assumes bun-expert skill is active for Bun runtime expertise.
ElysiaJS Expert Skill
This skill provides comprehensive expertise for building high-performance, fully type-safe web applications with Elysia on the Bun runtime. It assumes the bun-expert skill is active for Bun-specific patterns (file I/O, SQLite, testing, builds).
When to Use This Skill
- Building REST APIs with Elysia
- Implementing type-safe request/response validation with TypeBox
- Setting up authentication (JWT, Bearer tokens, sessions)
- Creating WebSocket servers
- Generating OpenAPI/Swagger documentation
- Building full-stack applications with Eden Treaty
- Configuring Elysia plugins (CORS, static files, cron, GraphQL, tRPC)
- Testing Elysia applications
- Production deployment optimization
Quick Start
import { Elysia, t } from 'elysia'
const app = new Elysia()
.get('/', () => 'Hello Elysia')
.get('/user/:id', ({ params }) => `User ${params.id}`)
.post('/user', ({ body }) => body, {
body: t.Object({
name: t.String(),
email: t.String({ format: 'email' })
})
})
.listen(3000)
export type App = typeof app // Export for Eden client
Core Concepts
Elysia Constructor Options
new Elysia({
name: 'my-app', // Plugin deduplication identifier
prefix: '/api', // Route prefix
seed: config, // Deduplication checksum seed
websocket: { // WebSocket configuration
idleTimeout: 30,
maxPayloadLength: 16777216
}
})
HTTP Methods
app
.get('/path', handler) // GET request
.post('/path', handler) // POST request
.put('/path', handler) // PUT request
.delete('/path', handler) // DELETE request
.patch('/path', handler) // PATCH request
.options('/path', handler) // OPTIONS request
.all('/path', handler) // All methods
.route('CUSTOM', '/path', handler) // Custom HTTP verb
Path Parameters
.get('/user/:id', ({ params }) => params.id) // Required param
.get('/user/:id?', ({ params }) => params.id ?? 'n/a') // Optional param
.get('/files/*', ({ params }) => params['*']) // Wildcard
.get('/org/:org/repo/:repo', ({ params }) => params) // Multiple params
Context Object
Every handler receives a context object with:
{
body, // Parsed request body
query, // Query string as object
params, // Path parameters
headers, // Request headers (lowercase keys)
cookie, // Cookie jar with get/set
store, // Global mutable state
set, // Response setters (status, headers)
request, // Raw Request object
path, // Request path
server, // Bun server instance
redirect, // Redirect function
status, // Status response function
// + decorated/derived properties
}
Response Patterns
// String
.get('/', () => 'Hello')
// JSON (auto-serialized)
.get('/json', () => ({ hello: 'world' }))
// Status with response
.get('/error', ({ status }) => status(418, "I'm a teapot"))
// Custom headers
.get('/custom', ({ set }) => {
set.headers['x-powered-by'] = 'Elysia'
return 'Hello'
})
// Redirect
.get('/old', ({ redirect }) => redirect('/new'))
// File
import { file } from 'elysia'
.get('/image', () => file('image.png'))
// Streaming (generator)
.get('/stream', function* () {
yield 'Hello '
yield 'World'
})
// Async streaming
.get('/async', async function* () {
for (let i = 0; i < 10; i++) {
yield `Event ${i}\n`
await Bun.sleep(100)
}
})
Lifecycle Hooks (Execution Order)
Request → Parse → Transform → Validation → BeforeHandle → Handler → AfterHandle → MapResponse → AfterResponse
onRequest (Global, Before Routing)
.onRequest(({ request, ip, set, status }) => {
// Rate limiting, CORS preflight, request logging
if (rateLimiter.exceeded(ip)) return status(429)
})
onParse (Body Parser)
.onParse(({ request, contentType }) => {
if (contentType === 'application/custom')
return request.text()
})
// Or specify parser explicitly
.post('/', handler, { parse: 'json' }) // 'json' | 'text' | 'formdata' | 'urlencoded' | 'none'
onTransform (Before Validation)
.get('/id/:id', handler, {
transform({ params }) {
params.id = +params.id // Convert to number before validation
}
})
derive (Creates Context Properties - Before Validation)
.derive(({ headers }) => ({
bearer: headers.authorization?.startsWith('Bearer ')
? headers.authorization.slice(7)
: null
}))
.get('/protected', ({ bearer }) => bearer)
onBeforeHandle (After Validation)
.onBeforeHandle(({ cookie, status }) => {
if (!validateSession(cookie.session.value))
return status(401, 'Unauthorized')
})
// Local hook
.get('/protected', handler, {
beforeHandle({ headers, status }) {
if (!headers.authorization) return status(401)
}
})
resolve (Creates Context Properties - After Validation, Type-Safe)
.guard({
headers: t.Object({ authorization: t.TemplateLiteral('Bearer ${string}') })
})
.resolve(({ headers }) => ({
token: headers.authorization.split(' ')[1],
userId: decodeToken(headers.authorization)
}))
.get('/me', ({ userId }) => userId)
onAfterHandle (Transform Response)
.onAfterHandle(({ responseValue, set }) => {
if (isHtml(responseValue))
set.headers['content-type'] = 'text/html'
})
mapResponse (Custom Response Mapping)
.mapResponse(({ responseValue, set }) => {
set.headers['content-encoding'] = 'gzip'
return new Response(Bun.gzipSync(JSON.stringify(responseValue)))
})
onError (Error Handling)
import { Elysia, NotFoundError } from 'elysia'
.onError(({ code, error, status }) => {
switch(code) {
case 'NOT_FOUND': return status(404, 'Not Found')
case 'VALIDATION': return { errors: error.all }
case 'PARSE': return status(400, 'Invalid body')
case 'INTERNAL_SERVER_ERROR': return status(500)
default: return new Response(error.toString())
}
})
onAfterResponse (Cleanup, Logging)
.onAfterResponse(({ set, request }) => {
console.log(`${request.method} ${request.url} - ${set.status}`)
})
Hook Scoping
// Hooks are LOCAL by default in Elysia 1.0+
.onBeforeHandle({ as: 'local' }, handler) // Current instance only
.onBeforeHandle({ as: 'scoped' }, handler) // Parent + current + descendants
.onBeforeHandle({ as: 'global' }, handler) // All instances
TypeBox Validation (Elysia.t)
Basic Types
import { Elysia, t } from 'elysia'
.post('/user', handler, {
body: t.Object({
name: t.String({ minLength: 2, maxLength: 100 }),
email: t.String({ format: 'email' }),
age: t.Number({ minimum: 0, maximum: 150 }),
active: t.Boolean(),
tags: t.Array(t.String()),
role: t.Union([t.Literal('admin'), t.Literal('user')]),
metadata: t.Optional(t.Object({ createdAt: t.String() }))
})
})
Schema Locations
.post('/example', handler, {
body: t.Object({ ... }), // Request body
query: t.Object({ ... }), // Query string
params: t.Object({ ... }), // Path params
headers: t.Object({ ... }), // Headers (lowercase keys!)
cookie: t.Cookie({ ... }), // Cookies
response: t.Object({ ... }) // Response validation
})
// Response per status code
.get('/user', handler, {
response: {
200: t.Object({ user: UserSchema }),
400: t.Object({ error: t.String() }),
404: t.Object({ message: t.String() })
}
})
Elysia-Specific Types
t.Numeric() // Coerces string to number (query/params)
t.File({ format: 'image/*' }) // Single file upload
t.Files() // Multiple files
t.Cookie({ session: t.String() }, {
secure: true, httpOnly: true, sameSite: 'strict'
})
t.TemplateLiteral('Bearer ${string}') // Template literal validation
t.UnionEnum(['draft', 'published']) // Enum-like union
Custom Error Messages
t.Object({
email: t.String({
format: 'email',
error: 'Please provide a valid email'
}),
age: t.Number({
minimum: 18,
error({ value }) {
return `Age must be 18+ (got ${value})`
}
})
})
Standard Schema Support (Zod, Valibot)
import { z } from 'zod'
import * as v from 'valibot'
.get('/user/:id', handler, {
params: z.object({ id: z.coerce.number() }),
query: v.object({ name: v.literal('test') })
})
State Management
state (Global Mutable Store)
.state('counter', 0)
.state('users', new Map())
.get('/count', ({ store }) => store.counter++)
decorate (Immutable Context Properties)
.decorate('logger', new Logger())
.decorate('version', '1.0.0')
.decorate({ db: database, cache: redis })
.get('/', ({ logger, version }) => {
logger.log('Request')
return version
})
Groups and Guards
Groups (Route Prefixes)
.group('/api/v1', app => app
.get('/users', handler)
.post('/users', handler)
)
// With guard configuration
.group('/admin', {
headers: t.Object({ 'x-admin-key': t.String() })
}, app => app
.get('/stats', handler)
)
Guards (Shared Validation/Hooks)
.guard({
headers: t.Object({ authorization: t.String() }),
beforeHandle: checkAuth
}, app => app
.get('/protected1', handler1)
.get('/protected2', handler2)
)
Plugin Architecture
Creating Plugins
// As Elysia instance (recommended)
const userPlugin = new Elysia({ name: 'user' })
.state('users', [])
.decorate('userService', new UserService())
.get('/users', ({ store }) => store.users)
// As function (access parent config)
const configPlugin = (config: Config) =>
new Elysia({ name: 'config', seed: config })
.decorate('config', config)
// Usage
new Elysia()
.use(userPlugin)
.use(configPlugin({ apiKey: '...' }))
Plugin Scoping
const authPlugin = new Elysia()
.onBeforeHandle({ as: 'scoped' }, checkAuth) // Applies to parent too
.derive({ as: 'global' }, getUser) // Applies everywhere
.as('scoped') // Lift entire plugin
Lazy Loading
.use(import('./heavy-plugin'))
await app.modules // Wait for all async plugins
WebSocket Support
Basic WebSocket
.ws('/ws', {
message(ws, message) {
ws.send('Received: ' + message)
}
})
Full WebSocket Handler
.ws('/chat', {
// Validation
body: t.Object({ message: t.String() }),
query: t.Object({ room: t.String() }),
open(ws) {
const { room } = ws.data.query
ws.subscribe(room)
ws.publish(room, 'User joined')
},
message(ws, { message }) {
ws.publish(ws.data.query.room, message)
},
close(ws) {
ws.publish(ws.data.query.room, 'User left')
},
// Authentication
beforeHandle({ headers, status }) {
if (!headers.authorization) return status(401)
}
})
WebSocket Methods
ws.send(data) // Send to connection
ws.publish(topic, data) // Publish to topic
ws.subscribe(topic) // Subscribe to topic
ws.unsubscribe(topic) // Unsubscribe
ws.close() // Close connection
ws.data // Access context (query, params)
ws.id // Unique connection ID
Macro Patterns
const authPlugin = new Elysia({ name: 'auth' })
.macro({
isSignIn: {
async resolve({ cookie, status }) {
if (!cookie.session.value) return status(401)
return { user: await getUser(cookie.session.value) }
}
}
})
// Usage
.use(authPlugin)
.get('/profile', ({ user }) => user, { isSignIn: true })
Official Plugins
@elysiajs/openapi (API Documentation)
import { openapi } from '@elysiajs/openapi'
.use(openapi({
provider: 'scalar', // 'scalar' | 'swagger-ui' | null
path: '/docs',
documentation: {
info: { title: 'My API', version: '1.0.0' },
tags: [{ name: 'User', description: 'User endpoints' }],
components: {
securitySchemes: {
bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }
}
}
},
exclude: { methods: ['OPTIONS'], paths: ['/health'] }
}))
.get('/user', handler, {
detail: {
tags: ['User'],
summary: 'Get user',
security: [{ bearerAuth: [] }]
}
})
@elysiajs/jwt (JSON Web Token)
import { jwt } from '@elysiajs/jwt'
.use(jwt({
name: 'jwt',
secret: process.env.JWT_SECRET!,
exp: '7d'
}))
.post('/login', async ({ jwt, body, cookie: { auth } }) => {
const token = await jwt.sign({ userId: body.id })
auth.set({ value: token, httpOnly: true, maxAge: 7 * 86400 })
return { token }
})
.get('/profile', async ({ jwt, bearer, status }) => {
const profile = await jwt.verify(bearer)
if (!profile) return status(401)
return profile
})
@elysiajs/bearer (Token Extraction)
import { bearer } from '@elysiajs/bearer'
.use(bearer())
.get('/protected', ({ bearer, status }) => {
if (!bearer) return status(401)
return `Token: ${bearer}`
})
@elysiajs/cors (Cross-Origin)
import { cors } from '@elysiajs/cors'
.use(cors({
origin: ['https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 600
}))
@elysiajs/static (Static Files)
import { staticPlugin } from '@elysiajs/static'
.use(staticPlugin({
assets: 'public',
prefix: '/static',
indexHTML: true
}))
@elysiajs/html (HTML/JSX)
import { html } from '@elysiajs/html'
.use(html())
.get('/', () => `
<html>
<body><h1>Hello</h1></body>
</html>
`)
@elysiajs/cron (Scheduled Tasks)
import { cron } from '@elysiajs/cron'
.use(cron({
name: 'heartbeat',
pattern: '*/10 * * * * *', // Every 10 seconds
run() { console.log('tick') }
}))
@elysiajs/graphql-yoga (GraphQL)
import { yoga } from '@elysiajs/graphql-yoga'
.use(yoga({
typeDefs: `type Query { hello: String }`,
resolvers: { Query: { hello: () => 'Hello' } },
path: '/graphql'
}))
@elysiajs/trpc (tRPC Integration)
import { trpc, compile as c } from '@elysiajs/trpc'
import { initTRPC } from '@trpc/server'
const tr = initTRPC.create()
const router = tr.router({
greet: tr.procedure
.input(c(t.String()))
.query(({ input }) => `Hello ${input}`)
})
.use(trpc(router, { endpoint: '/trpc' }))
@elysiajs/server-timing (Performance Headers)
import { serverTiming } from '@elysiajs/server-timing'
.use(serverTiming({
enabled: process.env.NODE_ENV !== 'production'
}))
Eden Treaty (Type-Safe Client)
Setup
// server.ts
const app = new Elysia()
.get('/user/:id', ({ params }) => ({ id: params.id }))
.post('/user', ({ body }) => body, {
body: t.Object({ name: t.String() })
})
.listen(3000)
export type App = typeof app
// client.ts
import { treaty } from '@elysiajs/eden'
import type { App } from './server'
const api = treaty<App>('localhost:3000')
Path Syntax
api.index.get() // /
api.user({ id: '123' }).get() // /user/123
api.deep.nested.path.get() // /deep/nested/path
Request Parameters
// POST with body
const { data, error } = await api.user.post({ name: 'John' })
// With headers/query
await api.user.post({ name: 'John' }, {
headers: { authorization: 'Bearer token' },
query: { source: 'web' }
})
// GET with query
await api.users.get({ query: { page: 1, limit: 10 } })
Error Handling
const { data, error, status } = await api.user.post({ name })
if (error) {
switch(error.status) {
case 400: throw new ValidationError(error.value)
case 401: throw new AuthError(error.value)
default: throw error.value
}
}
return data // Type-safe, non-null after error check
WebSocket Client
const chat = api.chat.subscribe()
chat.on('open', () => chat.send('hello'))
chat.subscribe(message => console.log(message))
chat.raw // Native WebSocket access
Stream Handling
const { data } = await api.stream.get()
for await (const chunk of data) {
console.log(chunk)
}
Eden Configuration
const api = treaty<App>('localhost:3000', {
fetch: { credentials: 'include' },
headers: { authorization: 'Bearer token' },
headers: (path) => ({ /* dynamic headers */ }),
onRequest: (path, options) => { /* modify request */ },
onResponse: (response) => { /* modify response */ }
})
Unit Testing with Eden
import { treaty } from '@elysiajs/eden'
import { app } from './server'
// Pass instance directly - no network calls
const api = treaty(app)
const { data } = await api.user.post({ name: 'Test' })
expect(data.name).toBe('Test')
Testing Patterns
Unit Testing with bun:test
import { describe, expect, it } from 'bun:test'
import { Elysia } from 'elysia'
describe('API', () => {
const app = new Elysia()
.get('/hello', () => 'Hello')
.post('/user', ({ body }) => body)
it('returns hello', async () => {
const res = await app.handle(new Request('http://localhost/hello'))
expect(await res.text()).toBe('Hello')
})
it('creates user', async () => {
const res = await app.handle(new Request('http://localhost/user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Test' })
}))
expect(await res.json()).toEqual({ name: 'Test' })
})
})
Testing with Eden
import { treaty } from '@elysiajs/eden'
import { app } from './server'
const api = treaty(app)
it('should create user with type safety', async () => {
const { data, error } = await api.users.post({
name: 'John',
email: 'john@example.com'
})
expect(error).toBeNull()
expect(data?.name).toBe('John')
})
Production Patterns
Recommended Project Structure
src/
├── modules/
│ ├── auth/
│ │ ├── index.ts # Routes
│ │ ├── service.ts # Business logic
│ │ └── model.ts # TypeBox schemas
│ ├── user/
│ └── product/
├── shared/
│ ├── middleware/
│ └── utils/
├── config/
│ └── env.ts
├── index.ts
└── server.ts
Module Pattern
// src/modules/user/index.ts
import { Elysia } from 'elysia'
import { UserService } from './service'
import { CreateUserSchema, UserSchema } from './model'
export const userRoutes = new Elysia({ prefix: '/users' })
.post('/', ({ body }) => UserService.create(body), {
body: CreateUserSchema,
response: UserSchema
})
.get('/:id', ({ params }) => UserService.findById(params.id))
Production Build
# Compile to binary
bun build --compile --minify-whitespace --minify-syntax \
--target bun-linux-x64 --outfile server src/index.ts
Cluster Mode
import cluster from 'node:cluster'
import os from 'node:os'
if (cluster.isPrimary) {
for (let i = 0; i < os.availableParallelism(); i++) {
cluster.fork()
}
} else {
await import('./server')
}
Best Practices
- Always use method chaining - Maintains type inference
- Name plugins - Enables deduplication
- Use resolve over derive - When validation is needed first
- Export type App - For Eden client type safety
- Use guards - For shared validation across routes
- Local hooks by default - Explicit
as: 'scoped'oras: 'global' - Extract services - Outside Elysia for testability
- Use status() function - For type-safe status responses
References
See <reference/core-api.md> for complete API documentation.
See <reference/lifecycle-hooks.md> for hook execution details.
See <reference/plugins.md> for all plugin configurations.
See <patterns/authentication.md> for auth implementations.
See <patterns/testing.md> for testing strategies.
You Might Also Like
Related Skills

gog
Google Workspace CLI for Gmail, Calendar, Drive, Contacts, Sheets, and Docs.
openclaw
orpc-contract-first
Guide for implementing oRPC contract-first API patterns in Dify frontend. Triggers when creating new API contracts, adding service endpoints, integrating TanStack Query with typed contracts, or migrating legacy service calls to oRPC. Use for all API layer work in web/contract and web/service directories.
langgenius
