界面已本地化;技能介绍与说明文档按所选语言由 AI 自动翻译,代码、命令、路径和专业术语保留原文,以原作者文档为准。
探索技能
开发与工程 · SKILL.md

elysia-architecture

elysia-architecture

【开发与工程】Use this skill when structuring Elysia.js applications — Bun-native web framework, plugin system, lifecycle hooks, type-safe routing with Eden Treaty. This skill enforces: plugin-based architecture, Elysia lifecycle (state/derive/resolve/onRequest/afterHandle), TypeScript strict mode, Eden Treaty client generation. Requires Bun (bun create elysia). D…

独立 GitHub 快照 · 自动分类 · 以原作者文档为准

技能说明

GitHub

name: elysia-architecture description: > Use this skill when structuring Elysia.js applications — Bun-native web framework, plugin system, lifecycle hooks, type-safe routing with Eden Treaty. This skill enforces: plugin-based architecture, Elysia lifecycle (state/derive/resolve/onRequest/afterHandle), TypeScript strict mode, Eden Treaty client generation. Requires Bun (bun create elysia). Do NOT use for: Express.js projects, Node.js apps, or non-Elysia Bun projects. version: "1.0.0" author: "j4flmao" license: "MIT" compatibility: claude-code: true cursor: true codex: true windsurf: true tags: [backend, elysia, phase-5]

Elysia Architecture

Purpose

Structure Elysia.js applications with plugin-based architecture — lifecycle hooks, type-safe schema, Eden Treaty client generation, and production middleware configuration.

Agent Protocol

Trigger

User request includes: elysia app structure, elysia project layout, elysia plugin, elysia lifecycle, elysia architecture, elysia folder structure, elysia config.

Input Context

  • Bun version (1.0+)
  • Elysia version (1.x)
  • Plugins used (cors, swagger, websocket, auth)
  • Database (Bun SQLite, Drizzle, Prisma)

Output Artifact

Project structure, plugin layout, lifecycle hook order, configuration, Eden Treaty client setup.

Response Format

Produce artifact directly. No preamble, no postamble, no explanations. No filler, no hedging.

Completion Criteria

  • Plugin-based architecture with clear separation
  • Lifecycle hooks in correct order
  • Type-safe schema with Elysia t.policy
  • Eden Treaty client generated for frontend
  • Environment config with Elysia config plugin

Max Response Length

4096 tokens

Architecture Decision Trees

Plugin Architecture vs Monolithic

CriterionPlugin ArchitectureMonolithic
Feature count5+ features1-3 features
Team size2+ developersSolo
ReuseMultiple apps share pluginsSingle app
TestingIsolated per pluginEnd-to-end
Bundle sizeLazy-loaded pluginsSingle bundle

Decision: Multi-feature app or shared modules → Plugin Architecture. Simple CRUD → Monolithic.

Lifecycle Hook Strategy

HookPurposeUse Case
stateInitialize app stateDB connections, config
deriveCompute derived stateRequest-local cache
resolveResolve dependenciesAuth user from token
onRequestPre-handlerLogging, CORS, rate limit
beforeHandlePre-validationAuth check, permissions
afterHandlePost-handlerTransform response, caching
onErrorError handlerGlobal error mapping
onResponseAfter response sentMetrics, cleanup

Order matters: state → derive → resolve → onRequest → beforeHandle → handler → afterHandle → onError/onResponse.

Workflow

Step 1: Project Structure

src/
  index.ts                    # Entry point — create app, register plugins
  app.ts                      # Elysia app factory
  config/
    env.ts                    # Environment config
    database.ts               # DB connection
  plugins/
    auth.ts                   # Auth plugin (resolve user, guard)
    cors.ts                   # CORS config plugin
    swagger.ts                # Swagger/OpenAPI plugin
    error-handler.ts          # Global error handler
  modules/
    users/
      index.ts                # User module plugin
      routes.ts               # User routes
      schema.ts               # Validation schemas
      service.ts              # Business logic
      repository.ts           # Data access
    orders/
      index.ts
      routes.ts
      schema.ts
      service.ts
  shared/
    response.ts               # Response helpers
    logger.ts                 # Logger setup
    pagination.ts             # Pagination types

