Build or update the BlueBubbles external channel plugin for Moltbot (extension package, REST...
npx skills add lammesen/skills --skill "elysiajs-expert"
Install specific skill from multi-skill repository
# Description
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.
# SKILL.md
name: elysiajs-expert
description: 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.
allowed-tools: Read, Write, Edit, Bash, Grep, Glob
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: '[email protected]'
})
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
See
See
See
See
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.