协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
elysia-patterns【开发与工程】Use this skill when implementing Elysia.js patterns — Eden Treaty client, WebSocket handlers, auth/guard macros, custom plugins, performance optimization, and type-safe validation. This skill enforces: type-safe client-server communication, macro-based guards, schema-first validation. Requires Bun (bun create elysia). Do NOT use for: Express middlewa…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Implement Elysia.js-specific patterns — Eden Treaty type-safe client, WebSocket with Bun, macro-based guards, custom plugins, and validation patterns.
User request includes: elysia pattern, elysia eden treaty, elysia websocket, elysia plugin, elysia macro, elysia validation, elysia type safe.
Code examples for the requested patterns — plugin, macro, WebSocket handler, Eden client.
Code-first: pattern name, problem, implementation. Compress output.
4096 tokens
| Criterion | Eden Treaty | tRPC | REST+OpenAPI |
|---|---|---|---|
| Backend | Elysia only | Node.js only | Any |
| Type safety | Full-stack inference | Full-stack inference | Generated client |
| Setup | Single typeof app | Manual procedure defs | External tooling |
| File upload | Declarative | Manual | Multipart |
| Websocket | Via plugin | Via subscription | Separate |
| Flexibility | Elysia-native | tRPC server | Any HTTP |
Decision: Elysia backend + any frontend → Eden Treaty. tRPC backend → tRPC fits better. Multi-language clients → REST+OpenAPI.
| Pattern | Scope | Use Case |
|---|---|---|
Macro (guard, resolve) | Route-level | Auth check, permission guard |
Middleware (onRequest, beforeHandle) | Global or group | Logging, CORS, rate limit |
Plugin (new Elysia({name})) | Modular feature | User module, auth module |
Decision: Reusable route modifier → Macro. Cross-cutting concern → Middleware. Standalone feature → Plugin.
// server/src/app.ts — EXPORT TYPE
import { Elysia } from 'elysia'
import { userModule } from './modules/users'
export const app = new Elysia()
.use(userModule)
.get('/health', () => ({ status: 'ok' }))
export type App = typeof app
// client/src/api.ts
import { edenTreaty } from '@elysiajs/eden'
import type { App } from '../../server/src/app'
export const client = edenTreaty<App>('http://localhost:3000', {
headers: () => ({
authorization: `Bearer ${localStorage.getItem('token')}`,
}),
})
// React component
async function UserProfile({ id }: { id: string }) {
const { data, error } = await client.users({ id }).get()
if (error) return <div>Error: {error.message}</div>
return <div>{data.name}</div>
}
// src/plugins/realtime.ts
import { Elysia, t } from 'elysia'
import { ws } from '@elysiajs/websocket'
export const realtimePlugin = new Elysia()
.use(ws())
.ws('/ws/orders/:orderId', {
body: t.Object({
type: t.String(),
payload: t.Any(),
}),
async open(ws) {
ws.subscribe(`order:${ws.data.params.orderId}`)
ws.send({ type: 'connected', payload: { orderId: ws.data.params.orderId } })
},
async message(ws, message) {
// Broadcast to all subscribers of this order
ws.publish(`order:${ws.data.params.orderId}`, message)
},
async close(ws) {
ws.unsubscribe(`order:${ws.data.params.orderId}`)
},
})
// Emit from service
function notifyOrderStatus(orderId: string, status: string) {
app.server?.publish(`order:${orderId}`, {
type: 'status_update',
payload: { orderId, status },
})
}
// src/plugins/audit-log.ts
import { Elysia } from 'elysia'
interface AuditLogConfig {
excludePaths?: string[]
db: { save: (entry: AuditEntry) => Promise<void> }
}
interface AuditEntry {
method: string
path: string
userId: string | null
statusCode: number
duration: number
timestamp: Date
}
export const auditLogPlugin = (config: AuditLogConfig) =>
new Elysia({ name: 'audit-log' })
.derive({ as: 'scoped' }, () => ({ startTime: performance.now() }))
.onResponse(({ request, set, store, startTime }) => {
const url = new URL(request.url)
if (config.excludePaths?.includes(url.pathname)) return
const entry: AuditEntry = {
method: request.method,
path: url.pathname,
userId: (request as any).user?.id || null,
statusCode: set.status || 200,
duration: performance.now() - startTime,
timestamp: new Date(),
}
config.db.save(entry).catch(console.error)
})
// Usage
app.use(auditLogPlugin({
excludePaths: ['/health'],
db: auditRepository,
}))
// src/plugins/rate-limit.ts
import { Elysia } from 'elysia'
const requestCounts = new Map<string, { count: number; resetAt: number }>()
export const rateLimitPlugin = new Elysia({ name: 'rate-limit' })
.macro({
// @ts-ignore
rateLimit: {
beforeHandle({ headers, set }, limit: number = 60) {
const ip = headers['x-forwarded-for'] || 'unknown'
const now = Date.now()
const entry = requestCounts.get(ip)
if (!entry || now > entry.resetAt) {
requestCounts.set(ip, { count: 1, resetAt: now + 60000 })
return
}
entry.count++
if (entry.count > limit) {
set.status = 429
set.headers['Retry-After'] = String(Math.ceil((entry.resetAt - now) / 1000))
return { error: 'Rate limit exceeded', retryAfter: Math.ceil((entry.resetAt - now) / 1000) }
}
}
}
})
// Usage
app.guard({ rateLimit: 100 })
.get('/api/data', () => fetchData())
// src/modules/orders/schema.ts
import { t } from 'elysia'
// Composable schema parts
const AddressSchema = t.Object({
street: t.String(),
city: t.String(),
zipCode: t.String({ pattern: '^\\d{5}#x27; }),
})
const OrderItemSchema = t.Object({
productId: t.String({ format: 'uuid' }),
quantity: t.Numeric({ minimum: 1, maximum: 999 }),
price: t.Numeric({ exclusiveMinimum: 0 }),
})
const PaymentSchema = t.Object({
method: t.Union([t.Literal('card'), t.Literal('transfer'), t.Literal('cod')]),
cardToken: t.Optional(t.String()),
})
// Composed schema
export const CreateOrderSchema = t.Object({
items: t.Array(OrderItemSchema, { minItems: 1 }),
shippingAddress: AddressSchema,
payment: PaymentSchema,
notes: t.Optional(t.String({ maxLength: 500 })),
})
export const OrderResponseSchema = t.Object({
id: t.String({ format: 'uuid' }),
orderNumber: t.String(),
status: t.String(),
total: t.Number(),
items: t.Array(t.Composite([OrderItemSchema, t.Object({ id: t.String() })])),
createdAt: t.String({ format: 'date-time' }),
})
// src/plugins/response-transform.ts
import { Elysia } from 'elysia'
export const responseTransformPlugin = new Elysia({ name: 'response-transform' })
.onAfterHandle({ as: 'global' }, ({ response, set }) => {
if (response && typeof response === 'object' && !('success' in response) && !(response instanceof Response)) {
return { success: true, data: response }
}
})
import { Elysia, t } from 'elysia'
export const uploadPlugin = new Elysia({ name: 'upload' })
.post('/api/upload', async ({ body: { file } }) => {
const buffer = await file.arrayBuffer()
const filename = `${crypto.randomUUID()}-${file.name}`
await Bun.write(`./uploads/${filename}`, buffer)
return { success: true, filename, size: buffer.byteLength }
}, {
body: t.Object({
file: t.File({ maxSize: 10 * 1024 * 1024 }), // 10MB limit
}),
})
.post('/api/upload/multiple', async ({ body: { files } }) => {
const results = await Promise.all(
files.map(async (f) => {
const buf = await f.arrayBuffer()
const name = `${crypto.randomUUID()}-${f.name}`
await Bun.write(`./uploads/${name}`, buf)
return { name, size: buf.byteLength }
})
)
return { success: true, files: results }
}, {
body: t.Object({
files: t.Array(t.File({ maxSize: 5 * 1024 * 1024 }), { maxItems: 10 }),
}),
})
import { Elysia } from 'elysia'
export const errorPlugin = new Elysia({ name: 'error-handler' })
.onError({ as: 'global' }, ({ error, code, set }) => {
if (code === 'VALIDATION') {
set.status = 400
return { success: false, code: 'VALIDATION_ERROR', details: error.validator.Errors(error.value).First() }
}
if (code === 'NOT_FOUND') {
set.status = 404
return { success: false, code: 'NOT_FOUND', message: error.message }
}
if (code === 'PARSE') {
set.status = 400
return { success: false, code: 'PARSE_ERROR', message: 'Invalid request body' }
}
console.error('Unhandled error:', error)
set.status = 500
return { success: false, code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' }
})
// Usage
const app = new Elysia()
.use(errorPlugin)
.get('/users/:id', ({ params: { id }, error }) => {
const user = findUser(id)
if (!user) return error(404, 'User not found')
return user
})
// Elysia production config
const app = new Elysia({
precompile: true, // Pre-compile handlers for faster startup
strictPath: true, // Strict path matching (/users != /users/)
normalize: false, // Skip path normalization in production
})
.use(compression())
.use(cors())
.listen(3000)
precompile: true — reduces cold start by 40-60%compression() plugin for gzip/brotli response encoding$fetch instead of fetch option for faster client requestsprocess.memoryUsage(), set --smol flag in Docker// Simple in-memory cache plugin
export const cachePlugin = new Elysia({ name: 'cache' })
.derive({ as: 'scoped' }, () => {
const cache = new Map<string, { data: any; expires: number }>()
return {
cacheGet: (key: string) => {
const entry = cache.get(key)
if (entry && entry.expires > Date.now()) return entry.data
cache.delete(key)
return null
},
cacheSet: (key: string, data: any, ttlMs: number = 60000) => {
cache.set(key, { data, expires: Date.now() + ttlMs })
}
}
})
const app = createApp().listen(port)
process.on('SIGTERM', async () => {
console.log('Shutting down...')
app.stop()
await db.close()
process.exit(0)
})
import { Elysia } from 'elysia'
// Define service
class UserService {
constructor(private db: Database) {}
async findById(id: string) { return this.db.query('SELECT * FROM users WHERE id = $1', [id]) }
}
// Register in app state
const app = new Elysia()
.decorate('db', new Database(process.env.DATABASE_URL!))
.decorate('userService', new UserService(new Database(process.env.DATABASE_URL!)))
.get('/users/:id', ({ params: { id }, userService }) => userService.findById(id))
// Plugin-scoped DI
const userModule = new Elysia({ name: 'user-module' })
.decorate('userService', new UserService(new Database(process.env.DATABASE_URL!)))
.get('/users/:id', ({ userService, params: { id } }) => userService.findById(id))
.post('/users', ({ body, userService }) => userService.create(body))
const app = new Elysia().use(userModule)
// `userService` is only available inside userModule routes
import { Elysia, t } from 'elysia'
import { swagger } from '@elysiajs/swagger'
const app = new Elysia()
.use(swagger({
path: '/docs',
documentation: {
info: { title: 'My API', version: '1.0.0', description: 'Elysia API documentation' },
tags: [{ name: 'Users', description: 'User endpoints' }, { name: 'Orders', description: 'Order endpoints' }],
},
}))
.get('/users', () => listUsers(), {
detail: {
tags: ['Users'],
summary: 'List all users',
parameters: [{ name: 'page', in: 'query', schema: { type: 'number' } }],
responses: { 200: { description: 'User list' } },
},
})
| Anti-Pattern | Why | Fix |
|---|---|---|
| Global schema reuse across modules | Tight coupling | Each module owns its schema |
Eden Treaty with typeof without export | Client can't infer types | Export App type from app.ts |
| WebSocket in non-ws plugin | Unclear ownership | Create dedicated realtime plugin |
| Heavy computation in macros | Blocks request pipeline | Offload to service layer |
Missing {as: 'scoped'} on derive | Memory leak (state persists) | Always scope per-request state |
| Using Express middleware directly | Elysia handles middleware differently | Convert to Elysia onRequest/beforeHandle |
Not using precompile: true in prod | Slower cold starts | Enable precompile for production deployments |
| Decorating state in base app for all modules | Memory grows with every module | Scope state to plugin via decorate in named plugins |
import { describe, expect, test } from 'bun:test'
import { Elysia } from 'elysia'
import { rateLimitPlugin } from './rate-limit'
test('rate limit blocks after threshold', async () => {
const app = new Elysia()
.use(rateLimitPlugin)
.guard({ rateLimit: 2 })
.get('/test', () => 'ok')
await app.handle(new Request('http://localhost/test')) // 200
await app.handle(new Request('http://localhost/test')) // 200
const res = await app.handle(new Request('http://localhost/test')) // 429
expect(res.status).toBe(429)
})
Use bun test with WebSocket constructor. Test connection, message send/receive, and disconnect handlers.
typeof app — export the App type from the main entry point.new Elysia({name})) for proper deduplication by Elysia's plugin system.as: 'scoped' on all per-request state. as: 'global' only for app-wide singletons.Hand off to backend/elysia/architecture/SKILL.md for project structure or backend/universal/api-response/SKILL.md for API response formatting.