Step 2: App Factory

// src/app.ts
import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'
import { swagger } from '@elysiajs/swagger'
import { authPlugin } from './plugins/auth'
import { errorHandlerPlugin } from './plugins/error-handler'
import { userModule } from './modules/users'
import { orderModule } from './modules/orders'

export const createApp = () => new Elysia()
  .use(cors({ origin: Bun.env.CORS_ORIGIN }))
  .use(swagger({ path: '/docs' }))
  .use(errorHandlerPlugin)
  .use(authPlugin)
  .use(userModule)
  .use(orderModule)
  .get('/health', () => ({ status: 'ok', uptime: process.uptime() }))

// src/index.ts
import { createApp } from './app'

const app = createApp()
const port = parseInt(Bun.env.PORT || '3000')
app.listen(port, () => console.log(`Server running on port ${port}`))

Step 3: Auth Plugin (Resolver Pattern)

// src/plugins/auth.ts
import { Elysia, t } from 'elysia'
import { jwt } from '@elysiajs/jwt'

export const authPlugin = new Elysia({ name: 'auth' })
  .use(jwt({ secret: Bun.env.JWT_SECRET! }))
  .derive({ as: 'scoped' }, async ({ jwt, headers }) => {
    const token = headers.authorization?.slice(7)
    if (!token) return { user: null }
    const payload = await jwt.verify(token)
    return { user: payload as { id: string; role: string } | null }
  })
  .macro({
    // @ts-ignore
    isAuthenticated: {
      beforeHandle({ user, error }) {
        if (!user) return error(401, { message: 'Unauthorized' })
      }
    },
    hasRole: (roles: string[]) => ({
      beforeHandle({ user, error }) {
        if (!user || !roles.includes(user.role))
          return error(403, { message: 'Forbidden' })
      }
    })
  })

// Usage in routes
app
  .guard({ isAuthenticated: true })
  .get('/users/me', ({ user }) => user)
  .guard({ hasRole: ['admin'] })
  .delete('/users/:id', ({ params }) => deleteUser(params.id))

Step 4: Module Pattern

// src/modules/users/schema.ts
import { t } from 'elysia'

export const CreateUserSchema = t.Object({
  name: t.String({ minLength: 2 }),
  email: t.String({ format: 'email' }),
  role: t.Optional(t.Union([t.Literal('admin'), t.Literal('user')])),
})

export const UserResponseSchema = t.Object({
  id: t.String(),
  name: t.String(),
  email: t.String(),
  role: t.String(),
})

export const UserParamsSchema = t.Object({
  id: t.String(),
})

// src/modules/users/routes.ts
import { Elysia, t } from 'elysia'
import { CreateUserSchema, UserResponseSchema, UserParamsSchema } from './schema'
import * as userService from './service'

export const userRoutes = new Elysia({ prefix: '/users' })
  .get('/', async ({ query }) => {
    const users = await userService.findAll(query)
    return { data: users }
  }, {
    query: t.Object({ page: t.Optional(t.Numeric()), limit: t.Optional(t.Numeric()) }),
  })
  .get('/:id', async ({ params }) => {
    const user = await userService.findById(params.id)
    if (!user) return { error: 'Not found', status: 404 }
    return { data: user }
  }, {
    params: UserParamsSchema,
    response: t.Object({ data: UserResponseSchema }),
  })
  .post('/', async ({ body }) => {
    const user = await userService.create(body)
    return { data: user }
  }, {
    body: CreateUserSchema,
    response: t.Object({ data: UserResponseSchema }),
    status: 201,
  })

// src/modules/users/index.ts
import { Elysia } from 'elysia'
import { userRoutes } from './routes'

export const userModule = new Elysia().use(userRoutes)

Step 5: Error Handler Plugin

// src/plugins/error-handler.ts
import { Elysia } from 'elysia'

