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' or as: '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.