elysiajs-expert

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.

1スター
0フォーク
更新日 1/8/2026
SKILL.md
readonlyread-only
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.

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

  1. Always use method chaining - Maintains type inference
  2. Name plugins - Enables deduplication
  3. Use resolve over derive - When validation is needed first
  4. Export type App - For Eden client type safety
  5. Use guards - For shared validation across routes
  6. Local hooks by default - Explicit as: 'scoped' or as: 'global'
  7. Extract services - Outside Elysia for testability
  8. 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

gog

169Kdev-api

Google Workspace CLI for Gmail, Calendar, Drive, Contacts, Sheets, and Docs.

openclaw avataropenclaw
入手
weather

weather

169Kdev-api

Get current weather and forecasts (no API key required).

openclaw avataropenclaw
入手

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 avatarlanggenius
入手
blucli

blucli

92Kdev-api

BluOS CLI (blu) for discovery, playback, grouping, and volume.

moltbot avatarmoltbot
入手
ordercli

ordercli

92Kdev-api

Foodora-only CLI for checking past orders and active order status (Deliveroo WIP).

moltbot avatarmoltbot
入手
gifgrep

gifgrep

92Kdev-api

Search GIF providers with CLI/TUI, download results, and extract stills/sheets.

moltbot avatarmoltbot
入手