export const errorHandlerPlugin = new Elysia({ name: 'error-handler' })
  .onError({ as: 'global' }, ({ code, error, set }) => {
    console.error(`[${code}] ${error.message}`)

    switch (code) {
      case 'VALIDATION':
        set.status = 400
        return { success: false, error: { code: 'VALIDATION_ERROR', message: error.message } }
      case 'NOT_FOUND':
        set.status = 404
        return { success: false, error: { code: 'NOT_FOUND', message: 'Resource not found' } }
      case 'INTERNAL_SERVER_ERROR':
        set.status = 500
        return { success: false, error: { code: 'INTERNAL_ERROR', message: 'Unexpected error' } }
      default:
        set.status = 500
        return { success: false, error: { code: 'UNKNOWN', message: error.message } }
    }
  })

Step 6: Environment Config

// src/config/env.ts
import { t } from 'elysia'

export const envSchema = t.Object({
  PORT: t.Optional(t.String()),
  NODE_ENV: t.Optional(t.Union([t.Literal('development'), t.Literal('production'), t.Literal('test')])),
  DATABASE_URL: t.String(),
  JWT_SECRET: t.String({ minLength: 32 }),
  CORS_ORIGIN: t.Optional(t.String()),
  LOG_LEVEL: t.Optional(t.String()),
})

export type Env = typeof envSchema.static

// Validate on startup
const envCheck = envSchema.safeParse(Bun.env)
if (!envCheck.success) {
  console.error('Invalid environment:', envCheck.error)
  process.exit(1)
}

Step 7: Eden Treaty Client

// client/eden.ts (separate frontend package)
import { edenTreaty } from '@elysiajs/eden'
import type { App } from '../server/src/app'

export const client = edenTreaty<App>('http://localhost:3000')

// Usage
const { data, error } = await client.users({ headers: { authorization: `Bearer ${token}` }}).get()
if (error) console.error(error)
else console.log(data)

Implementation Patterns

Pattern: Scoped State per Request

const app = new Elysia()
  .state('db', dbConnection)
  .derive({ as: 'scoped' }, ({ request }) => ({
    requestId: crypto.randomUUID(),
  }))
  .onRequest(({ request, store, requestId }) => {
    console.log(`[${requestId}] ${request.method} ${request.url}`)
  })

Pattern: Type-Safe Response

const app = new Elysia()
  .get('/orders/:id', ({ params }) => {
    return {
      id: params.id,
      items: [],
      total: 0
    }
  }, {
    response: t.Object({
      id: t.String(),
      items: t.Array(t.Object({
        product: t.String(),
        price: t.Number(),
        qty: t.Number(),
      })),
      total: t.Number(),
    })
  })

Production Considerations

Performance

  • Elysia uses Bun's HTTP server (native, fast) — no node:http overhead
  • Keep handlers async to leverage Bun's I/O multiplexing
  • Use t.Object validation — compile-time schemas, zero runtime overhead
  • Avoid resolve for every request — prefer derive for per-request data
  • Static files via @elysiajs/static plugin (Bun.file for custom serving)

CORS Configuration

app.use(cors({
  origin: ['https://app.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
}))

Rate Limiting

// Manual rate limiter as derive
app.derive({ as: 'scoped' }, ({ headers, set }) => {
  const ip = headers['x-forwarded-for'] || 'unknown'
  const key = `rate:${ip}`
  const count = parseInt(Bun.env[key] || '0')
  if (count > 100) {
    set.status = 429
    return { rateLimited: true }
  }
  Bun.env[key] = String(count + 1)
  return { rateLimited: false }
})

Anti-Patterns

Anti-PatternWhyFix
Global state mutationRace conditionsUse scoped derive or resolve
Inline schema everywhereDuplication, hard to maintainShared schema files per module
Missing onError handlerRaw stack traces to clientGlobal error handler plugin
Direct Bun.env accessUntyped, missing defaultsValidated env config on startup
No as: 'scoped' on deriveState leaks between requestsAlways scope per-request data
Heavy logic in guardsRoutes hard to testExtract to service layer

Security Considerations

  • Elysia's t.Object with format: 'email' and minLength for input validation
  • JWT via @elysiajs/jwt — algorithm defaults to HS256, never none
  • Rate limiting at proxy/reverse proxy level for production (nginx, cloudflare)
  • CORS with explicit origins — never * in production
  • Helmet-like headers: X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security
  • Auth plugin uses resolve/derive — tokens verified before handler runs

Testing Strategies

Unit Testing Services

import { describe, expect, test } from 'bun:test'

test('user service creates valid user', async () => {
  const user = await userService.create({ name: 'Test', email: 'test@test.com' })
  expect(user.id).toBeString()
  expect(user.email).toBe('test@test.com')
})

Integration Testing with Elysia

import { describe, expect, test } from 'bun:test'
import { createApp } from '../src/app'

const app = createApp()

test('POST /users creates user', async () => {
  const res = await app.handle(
    new Request('http://localhost/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'Test', email: 'test@test.com' }),
    })
  )
  expect(res.status).toBe(201)
  const body = await res.json()
  expect(body.data.name).toBe('Test')
})

