关于 elysiajs-expert
elysiajs-expert 是一项技能,提供使用 Elysia 和 Bun 构建类型安全 Web 应用的指导,涵盖 API、验证、认证、WebSocket、插件、测试与部署。
主要功能
- 01Elysia 路由与请求生命周期
- 02TypeBox 和 Schema 验证
- 03认证与会话模式
- 04WebSocket 服务开发
- 05插件与 API 文档
- 06测试和部署指导
适用场景
- 01使用 Elysia 构建经过验证的 REST API
- 02添加认证或 WebSocket 路由
- 03编写 API 文档并配置插件
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ lammesen/ skills --skill elysiajs-expert安装后在 Claude Code 中输入/elysiajs-expert确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
patterns/api-design.mdauthentication.mdtesting.mdwebsocket.mdreference/core-api.mdeden-client.mdlifecycle-hooks.mdplugins.mdtypebox-validation.mdtemplates/auth-api.mdbasic-api.mdfullstack.md详细介绍
ElysiaJS 专家技能
此技能提供全面的专业知识,帮助你在 Bun 运行时上使用 Elysia 构建高性能、完全类型安全的 Web 应用。此技能假设 bun-expert 技能已启用,用于处理 Bun 特有模式(文件 I/O、SQLite、测试、构建)。
适用场景
- 使用 Elysia 构建 REST API
- 使用 TypeBox 实现类型安全的请求/响应验证
- 设置身份验证(JWT、Bearer 令牌、会话)
- 创建 WebSocket 服务
- 生成 OpenAPI/Swagger 文档
- 使用 Eden Treaty 构建全栈应用
- 配置 Elysia 插件(CORS、静态文件、cron、GraphQL、tRPC)
- 测试 Elysia 应用
- 优化生产环境部署
快速开始
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
核心概念
Elysia 构造函数选项
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 方法
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
路径参数
.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
上下文对象
每个处理器都会接收一个上下文对象,其中包含:
{
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
}
响应模式
// 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)
}
})
生命周期钩子(执行顺序)
请求 → 解析 → 转换 → 验证 → BeforeHandle → 处理器 → AfterHandle → MapResponse → AfterResponse
onRequest(全局,在路由匹配前)
.onRequest(({ request, ip, set, status }) => {
// Rate limiting, CORS preflight, request logging
if (rateLimiter.exceeded(ip)) return status(429)
})
onParse(请求体解析器)
.onParse(({ request, contentType }) => {
if (contentType === 'application/custom')
return request.text()
})
// Or specify parser explicitly
.post('/', handler, { parse: 'json' }) // 'json' | 'text' | 'formdata' | 'urlencoded' | 'none'
onTransform(验证前)
.get('/id/:id', handler, {
transform({ params }) {
params.id = +params.id // Convert to number before validation
}
})
derive(创建上下文属性——验证前)
.derive(({ headers }) => ({
bearer: headers.authorization?.startsWith('Bearer ')
? headers.authorization.slice(7)
: null
}))
.get('/protected', ({ bearer }) => bearer)
onBeforeHandle(验证后)
.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(创建上下文属性——验证后,类型安全)
.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(转换响应)
.onAfterHandle(({ responseValue, set }) => {
if (isHtml(responseValue))
set.headers['content-type'] = 'text/html'
})
mapResponse(自定义响应映射)
.mapResponse(({ responseValue, set }) => {
set.headers['content-encoding'] = 'gzip'
return new Response(Bun.gzipSync(JSON.stringify(responseValue)))
})
onError(错误处理)
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(清理、日志记录)
.onAfterResponse(({ set, request }) => {
console.log(`${request.method} ${request.url} - ${set.status}`)
})
钩子作用域
// 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 验证(Elysia.t)
基本类型
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 位置
.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 专属类型
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
自定义错误消息
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 支持(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(全局可变存储)
.state('counter', 0)
.state('users', new Map())
.get('/count', ({ store }) => store.counter++)
decorate(不可变上下文属性)
.decorate('logger', new Logger())
.decorate('version', '1.0.0')
.decorate({ db: database, cache: redis })
.get('/', ({ logger, version }) => {
logger.log('Request')
return version
})
分组与守卫
分组(路由前缀)
.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)
)
守卫(共享验证/钩子)
.guard({
headers: t.Object({ authorization: t.String() }),
beforeHandle: checkAuth
}, app => app
.get('/protected1', handler1)
.get('/protected2', handler2)
)
插件架构
创建插件
// 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: '...' }))
插件作用域
const authPlugin = new Elysia()
.onBeforeHandle({ as: 'scoped' }, checkAuth) // Applies to parent too
.derive({ as: 'global' }, getUser) // Applies everywhere
.as('scoped') // Lift entire plugin
延迟加载
.use(import('./heavy-plugin'))
await app.modules // Wait for all async plugins
WebSocket 支持
基本 WebSocket
.ws('/ws', {
message(ws, message) {
ws.send('Received: ' + message)
}
})
完整 WebSocket 处理器
.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 方法
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
宏模式
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 })
官方插件
@elysiajs/openapi(API 文档)
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(令牌提取)
import { bearer } from '@elysiajs/bearer'
.use(bearer())
.get('/protected', ({ bearer, status }) => {
if (!bearer) return status(401)
return `Token: ${bearer}`
})
@elysiajs/cors(跨域)
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(静态文件)
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(定时任务)
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 集成)
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(性能标头)
import { serverTiming } from '@elysiajs/server-timing'
.use(serverTiming({
enabled: process.env.NODE_ENV !== 'production'
}))
Eden Treaty(类型安全客户端)
设置
// 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')
路径语法
api.index.get() // /
api.user({ id: '123' }).get() // /user/123
api.deep.nested.path.get() // /deep/nested/path
请求参数
// 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 } })
错误处理
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 客户端
const chat = api.chat.subscribe()
chat.on('open', () => chat.send('hello'))
chat.subscribe(message => console.log(message))
chat.raw // Native WebSocket access
流处理
const { data } = await api.stream.get()
for await (const chunk of data) {
console.log(chunk)
}
Eden 配置
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 */ }
})
使用 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')
测试模式
使用 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' })
})
})
使用 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')
})
生产环境模式
推荐的项目结构
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
模块模式
// 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))
生产构建
# Compile to binary
bun build --compile --minify-whitespace --minify-syntax \
--target bun-linux-x64 --outfile server src/index.ts
集群模式
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')
}
最佳实践
- 始终使用方法链式调用 - 保持类型推断
- 为插件命名 - 启用去重
- 优先使用 resolve,而非 derive - 需要先进行验证时
- 导出类型 App - 确保 Eden 客户端的类型安全
- 使用守卫 - 在多个路由间共享验证
- 默认使用局部钩子 - 明确指定
as: 'scoped'或as: 'global' - 提取服务 - 将服务放在 Elysia 外部,以便进行测试
- 使用 status() 函数 - 实现类型安全的状态响应
参考资料
完整的 API 文档请参阅 <reference/core-api.md>。 钩子执行详情请参阅 <reference/lifecycle-hooks.md>。 所有插件配置请参阅 <reference/plugins.md>。 身份验证实现请参阅 <patterns/authentication.md>。 测试策略请参阅 <patterns/testing.md>。
评论
支持 Markdown