mcpField
E
elysiajs-expert

elysiajs-expert

github.com/lammesen开发52 次安装
代码仓库

关于 elysiajs-expert

elysiajs-expert 是一项技能,提供使用 Elysia 和 Bun 构建类型安全 Web 应用的指导,涵盖 API、验证、认证、WebSocket、插件、测试与部署。

主要功能

  1. 01Elysia 路由与请求生命周期
  2. 02TypeBox 和 Schema 验证
  3. 03认证与会话模式
  4. 04WebSocket 服务开发
  5. 05插件与 API 文档
  6. 06测试和部署指导

适用场景

  1. 01使用 Elysia 构建经过验证的 REST API
  2. 02添加认证或 WebSocket 路由
  3. 03编写 API 文档并配置插件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/lammesen/skills --skill elysiajs-expert

安装后在 Claude Code 中输入/elysiajs-expert确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

patterns/
api-design.md
authentication.md
testing.md
websocket.md
reference/
core-api.md
eden-client.md
lifecycle-hooks.md
plugins.md
typebox-validation.md
templates/
auth-api.md
basic-api.md
fullstack.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')
}

最佳实践

  1. 始终使用方法链式调用 - 保持类型推断
  2. 为插件命名 - 启用去重
  3. 优先使用 resolve,而非 derive - 需要先进行验证时
  4. 导出类型 App - 确保 Eden 客户端的类型安全
  5. 使用守卫 - 在多个路由间共享验证
  6. 默认使用局部钩子 - 明确指定 as: 'scoped' 或 as: 'global'
  7. 提取服务 - 将服务放在 Elysia 外部,以便进行测试
  8. 使用 status() 函数 - 实现类型安全的状态响应

参考资料

完整的 API 文档请参阅 <reference/core-api.md>。 钩子执行详情请参阅 <reference/lifecycle-hooks.md>。 所有插件配置请参阅 <reference/plugins.md>。 身份验证实现请参阅 <patterns/authentication.md>。 测试策略请参阅 <patterns/testing.md>。

评论

支持 Markdown