test('GET /users/:id returns 404', async () => {
  const res = await app.handle(new Request('http://localhost/users/nonexistent'))
  expect(res.status).toBe(404)
})

Rules

  • Plugins registered before modules. Global plugins (cors, swagger, error handler) before domain plugins.
  • Each module is an Elysia instance with { prefix } — no inline route registration in app.ts.
  • Validation schemas defined per module, not globally. Reused via t.Object exports.
  • All environment vars validated at startup — fail fast on missing config.
  • derive for per-request data (user, requestId), state for app-wide data (db, config).
  • Error handler as global onError — never try/catch in handlers.
  • Eden Treaty client generated from server type exports for full-stack type safety.

References

  • references/elysia-custom-plugins.md — Custom Plugin Development
  • references/elysia-deployment.md — Deployment Guide
  • references/elysia-lifecycle.md — Lifecycle Hooks
  • references/elysia-plugins-ecosystem.md — Plugin Ecosystem
  • references/elysia-plugins.md — Elysia Plugins
  • references/elysia-routing-validation.md — Routing and Validation
  • references/elysia-testing.md — Testing Patterns
  • references/elysia-type-safety-patterns.md — Type Safety with Eden

Handoff

Hand off to backend/elysia/patterns/SKILL.md for Elysia-specific patterns or backend/universal/api-response/SKILL.md for API response formatting.

Implementation Patterns

Factory Pattern for Module Creation

function createModule<T>(config: ModuleConfig): T { const dependencies = initializeDependencies(config); const module = new Module(dependencies); module.hooks.onInit(); return module as T; }

Builder Pattern for Complex Configuration

class ConfigBuilder { private config: AppConfig = new AppConfig(); withDatabase(url: string): ConfigBuilder { ... } withCache(ttl: number): ConfigBuilder { ... } withLogging(level: string): ConfigBuilder { ... } build(): AppConfig { return this.config; } }

Production Considerations

Deployment Checklist

  • Production build with optimizations enabled
  • Environment variables configured per environment
  • Health check endpoint responds correctly
  • Error tracking and monitoring integrated
  • Logging level configured (not debug in production)
  • Resource limits configured
  • Database migrations applied
  • Static assets built and served from CDN or cache
  • Feature flags toggled appropriately
  • Rollback plan documented and tested

Monitoring and Alerting

MetricThresholdSeverityAction
Error rate> 1%CriticalRollback or fix
p95 latency> 500msWarningProfile and optimize
Uptime< 99.9%CriticalInvestigate infrastructure
Memory usage> 80%WarningCheck for leaks
CPU usage> 80%WarningScale up or optimize

Rules

  • Prefer composition over inheritance
  • Favor immutable data structures
  • Use dependency injection for testability
  • Keep functions pure when possible — no side effects
  • Fail fast with clear error messages
  • Don't repeat yourself (DRY) — extract shared logic
  • Keep it simple (KISS) — avoid unnecessary complexity
  • You aren't gonna need it (YAGNI) — build what's required
  • Separate concerns — single responsibility per module
  • Code to interfaces, not implementations
  • Write self-documenting code — clear names over comments
  • Prefer standard library over third-party dependencies
  • Handle errors explicitly — no silent failures
  • Validate inputs at boundaries
  • Log at appropriate levels (debug, info, warn, error)
我的收藏