feat: add bun-fullstack agent and update skills

This commit is contained in:
ken
2026-02-17 23:14:16 +08:00
parent fe71e602ea
commit be3809f388
170 changed files with 23309 additions and 8 deletions

View File

@@ -0,0 +1,129 @@
# Fullstack Dev Server
## What It Is
Bun 1.3 Fullstack Dev Server with HMR. React without bundler (no Vite/Webpack).
Example: [elysia-fullstack-example](https://github.com/saltyaom/elysia-fullstack-example)
## Setup
1. Install + use Elysia Static:
```typescript
import { Elysia } from 'elysia'
import { staticPlugin } from '@elysiajs/static'
new Elysia()
.use(await staticPlugin()) // await required for HMR hooks
.listen(3000)
```
2. Create `public/index.html` + `public/index.tsx`:
```html
<!-- public/index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Elysia React App</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id="root"></div>
<script type="module" src="./index.tsx"></script>
</body>
</html>
```
```tsx
// public/index.tsx
import { useState } from 'react'
import { createRoot } from 'react-dom/client'
function App() {
const [count, setCount] = useState(0)
const increase = () => setCount((c) => c + 1)
return (
<main>
<h2>{count}</h2>
<button onClick={increase}>Increase</button>
</main>
)
}
const root = createRoot(document.getElementById('root')!)
root.render(<App />)
```
3. Enable JSX in `tsconfig.json`:
```json
{
"compilerOptions": {
"jsx": "react-jsx"
}
}
```
4. Navigate to `http://localhost:3000/public`.
Frontend + backend in single project. No bundler. Works with HMR, Tailwind, Tanstack Query, Eden Treaty, path alias.
## Custom Prefix
```typescript
.use(await staticPlugin({ prefix: '/' }))
```
Serves at `/` instead of `/public`.
## Tailwind CSS
1. Install:
```bash
bun add tailwindcss@4
bun add -d bun-plugin-tailwind
```
2. Create `bunfig.toml`:
```toml
[serve.static]
plugins = ["bun-plugin-tailwind"]
```
3. Create `public/global.css`:
```css
@tailwind base;
```
4. Add to HTML or TS:
```html
<link rel="stylesheet" href="tailwindcss">
```
Or:
```tsx
import './global.css'
```
## Path Alias
1. Add to `tsconfig.json`:
```json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@public/*": ["public/*"]
}
}
}
```
2. Use:
```tsx
import '@public/global.css'
```
Works out of box.
## Production Build
```bash
bun build --compile --target bun --outfile server src/index.ts
```
Creates single executable `server`. Include `public` folder when running.

View File

@@ -0,0 +1,187 @@
# Cookie
## What It Is
Reactive mutable signal for cookie interaction. Auto-encodes/decodes objects.
## Basic Usage
No get/set - direct value access:
```typescript
import { Elysia } from 'elysia'
new Elysia()
.get('/', ({ cookie: { name } }) => {
// Get
name.value
// Set
name.value = "New Value"
})
```
Auto-encodes/decodes objects. Just works.
## Reactivity
Signal-like approach. Single source of truth. Auto-sets headers, syncs values.
Cookie jar = Proxy object. Extract value always `Cookie<unknown>`, never `undefined`. Access via `.value`.
Iterate over cookie jar → only existing cookies.
## Cookie Attributes
### Direct Property Assignment
```typescript
.get('/', ({ cookie: { name } }) => {
// Get
name.domain
// Set
name.domain = 'millennium.sh'
name.httpOnly = true
})
```
### set - Reset All Properties
```typescript
.get('/', ({ cookie: { name } }) => {
name.set({
domain: 'millennium.sh',
httpOnly: true
})
})
```
Overwrites all properties.
### add - Update Specific Properties
Like `set` but only overwrites defined properties.
## Remove Cookie
```typescript
.get('/', ({ cookie, cookie: { name } }) => {
name.remove()
// or
delete cookie.name
})
```
## Cookie Schema
Strict validation + type inference with `t.Cookie`:
```typescript
import { Elysia, t } from 'elysia'
new Elysia()
.get('/', ({ cookie: { name } }) => {
name.value = {
id: 617,
name: 'Summoning 101'
}
}, {
cookie: t.Cookie({
name: t.Object({
id: t.Numeric(),
name: t.String()
})
})
})
```
### Nullable Cookie
```typescript
cookie: t.Cookie({
name: t.Optional(
t.Object({
id: t.Numeric(),
name: t.String()
})
)
})
```
## Cookie Signature
Cryptographic hash for verification. Prevents malicious modification.
```typescript
new Elysia()
.get('/', ({ cookie: { profile } }) => {
profile.value = { id: 617, name: 'Summoning 101' }
}, {
cookie: t.Cookie({
profile: t.Object({
id: t.Numeric(),
name: t.String()
})
}, {
secrets: 'Fischl von Luftschloss Narfidort',
sign: ['profile']
})
})
```
Auto-signs/unsigns.
### Global Config
```typescript
new Elysia({
cookie: {
secrets: 'Fischl von Luftschloss Narfidort',
sign: ['profile']
}
})
```
## Cookie Rotation
Auto-handles secret rotation. Old signature verification + new signature signing.
```typescript
new Elysia({
cookie: {
secrets: ['Vengeance will be mine', 'Fischl von Luftschloss Narfidort']
}
})
```
Array = key rotation (retire old, replace with new).
## Config
### secrets
Secret key for signing/unsigning. Array = key rotation.
### domain
Domain Set-Cookie attribute. Default: none (current domain only).
### encode
Function to encode value. Default: `encodeURIComponent`.
### expires
Date for Expires attribute. Default: none (non-persistent, deleted on browser exit).
If both `expires` and `maxAge` set, `maxAge` takes precedence (spec-compliant clients).
### httpOnly (false)
HttpOnly attribute. If true, JS can't access via `document.cookie`.
### maxAge (undefined)
Seconds for Max-Age attribute. Rounded down to integer.
If both `expires` and `maxAge` set, `maxAge` takes precedence (spec-compliant clients).
### path
Path attribute. Default: handler path.
### priority
Priority attribute: `low` | `medium` | `high`. Not fully standardized.
### sameSite
SameSite attribute:
- `true` = Strict
- `false` = not set
- `'lax'` = Lax
- `'none'` = None (explicit cross-site)
- `'strict'` = Strict
Not fully standardized.
### secure
Secure attribute. If true, only HTTPS. Clients won't send over HTTP.

View File

@@ -0,0 +1,413 @@
# Deployment
## Production Build
### Compile to Binary (Recommended)
```bash
bun build \
--compile \
--minify-whitespace \
--minify-syntax \
--target bun \
--outfile server \
src/index.ts
```
**Benefits:**
- No runtime needed on deployment server
- Smaller memory footprint (2-3x reduction)
- Faster startup
- Single portable executable
**Run the binary:**
```bash
./server
```
### Compile to JavaScript
```bash
bun build \
--minify-whitespace \
--minify-syntax \
--outfile ./dist/index.js \
src/index.ts
```
**Run:**
```bash
NODE_ENV=production bun ./dist/index.js
```
## Docker
### Basic Dockerfile
```dockerfile
FROM oven/bun:1 AS build
WORKDIR /app
# Cache dependencies
COPY package.json bun.lock ./
RUN bun install
COPY ./src ./src
ENV NODE_ENV=production
RUN bun build \
--compile \
--minify-whitespace \
--minify-syntax \
--outfile server \
src/index.ts
FROM gcr.io/distroless/base
WORKDIR /app
COPY --from=build /app/server server
ENV NODE_ENV=production
CMD ["./server"]
EXPOSE 3000
```
### Build and Run
```bash
docker build -t my-elysia-app .
docker run -p 3000:3000 my-elysia-app
```
### With Environment Variables
```dockerfile
FROM gcr.io/distroless/base
WORKDIR /app
COPY --from=build /app/server server
ENV NODE_ENV=production
ENV PORT=3000
ENV DATABASE_URL=""
ENV JWT_SECRET=""
CMD ["./server"]
EXPOSE 3000
```
## Cluster Mode (Multiple CPU Cores)
```typescript
// src/index.ts
import cluster from 'node:cluster'
import os from 'node:os'
import process from 'node:process'
if (cluster.isPrimary) {
for (let i = 0; i < os.availableParallelism(); i++) {
cluster.fork()
}
} else {
await import('./server')
console.log(`Worker ${process.pid} started`)
}
```
```typescript
// src/server.ts
import { Elysia } from 'elysia'
new Elysia()
.get('/', () => 'Hello World!')
.listen(3000)
```
## Environment Variables
### .env File
```env
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/db
JWT_SECRET=your-secret-key
CORS_ORIGIN=https://example.com
```
### Load in App
```typescript
import { Elysia } from 'elysia'
const app = new Elysia()
.get('/env', () => ({
env: process.env.NODE_ENV,
port: process.env.PORT
}))
.listen(parseInt(process.env.PORT || '3000'))
```
## Platform-Specific Deployments
### Railway
```typescript
// Railway assigns random PORT via env variable
new Elysia()
.get('/', () => 'Hello Railway')
.listen(process.env.PORT ?? 3000)
```
### Vercel
```typescript
// src/index.ts
import { Elysia } from 'elysia'
export default new Elysia()
.get('/', () => 'Hello Vercel')
export const GET = app.fetch
export const POST = app.fetch
```
```json
// vercel.json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"bunVersion": "1.x"
}
```
### Cloudflare Workers
```typescript
import { Elysia } from 'elysia'
import { CloudflareAdapter } from 'elysia/adapter/cloudflare-worker'
export default new Elysia({
adapter: CloudflareAdapter
})
.get('/', () => 'Hello Cloudflare!')
.compile()
```
```toml
# wrangler.toml
name = "elysia-app"
main = "src/index.ts"
compatibility_date = "2025-06-01"
```
### Node.js Adapter
```typescript
import { Elysia } from 'elysia'
import { node } from '@elysiajs/node'
const app = new Elysia({ adapter: node() })
.get('/', () => 'Hello Node.js')
.listen(3000)
```
## Performance Optimization
### Enable AoT Compilation
```typescript
new Elysia({
aot: true // Ahead-of-time compilation
})
```
### Use Native Static Response
```typescript
new Elysia({
nativeStaticResponse: true
})
.get('/version', 1) // Optimized for Bun.serve.static
```
### Precompile Routes
```typescript
new Elysia({
precompile: true // Compile all routes ahead of time
})
```
## Health Checks
```typescript
new Elysia()
.get('/health', () => ({
status: 'ok',
timestamp: Date.now()
}))
.get('/ready', ({ db }) => {
// Check database connection
const isDbReady = checkDbConnection()
if (!isDbReady) {
return status(503, { status: 'not ready' })
}
return { status: 'ready' }
})
```
## Graceful Shutdown
```typescript
import { Elysia } from 'elysia'
const app = new Elysia()
.get('/', () => 'Hello')
.listen(3000)
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully')
app.stop()
process.exit(0)
})
process.on('SIGINT', () => {
console.log('SIGINT received, shutting down gracefully')
app.stop()
process.exit(0)
})
```
## Monitoring
### OpenTelemetry
```typescript
import { opentelemetry } from '@elysiajs/opentelemetry'
new Elysia()
.use(opentelemetry({
serviceName: 'my-service',
endpoint: 'http://localhost:4318'
}))
```
### Custom Logging
```typescript
.onRequest(({ request }) => {
console.log(`[${new Date().toISOString()}] ${request.method} ${request.url}`)
})
.onAfterResponse(({ request, set }) => {
console.log(`[${new Date().toISOString()}] ${request.method} ${request.url} - ${set.status}`)
})
```
## SSL/TLS (HTTPS)
```typescript
import { Elysia, file } from 'elysia'
new Elysia({
serve: {
tls: {
cert: file('cert.pem'),
key: file('key.pem')
}
}
})
.get('/', () => 'Hello HTTPS')
.listen(3000)
```
## Best Practices
1. **Always compile to binary for production**
- Reduces memory usage
- Smaller deployment size
- No runtime needed
2. **Use environment variables**
- Never hardcode secrets
- Use different configs per environment
3. **Enable health checks**
- Essential for load balancers
- K8s/Docker orchestration
4. **Implement graceful shutdown**
- Handle SIGTERM/SIGINT
- Close connections properly
5. **Use cluster mode**
- Utilize all CPU cores
- Better performance under load
6. **Monitor your app**
- Use OpenTelemetry
- Log requests/responses
- Track errors
## Example Production Setup
```typescript
// src/server.ts
import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'
import { opentelemetry } from '@elysiajs/opentelemetry'
export const app = new Elysia({
aot: true,
nativeStaticResponse: true
})
.use(cors({
origin: process.env.CORS_ORIGIN || 'http://localhost:3000'
}))
.use(opentelemetry({
serviceName: 'my-service'
}))
.get('/health', () => ({ status: 'ok' }))
.get('/', () => 'Hello Production')
.listen(parseInt(process.env.PORT || '3000'))
// Graceful shutdown
process.on('SIGTERM', () => {
app.stop()
process.exit(0)
})
```
```typescript
// src/index.ts (cluster)
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')
}
```
```dockerfile
# Dockerfile
FROM oven/bun:1 AS build
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install
COPY ./src ./src
ENV NODE_ENV=production
RUN bun build --compile --outfile server src/index.ts
FROM gcr.io/distroless/base
WORKDIR /app
COPY --from=build /app/server server
ENV NODE_ENV=production
CMD ["./server"]
EXPOSE 3000
```

View File

@@ -0,0 +1,158 @@
# Eden Treaty
e2e type safe RPC client for share type from backend to frontend.
## What It Is
Type-safe object representation for Elysia server. Auto-completion + error handling.
## Installation
```bash
bun add @elysiajs/eden
bun add -d elysia
```
Export Elysia server type:
```typescript
const app = new Elysia()
.get('/', () => 'Hi Elysia')
.get('/id/:id', ({ params: { id } }) => id)
.post('/mirror', ({ body }) => body, {
body: t.Object({
id: t.Number(),
name: t.String()
})
})
.listen(3000)
export type App = typeof app
```
Consume on client side:
```typescript
import { treaty } from '@elysiajs/eden'
import type { App } from './server'
const client = treaty<App>('localhost:3000')
// response: Hi Elysia
const { data: index } = await client.get()
// response: 1895
const { data: id } = await client.id({ id: 1895 }).get()
// response: { id: 1895, name: 'Skadi' }
const { data: nendoroid } = await client.mirror.post({
id: 1895,
name: 'Skadi'
})
```
## Common Errors & Fixes
- **Strict mode**: Enable in tsconfig
- **Version mismatch**: `npm why elysia` - must match server/client
- **TypeScript**: Min 5.0
- **Method chaining**: Required on server
- **Bun types**: `bun add -d @types/bun` if using Bun APIs
- **Path alias**: Must resolve same on frontend/backend
### Monorepo Path Alias
Must resolve to same file on frontend/backend
```json
// tsconfig.json at root
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@frontend/*": ["./apps/frontend/src/*"],
"@backend/*": ["./apps/backend/src/*"]
}
}
}
```
## Syntax Mapping
| Path | Method | Treaty |
|----------------|--------|-------------------------------|
| / | GET | `.get()` |
| /hi | GET | `.hi.get()` |
| /deep/nested | POST | `.deep.nested.post()` |
| /item/:name | GET | `.item({ name: 'x' }).get()` |
## Parameters
### With body (POST/PUT/PATCH/DELETE):
```typescript
.user.post(
{ name: 'Elysia' }, // body
{ headers: {}, query: {}, fetch: {} } // optional
)
```
### No body (GET/HEAD):
```typescript
.hello.get({ headers: {}, query: {}, fetch: {} })
```
### Empty body with query/headers:
```typescript
.user.post(null, { query: { name: 'Ely' } })
```
### Fetch options:
```typescript
.hello.get({ fetch: { signal: controller.signal } })
```
### File upload:
```typescript
// Accepts: File | File[] | FileList | Blob
.image.post({
title: 'Title',
image: fileInput.files!
})
```
## Response
```typescript
const { data, error, response, status, headers } = await api.user.post({ name: 'x' })
if (error) {
switch (error.status) {
case 400: throw error.value
default: throw error.value
}
}
// data unwrapped after error handling
return data
```
status >= 300 → `data = null`, `error` has value
## Stream/SSE
Interpreted as `AsyncGenerator`:
```typescript
const { data, error } = await treaty(app).ok.get()
if (error) throw error
for await (const chunk of data) console.log(chunk)
```
## Utility Types
```typescript
import { Treaty } from '@elysiajs/eden'
type UserData = Treaty.Data<typeof api.user.post>
type UserError = Treaty.Error<typeof api.user.post>
```
## WebSocket
```typescript
const chat = api.chat.subscribe()
chat.subscribe((message) => console.log('got', message))
chat.on('open', () => chat.send('hello'))
// Native access: chat.raw
```
`.subscribe()` accepts same params as `get`/`head`

View File

@@ -0,0 +1,198 @@
# Lifecycle
Instead of a sequential process, Elysia's request handling is divided into multiple stages called lifecycle events.
It's designed to separate the process into distinct phases based on their responsibility without interfering with each others.
### List of events in order
1. **request** - early, global
2. **parse** - body parsing
3. **transform** / **derive** - mutate context pre validation
4. **beforeHandle** / **resolve** - auth/guard logic
5. **handler** - your business code
6. **afterHandle** - tweak response, set headers
7. **mapResponse** - turn anything into a proper `Response`
8. **onError** - centralized error handling
9. **onAfterResponse** - post response/cleanup tasks
## Request (`onRequest`)
Runs first for every incoming request.
- Ideal for **caching, rate limiting, CORS, adding global headers**.
- If the hook returns a value, the whole lifecycle stops and that value becomes the response.
```ts
new Elysia().onRequest(({ ip, set }) => {
if (blocked(ip)) return (set.status = 429)
})
```
---
## Parse (`onParse`)
_Body parsing stage._
- Handles `text/plain`, `application/json`, `multipart/form-data`, `application/x www-form-urlencoded` by default.
- Use to add **custom parsers** or support extra `Content Type`s.
```ts
new Elysia().onParse(({ request, contentType }) => {
if (contentType === 'application/custom') return request.text()
})
```
---
## Transform (`onTransform`)
_Runs **just before validation**; can mutate the request context._
- Perfect for **type coercion**, trimming strings, or adding temporary fields that validation will use.
```ts
new Elysia().onTransform(({ params }) => {
params.id = Number(params.id)
})
```
---
## Derive
_Runs along with `onTransform` **but before validation**; adds per request values to the context._
- Useful for extracting info from headers, cookies, query, etc., that you want to reuse in handlers.
```ts
new Elysia().derive(({ headers }) => ({
bearer: headers.authorization?.replace(/^Bearer /, '')
}))
```
---
## Before Handle (`onBeforeHandle`)
_Executed after validation, right before the route handler._
- Great for **auth checks, permission gating, custom pre validation logic**.
- Returning a value skips the handler.
```ts
new Elysia().get('/', () => 'hi', {
beforeHandle({ cookie, status }) {
if (!cookie.session) return status(401)
}
})
```
---
## Resolve
_Like `derive` but runs **after validation** along "Before Handle" (so you can rely on validated data)._
- Usually placed inside a `guard` because it isn't available as a local hook.
```ts
new Elysia().guard(
{ headers: t.Object({ authorization: t.String() }) },
(app) =>
app
.resolve(({ headers }) => ({
bearer: headers.authorization.split(' ')[1]
}))
.get('/', ({ bearer }) => bearer)
)
```
---
## After Handle (`onAfterHandle`)
_Runs after the handler finishes._
- Can **modify response headers**, wrap the result in a `Response`, or transform the payload.
- Returning a value **replaces** the handler’s result, but the next `afterHandle` hooks still run.
```ts
new Elysia().get('/', () => '<h1>Hello</h1>', {
afterHandle({ response, set }) {
if (isHtml(response)) {
set.headers['content-type'] = 'text/html; charset=utf-8'
return new Response(response)
}
}
})
```
---
## Map Response (`mapResponse`)
_Runs right after all `afterHandle` hooks; maps **any** value to a Web standard `Response`._
- Ideal for **compression, custom content type mapping, streaming**.
```ts
new Elysia().mapResponse(({ responseValue, set }) => {
const body =
typeof responseValue === 'object'
? JSON.stringify(responseValue)
: String(responseValue ?? '')
set.headers['content-encoding'] = 'gzip'
return new Response(Bun.gzipSync(new TextEncoder().encode(body)), {
headers: {
'Content-Type':
typeof responseValue === 'object'
? 'application/json'
: 'text/plain'
}
})
})
```
---
## On Error (`onError`)
_Caught whenever an error bubbles up from any lifecycle stage._
- Use to **customize error messages**, **handle 404**, **log**, or **retry**.
- Must be registered **before** the routes it should protect.
```ts
new Elysia().onError(({ code, status }) => {
if (code === 'NOT_FOUND') return status(404, '❓ Not found')
return new Response('Oops', { status: 500 })
})
```
---
## After Response (`onAfterResponse`)
_Runs **after** the response has been sent to the client._
- Perfect for **logging, metrics, cleanup**.
```ts
new Elysia().onAfterResponse(() =>
console.log('✅ response sent at', Date.now())
)
```
---
## Hook Types
| Type | Scope | How to add |
| -------------------- | --------------------------------- | --------------------------------------------------------- |
| **Local Hook** | Single route | Inside route options (`afterHandle`, `beforeHandle`, …) |
| **Interceptor Hook** | Whole instance (and later routes) | `.onXxx(cb)` or `.use(plugin)` |
> **Remember:** Hooks only affect routes **defined after** they are registered, except `onRequest` which is global because it runs before route matching.

View File

@@ -0,0 +1,83 @@
# Macro
Composable Elysia function for controlling lifecycle/schema/context with full type safety. Available in hook after definition control by key-value label.
## Basic Pattern
```typescript
.macro({
hi: (word: string) => ({
beforeHandle() { console.log(word) }
})
})
.get('/', () => 'hi', { hi: 'Elysia' })
```
## Property Shorthand
Object → function accepting boolean:
```typescript
.macro({
// These equivalent:
isAuth: { resolve: () => ({ user: 'saltyaom' }) },
isAuth(enabled: boolean) { if(enabled) return { resolve() {...} } }
})
```
## Error Handling
Return `status`, don't throw:
```typescript
.macro({
auth: {
resolve({ headers }) {
if(!headers.authorization) return status(401, 'Unauthorized')
return { user: 'SaltyAom' }
}
}
})
```
## Resolve - Add Context Props
```typescript
.macro({
user: (enabled: true) => ({
resolve: () => ({ user: 'Pardofelis' })
})
})
.get('/', ({ user }) => user, { user: true })
```
### Named Macro for Type Inference
TypeScript limitation workaround:
```typescript
.macro('user', { resolve: () => ({ user: 'lilith' }) })
.macro('user2', { user: true, resolve: ({ user }) => {} })
```
## Schema
Auto-validates, infers types, stacks with other schemas:
```typescript
.macro({
withFriends: {
body: t.Object({ friends: t.Tuple([...]) })
}
})
```
Use named single macro for lifecycle type inference within same macro.
## Extension
Stack macros:
```typescript
.macro({
sartre: { body: t.Object({...}) },
fouco: { body: t.Object({...}) },
lilith: { fouco: true, sartre: true, body: t.Object({...}) }
})
```
## Deduplication
Auto-dedupes by property value. Custom seed:
```typescript
.macro({ sartre: (role: string) => ({ seed: role, ... }) })
```
Max stack: 16 (prevents infinite loops)

View File

@@ -0,0 +1,207 @@
# Plugins
## Plugin = Decoupled Elysia Instance
```ts
const plugin = new Elysia()
.decorate('plugin', 'hi')
.get('/plugin', ({ plugin }) => plugin)
const app = new Elysia()
.use(plugin) // inherit properties
.get('/', ({ plugin }) => plugin)
```
**Inherits**: state, decorate
**Does NOT inherit**: lifecycle (isolated by default)
## Dependency
Each instance runs independently like microservice. **Must explicitly declare dependencies**.
```ts
const auth = new Elysia()
.decorate('Auth', Auth)
// ❌ Missing dependency
const main = new Elysia()
.get('/', ({ Auth }) => Auth.getProfile())
// ✅ Declare dependency
const main = new Elysia()
.use(auth) // required for Auth
.get('/', ({ Auth }) => Auth.getProfile())
```
## Deduplication
**Every plugin re-executes by default**. Use `name` + optional `seed` to deduplicate:
```ts
const ip = new Elysia({ name: 'ip' }) // unique identifier
.derive({ as: 'global' }, ({ server, request }) => ({
ip: server?.requestIP(request)
}))
const router1 = new Elysia().use(ip)
const router2 = new Elysia().use(ip)
const server = new Elysia().use(router1).use(router2)
// `ip` only executes once due to deduplication
```
## Global vs Explicit Dependency
**Global plugin** (rare, apply everywhere):
- Doesn't add types - cors, compress, helmet
- Global lifecycle no instance controls - tracing, logging
- Examples: OpenAPI docs, OpenTelemetry, logging
**Explicit dependency** (default, recommended):
- Adds types - macro, state, model
- Business logic instances interact with - Auth, DB
- Examples: state management, ORM, auth, features
## Scope
**Lifecycle isolated by default**. Must specify scope to export.
```ts
// ❌ NOT inherited by app
const profile = new Elysia()
.onBeforeHandle(({ cookie }) => throwIfNotSignIn(cookie))
.get('/profile', () => 'Hi')
const app = new Elysia()
.use(profile)
.patch('/rename', ({ body }) => updateProfile(body)) // No sign-in check
// ✅ Exported to app
const profile = new Elysia()
.onBeforeHandle({ as: 'global' }, ({ cookie }) => throwIfNotSignIn(cookie))
.get('/profile', () => 'Hi')
```
## Scope Levels
1. **local** (default) - current + descendants only
2. **scoped** - parent + current + descendants
3. **global** - all instances (all parents, current, descendants)
Example with `.onBeforeHandle({ as: 'local' }, ...)`:
| type | child | current | parent | main |
|------|-------|---------|--------|------|
| local | ✅ | ✅ | ❌ | ❌ |
| scoped | ✅ | ✅ | ✅ | ❌ |
| global | ✅ | ✅ | ✅ | ✅ |
## Config
```ts
// Instance factory with config
const version = (v = 1) => new Elysia()
.get('/version', v)
const app = new Elysia()
.use(version(1))
```
## Functional Callback (not recommended)
```ts
// Harder to handle scope/encapsulation
const plugin = (app: Elysia) => app
.state('counter', 0)
.get('/plugin', () => 'Hi')
// Prefer new instance (better type inference, no perf diff)
```
## Guard (Apply to Multiple Routes)
```ts
.guard(
{ body: t.Object({ username: t.String(), password: t.String() }) },
(app) =>
app.post('/sign-up', ({ body }) => signUp(body))
.post('/sign-in', ({ body }) => signIn(body))
)
```
**Grouped guard** (merge group + guard):
```ts
.group(
'/v1',
{ body: t.Literal('Rikuhachima Aru') }, // guard here
(app) => app.post('/student', ({ body }) => body)
)
```
## Scope Casting
**3 methods to apply hook to parent**:
1. **Inline as** (single hook):
```ts
.derive({ as: 'scoped' }, () => ({ hi: 'ok' }))
```
2. **Guard as** (multiple hooks, no derive/resolve):
```ts
.guard({
as: 'scoped',
response: t.String(),
beforeHandle() { console.log('ok') }
})
```
3. **Instance as** (all hooks + schema):
```ts
const plugin = new Elysia()
.derive(() => ({ hi: 'ok' }))
.get('/child', ({ hi }) => hi)
.as('scoped') // lift scope up
```
`.as()` lifts scope: local → scoped → global
## Lazy Load
**Deferred module** (async plugin, non-blocking startup):
```ts
// plugin.ts
export const loadStatic = async (app: Elysia) => {
const files = await loadAllFiles()
files.forEach((asset) => app.get(asset, file(asset)))
return app
}
// main.ts
const app = new Elysia().use(loadStatic)
```
**Lazy-load module** (dynamic import):
```ts
const app = new Elysia()
.use(import('./plugin')) // loaded after startup
```
**Testing** (wait for modules):
```ts
await app.modules // ensure all deferred/lazy modules loaded
```
## Notes
[Inference] Based on docs patterns:
- Use inline values for static resources (performance optimization)
- Group routes by prefix for organization
- Extend context minimally (separation of concerns)
- Use `status()` over `set.status` for type safety
- Prefer `resolve()` over `derive()` when type integrity matters
- Plugins isolated by default (must declare scope explicitly)
- Use `name` for deduplication when plugin used multiple times
- Prefer explicit dependency over global (better modularity/tracking)

View File

@@ -0,0 +1,331 @@
# ElysiaJS: Routing, Handlers & Context
## Routing
### Path Types
```ts
new Elysia()
.get('/static', 'static path') // exact match
.get('/id/:id', 'dynamic path') // captures segment
.get('/id/*', 'wildcard path') // captures rest
```
**Path Priority**: static > dynamic > wildcard
### Dynamic Paths
```ts
new Elysia()
.get('/id/:id', ({ params: { id } }) => id)
.get('/id/:id/:name', ({ params: { id, name } }) => id + ' ' + name)
```
**Optional params**: `.get('/id/:id?', ...)`
### HTTP Verbs
- `.get()` - retrieve data
- `.post()` - submit/create
- `.put()` - replace
- `.patch()` - partial update
- `.delete()` - remove
- `.all()` - any method
- `.route(method, path, handler)` - custom verb
### Grouping Routes
```ts
new Elysia()
.group('/user', { body: t.Literal('auth') }, (app) =>
app.post('/sign-in', ...)
.post('/sign-up', ...)
)
// Or use prefix in constructor
new Elysia({ prefix: '/user' })
.post('/sign-in', ...)
```
## Handlers
### Handler = function accepting HTTP request, returning response
```ts
// Inline value (compiled ahead, optimized)
.get('/', 'Hello Elysia')
.get('/video', file('video.mp4'))
// Function handler
.get('/', () => 'hello')
.get('/', ({ params, query, body }) => {...})
```
### Context Properties
- `body` - HTTP message/form/file
- `query` - query string as object
- `params` - path parameters
- `headers` - HTTP headers
- `cookie` - mutable signal for cookies
- `store` - global mutable state
- `request` - Web Standard Request
- `server` - Bun server instance
- `path` - request pathname
### Context Utilities
```ts
import { redirect, form } from 'elysia'
new Elysia().get('/', ({ status, set, form }) => {
// Status code (type-safe)
status(418, "I'm a teapot")
// Set response props
set.headers['x-custom'] = 'value'
set.status = 418 // legacy, no type inference
// Redirect
return redirect('https://...', 302)
// Cookies (mutable signal, no get/set)
cookie.name.value // get
cookie.name.value = 'new' // set
// FormData response
return form({ name: 'Party', images: [file('a.jpg')] })
// Single file
return file('document.pdf')
})
```
### Streaming
```ts
new Elysia()
.get('/stream', function* () {
yield 1
yield 2
yield 3
})
// Server-Sent Events
.get('/sse', function* () {
yield sse('hello')
yield sse({ event: 'msg', data: {...} })
})
```
**Note**: Headers only settable before first yield
**Conditional stream**: returning without yield converts to normal response
## Context Extension
[Inference] Extend when property is:
- Global mutable (use `state`)
- Request/response related (use `decorate`)
- Derived from existing props (use `derive`/`resolve`)
### state() - Global Mutable
```ts
new Elysia()
`.state('version', 1)
.get('/', ({ store: { version } }) => version)
// Multiple
.state({ counter: 0, visits: 0 })
// Remap (create new from existing)
.state(({ version, ...store }) => ({
...store,
apiVersion: version
}))
````
**Gotcha**: Use reference not value
```ts
new Elysia()
// ✅ Correct
.get('/', ({ store }) => store.counter++)
// ❌ Wrong - loses reference
.get('/', ({ store: { counter } }) => counter++)
```
### decorate() - Additional Context Props
```ts
new Elysia()
.decorate('logger', new Logger())
.get('/', ({ logger }) => logger.log('hi'))
// Multiple
.decorate({ logger: new Logger(), db: connection })
```
**When**: constant/readonly values, classes with internal state, singletons
### derive() - Create from Existing (Transform Lifecycle)
```ts
new Elysia()
.derive(({ headers }) => ({
bearer: headers.authorization?.startsWith('Bearer ')
? headers.authorization.slice(7)
: null
}))
.get('/', ({ bearer }) => bearer)
```
**Timing**: runs at transform (before validation)
**Type safety**: request props typed as `unknown`
### resolve() - Type-Safe Derive (beforeHandle Lifecycle)
```ts
new Elysia()
.guard({
headers: t.Object({
bearer: t.String({ pattern: '^Bearer .+$' })
})
})
.resolve(({ headers }) => ({
bearer: headers.bearer.slice(7) // typed correctly
}))
```
**Timing**: runs at beforeHandle (after validation)
**Type safety**: request props fully typed
### Error from derive/resolve
```ts
new Elysia()
.derive(({ headers, status }) => {
if (!headers.authorization) return status(400)
return { bearer: ... }
})
```
Returns early if error returned
## Patterns
### Affix (Bulk Remap)
```ts
const plugin = new Elysia({ name: 'setup' }).decorate({
argon: 'a',
boron: 'b'
})
new Elysia()
.use(plugin)
.prefix('decorator', 'setup') // setupArgon, setupBoron
.prefix('all', 'setup') // remap everything
```
### Assignment Patterns
1. **key-value**: `.state('key', value)`
2. **object**: `.state({ k1: v1, k2: v2 })`
3. **remap**: `.state(({old}) => ({new}))`
## Testing
```ts
const app = new Elysia().get('/', 'hi')
// Programmatic test
app.handle(new Request('http://localhost/'))
```
## To Throw or Return
Most of an error handling in Elysia can be done by throwing an error and will be handle in `onError`.
But for `status` it can be a little bit confusing, since it can be used both as a return value or throw an error.
It could either be **return** or **throw** based on your specific needs.
- If an `status` is **throw**, it will be caught by `onError` middleware.
- If an `status` is **return**, it will be **NOT** caught by `onError` middleware.
See the following code:
```typescript
import { Elysia, file } from 'elysia'
new Elysia()
.onError(({ code, error, path }) => {
if (code === 418) return 'caught'
})
.get('/throw', ({ status }) => {
// This will be caught by onError
throw status(418)
})
.get('/return', ({ status }) => {
// This will NOT be caught by onError
return status(418)
})
```
## To Throw or Return
Elysia provide a `status` function for returning HTTP status code, prefers over `set.status`.
`status` can be import from Elysia but preferably extract from route handler Context for type safety.
```ts
import { Elysia, status } from 'elysia'
function doThing() {
if (Math.random() > 0.33) return status(418, "I'm a teapot")
}
new Elysia().get('/', ({ status }) => {
if (Math.random() > 0.33) return status(418)
return 'ok'
})
```
Error Handling in Elysia can be done by throwing an error and will be handle in `onError`.
Status could either be **return** or **throw** based on your specific needs.
- If an `status` is **throw**, it will be caught by `onError` middleware.
- If an `status` is **return**, it will be **NOT** caught by `onError` middleware.
See the following code:
```typescript
import { Elysia, file } from 'elysia'
new Elysia()
.onError(({ code, error, path }) => {
if (code === 418) return 'caught'
})
.get('/throw', ({ status }) => {
// This will be caught by onError
throw status(418)
})
.get('/return', ({ status }) => {
// This will NOT be caught by onError
return status(418)
})
```
## Notes
[Inference] Based on docs patterns:
- Use inline values for static resources (performance optimization)
- Group routes by prefix for organization
- Extend context minimally (separation of concerns)
- Use `status()` over `set.status` for type safety
- Prefer `resolve()` over `derive()` when type integrity matters

View File

@@ -0,0 +1,385 @@
# Unit Testing
## Basic Test Setup
### Installation
```bash
bun add -d @elysiajs/eden
```
### Basic Test
```typescript
// test/app.test.ts
import { describe, expect, it } from 'bun:test'
import { Elysia } from 'elysia'
describe('Elysia App', () => {
it('should return hello world', async () => {
const app = new Elysia()
.get('/', () => 'Hello World')
const res = await app.handle(
new Request('http://localhost/')
)
expect(res.status).toBe(200)
expect(await res.text()).toBe('Hello World')
})
})
```
## Testing Routes
### GET Request
```typescript
it('should get user by id', async () => {
const app = new Elysia()
.get('/user/:id', ({ params: { id } }) => ({
id,
name: 'John Doe'
}))
const res = await app.handle(
new Request('http://localhost/user/123')
)
const data = await res.json()
expect(res.status).toBe(200)
expect(data).toEqual({
id: '123',
name: 'John Doe'
})
})
```
### POST Request
```typescript
it('should create user', async () => {
const app = new Elysia()
.post('/user', ({ body }) => body)
const res = await app.handle(
new Request('http://localhost/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Jane Doe',
email: 'jane@example.com'
})
})
)
const data = await res.json()
expect(res.status).toBe(200)
expect(data.name).toBe('Jane Doe')
})
```
## Testing Module/Plugin
### Module Structure
```
src/
├── modules/
│ └── auth/
│ ├── index.ts # Elysia instance
│ ├── service.ts
│ └── model.ts
└── index.ts
```
### Auth Module
```typescript
// src/modules/auth/index.ts
import { Elysia, t } from 'elysia'
export const authModule = new Elysia({ prefix: '/auth' })
.post('/login', ({ body, cookie: { session } }) => {
if (body.username === 'admin' && body.password === 'password') {
session.value = 'valid-session'
return { success: true }
}
return { success: false }
}, {
body: t.Object({
username: t.String(),
password: t.String()
})
})
.get('/profile', ({ cookie: { session }, status }) => {
if (!session.value) {
return status(401, { error: 'Unauthorized' })
}
return { username: 'admin' }
})
```
### Auth Module Test
```typescript
// test/auth.test.ts
import { describe, expect, it } from 'bun:test'
import { authModule } from '../src/modules/auth'
describe('Auth Module', () => {
it('should login successfully', async () => {
const res = await authModule.handle(
new Request('http://localhost/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'admin',
password: 'password'
})
})
)
const data = await res.json()
expect(res.status).toBe(200)
expect(data.success).toBe(true)
})
it('should reject invalid credentials', async () => {
const res = await authModule.handle(
new Request('http://localhost/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'wrong',
password: 'wrong'
})
})
)
const data = await res.json()
expect(data.success).toBe(false)
})
it('should return 401 for unauthenticated profile request', async () => {
const res = await authModule.handle(
new Request('http://localhost/auth/profile')
)
expect(res.status).toBe(401)
})
})
```
## Eden Treaty Testing
### Setup
```typescript
import { treaty } from '@elysiajs/eden'
import { app } from '../src/modules/auth'
const api = treaty(app)
```
### Eden Tests
```typescript
describe('Auth Module with Eden', () => {
it('should login with Eden', async () => {
const { data, error } = await api.auth.login.post({
username: 'admin',
password: 'password'
})
expect(error).toBeNull()
expect(data?.success).toBe(true)
})
it('should get profile with Eden', async () => {
// First login
await api.auth.login.post({
username: 'admin',
password: 'password'
})
// Then get profile
const { data, error } = await api.auth.profile.get()
expect(error).toBeNull()
expect(data?.username).toBe('admin')
})
})
```
## Mocking Dependencies
### With Decorators
```typescript
// app.ts
export const app = new Elysia()
.decorate('db', realDatabase)
.get('/users', ({ db }) => db.getUsers())
// test
import { app } from '../src/app'
describe('App with mocked DB', () => {
it('should use mock database', async () => {
const mockDb = {
getUsers: () => [{ id: 1, name: 'Test User' }]
}
const testApp = app.decorate('db', mockDb)
const res = await testApp.handle(
new Request('http://localhost/users')
)
const data = await res.json()
expect(data).toEqual([{ id: 1, name: 'Test User' }])
})
})
```
## Testing with Headers
```typescript
it('should require authorization', async () => {
const app = new Elysia()
.get('/protected', ({ headers, status }) => {
if (!headers.authorization) {
return status(401)
}
return { data: 'secret' }
})
const res = await app.handle(
new Request('http://localhost/protected', {
headers: {
'Authorization': 'Bearer token123'
}
})
)
expect(res.status).toBe(200)
})
```
## Testing Validation
```typescript
import { Elysia, t } from 'elysia'
it('should validate request body', async () => {
const app = new Elysia()
.post('/user', ({ body }) => body, {
body: t.Object({
name: t.String(),
age: t.Number({ minimum: 0 })
})
})
// Valid request
const validRes = await app.handle(
new Request('http://localhost/user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'John',
age: 25
})
})
)
expect(validRes.status).toBe(200)
// Invalid request (negative age)
const invalidRes = await app.handle(
new Request('http://localhost/user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'John',
age: -5
})
})
)
expect(invalidRes.status).toBe(400)
})
```
## Testing WebSocket
```typescript
it('should handle websocket connection', (done) => {
const app = new Elysia()
.ws('/chat', {
message(ws, message) {
ws.send('Echo: ' + message)
}
})
const ws = new WebSocket('ws://localhost:3000/chat')
ws.onopen = () => {
ws.send('Hello')
}
ws.onmessage = (event) => {
expect(event.data).toBe('Echo: Hello')
ws.close()
done()
}
})
```
## Complete Example
```typescript
// src/modules/auth/index.ts
import { Elysia, t } from 'elysia'
export const authModule = new Elysia({ prefix: '/auth' })
.post('/login', ({ body, cookie: { session } }) => {
if (body.username === 'admin' && body.password === 'password') {
session.value = 'valid-session'
return { success: true }
}
return { success: false }
}, {
body: t.Object({
username: t.String(),
password: t.String()
})
})
.get('/profile', ({ cookie: { session }, status }) => {
if (!session.value) {
return status(401)
}
return { username: 'admin' }
})
// test/auth.test.ts
import { describe, expect, it } from 'bun:test'
import { treaty } from '@elysiajs/eden'
import { authModule } from '../src/modules/auth'
const api = treaty(authModule)
describe('Auth Module', () => {
it('should login successfully', async () => {
const { data, error } = await api.auth.login.post({
username: 'admin',
password: 'password'
})
expect(error).toBeNull()
expect(data?.success).toBe(true)
})
it('should return 401 for unauthorized access', async () => {
const { error } = await api.auth.profile.get()
expect(error?.status).toBe(401)
})
})
```

View File

@@ -0,0 +1,491 @@
# Validation Schema - SKILLS.md
## What It Is
Runtime validation + type inference + OpenAPI schema from single source. TypeBox-based with Standard Schema support.
## Basic Usage
```typescript
import { Elysia, t } from 'elysia'
new Elysia()
.get('/id/:id', ({ params: { id } }) => id, {
params: t.Object({ id: t.Number({ minimum: 1 }) }),
response: {
200: t.Number(),
404: t.Literal('Not Found')
}
})
```
## Schema Types
Third parameter of HTTP method:
- **body** - HTTP message
- **query** - URL query params
- **params** - Path params
- **headers** - Request headers
- **cookie** - Request cookies
- **response** - Response (per status)
## Standard Schema Support
Use Zod, Valibot, ArkType, Effect, Yup, Joi:
```typescript
import { z } from 'zod'
import * as v from 'valibot'
.get('/', ({ params, query }) => params.id, {
params: z.object({ id: z.coerce.number() }),
query: v.object({ name: v.literal('Lilith') })
})
```
Mix validators in same handler.
## Body
```typescript
body: t.Object({ name: t.String() })
```
GET/HEAD: body-parser disabled by default (RFC2616).
### File Upload
```typescript
body: t.Object({
file: t.File({ format: 'image/*' }),
multipleFiles: t.Files()
})
// Auto-assumes multipart/form-data
```
### File (Standard Schema)
```typescript
import { fileType } from 'elysia'
body: z.object({
file: z.file().refine((file) => fileType(file, 'image/jpeg'))
})
```
Use `fileType` for security (validates magic number, not just MIME).
## Query
```typescript
query: t.Object({ name: t.String() })
// /?name=Elysia
```
Auto-coerces to specified type.
### Arrays
```typescript
query: t.Object({ name: t.Array(t.String()) })
```
Formats supported:
- **nuqs**: `?name=a,b,c` (comma delimiter)
- **HTML form**: `?name=a&name=b&name=c` (multiple keys)
## Params
```typescript
params: t.Object({ id: t.Number() })
// /id/1
```
Auto-inferred as string if schema not provided.
## Headers
```typescript
headers: t.Object({ authorization: t.String() })
```
`additionalProperties: true` by default. Always lowercase keys.
## Cookie
```typescript
cookie: t.Cookie({
name: t.String()
}, {
secure: true,
httpOnly: true
})
```
Or use `t.Object`. `additionalProperties: true` by default.
## Response
```typescript
response: t.Object({ name: t.String() })
```
### Per Status
```typescript
response: {
200: t.Object({ name: t.String() }),
400: t.Object({ error: t.String() })
}
```
## Error Handling
### Inline Error Property
```typescript
body: t.Object({
x: t.Number({ error: 'x must be number' })
})
```
Or function:
```typescript
x: t.Number({
error({ errors, type, validation, value }) {
return 'Expected x to be number'
}
})
```
### onError Hook
```typescript
.onError(({ code, error }) => {
if (code === 'VALIDATION')
return error.message // or error.all[0].message
})
```
`error.all` - list all error causes. `error.all.find(x => x.path === '/name')` - find specific field.
## Reference Models
Name + reuse models:
```typescript
.model({
sign: t.Object({
username: t.String(),
password: t.String()
})
})
.post('/sign-in', ({ body }) => body, {
body: 'sign',
response: 'sign'
})
```
Extract to plugin:
```typescript
// auth.model.ts
export const authModel = new Elysia().model({ sign: t.Object({...}) })
// main.ts
new Elysia().use(authModel).post('/', ..., { body: 'sign' })
```
### Naming Convention
Prevent duplicates with namespaces:
```typescript
.model({
'auth.admin': t.Object({...}),
'auth.user': t.Object({...})
})
```
Or use `prefix` / `suffix` to rename models in current instance
```typescript
.model({ sign: t.Object({...}) })
.prefix('model', 'auth')
.post('/', () => '', {
body: 'auth.User'
})
```
Models with `prefix` will be capitalized.
## TypeScript Types
```typescript
const MyType = t.Object({ hello: t.Literal('Elysia') })
type MyType = typeof MyType.static
```
Single schema → runtime validation + coercion + TypeScript type + OpenAPI.
## Guard
Apply schema to multiple handlers. Affects all handlers after definition.
### Basic Usage
```typescript
import { Elysia, t } from 'elysia'
new Elysia()
.get('/none', ({ query }) => 'hi')
.guard({
query: t.Object({
name: t.String()
})
})
.get('/query', ({ query }) => query)
.listen(3000)
```
Ensures `query.name` string required for all handlers after guard.
### Behavior
| Path | Response |
|---------------|----------|
| /none | hi |
| /none?name=a | hi |
| /query | error |
| /query?name=a | a |
### Precedence
- Multiple global schemas: latest wins
- Global vs local: local wins
### Schema Types
1. override (default)
Latest schema overrides collided schema.
```typescript
.guard({ query: t.Object({ name: t.String() }) })
.guard({ query: t.Object({ id: t.Number() }) })
// Only id required, name overridden
```
2. standalone
Both schemas run independently. Both validated.
```typescript
.guard({ query: t.Object({ name: t.String() }) }, { type: 'standalone' })
.guard({ query: t.Object({ id: t.Number() }) }, { type: 'standalone' })
// Both name AND id required
```
# Typebox Validation (Elysia.t)
Elysia.t = TypeBox with server-side pre-configuration + HTTP-specific types
**TypeBox API mirrors TypeScript syntax** but provides runtime validation
## Basic Types
| TypeBox | TypeScript | Example Value |
|---------|------------|---------------|
| `t.String()` | `string` | `"hello"` |
| `t.Number()` | `number` | `42` |
| `t.Boolean()` | `boolean` | `true` |
| `t.Array(t.Number())` | `number[]` | `[1, 2, 3]` |
| `t.Object({ x: t.Number() })` | `{ x: number }` | `{ x: 10 }` |
| `t.Null()` | `null` | `null` |
| `t.Literal(42)` | `42` | `42` |
## Attributes (JSON Schema 7)
```ts
// Email format
t.String({ format: 'email' })
// Number constraints
t.Number({ minimum: 10, maximum: 100 })
// Array constraints
t.Array(t.Number(), {
minItems: 1, // min items
maxItems: 5 // max items
})
// Object - allow extra properties
t.Object(
{ x: t.Number() },
{ additionalProperties: true } // default: false
)
```
## Common Patterns
### Union (Multiple Types)
```ts
t.Union([t.String(), t.Number()])
// type: string | number
// values: "Hello" or 123
```
### Optional (Field Optional)
```ts
t.Object({
x: t.Number(),
y: t.Optional(t.Number()) // can be undefined
})
// type: { x: number, y?: number }
// value: { x: 123 } or { x: 123, y: 456 }
```
### Partial (All Fields Optional)
```ts
t.Partial(t.Object({
x: t.Number(),
y: t.Number()
}))
// type: { x?: number, y?: number }
// value: {} or { y: 123 } or { x: 1, y: 2 }
```
## Elysia-Specific Types
### UnionEnum (One of Values)
```ts
t.UnionEnum(['rapi', 'anis', 1, true, false])
```
### File (Single File Upload)
```ts
t.File({
type: 'image', // or ['image', 'video']
minSize: '1k', // 1024 bytes
maxSize: '5m' // 5242880 bytes
})
```
**File unit suffixes**:
- `m` = MegaByte (1048576 bytes)
- `k` = KiloByte (1024 bytes)
### Files (Multiple Files)
```ts
t.Files() // extends File + array
```
### Cookie (Cookie Jar)
```ts
t.Cookie({
name: t.String()
}, {
secrets: 'secret-key' // or ['key1', 'key2'] for rotation
})
```
### Nullable (Allow null)
```ts
t.Nullable(t.String())
// type: string | null
```
### MaybeEmpty (Allow null + undefined)
```ts
t.MaybeEmpty(t.String())
// type: string | null | undefined
```
### Form (FormData Validation)
```ts
t.Form({
someValue: t.File()
})
// Syntax sugar for t.Object with FormData support
```
### UInt8Array (Buffer → Uint8Array)
```ts
t.UInt8Array()
// For binary file uploads with arrayBuffer parser
```
### ArrayBuffer (Buffer → ArrayBuffer)
```ts
t.ArrayBuffer()
// For binary file uploads with arrayBuffer parser
```
### ObjectString (String → Object)
```ts
t.ObjectString()
// Accepts: '{"x":1}' → parses to { x: 1 }
// Use in: query string, headers, FormData
```
### BooleanString (String → Boolean)
```ts
t.BooleanString()
// Accepts: 'true'/'false' → parses to boolean
// Use in: query string, headers, FormData
```
### Numeric (String/Number → Number)
```ts
t.Numeric()
// Accepts: '123' or 123 → transforms to 123
// Use in: path params, query string
```
## Elysia Behavior Differences from TypeBox
### 1. Optional Behavior
In Elysia, `t.Optional` makes **entire route parameter** optional (not object field):
```ts
.get('/optional', ({ query }) => query, {
query: t.Optional( // makes query itself optional
t.Object({ name: t.String() })
)
})
```
**Different from TypeBox**: TypeBox uses Optional for object fields only
### 2. Number → Numeric Auto-Conversion
**Route schema only** (not nested objects):
```ts
.get('/:id', ({ id }) => id, {
params: t.Object({
id: t.Number() // ✅ Auto-converts to t.Numeric()
}),
body: t.Object({
id: t.Number() // ❌ NOT converted (stays t.Number())
})
})
// Outside route schema
t.Number() // ❌ NOT converted
```
**Why**: HTTP headers/query/params always strings. Auto-conversion parses numeric strings.
### 3. Boolean → BooleanString Auto-Conversion
Same as Number → Numeric:
```ts
.get('/:active', ({ active }) => active, {
params: t.Object({
active: t.Boolean() // ✅ Auto-converts to t.BooleanString()
}),
body: t.Object({
active: t.Boolean() // ❌ NOT converted
})
})
```
## Usage Pattern
```ts
import { Elysia, t } from 'elysia'
new Elysia()
.post('/', ({ body }) => `Hello ${body}`, {
body: t.String() // validates body is string
})
.listen(3000)
```
**Validation flow**:
1. Request arrives
2. Schema validates against HTTP body/params/query/headers
3. If valid → handler executes
4. If invalid → Error Life Cycle
## Notes
[Inference] Based on docs:
- TypeBox mirrors TypeScript but adds runtime validation
- Elysia.t extends TypeBox with HTTP-specific types
- Auto-conversion (Number→Numeric, Boolean→BooleanString) only for route schemas
- Use `t.Optional` for optional route params (different from TypeBox behavior)
- File validation supports unit suffixes ('1k', '5m')
- ObjectString/BooleanString for parsing strings in query/headers
- Cookie supports key rotation with array of secrets

View File

@@ -0,0 +1,250 @@
# WebSocket
## Basic WebSocket
```typescript
import { Elysia } from 'elysia'
new Elysia()
.ws('/chat', {
message(ws, message) {
ws.send(message) // Echo back
}
})
.listen(3000)
```
## With Validation
```typescript
import { Elysia, t } from 'elysia'
.ws('/chat', {
body: t.Object({
message: t.String(),
username: t.String()
}),
response: t.Object({
message: t.String(),
timestamp: t.Number()
}),
message(ws, body) {
ws.send({
message: body.message,
timestamp: Date.now()
})
}
})
```
## Lifecycle Events
```typescript
.ws('/chat', {
open(ws) {
console.log('Client connected')
},
message(ws, message) {
console.log('Received:', message)
ws.send('Echo: ' + message)
},
close(ws) {
console.log('Client disconnected')
},
error(ws, error) {
console.error('Error:', error)
}
})
```
## Broadcasting
```typescript
const connections = new Set<any>()
.ws('/chat', {
open(ws) {
connections.add(ws)
},
message(ws, message) {
// Broadcast to all connected clients
for (const client of connections) {
client.send(message)
}
},
close(ws) {
connections.delete(ws)
}
})
```
## With Authentication
```typescript
.ws('/chat', {
beforeHandle({ headers, status }) {
const token = headers.authorization?.replace('Bearer ', '')
if (!verifyToken(token)) {
return status(401)
}
},
message(ws, message) {
ws.send(message)
}
})
```
## Room-Based Chat
```typescript
const rooms = new Map<string, Set<any>>()
.ws('/chat/:room', {
open(ws) {
const room = ws.data.params.room
if (!rooms.has(room)) {
rooms.set(room, new Set())
}
rooms.get(room)!.add(ws)
},
message(ws, message) {
const room = ws.data.params.room
const clients = rooms.get(room)
if (clients) {
for (const client of clients) {
client.send(message)
}
}
},
close(ws) {
const room = ws.data.params.room
const clients = rooms.get(room)
if (clients) {
clients.delete(ws)
if (clients.size === 0) {
rooms.delete(room)
}
}
}
})
```
## With State/Context
```typescript
.ws('/chat', {
open(ws) {
ws.data.userId = generateUserId()
ws.data.joinedAt = Date.now()
},
message(ws, message) {
const response = {
userId: ws.data.userId,
message,
timestamp: Date.now()
}
ws.send(response)
}
})
```
## Client Usage (Browser)
```typescript
const ws = new WebSocket('ws://localhost:3000/chat')
ws.onopen = () => {
console.log('Connected')
ws.send('Hello Server!')
}
ws.onmessage = (event) => {
console.log('Received:', event.data)
}
ws.onerror = (error) => {
console.error('Error:', error)
}
ws.onclose = () => {
console.log('Disconnected')
}
```
## Eden Treaty WebSocket
```typescript
// Server
export const app = new Elysia()
.ws('/chat', {
message(ws, message) {
ws.send(message)
}
})
export type App = typeof app
// Client
import { treaty } from '@elysiajs/eden'
import type { App } from './server'
const api = treaty<App>('localhost:3000')
const chat = api.chat.subscribe()
chat.subscribe((message) => {
console.log('Received:', message)
})
chat.send('Hello!')
```
## Headers in WebSocket
```typescript
.ws('/chat', {
header: t.Object({
authorization: t.String()
}),
beforeHandle({ headers, status }) {
const token = headers.authorization?.replace('Bearer ', '')
if (!token) return status(401)
},
message(ws, message) {
ws.send(message)
}
})
```
## Query Parameters
```typescript
.ws('/chat', {
query: t.Object({
username: t.String()
}),
message(ws, message) {
const username = ws.data.query.username
ws.send(`${username}: ${message}`)
}
})
// Client
const ws = new WebSocket('ws://localhost:3000/chat?username=john')
```
## Compression
```typescript
new Elysia({
websocket: {
perMessageDeflate: true
}
})
.ws('/chat', {
message(ws, message) {
ws.send(message)
}
})
```