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,475 @@
---
name: elysiajs
description: Create backend with ElysiaJS, a type-safe, high-performance framework.
---
# ElysiaJS Development Skill
Always consult [elysiajs.com/llms.txt](https://elysiajs.com/llms.txt) for code examples and latest API.
## Overview
ElysiaJS is a TypeScript framework for building Bun-first (but not limited to Bun) type-safe, high-performance backend servers. This skill provides comprehensive guidance for developing with Elysia, including routing, validation, authentication, plugins, integrations, and deployment.
## When to Use This Skill
Trigger this skill when the user asks to:
- Create or modify ElysiaJS routes, handlers, or servers
- Setup validation with TypeBox or other schema libraries (Zod, Valibot)
- Implement authentication (JWT, session-based, macros, guards)
- Add plugins (CORS, OpenAPI, Static files, JWT)
- Integrate with external services (Drizzle ORM, Better Auth, Next.js, Eden Treaty)
- Setup WebSocket endpoints for real-time features
- Create unit tests for Elysia instances
- Deploy Elysia servers to production
## Quick Start
Quick scaffold:
```bash
bun create elysia app
```
### Basic Server
```typescript
import { Elysia, t, status } from 'elysia'
const app = new Elysia()
.get('/', () => 'Hello World')
.post('/user', ({ body }) => body, {
body: t.Object({
name: t.String(),
age: t.Number()
})
})
.get('/id/:id', ({ params: { id } }) => {
if(id > 1_000_000) return status(404, 'Not Found')
return id
}, {
params: t.Object({
id: t.Number({
minimum: 1
})
}),
response: {
200: t.Number(),
404: t.Literal('Not Found')
}
})
.listen(3000)
```
## Basic Usage
### HTTP Methods
```typescript
import { Elysia } from 'elysia'
new Elysia()
.get('/', 'GET')
.post('/', 'POST')
.put('/', 'PUT')
.patch('/', 'PATCH')
.delete('/', 'DELETE')
.options('/', 'OPTIONS')
.head('/', 'HEAD')
```
### Path Parameters
```typescript
.get('/user/:id', ({ params: { id } }) => id)
.get('/post/:id/:slug', ({ params }) => params)
```
### Query Parameters
```typescript
.get('/search', ({ query }) => query.q)
// GET /search?q=elysia → "elysia"
```
### Request Body
```typescript
.post('/user', ({ body }) => body)
```
### Headers
```typescript
.get('/', ({ headers }) => headers.authorization)
```
## TypeBox Validation
### Basic Types
```typescript
import { Elysia, t } from 'elysia'
.post('/user', ({ body }) => body, {
body: t.Object({
name: t.String(),
age: t.Number(),
email: t.String({ format: 'email' }),
website: t.Optional(t.String({ format: 'uri' }))
})
})
```
### Nested Objects
```typescript
body: t.Object({
user: t.Object({
name: t.String(),
address: t.Object({
street: t.String(),
city: t.String()
})
})
})
```
### Arrays
```typescript
body: t.Object({
tags: t.Array(t.String()),
users: t.Array(t.Object({
id: t.String(),
name: t.String()
}))
})
```
### File Upload
```typescript
.post('/upload', ({ body }) => body.file, {
body: t.Object({
file: t.File({
type: 'image', // image/* mime types
maxSize: '5m' // 5 megabytes
}),
files: t.Files({ // Multiple files
type: ['image/png', 'image/jpeg']
})
})
})
```
### Response Validation
```typescript
.get('/user/:id', ({ params: { id } }) => ({
id,
name: 'John',
email: 'john@example.com'
}), {
params: t.Object({
id: t.Number()
}),
response: {
200: t.Object({
id: t.Number(),
name: t.String(),
email: t.String()
}),
404: t.String()
}
})
```
## Standard Schema (Zod, Valibot, ArkType)
### Zod
```typescript
import { z } from 'zod'
.post('/user', ({ body }) => body, {
body: z.object({
name: z.string(),
age: z.number().min(0),
email: z.string().email()
})
})
```
## Error Handling
```typescript
.get('/user/:id', ({ params: { id }, status }) => {
const user = findUser(id)
if (!user) {
return status(404, 'User not found')
}
return user
})
```
## Guards (Apply to Multiple Routes)
```typescript
.guard({
params: t.Object({
id: t.Number()
})
}, app => app
.get('/user/:id', ({ params: { id } }) => id)
.delete('/user/:id', ({ params: { id } }) => id)
)
```
## Macro
```typescript
.macro({
hi: (word: string) => ({
beforeHandle() { console.log(word) }
})
})
.get('/', () => 'hi', { hi: 'Elysia' })
```
### Project Structure (Recommended)
Elysia takes an unopinionated approach but based on user request. But without any specific preference, we recommend a feature-based and domain driven folder structure where each feature has its own folder containing controllers, services, and models.
```
src/
├── index.ts # Main server entry
├── modules/
│ ├── auth/
│ │ ├── index.ts # Auth routes (Elysia instance)
│ │ ├── service.ts # Business logic
│ │ └── model.ts # TypeBox schemas/DTOs
│ └── user/
│ ├── index.ts
│ ├── service.ts
│ └── model.ts
└── plugins/
└── custom.ts
public/ # Static files (if using static plugin)
test/ # Unit tests
```
Each file has its own responsibility as follows:
- **Controller (index.ts)**: Handle HTTP routing, request validation, and cookie.
- **Service (service.ts)**: Handle business logic, decoupled from Elysia controller if possible.
- **Model (model.ts)**: Define the data structure and validation for the request and response.
## Best Practice
Elysia is unopinionated on design pattern, but if not provided, we can relies on MVC pattern pair with feature based folder structure.
- Controller:
- Prefers Elysia as a controller for HTTP dependant controller
- For non HTTP dependent, prefers service instead unless explicitly asked
- Use `onError` to handle local custom errors
- Register Model to Elysia instance via `Elysia.models({ ...models })` and prefix model by namespace `Elysia.prefix('model', 'Namespace.')
- Prefers Reference Model by name provided by Elysia instead of using an actual `Model.name`
- Service:
- Prefers class (or abstract class if possible)
- Prefers interface/type derive from `Model`
- Return `status` (`import { status } from 'elysia'`) for error
- Prefers `return Error` instead of `throw Error`
- Models:
- Always export validation model and type of validation model
- Custom Error should be in contains in Model
## Elysia Key Concept
Elysia has a every important concepts/rules to understand before use.
## Encapsulation - Isolates by Default
Lifecycles (hooks, middleware) **don't leak** between instances unless scoped.
**Scope levels:**
- `local` (default) - current instance + descendants
- `scoped` - parent + current + descendants
- `global` - all instances
```ts
.onBeforeHandle(() => {}) // only local instance
.onBeforeHandle({ as: 'global' }, () => {}) // exports to all
```
## Method Chaining - Required for Types
**Must chain**. Each method returns new type reference.
❌ Don't:
```ts
const app = new Elysia()
app.state('build', 1) // loses type
app.get('/', ({ store }) => store.build) // build doesn't exists
```
✅ Do:
```ts
new Elysia()
.state('build', 1)
.get('/', ({ store }) => store.build)
```
## Explicit Dependencies
Each instance independent. **Declare what you use.**
```ts
const auth = new Elysia()
.decorate('Auth', Auth)
.model(Auth.models)
new Elysia()
.get('/', ({ Auth }) => Auth.getProfile()) // Auth doesn't exists
new Elysia()
.use(auth) // must declare
.get('/', ({ Auth }) => Auth.getProfile())
```
**Global scope when:**
- No types added (cors, helmet)
- Global lifecycle (logging, tracing)
**Explicit when:**
- Adds types (state, models)
- Business logic (auth, db)
## Deduplication
Plugins re-execute unless named:
```ts
new Elysia() // rerun on `.use`
new Elysia({ name: 'ip' }) // runs once across all instances
```
## Order Matters
Events apply to routes **registered after** them.
```ts
.onBeforeHandle(() => console.log('1'))
.get('/', () => 'hi') // has hook
.onBeforeHandle(() => console.log('2')) // doesn't affect '/'
```
## Type Inference
**Inline functions only** for accurate types.
For controllers, destructure in inline wrapper:
```ts
.post('/', ({ body }) => Controller.greet(body), {
body: t.Object({ name: t.String() })
})
```
Get type from schema:
```ts
type MyType = typeof MyType.static
```
## Reference Model
Model can be reference by name, especially great for documenting an API
```ts
new Elysia()
.model({
book: t.Object({
name: t.String()
})
})
.post('/', ({ body }) => body.name, {
body: 'book'
})
```
Model can be renamed by using `.prefix` / `.suffix`
```ts
new Elysia()
.model({
book: t.Object({
name: t.String()
})
})
.prefix('model', 'Namespace')
.post('/', ({ body }) => body.name, {
body: 'Namespace.Book'
})
```
Once `prefix`, model name will be capitalized by default.
## Technical Terms
The following are technical terms that is use for Elysia:
- `OpenAPI Type Gen` - function name `fromTypes` from `@elysiajs/openapi` for generating OpenAPI from types, see `plugins/openapi.md`
- `Eden`, `Eden Treaty` - e2e type safe RPC client for share type from backend to frontend
## Resources
Use the following references as needed.
It's recommended to checkout `route.md` for as it contains the most important foundation building blocks with examples.
`plugin.md` and `validation.md` is important as well but can be check as needed.
### references/
Detailed documentation split by topic:
- `bun-fullstack-dev-server.md` - Bun Fullstack Dev Server with HMR. React without bundler.
- `cookie.md` - Detailed documentation on cookie
- `deployment.md` - Production deployment guide / Docker
- `eden.md` - e2e type safe RPC client for share type from backend to frontend
- `guard.md` - Setting validation/lifecycle all at once
- `macro.md` - Compose multiple schema/lifecycle as a reusable Elysia via key-value (recommended for complex setup, eg. authentication, authorization, Role-based Access Check)
- `plugin.md` - Decouple part of Elysia into a standalone component
- `route.md` - Elysia foundation building block: Routing, Handler and Context
- `testing.md` - Unit tests with examples
- `validation.md` - Setup input/output validation and list of all custom validation rules
- `websocket.md` - Real-time features
### plugins/
Detailed documentation, usage and configuration reference for official Elysia plugin:
- `bearer.md` - Add bearer capability to Elysia (`@elysiajs/bearer`)
- `cors.md` - Out of box configuration for CORS (`@elysiajs/cors`)
- `cron.md` - Run cron job with access to Elysia context (`@elysiajs/cron`)
- `graphql-apollo.md` - Integration GraphQL Apollo (`@elysiajs/graphql-apollo`)
- `graphql-yoga.md` - Integration with GraphQL Yoga (`@elysiajs/graphql-yoga`)
- `html.md` - HTML and JSX plugin setup and usage (`@elysiajs/html`)
- `jwt.md` - JWT / JWK plugin (`@elysiajs/jwt`)
- `openapi.md` - OpenAPI documentation and OpenAPI Type Gen / OpenAPI from types (`@elysiajs/openapi`)
- `opentelemetry.md` - OpenTelemetry, instrumentation, and record span utilities (`@elysiajs/opentelemetry`)
- `server-timing.md` - Server Timing metric for debug (`@elysiajs/server-timing`)
- `static.md` - Serve static files/folders for Elysia Server (`@elysiajs/static`)
### integrations/
Guide to integrate Elysia with external library/runtime:
- `ai-sdk.md` - Using Vercel AI SDK with Elysia
- `astro.md` - Elysia in Astro API route
- `better-auth.md` - Integrate Elysia with better-auth
- `cloudflare-worker.md` - Elysia on Cloudflare Worker adapter
- `deno.md` - Elysia on Deno
- `drizzle.md` - Integrate Elysia with Drizzle ORM
- `expo.md` - Elysia in Expo API route
- `nextjs.md` - Elysia in Nextjs API route
- `nodejs.md` - Run Elysia on Node.js
- `nuxt.md` - Elysia on API route
- `prisma.md` - Integrate Elysia with Prisma
- `react-email.d` - Create and Send Email with React and Elysia
- `sveltekit.md` - Run Elysia on Svelte Kit API route
- `tanstack-start.md` - Run Elysia on Tanstack Start / React Query
- `vercel.md` - Deploy Elysia to Vercel
### examples/ (optional)
- `basic.ts` - Basic Elysia example
- `body-parser.ts` - Custom body parser example via `.onParse`
- `complex.ts` - Comprehensive usage of Elysia server
- `cookie.ts` - Setting cookie
- `error.ts` - Error handling
- `file.ts` - Returning local file from server
- `guard.ts` - Setting mulitple validation schema and lifecycle
- `map-response.ts` - Custom response mapper
- `redirect.ts` - Redirect response
- `rename.ts` - Rename context's property
- `schema.ts` - Setup validation
- `state.ts` - Setup global state
- `upload-file.ts` - File upload with validation
- `websocket.ts` - Web Socket for realtime communication
### patterns/ (optional)
- `patterns/mvc.md` - Detail guideline for using Elysia with MVC patterns

View File

@@ -0,0 +1,9 @@
import { Elysia, t } from 'elysia'
new Elysia()
.get('/', 'Hello Elysia')
.post('/', ({ body: { name } }) => name, {
body: t.Object({
name: t.String()
})
})

View File

@@ -0,0 +1,33 @@
import { Elysia, t } from 'elysia'
const app = new Elysia()
// Add custom body parser
.onParse(async ({ request, contentType }) => {
switch (contentType) {
case 'application/Elysia':
return request.text()
}
})
.post('/', ({ body: { username } }) => `Hi ${username}`, {
body: t.Object({
id: t.Number(),
username: t.String()
})
})
// Increase id by 1 from body before main handler
.post('/transform', ({ body }) => body, {
transform: ({ body }) => {
body.id = body.id + 1
},
body: t.Object({
id: t.Number(),
username: t.String()
}),
detail: {
summary: 'A'
}
})
.post('/mirror', ({ body }) => body)
.listen(3000)
console.log('🦊 Elysia is running at :8080')

View File

@@ -0,0 +1,112 @@
import { Elysia, t, file } from 'elysia'
const loggerPlugin = new Elysia()
.get('/hi', () => 'Hi')
.decorate('log', () => 'A')
.decorate('date', () => new Date())
.state('fromPlugin', 'From Logger')
.use((app) => app.state('abc', 'abc'))
const app = new Elysia()
.onRequest(({ set }) => {
set.headers = {
'Access-Control-Allow-Origin': '*'
}
})
.onError(({ code }) => {
if (code === 'NOT_FOUND')
return 'Not Found :('
})
.use(loggerPlugin)
.state('build', Date.now())
.get('/', 'Elysia')
.get('/tako', file('./example/takodachi.png'))
.get('/json', () => ({
hi: 'world'
}))
.get('/root/plugin/log', ({ log, store: { build } }) => {
log()
return build
})
.get('/wildcard/*', () => 'Hi Wildcard')
.get('/query', () => 'Elysia', {
beforeHandle: ({ query }) => {
console.log('Name:', query?.name)
if (query?.name === 'aom') return 'Hi saltyaom'
},
query: t.Object({
name: t.String()
})
})
.post('/json', async ({ body }) => body, {
body: t.Object({
name: t.String(),
additional: t.String()
})
})
.post('/transform-body', async ({ body }) => body, {
beforeHandle: (ctx) => {
ctx.body = {
...ctx.body,
additional: 'Elysia'
}
},
body: t.Object({
name: t.String(),
additional: t.String()
})
})
.get('/id/:id', ({ params: { id } }) => id, {
transform({ params }) {
params.id = +params.id
},
params: t.Object({
id: t.Number()
})
})
.post('/new/:id', async ({ body, params }) => body, {
params: t.Object({
id: t.Number()
}),
body: t.Object({
username: t.String()
})
})
.get('/trailing-slash', () => 'A')
.group('/group', (app) =>
app
.onBeforeHandle(({ query }) => {
if (query?.name === 'aom') return 'Hi saltyaom'
})
.get('/', () => 'From Group')
.get('/hi', () => 'HI GROUP')
.get('/elysia', () => 'Welcome to Elysian Realm')
.get('/fbk', () => 'FuBuKing')
)
.get('/response-header', ({ set }) => {
set.status = 404
set.headers['a'] = 'b'
return 'A'
})
.get('/this/is/my/deep/nested/root', () => 'Hi')
.get('/build', ({ store: { build } }) => build)
.get('/ref', ({ date }) => date())
.get('/response', () => new Response('Hi'))
.get('/error', () => new Error('Something went wrong'))
.get('/401', ({ set }) => {
set.status = 401
return 'Status should be 401'
})
.get('/timeout', async () => {
await new Promise((resolve) => setTimeout(resolve, 2000))
return 'A'
})
.all('/all', () => 'hi')
.listen(8080, ({ hostname, port }) => {
console.log(`🦊 Elysia is running at http://${hostname}:${port}`)
})

View File

@@ -0,0 +1,45 @@
import { Elysia, t } from 'elysia'
const app = new Elysia({
cookie: {
secrets: 'Fischl von Luftschloss Narfidort',
sign: ['name']
}
})
.get(
'/council',
({ cookie: { council } }) =>
(council.value = [
{
name: 'Rin',
affilation: 'Administration'
}
]),
{
cookie: t.Cookie({
council: t.Array(
t.Object({
name: t.String(),
affilation: t.String()
})
)
})
}
)
.get('/create', ({ cookie: { name } }) => (name.value = 'Himari'))
.get(
'/update',
({ cookie: { name } }) => {
name.value = 'seminar: Rio'
name.value = 'seminar: Himari'
name.maxAge = 86400
return name.value
},
{
cookie: t.Cookie({
name: t.Optional(t.String())
})
}
)
.listen(3000)

View File

@@ -0,0 +1,38 @@
import { Elysia, t } from 'elysia'
class CustomError extends Error {
constructor(public name: string) {
super(name)
}
}
new Elysia()
.error({
CUSTOM_ERROR: CustomError
})
// global handler
.onError(({ code, error, status }) => {
switch (code) {
case "CUSTOM_ERROR":
return status(401, { message: error.message })
case "NOT_FOUND":
return "Not found :("
}
})
.post('/', ({ body }) => body, {
body: t.Object({
username: t.String(),
password: t.String(),
nested: t.Optional(
t.Object({
hi: t.String()
})
)
}),
// local handler
error({ error }) {
console.log(error)
}
})
.listen(3000)

View File

@@ -0,0 +1,10 @@
import { Elysia, file } from 'elysia'
/**
* Example of handle single static file
*
* @see https://github.com/elysiajs/elysia-static
*/
new Elysia()
.get('/tako', file('./example/takodachi.png'))
.listen(3000)

View File

@@ -0,0 +1,34 @@
import { Elysia, t } from 'elysia'
new Elysia()
.state('name', 'salt')
.get('/', ({ store: { name } }) => `Hi ${name}`, {
query: t.Object({
name: t.String()
})
})
// If query 'name' is not preset, skip the whole handler
.guard(
{
query: t.Object({
name: t.String()
})
},
(app) =>
app
// Query type is inherited from guard
.get('/profile', ({ query }) => `Hi`)
// Store is inherited
.post('/name', ({ store: { name }, body, query }) => name, {
body: t.Object({
id: t.Number({
minimum: 5
}),
username: t.String(),
profile: t.Object({
name: t.String()
})
})
})
)
.listen(3000)

View File

@@ -0,0 +1,15 @@
import { Elysia } from 'elysia'
const prettyJson = new Elysia()
.mapResponse(({ response }) => {
if (response instanceof Object)
return new Response(JSON.stringify(response, null, 4))
})
.as('scoped')
new Elysia()
.use(prettyJson)
.get('/', () => ({
hello: 'world'
}))
.listen(3000)

View File

@@ -0,0 +1,6 @@
import { Elysia } from 'elysia'
new Elysia()
.get('/', () => 'Hi')
.get('/redirect', ({ redirect }) => redirect('/'))
.listen(3000)

View File

@@ -0,0 +1,32 @@
import { Elysia, t } from 'elysia'
// ? Elysia#83 | Proposal: Standardized way of renaming third party plugin-scoped stuff
// this would be a plugin provided by a third party
const myPlugin = new Elysia()
.decorate('myProperty', 42)
.model('salt', t.String())
new Elysia()
.use(
myPlugin
// map decorator, rename "myProperty" to "renamedProperty"
.decorate(({ myProperty, ...decorators }) => ({
renamedProperty: myProperty,
...decorators
}))
// map model, rename "salt" to "pepper"
.model(({ salt, ...models }) => ({
...models,
pepper: t.String()
}))
// Add prefix
.prefix('decorator', 'unstable')
)
.get(
'/mapped',
({ unstableRenamedProperty }) => unstableRenamedProperty
)
.post('/pepper', ({ body }) => body, {
body: 'pepper',
// response: t.String()
})

View File

@@ -0,0 +1,61 @@
import { Elysia, t } from 'elysia'
const app = new Elysia()
.model({
name: t.Object({
name: t.String()
}),
b: t.Object({
response: t.Number()
}),
authorization: t.Object({
authorization: t.String()
})
})
// Strictly validate response
.get('/', () => 'hi')
// Strictly validate body and response
.post('/', ({ body, query }) => body.id, {
body: t.Object({
id: t.Number(),
username: t.String(),
profile: t.Object({
name: t.String()
})
})
})
// Strictly validate query, params, and body
.get('/query/:id', ({ query: { name }, params }) => name, {
query: t.Object({
name: t.String()
}),
params: t.Object({
id: t.String()
}),
response: {
200: t.String(),
300: t.Object({
error: t.String()
})
}
})
.guard(
{
headers: 'authorization'
},
(app) =>
app
.derive(({ headers }) => ({
userId: headers.authorization
}))
.get('/', ({ userId }) => 'A')
.post('/id/:id', ({ query, body, params, userId }) => body, {
params: t.Object({
id: t.Number()
}),
transform({ params }) {
params.id = +params.id
}
})
)
.listen(3000)

View File

@@ -0,0 +1,6 @@
import { Elysia } from 'elysia'
new Elysia()
.state('counter', 0)
.get('/', ({ store }) => store.counter++)
.listen(3000)

View File

@@ -0,0 +1,20 @@
import { Elysia, t } from 'elysia'
const app = new Elysia()
.post('/single', ({ body: { file } }) => file, {
body: t.Object({
file: t.File({
maxSize: '1m'
})
})
})
.post(
'/multiple',
({ body: { files } }) => files.reduce((a, b) => a + b.size, 0),
{
body: t.Object({
files: t.Files()
})
}
)
.listen(3000)

View File

@@ -0,0 +1,25 @@
import { Elysia } from 'elysia'
const app = new Elysia()
.state('start', 'here')
.ws('/ws', {
open(ws) {
ws.subscribe('asdf')
console.log('Open Connection:', ws.id)
},
close(ws) {
console.log('Closed Connection:', ws.id)
},
message(ws, message) {
ws.publish('asdf', message)
ws.send(message)
}
})
.get('/publish/:publish', ({ params: { publish: text } }) => {
app.server!.publish('asdf', text)
return text
})
.listen(3000, (server) => {
console.log(`http://${server.hostname}:${server.port}`)
})

View File

@@ -0,0 +1,92 @@
# AI SDK Integration
## What It Is
Seamless integration with Vercel AI SDK via response streaming.
## Response Streaming
Return `ReadableStream` or `Response` directly:
```typescript
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
new Elysia().get('/', () => {
const stream = streamText({
model: openai('gpt-5'),
system: 'You are Yae Miko from Genshin Impact',
prompt: 'Hi! How are you doing?'
})
return stream.textStream // ReadableStream
// or
return stream.toUIMessageStream() // UI Message Stream
})
```
Elysia auto-handles stream.
## Server-Sent Events
Wrap `ReadableStream` with `sse`:
```typescript
import { sse } from 'elysia'
.get('/', () => {
const stream = streamText({ /* ... */ })
return sse(stream.textStream)
// or
return sse(stream.toUIMessageStream())
})
```
Each chunk → SSE.
## As Response
Return stream directly (no Eden type safety):
```typescript
.get('/', () => {
const stream = streamText({ /* ... */ })
return stream.toTextStreamResponse()
// or
return stream.toUIMessageStreamResponse() // Uses SSE
})
```
## Manual Streaming
Generator function for control:
```typescript
import { sse } from 'elysia'
.get('/', async function* () {
const stream = streamText({ /* ... */ })
for await (const data of stream.textStream)
yield sse({ data, event: 'message' })
yield sse({ event: 'done' })
})
```
## Fetch for Unsupported Models
Direct fetch with streaming proxy:
```typescript
.get('/', () => {
return fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`
},
body: JSON.stringify({
model: 'gpt-5',
stream: true,
messages: [
{ role: 'system', content: 'You are Yae Miko' },
{ role: 'user', content: 'Hi! How are you doing?' }
]
})
})
})
```
Elysia auto-proxies fetch response with streaming.

View File

@@ -0,0 +1,59 @@
# Astro Integration - SKILLS.md
## What It Is
Run Elysia on Astro via Astro Endpoint.
## Setup
1. Set output to server:
```javascript
// astro.config.mjs
export default defineConfig({
output: 'server'
})
```
2. Create `pages/[...slugs].ts`
3. Define Elysia server + export handlers:
```typescript
// pages/[...slugs].ts
import { Elysia, t } from 'elysia'
const app = new Elysia()
.get('/api', () => 'hi')
.post('/api', ({ body }) => body, {
body: t.Object({ name: t.String() })
})
const handle = ({ request }: { request: Request }) => app.handle(request)
export const GET = handle
export const POST = handle
```
WinterCG compliance - works normally.
Recommended: Run Astro on Bun (Elysia designed for Bun).
## Prefix for Non-Root
If placed in `pages/api/[...slugs].ts`, set prefix:
```typescript
// pages/api/[...slugs].ts
const app = new Elysia({ prefix: '/api' })
.get('/', () => 'hi')
const handle = ({ request }: { request: Request }) => app.handle(request)
export const GET = handle
export const POST = handle
```
Ensures routing works in any location.
## Benefits
Co-location of frontend + backend. End-to-end type safety with Eden.
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,117 @@
# Better Auth Integration
Elysia + Better Auth integration guide
## What It Is
Framework-agnostic TypeScript auth/authz. Comprehensive features + plugin ecosystem.
## Setup
```typescript
import { betterAuth } from 'better-auth'
import { Pool } from 'pg'
export const auth = betterAuth({
database: new Pool()
})
```
## Handler Mounting
```typescript
import { auth } from './auth'
new Elysia()
.mount(auth.handler) // http://localhost:3000/api/auth
.listen(3000)
```
### Custom Endpoint
```typescript
// Mount with prefix
.mount('/auth', auth.handler) // http://localhost:3000/auth/api/auth
// Customize basePath
export const auth = betterAuth({
basePath: '/api' // http://localhost:3000/auth/api
})
```
Cannot set `basePath` to empty or `/`.
## OpenAPI Integration
Extract docs from Better Auth:
```typescript
import { openAPI } from 'better-auth/plugins'
let _schema: ReturnType<typeof auth.api.generateOpenAPISchema>
const getSchema = async () => (_schema ??= auth.api.generateOpenAPISchema())
export const OpenAPI = {
getPaths: (prefix = '/auth/api') =>
getSchema().then(({ paths }) => {
const reference: typeof paths = Object.create(null)
for (const path of Object.keys(paths)) {
const key = prefix + path
reference[key] = paths[path]
for (const method of Object.keys(paths[path])) {
const operation = (reference[key] as any)[method]
operation.tags = ['Better Auth']
}
}
return reference
}) as Promise<any>,
components: getSchema().then(({ components }) => components) as Promise<any>
} as const
```
Apply to Elysia:
```typescript
new Elysia().use(openapi({
documentation: {
components: await OpenAPI.components,
paths: await OpenAPI.getPaths()
}
}))
```
## CORS
```typescript
import { cors } from '@elysiajs/cors'
new Elysia()
.use(cors({
origin: 'http://localhost:3001',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
credentials: true,
allowedHeaders: ['Content-Type', 'Authorization']
}))
.mount(auth.handler)
```
## Macro for Auth
Use macro + resolve for session/user:
```typescript
const betterAuth = new Elysia({ name: 'better-auth' })
.mount(auth.handler)
.macro({
auth: {
async resolve({ status, request: { headers } }) {
const session = await auth.api.getSession({ headers })
if (!session) return status(401)
return {
user: session.user,
session: session.session
}
}
}
})
new Elysia()
.use(betterAuth)
.get('/user', ({ user }) => user, { auth: true })
```
Access `user` and `session` in all routes.

View File

@@ -0,0 +1,95 @@
# Cloudflare Worker Integration
## What It Is
**Experimental** Cloudflare Worker adapter for Elysia.
## Setup
1. Install Wrangler:
```bash
wrangler init elysia-on-cloudflare
```
2. Apply adapter + compile:
```typescript
import { Elysia } from 'elysia'
import { CloudflareAdapter } from 'elysia/adapter/cloudflare-worker'
export default new Elysia({
adapter: CloudflareAdapter
})
.get('/', () => 'Hello Cloudflare Worker!')
.compile() // Required
```
3. Set compatibility date (min `2025-06-01`):
```json
// wrangler.json
{
"name": "elysia-on-cloudflare",
"main": "src/index.ts",
"compatibility_date": "2025-06-01"
}
```
4. Dev server:
```bash
wrangler dev
# http://localhost:8787
```
No `nodejs_compat` flag needed.
## Limitations
1. `Elysia.file` + Static Plugin don't work (no `fs` module)
2. OpenAPI Type Gen doesn't work (no `fs` module)
3. Cannot define Response before server start
4. Cannot inline values:
```typescript
// ❌ Throws error
.get('/', 'Hello Elysia')
// ✅ Works
.get('/', () => 'Hello Elysia')
```
## Static Files
Use Cloudflare's built-in static serving:
```json
// wrangler.json
{
"assets": { "directory": "public" }
}
```
Structure:
```
├─ public
│ ├─ kyuukurarin.mp4
│ └─ static/mika.webp
```
Access:
- `http://localhost:8787/kyuukurarin.mp4`
- `http://localhost:8787/static/mika.webp`
## Binding
Import env from `cloudflare:workers`:
```typescript
import { env } from 'cloudflare:workers'
export default new Elysia({ adapter: CloudflareAdapter })
.get('/', () => `Hello ${await env.KV.get('my-key')}`)
.compile()
```
## AoT Compilation
As of Elysia 1.4.7, AoT works with Cloudflare Worker. Drop `aot: false` flag.
Cloudflare now supports Function compilation during startup.
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,34 @@
# Deno Integration
Run Elysia on Deno
## What It Is
Run Elysia on Deno via Web Standard Request/Response.
## Setup
Wrap `Elysia.fetch` in `Deno.serve`:
```typescript
import { Elysia } from 'elysia'
const app = new Elysia()
.get('/', () => 'Hello Elysia')
.listen(3000)
Deno.serve(app.fetch)
```
Run:
```bash
deno serve --watch src/index.ts
```
## Port Config
```typescript
Deno.serve(app.fetch) // Default
Deno.serve({ port: 8787 }, app.fetch) // Custom port
```
## pnpm
[Inference] pnpm doesn't auto-install peer deps. Manual install required:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,258 @@
# Drizzle Integration
Elysia + Drizzle integration guide
## What It Is
Headless TypeScript ORM. Convert Drizzle schema → Elysia validation models via `drizzle-typebox`.
## Flow
```
Drizzle → drizzle-typebox → Elysia validation → OpenAPI + Eden Treaty
```
## Installation
```bash
bun add drizzle-orm drizzle-typebox
```
### Pin TypeBox Version
Prevent Symbol conflicts:
```bash
grep "@sinclair/typebox" node_modules/elysia/package.json
```
Add to `package.json`:
```json
{
"overrides": {
"@sinclair/typebox": "0.32.4"
}
}
```
## Drizzle Schema
```typescript
// src/database/schema.ts
import { pgTable, varchar, timestamp } from 'drizzle-orm/pg-core'
import { createId } from '@paralleldrive/cuid2'
export const user = pgTable('user', {
id: varchar('id').$defaultFn(() => createId()).primaryKey(),
username: varchar('username').notNull().unique(),
password: varchar('password').notNull(),
email: varchar('email').notNull().unique(),
salt: varchar('salt', { length: 64 }).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull()
})
export const table = { user } as const
export type Table = typeof table
```
## drizzle-typebox
```typescript
import { t } from 'elysia'
import { createInsertSchema } from 'drizzle-typebox'
import { table } from './database/schema'
const _createUser = createInsertSchema(table.user, {
email: t.String({ format: 'email' }) // Replace with Elysia type
})
new Elysia()
.post('/sign-up', ({ body }) => {}, {
body: t.Omit(_createUser, ['id', 'salt', 'createdAt'])
})
```
## Type Instantiation Error
**Error**: "Type instantiation is possibly infinite"
**Cause**: Circular reference when nesting drizzle-typebox into Elysia schema.
**Fix**: Explicitly define type between them:
```typescript
// ✅ Works
const _createUser = createInsertSchema(table.user, {
email: t.String({ format: 'email' })
})
const createUser = t.Omit(_createUser, ['id', 'salt', 'createdAt'])
// ❌ Infinite loop
const createUser = t.Omit(
createInsertSchema(table.user, { email: t.String({ format: 'email' }) }),
['id', 'salt', 'createdAt']
)
```
Always declare variable for drizzle-typebox then reference it.
## Utility Functions
Copy as-is for simplified usage:
```typescript
// src/database/utils.ts
/**
* @lastModified 2025-02-04
* @see https://elysiajs.com/recipe/drizzle.html#utility
*/
import { Kind, type TObject } from '@sinclair/typebox'
import {
createInsertSchema,
createSelectSchema,
BuildSchema,
} from 'drizzle-typebox'
import { table } from './schema'
import type { Table } from 'drizzle-orm'
type Spread<
T extends TObject | Table,
Mode extends 'select' | 'insert' | undefined,
> =
T extends TObject<infer Fields>
? {
[K in keyof Fields]: Fields[K]
}
: T extends Table
? Mode extends 'select'
? BuildSchema<
'select',
T['_']['columns'],
undefined
>['properties']
: Mode extends 'insert'
? BuildSchema<
'insert',
T['_']['columns'],
undefined
>['properties']
: {}
: {}
/**
* Spread a Drizzle schema into a plain object
*/
export const spread = <
T extends TObject | Table,
Mode extends 'select' | 'insert' | undefined,
>(
schema: T,
mode?: Mode,
): Spread<T, Mode> => {
const newSchema: Record<string, unknown> = {}
let table
switch (mode) {
case 'insert':
case 'select':
if (Kind in schema) {
table = schema
break
}
table =
mode === 'insert'
? createInsertSchema(schema)
: createSelectSchema(schema)
break
default:
if (!(Kind in schema)) throw new Error('Expect a schema')
table = schema
}
for (const key of Object.keys(table.properties))
newSchema[key] = table.properties[key]
return newSchema as any
}
/**
* Spread a Drizzle Table into a plain object
*
* If `mode` is 'insert', the schema will be refined for insert
* If `mode` is 'select', the schema will be refined for select
* If `mode` is undefined, the schema will be spread as is, models will need to be refined manually
*/
export const spreads = <
T extends Record<string, TObject | Table>,
Mode extends 'select' | 'insert' | undefined,
>(
models: T,
mode?: Mode,
): {
[K in keyof T]: Spread<T[K], Mode>
} => {
const newSchema: Record<string, unknown> = {}
const keys = Object.keys(models)
for (const key of keys) newSchema[key] = spread(models[key], mode)
return newSchema as any
}
```
Usage:
```typescript
// ✅ Using spread
const user = spread(table.user, 'insert')
const createUser = t.Object({
id: user.id,
username: user.username,
password: user.password
})
// ⚠️ Using t.Pick
const _createUser = createInsertSchema(table.user)
const createUser = t.Pick(_createUser, ['id', 'username', 'password'])
```
## Table Singleton Pattern
```typescript
// src/database/model.ts
import { table } from './schema'
import { spreads } from './utils'
export const db = {
insert: spreads({ user: table.user }, 'insert'),
select: spreads({ user: table.user }, 'select')
} as const
```
Usage:
```typescript
// src/index.ts
import { db } from './database/model'
const { user } = db.insert
new Elysia()
.post('/sign-up', ({ body }) => {}, {
body: t.Object({
id: user.username,
username: user.username,
password: user.password
})
})
```
## Refinement
```typescript
// src/database/model.ts
import { createInsertSchema, createSelectSchema } from 'drizzle-typebox'
export const db = {
insert: spreads({
user: createInsertSchema(table.user, {
email: t.String({ format: 'email' })
})
}, 'insert'),
select: spreads({
user: createSelectSchema(table.user, {
email: t.String({ format: 'email' })
})
}, 'select')
} as const
```
`spread` skips refined schemas.

View File

@@ -0,0 +1,95 @@
# Expo Integration
Run Elysia on Expo (React Native)
## What It Is
Create API routes in Expo app (SDK 50+, App Router v3).
## Setup
1. Create `app/[...slugs]+api.ts`
2. Define Elysia server
3. Export `Elysia.fetch` as HTTP methods
```typescript
// app/[...slugs]+api.ts
import { Elysia, t } from 'elysia'
const app = new Elysia()
.get('/', 'hello Expo')
.post('/', ({ body }) => body, {
body: t.Object({ name: t.String() })
})
export const GET = app.fetch
export const POST = app.fetch
```
## Prefix for Non-Root
If placed in `app/api/[...slugs]+api.ts`, set prefix:
```typescript
const app = new Elysia({ prefix: '/api' })
.get('/', 'Hello Expo')
export const GET = app.fetch
export const POST = app.fetch
```
Ensures routing works in any location.
## Eden (End-to-End Type Safety)
1. Export type:
```typescript
// app/[...slugs]+api.ts
const app = new Elysia()
.get('/', 'Hello Nextjs')
.post('/user', ({ body }) => body, {
body: treaty.schema('User', { name: 'string' })
})
export type app = typeof app
export const GET = app.fetch
export const POST = app.fetch
```
2. Create client:
```typescript
// lib/eden.ts
import { treaty } from '@elysiajs/eden'
import type { app } from '../app/[...slugs]+api'
export const api = treaty<app>('localhost:3000/api')
```
3. Use in components:
```tsx
// app/page.tsx
import { api } from '../lib/eden'
export default async function Page() {
const message = await api.get()
return <h1>Hello, {message}</h1>
}
```
## Deployment
- Deploy as normal Elysia app OR
- Use experimental Expo server runtime
With Expo runtime:
```bash
expo export
# Creates dist/server/_expo/functions/[...slugs]+api.js
```
Edge function, not normal server (no port allocation).
### Adapters
- Express
- Netlify
- Vercel
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,103 @@
# Next.js Integration
## What It Is
Run Elysia on Next.js App Router.
## Setup
1. Create `app/api/[[...slugs]]/route.ts`
2. Define Elysia + export handlers:
```typescript
// app/api/[[...slugs]]/route.ts
import { Elysia, t } from 'elysia'
const app = new Elysia({ prefix: '/api' })
.get('/', 'Hello Nextjs')
.post('/', ({ body }) => body, {
body: t.Object({ name: t.String() })
})
export const GET = app.fetch
export const POST = app.fetch
```
WinterCG compliance - works as normal Next.js API route.
## Prefix for Non-Root
If placed in `app/user/[[...slugs]]/route.ts`, set prefix:
```typescript
const app = new Elysia({ prefix: '/user' })
.get('/', 'Hello Nextjs')
export const GET = app.fetch
export const POST = app.fetch
```
## Eden (End-to-End Type Safety)
Isomorphic fetch pattern:
- Server: Direct calls (no network)
- Client: Network calls
1. Export type:
```typescript
// app/api/[[...slugs]]/route.ts
export const app = new Elysia({ prefix: '/api' })
.get('/', 'Hello Nextjs')
.post('/user', ({ body }) => body, {
body: treaty.schema('User', { name: 'string' })
})
export type app = typeof app
export const GET = app.fetch
export const POST = app.fetch
```
2. Create client:
```typescript
// lib/eden.ts
import { treaty } from '@elysiajs/eden'
import type { app } from '../app/api/[[...slugs]]/route'
export const api =
typeof process !== 'undefined'
? treaty(app).api
: treaty<typeof app>('localhost:3000').api
```
Use `typeof process` not `typeof window` (window undefined at build time → hydration error).
3. Use in components:
```tsx
// app/page.tsx
import { api } from '../lib/eden'
export default async function Page() {
const message = await api.get()
return <h1>Hello, {message}</h1>
}
```
Works with server/client components + ISR.
## React Query
```tsx
import { useQuery } from '@tanstack/react-query'
function App() {
const { data: response } = useQuery({
queryKey: ['get'],
queryFn: () => getTreaty().get()
})
return response?.data
}
```
Works with all React Query features.
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,64 @@
# Node.js Integration
Run Elysia on Node.js
## What It Is
Runtime adapter to run Elysia on Node.js.
## Installation
```bash
bun add elysia @elysiajs/node
```
## Setup
Apply node adapter:
```typescript
import { Elysia } from 'elysia'
import { node } from '@elysiajs/node'
const app = new Elysia({ adapter: node() })
.get('/', () => 'Hello Elysia')
.listen(3000)
```
## Additional Setup (Recommended)
Install `tsx` for hot-reload:
```bash
bun add -d tsx @types/node typescript
```
Scripts in `package.json`:
```json
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc src/index.ts --outDir dist",
"start": "NODE_ENV=production node dist/index.js"
}
}
```
- **dev**: Hot-reload dev mode
- **build**: Production build
- **start**: Production server
Create `tsconfig.json`:
```bash
tsc --init
```
Update strict mode:
```json
{
"compilerOptions": {
"strict": true
}
}
```
Provides hot-reload + JSX support similar to `bun dev`.
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,67 @@
# Nuxt Integration
## What It Is
Community plugin `nuxt-elysia` for Nuxt API routes with Eden Treaty.
## Installation
```bash
bun add elysia @elysiajs/eden
bun add -d nuxt-elysia
```
## Setup
1. Add to Nuxt config:
```typescript
export default defineNuxtConfig({
modules: ['nuxt-elysia']
})
```
2. Create `api.ts` at project root:
```typescript
// api.ts
export default () => new Elysia()
.get('/hello', () => ({ message: 'Hello world!' }))
```
3. Use Eden Treaty:
```vue
<template>
<div>
<p>{{ data.message }}</p>
</div>
</template>
<script setup lang="ts">
const { $api } = useNuxtApp()
const { data } = await useAsyncData(async () => {
const { data, error } = await $api.hello.get()
if (error) throw new Error('Failed to call API')
return data
})
</script>
```
Auto-setup on Nuxt API route.
## Prefix
Default: `/_api`. Customize:
```typescript
export default defineNuxtConfig({
nuxtElysia: {
path: '/api'
}
})
```
Mounts on `/api` instead of `/_api`.
See [nuxt-elysia](https://github.com/tkesgar/nuxt-elysia) for more config.
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,93 @@
# Prisma Integration
Elysia + Prisma integration guide
## What It Is
Type-safe ORM. Generate Elysia validation models from Prisma schema via `prismabox`.
## Flow
```
Prisma → prismabox → Elysia validation → OpenAPI + Eden Treaty
```
## Installation
```bash
bun add @prisma/client prismabox && \
bun add -d prisma
```
## Prisma Schema
Add `prismabox` generator:
```prisma
// prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator prismabox {
provider = "prismabox"
typeboxImportDependencyName = "elysia"
typeboxImportVariableName = "t"
inputModel = true
output = "../generated/prismabox"
}
model User {
id String @id @default(cuid())
email String @unique
name String?
posts Post[]
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
}
```
Generates:
- `User``generated/prismabox/User.ts`
- `Post``generated/prismabox/Post.ts`
## Using Generated Models
```typescript
// src/index.ts
import { Elysia, t } from 'elysia'
import { PrismaClient } from '../generated/prisma'
import { UserPlain, UserPlainInputCreate } from '../generated/prismabox/User'
const prisma = new PrismaClient()
new Elysia()
.put('/', async ({ body }) =>
prisma.user.create({ data: body }), {
body: UserPlainInputCreate,
response: UserPlain
}
)
.get('/id/:id', async ({ params: { id }, status }) => {
const user = await prisma.user.findUnique({ where: { id } })
if (!user) return status(404, 'User not found')
return user
}, {
response: {
200: UserPlain,
404: t.String()
}
})
.listen(3000)
```
Reuses DB schema in Elysia validation models.

View File

@@ -0,0 +1,134 @@
# React Email Integration
## What It Is
Use React components to create emails. Direct JSX import via Bun.
## Installation
```bash
bun add -d react-email
bun add @react-email/components react react-dom
```
Script in `package.json`:
```json
{
"scripts": {
"email": "email dev --dir src/emails"
}
}
```
Email templates → `src/emails` directory.
### TypeScript
Add to `tsconfig.json`:
```json
{
"compilerOptions": {
"jsx": "react"
}
}
```
## Email Template
```tsx
// src/emails/otp.tsx
import * as React from 'react'
import { Tailwind, Section, Text } from '@react-email/components'
export default function OTPEmail({ otp }: { otp: number }) {
return (
<Tailwind>
<Section className="flex justify-center items-center w-full min-h-screen font-sans">
<Section className="flex flex-col items-center w-76 rounded-2xl px-6 py-1 bg-gray-50">
<Text className="text-xs font-medium text-violet-500">
Verify your Email Address
</Text>
<Text className="text-gray-500 my-0">
Use the following code to verify your email address
</Text>
<Text className="text-5xl font-bold pt-2">{otp}</Text>
<Text className="text-gray-400 font-light text-xs pb-4">
This code is valid for 10 minutes
</Text>
<Text className="text-gray-600 text-xs">
Thank you for joining us
</Text>
</Section>
</Section>
</Tailwind>
)
}
OTPEmail.PreviewProps = { otp: 123456 }
```
`@react-email/components` → email-client compatible (Gmail, Outlook). Tailwind support.
`PreviewProps` → playground only.
## Preview
```bash
bun email
```
Opens browser with preview.
## Send Email
Render with `react-dom/server`, submit via provider:
### Nodemailer
```typescript
import { renderToStaticMarkup } from 'react-dom/server'
import OTPEmail from './emails/otp'
import nodemailer from 'nodemailer'
const transporter = nodemailer.createTransport({
host: 'smtp.gehenna.sh',
port: 465,
auth: { user: 'makoto', pass: '12345678' }
})
.get('/otp', async ({ body }) => {
const otp = ~~(Math.random() * 900_000) + 100_000
const html = renderToStaticMarkup(<OTPEmail otp={otp} />)
await transporter.sendMail({
from: '[email protected]',
to: body,
subject: 'Verify your email address',
html
})
return { success: true }
}, {
body: t.String({ format: 'email' })
})
```
### Resend
```typescript
import OTPEmail from './emails/otp'
import Resend from 'resend'
const resend = new Resend('re_123456789')
.get('/otp', ({ body }) => {
const otp = ~~(Math.random() * 900_000) + 100_000
await resend.emails.send({
from: '[email protected]',
to: body,
subject: 'Verify your email address',
html: <OTPEmail otp={otp} /> // Direct JSX
})
return { success: true }
})
```
Direct JSX import thanks to Bun.
Other providers: AWS SES, SendGrid.
See [React Email Integrations](https://react.email/docs/integrations/overview).

View File

@@ -0,0 +1,53 @@
# SvelteKit Integration
## What It Is
Run Elysia on SvelteKit server routes.
## Setup
1. Create `src/routes/[...slugs]/+server.ts`
2. Define Elysia server
3. Export fallback handler:
```typescript
// src/routes/[...slugs]/+server.ts
import { Elysia, t } from 'elysia'
const app = new Elysia()
.get('/', 'hello SvelteKit')
.post('/', ({ body }) => body, {
body: t.Object({ name: t.String() })
})
interface WithRequest {
request: Request
}
export const fallback = ({ request }: WithRequest) => app.handle(request)
```
Treat as normal SvelteKit server route.
## Prefix for Non-Root
If placed in `src/routes/api/[...slugs]/+server.ts`, set prefix:
```typescript
// src/routes/api/[...slugs]/+server.ts
import { Elysia, t } from 'elysia'
const app = new Elysia({ prefix: '/api' })
.get('/', () => 'hi')
.post('/', ({ body }) => body, {
body: t.Object({ name: t.String() })
})
type RequestHandler = (v: { request: Request }) => Response | Promise<Response>
export const fallback: RequestHandler = ({ request }) => app.handle(request)
```
Ensures routing works in any location.
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,87 @@
# Tanstack Start Integration
## What It Is
Elysia runs inside Tanstack Start server routes.
## Setup
1. Create `src/routes/api.$.ts`
2. Define Elysia server
3. Export handlers in `server.handlers`:
```typescript
// src/routes/api.$.ts
import { Elysia } from 'elysia'
import { createFileRoute } from '@tanstack/react-router'
import { createIsomorphicFn } from '@tanstack/react-start'
const app = new Elysia({
prefix: '/api'
}).get('/', 'Hello Elysia!')
const handle = ({ request }: { request: Request }) => app.fetch(request)
export const Route = createFileRoute('/api/$')({
server: {
handlers: {
GET: handle,
POST: handle
}
}
})
```
Runs on `/api`. Add methods to `server.handlers` as needed.
## Eden (End-to-End Type Safety)
Isomorphic pattern with `createIsomorphicFn`:
```typescript
// src/routes/api.$.ts
export const getTreaty = createIsomorphicFn()
.server(() => treaty(app).api)
.client(() => treaty<typeof app>('localhost:3000').api)
```
- Server: Direct call (no HTTP overhead)
- Client: HTTP call
## Loader Data
Fetch before render:
```tsx
// src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router'
import { getTreaty } from './api.$'
export const Route = createFileRoute('/a')({
component: App,
loader: () => getTreaty().get().then((res) => res.data)
})
function App() {
const data = Route.useLoaderData()
return data
}
```
Executed server-side during SSR. No HTTP overhead. Type-safe.
## React Query
```tsx
import { useQuery } from '@tanstack/react-query'
import { getTreaty } from './api.$'
function App() {
const { data: response } = useQuery({
queryKey: ['get'],
queryFn: () => getTreaty().get()
})
return response?.data
}
```
Works with all React Query features.
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```

View File

@@ -0,0 +1,55 @@
# Vercel Integration
Deploy Elysia on Vercel
## What It Is
Zero-config deployment on Vercel (Bun or Node runtime).
## Setup
1. Create/import Elysia server in `src/index.ts`
2. Export as default:
```typescript
import { Elysia, t } from 'elysia'
export default new Elysia()
.get('/', () => 'Hello Vercel Function')
.post('/', ({ body }) => body, {
body: t.Object({ name: t.String() })
})
```
3. Develop locally:
```bash
vc dev
```
4. Deploy:
```bash
vc deploy
```
## Node.js Runtime
Set in `package.json`:
```json
{
"name": "elysia-app",
"type": "module"
}
```
## Bun Runtime
Set in `vercel.json`:
```json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"bunVersion": "1.x"
}
```
## pnpm
Manual install:
```bash
pnpm add @sinclair/typebox openapi-types
```
## Troubleshooting
Vercel has zero config for Elysia. For additional config, see [Vercel docs](https://vercel.com/docs/frameworks/backend/elysia).

View File

@@ -0,0 +1,7 @@
{
"version": "1.0.1",
"organization": "ElysiaJS",
"date": "20 Jan 2026",
"abstract": "Create backend with ElysiaJS, a type-safe, high-performance framework.",
"references": ["https://elysiajs.com/llms.txt"]
}

View File

@@ -0,0 +1,380 @@
# MVC pattern
This file contains a guideline for using Elysia with MVC or Model View Controller patterns
- Controller:
- Prefers Elysia as a controller for HTTP dependant
- For non HTTP dependent, prefers service instead unless explicitly asked
- Use `onError` to handle local custom errors
- Register Model to Elysia instance via `Elysia.models({ ...models })` and prefix model by namespace `Elysia.prefix('model', 'Namespace.')
- Prefers Reference Model by name provided by Elysia instead of using an actual `Model.name`
- Service:
- Prefers class (or abstract class if possible)
- Prefers interface/type derive from `Model`
- Return `status` (`import { status } from 'elysia'`) for error
- Prefers `return Error` instead of `throw Error`
- Models:
- Always export validation model and type of validation model
- Custom Error should be in contains in Model
## Controller
Due to type soundness of Elysia, it's not recommended to use a traditional controller class that is tightly coupled with Elysia's `Context` because:
1. **Elysia type is complex** and heavily depends on plugin and multiple level of chaining.
2. **Hard to type**, Elysia type could change at anytime, especially with decorators, and store
3. **Loss of type integrity**, and inconsistency between types and runtime code.
We recommended one of the following approach to implement a controller in Elysia.
1. Use Elysia instance as a controller itself
2. Create a controller that is not tied with HTTP request or Elysia.
---
### 1. Elysia instance as a controller
> 1 Elysia instance = 1 controller
Treat an Elysia instance as a controller, and define your routes directly on the Elysia instance.
```typescript
// Do
import { Elysia } from 'elysia'
import { Service } from './service'
new Elysia()
.get('/', ({ stuff }) => {
Service.doStuff(stuff)
})
```
This approach allows Elysia to infer the `Context` type automatically, ensuring type integrity and consistency between types and runtime code.
```typescript
// Don't
import { Elysia, t, type Context } from 'elysia'
abstract class Controller {
static root(context: Context) {
return Service.doStuff(context.stuff)
}
}
new Elysia()
.get('/', Controller.root)
```
This approach makes it hard to type `Context` properly, and may lead to loss of type integrity.
### 2. Controller without HTTP request
If you want to create a controller class, we recommend creating a class that is not tied to HTTP request or Elysia at all.
This approach allows you to decouple the controller from Elysia, making it easier to test, reuse, and even swap a framework while still follows the MVC pattern.
```typescript
import { Elysia } from 'elysia'
abstract class Controller {
static doStuff(stuff: string) {
return Service.doStuff(stuff)
}
}
new Elysia()
.get('/', ({ stuff }) => Controller.doStuff(stuff))
```
Tying the controller to Elysia Context may lead to:
1. Loss of type integrity
2. Make it harder to test and reuse
3. Lead to vendor lock-in
We recommended to keep the controller decoupled from Elysia as much as possible.
### Don't: Pass entire `Context` to a controller
**Context is a highly dynamic type** that can be inferred from Elysia instance.
Do not pass an entire `Context` to a controller, instead use object destructuring to extract what you need and pass it to the controller.
```typescript
import type { Context } from 'elysia'
abstract class Controller {
constructor() {}
// Don't do this
static root(context: Context) {
return Service.doStuff(context.stuff)
}
}
```
This approach makes it hard to type `Context` properly, and may lead to loss of type integrity.
### Testing
If you're using Elysia as a controller, you can test your controller using `handle` to directly call a function (and it's lifecycle)
```typescript
import { Elysia } from 'elysia'
import { Service } from './service'
import { describe, it, expect } from 'bun:test'
const app = new Elysia()
.get('/', ({ stuff }) => {
Service.doStuff(stuff)
return 'ok'
})
describe('Controller', () => {
it('should work', async () => {
const response = await app
.handle(new Request('http://localhost/'))
.then((x) => x.text())
expect(response).toBe('ok')
})
})
```
You may find more information about testing in [Unit Test](/patterns/unit-test.html).
## Service
Service is a set of utility/helper functions decoupled as a business logic to use in a module/controller, in our case, an Elysia instance.
Any technical logic that can be decoupled from controller may live inside a **Service**.
There are 2 types of service in Elysia:
1. Non-request dependent service
2. Request dependent service
### 1. Abstract away Non-request dependent service
We recommend abstracting a service class/function away from Elysia.
If the service or function isn't tied to an HTTP request or doesn't access a `Context`, it's recommended to implement it as a static class or function.
```typescript
import { Elysia, t } from 'elysia'
abstract class Service {
static fibo(number: number): number {
if(number < 2)
return number
return Service.fibo(number - 1) + Service.fibo(number - 2)
}
}
new Elysia()
.get('/fibo', ({ body }) => {
return Service.fibo(body)
}, {
body: t.Numeric()
})
```
If your service doesn't need to store a property, you may use `abstract class` and `static` instead to avoid allocating class instance.
### 2. Request dependent service as Elysia instance
**If the service is a request-dependent service** or needs to process HTTP requests, we recommend abstracting it as an Elysia instance to ensure type integrity and inference:
```typescript
import { Elysia } from 'elysia'
// Do
const AuthService = new Elysia({ name: 'Auth.Service' })
.macro({
isSignIn: {
resolve({ cookie, status }) {
if (!cookie.session.value) return status(401)
return {
session: cookie.session.value,
}
}
}
})
const UserController = new Elysia()
.use(AuthService)
.get('/profile', ({ Auth: { user } }) => user, {
isSignIn: true
})
```
### Do: Decorate only request dependent property
It's recommended to `decorate` only request-dependent properties, such as `requestIP`, `requestTime`, or `session`.
Overusing decorators may tie your code to Elysia, making it harder to test and reuse.
```typescript
import { Elysia } from 'elysia'
new Elysia()
.decorate('requestIP', ({ request }) => request.headers.get('x-forwarded-for') || request.ip)
.decorate('requestTime', () => Date.now())
.decorate('session', ({ cookie }) => cookie.session.value)
.get('/', ({ requestIP, requestTime, session }) => {
return { requestIP, requestTime, session }
})
```
### Don't: Pass entire `Context` to a service
**Context is a highly dynamic type** that can be inferred from Elysia instance.
Do not pass an entire `Context` to a service, instead use object destructuring to extract what you need and pass it to the service.
```typescript
import type { Context } from 'elysia'
class AuthService {
constructor() {}
// Don't do this
isSignIn({ status, cookie: { session } }: Context) {
if (session.value)
return status(401)
}
}
```
As Elysia type is complex, and heavily depends on plugin and multiple level of chaining, it can be challenging to manually type as it's highly dynamic.
## Model
Model or [DTO (Data Transfer Object)](https://en.wikipedia.org/wiki/Data_transfer_object) is handle by [Elysia.t (Validation)](/essential/validation.html#elysia-type).
Elysia has a validation system built-in which can infers type from your code and validate it at runtime.
### Do: Use Elysia's validation system
Elysia strength is prioritizing a single source of truth for both type and runtime validation.
Instead of declaring an interface, reuse validation's model instead:
```typescript twoslash
// Do
import { Elysia, t } from 'elysia'
const customBody = t.Object({
username: t.String(),
password: t.String()
})
// Optional if you want to get the type of the model
// Usually if we didn't use the type, as it's already inferred by Elysia
type CustomBody = typeof customBody.static
export { customBody }
```
We can get type of model by using `typeof` with `.static` property from the model.
Then you can use the `CustomBody` type to infer the type of the request body.
```typescript twoslash
// Do
new Elysia()
.post('/login', ({ body }) => {
return body
}, {
body: customBody
})
```
### Don't: Declare a class instance as a model
Do not declare a class instance as a model:
```typescript
// Don't
class CustomBody {
username: string
password: string
constructor(username: string, password: string) {
this.username = username
this.password = password
}
}
// Don't
interface ICustomBody {
username: string
password: string
}
```
### Don't: Declare type separate from the model
Do not declare a type separate from the model, instead use `typeof` with `.static` property to get the type of the model.
```typescript
// Don't
import { Elysia, t } from 'elysia'
const customBody = t.Object({
username: t.String(),
password: t.String()
})
type CustomBody = {
username: string
password: string
}
// Do
const customBody = t.Object({
username: t.String(),
password: t.String()
})
type CustomBody = typeof customBody.static
```
### Group
You can group multiple models into a single object to make it more organized.
```typescript
import { Elysia, t } from 'elysia'
export const AuthModel = {
sign: t.Object({
username: t.String(),
password: t.String()
})
}
const models = AuthModel.models
```
### Model Injection
Though this is optional, if you are strictly following MVC pattern, you may want to inject like a service into a controller. We recommended using Elysia reference model
Using Elysia's model reference
```typescript twoslash
import { Elysia, t } from 'elysia'
const customBody = t.Object({
username: t.String(),
password: t.String()
})
const AuthModel = new Elysia()
.model({
sign: customBody
})
const models = AuthModel.models
const UserController = new Elysia({ prefix: '/auth' })
.use(AuthModel)
.prefix('model', 'auth.')
.post('/sign-in', async ({ body, cookie: { session } }) => {
return true
}, {
body: 'auth.Sign'
})
```
This approach provide several benefits:
1. Allow us to name a model and provide auto-completion.
2. Modify schema for later usage, or perform a [remap](/essential/handler.html#remap).
3. Show up as "models" in OpenAPI compliance client, eg. OpenAPI.
4. Improve TypeScript inference speed as model type will be cached during registration.

View File

@@ -0,0 +1,30 @@
# Bearer
Plugin for Elysia for retrieving the Bearer token.
## Installation
```bash
bun add @elysiajs/bearer
```
## Basic Usage
```typescript twoslash
import { Elysia } from 'elysia'
import { bearer } from '@elysiajs/bearer'
const app = new Elysia()
.use(bearer())
.get('/sign', ({ bearer }) => bearer, {
beforeHandle({ bearer, set, status }) {
if (!bearer) {
set.headers[
'WWW-Authenticate'
] = `Bearer realm='sign', error="invalid_request"`
return status(400, 'Unauthorized')
}
}
})
.listen(3000)
```
This plugin is for retrieving a Bearer token specified in RFC6750

View File

@@ -0,0 +1,141 @@
# CORS
Plugin for Elysia that adds support for customizing Cross-Origin Resource Sharing behavior.
## Installation
```bash
bun add @elysiajs/cors
```
## Basic Usage
```typescript twoslash
import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'
new Elysia().use(cors()).listen(3000)
```
This will set Elysia to accept requests from any origin.
## Config
Below is a config which is accepted by the plugin
### origin
@default `true`
Indicates whether the response can be shared with the requesting code from the given origins.
Value can be one of the following:
- **string** - Name of origin which will directly assign to Access-Control-Allow-Origin header.
- **boolean** - If set to true, Access-Control-Allow-Origin will be set to `*` (any origins)
- **RegExp** - Pattern to match request's URL, allowed if matched.
- **Function** - Custom logic to allow resource sharing, allow if `true` is returned.
- Expected to have the type of:
```typescript
cors(context: Context) => boolean | void
```
- **Array<string | RegExp | Function>** - iterate through all cases above in order, allowed if any of the values are `true`.
---
### methods
@default `*`
Allowed methods for cross-origin requests by assign `Access-Control-Allow-Methods` header.
Value can be one of the following:
- **undefined | null | ''** - Ignore all methods.
- **\*** - Allows all methods.
- **string** - Expects either a single method or a comma-delimited string
- (eg: `'GET, PUT, POST'`)
- **string[]** - Allow multiple HTTP methods.
- eg: `['GET', 'PUT', 'POST']`
---
### allowedHeaders
@default `*`
Allowed headers for an incoming request by assign `Access-Control-Allow-Headers` header.
Value can be one of the following:
- **string** - Expects either a single header or a comma-delimited string
- eg: `'Content-Type, Authorization'`.
- **string[]** - Allow multiple HTTP headers.
- eg: `['Content-Type', 'Authorization']`
---
### exposeHeaders
@default `*`
Response CORS with specified headers by sssign Access-Control-Expose-Headers header.
Value can be one of the following:
- **string** - Expects either a single header or a comma-delimited string.
- eg: `'Content-Type, X-Powered-By'`.
- **string[]** - Allow multiple HTTP headers.
- eg: `['Content-Type', 'X-Powered-By']`
---
### credentials
@default `true`
The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to the frontend JavaScript code when the request's credentials mode Request.credentials is `include`.
Credentials are cookies, authorization headers, or TLS client certificates by assign `Access-Control-Allow-Credentials` header.
---
### maxAge
@default `5`
Indicates how long the results of a preflight request that is the information contained in the `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` headers) can be cached.
Assign `Access-Control-Max-Age` header.
---
### preflight
The preflight request is a request sent to check if the CORS protocol is understood and if a server is aware of using specific methods and headers.
Response with **OPTIONS** request with 3 HTTP request headers:
- **Access-Control-Request-Method**
- **Access-Control-Request-Headers**
- **Origin**
This config indicates if the server should respond to preflight requests.
---
## Pattern
Below you can find the common patterns to use the plugin.
## Allow CORS by top-level domain
```typescript twoslash
import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'
const app = new Elysia()
.use(
cors({
origin: /.*\.saltyaom\.com$/
})
)
.get('/', () => 'Hi')
.listen(3000)
```
This will allow requests from top-level domains with `saltyaom.com`

View File

@@ -0,0 +1,265 @@
# Cron Plugin
This plugin adds support for running cronjob to Elysia server.
## Installation
```bash
bun add @elysiajs/cron
```
## Basic Usage
```typescript twoslash
import { Elysia } from 'elysia'
import { cron } from '@elysiajs/cron'
new Elysia()
.use(
cron({
name: 'heartbeat',
pattern: '*/10 * * * * *',
run() {
console.log('Heartbeat')
}
})
)
.listen(3000)
```
The above code will log `heartbeat` every 10 seconds.
## Config
Below is a config which is accepted by the plugin
### cron
Create a cronjob for the Elysia server.
```
cron(config: CronConfig, callback: (Instance['store']) => void): this
```
`CronConfig` accepts the parameters specified below:
---
### CronConfig.name
Job name to register to `store`.
This will register the cron instance to `store` with a specified name, which can be used to reference in later processes eg. stop the job.
---
### CronConfig.pattern
Time to run the job as specified by cron syntax.
```
┌────────────── second (optional)
│ ┌──────────── minute
│ │ ┌────────── hour
│ │ │ ┌──────── day of the month
│ │ │ │ ┌────── month
│ │ │ │ │ ┌──── day of week
│ │ │ │ │ │
* * * * * *
```
---
### CronConfig.timezone
Time zone in Europe/Stockholm format
---
### CronConfig.startAt
Schedule start time for the job
---
### CronConfig.stopAt
Schedule stop time for the job
---
### CronConfig.maxRuns
Maximum number of executions
---
### CronConfig.catch
Continue execution even if an unhandled error is thrown by a triggered function.
### CronConfig.interval
The minimum interval between executions, in seconds.
---
## CronConfig.Pattern
Below you can find the common patterns to use the plugin.
---
## Pattern
Below you can find the common patterns to use the plugin.
## Stop cronjob
You can stop cronjob manually by accessing the cronjob name registered to `store`.
```typescript
import { Elysia } from 'elysia'
import { cron } from '@elysiajs/cron'
const app = new Elysia()
.use(
cron({
name: 'heartbeat',
pattern: '*/1 * * * * *',
run() {
console.log('Heartbeat')
}
})
)
.get(
'/stop',
({
store: {
cron: { heartbeat }
}
}) => {
heartbeat.stop()
return 'Stop heartbeat'
}
)
.listen(3000)
```
---
## Predefined patterns
You can use predefined patterns from `@elysiajs/cron/schedule`
```typescript
import { Elysia } from 'elysia'
import { cron, Patterns } from '@elysiajs/cron'
const app = new Elysia()
.use(
cron({
name: 'heartbeat',
pattern: Patterns.everySecond(),
run() {
console.log('Heartbeat')
}
})
)
.get(
'/stop',
({
store: {
cron: { heartbeat }
}
}) => {
heartbeat.stop()
return 'Stop heartbeat'
}
)
.listen(3000)
```
### Functions
| Function | Description |
| ---------------------------------------- | ----------------------------------------------------- |
| `.everySeconds(2)` | Run the task every 2 seconds |
| `.everyMinutes(5)` | Run the task every 5 minutes |
| `.everyHours(3)` | Run the task every 3 hours |
| `.everyHoursAt(3, 15)` | Run the task every 3 hours at 15 minutes |
| `.everyDayAt('04:19')` | Run the task every day at 04:19 |
| `.everyWeekOn(Patterns.MONDAY, '19:30')` | Run the task every Monday at 19:30 |
| `.everyWeekdayAt('17:00')` | Run the task every day from Monday to Friday at 17:00 |
| `.everyWeekendAt('11:00')` | Run the task on Saturday and Sunday at 11:00 |
### Function aliases to constants
| Function | Constant |
| ----------------- | ---------------------------------- |
| `.everySecond()` | EVERY_SECOND |
| `.everyMinute()` | EVERY_MINUTE |
| `.hourly()` | EVERY_HOUR |
| `.daily()` | EVERY_DAY_AT_MIDNIGHT |
| `.everyWeekday()` | EVERY_WEEKDAY |
| `.everyWeekend()` | EVERY_WEEKEND |
| `.weekly()` | EVERY_WEEK |
| `.monthly()` | EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT |
| `.everyQuarter()` | EVERY_QUARTER |
| `.yearly()` | EVERY_YEAR |
### Constants
| Constant | Pattern |
| ---------------------------------------- | -------------------- |
| `.EVERY_SECOND` | `* * * * * *` |
| `.EVERY_5_SECONDS` | `*/5 * * * * *` |
| `.EVERY_10_SECONDS` | `*/10 * * * * *` |
| `.EVERY_30_SECONDS` | `*/30 * * * * *` |
| `.EVERY_MINUTE` | `*/1 * * * *` |
| `.EVERY_5_MINUTES` | `0 */5 * * * *` |
| `.EVERY_10_MINUTES` | `0 */10 * * * *` |
| `.EVERY_30_MINUTES` | `0 */30 * * * *` |
| `.EVERY_HOUR` | `0 0-23/1 * * *` |
| `.EVERY_2_HOURS` | `0 0-23/2 * * *` |
| `.EVERY_3_HOURS` | `0 0-23/3 * * *` |
| `.EVERY_4_HOURS` | `0 0-23/4 * * *` |
| `.EVERY_5_HOURS` | `0 0-23/5 * * *` |
| `.EVERY_6_HOURS` | `0 0-23/6 * * *` |
| `.EVERY_7_HOURS` | `0 0-23/7 * * *` |
| `.EVERY_8_HOURS` | `0 0-23/8 * * *` |
| `.EVERY_9_HOURS` | `0 0-23/9 * * *` |
| `.EVERY_10_HOURS` | `0 0-23/10 * * *` |
| `.EVERY_11_HOURS` | `0 0-23/11 * * *` |
| `.EVERY_12_HOURS` | `0 0-23/12 * * *` |
| `.EVERY_DAY_AT_1AM` | `0 01 * * *` |
| `.EVERY_DAY_AT_2AM` | `0 02 * * *` |
| `.EVERY_DAY_AT_3AM` | `0 03 * * *` |
| `.EVERY_DAY_AT_4AM` | `0 04 * * *` |
| `.EVERY_DAY_AT_5AM` | `0 05 * * *` |
| `.EVERY_DAY_AT_6AM` | `0 06 * * *` |
| `.EVERY_DAY_AT_7AM` | `0 07 * * *` |
| `.EVERY_DAY_AT_8AM` | `0 08 * * *` |
| `.EVERY_DAY_AT_9AM` | `0 09 * * *` |
| `.EVERY_DAY_AT_10AM` | `0 10 * * *` |
| `.EVERY_DAY_AT_11AM` | `0 11 * * *` |
| `.EVERY_DAY_AT_NOON` | `0 12 * * *` |
| `.EVERY_DAY_AT_1PM` | `0 13 * * *` |
| `.EVERY_DAY_AT_2PM` | `0 14 * * *` |
| `.EVERY_DAY_AT_3PM` | `0 15 * * *` |
| `.EVERY_DAY_AT_4PM` | `0 16 * * *` |
| `.EVERY_DAY_AT_5PM` | `0 17 * * *` |
| `.EVERY_DAY_AT_6PM` | `0 18 * * *` |
| `.EVERY_DAY_AT_7PM` | `0 19 * * *` |
| `.EVERY_DAY_AT_8PM` | `0 20 * * *` |
| `.EVERY_DAY_AT_9PM` | `0 21 * * *` |
| `.EVERY_DAY_AT_10PM` | `0 22 * * *` |
| `.EVERY_DAY_AT_11PM` | `0 23 * * *` |
| `.EVERY_DAY_AT_MIDNIGHT` | `0 0 * * *` |
| `.EVERY_WEEK` | `0 0 * * 0` |
| `.EVERY_WEEKDAY` | `0 0 * * 1-5` |
| `.EVERY_WEEKEND` | `0 0 * * 6,0` |
| `.EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT` | `0 0 1 * *` |
| `.EVERY_1ST_DAY_OF_MONTH_AT_NOON` | `0 12 1 * *` |
| `.EVERY_2ND_HOUR` | `0 */2 * * *` |
| `.EVERY_2ND_HOUR_FROM_1AM_THROUGH_11PM` | `0 1-23/2 * * *` |
| `.EVERY_2ND_MONTH` | `0 0 1 */2 *` |
| `.EVERY_QUARTER` | `0 0 1 */3 *` |
| `.EVERY_6_MONTHS` | `0 0 1 */6 *` |
| `.EVERY_YEAR` | `0 0 1 1 *` |
| `.EVERY_30_MINUTES_BETWEEN_9AM_AND_5PM` | `0 */30 9-17 * * *` |
| `.EVERY_30_MINUTES_BETWEEN_9AM_AND_6PM` | `0 */30 9-18 * * *` |
| `.EVERY_30_MINUTES_BETWEEN_10AM_AND_7PM` | `0 */30 10-19 * * *` |

View File

@@ -0,0 +1,90 @@
# GraphQL Apollo
Plugin for Elysia to use GraphQL Apollo.
## Installation
```bash
bun add graphql @elysiajs/apollo @apollo/server
```
## Basic Usage
```typescript
import { Elysia } from 'elysia'
import { apollo, gql } from '@elysiajs/apollo'
const app = new Elysia()
.use(
apollo({
typeDefs: gql`
type Book {
title: String
author: String
}
type Query {
books: [Book]
}
`,
resolvers: {
Query: {
books: () => {
return [
{
title: 'Elysia',
author: 'saltyAom'
}
]
}
}
}
})
)
.listen(3000)
```
Accessing `/graphql` should show Apollo GraphQL playground work with.
## Context
Because Elysia is based on Web Standard Request and Response which is different from Node's `HttpRequest` and `HttpResponse` that Express uses, results in `req, res` being undefined in context.
Because of this, Elysia replaces both with `context` like route parameters.
```typescript
const app = new Elysia()
.use(
apollo({
typeDefs,
resolvers,
context: async ({ request }) => {
const authorization = request.headers.get('Authorization')
return {
authorization
}
}
})
)
.listen(3000)
```
## Config
This plugin extends Apollo's [ServerRegistration](https://www.apollographql.com/docs/apollo-server/api/apollo-server/#options) (which is `ApolloServer`'s' constructor parameter).
Below are the extended parameters for configuring Apollo Server with Elysia.
### path
@default `"/graphql"`
Path to expose Apollo Server.
---
### enablePlayground
@default `process.env.ENV !== 'production'`
Determine whether should Apollo should provide Apollo Playground.

View File

@@ -0,0 +1,87 @@
# GraphQL Yoga
This plugin integrates GraphQL yoga with Elysia
## Installation
```bash
bun add @elysiajs/graphql-yoga
```
## Basic Usage
```typescript
import { Elysia } from 'elysia'
import { yoga } from '@elysiajs/graphql-yoga'
const app = new Elysia()
.use(
yoga({
typeDefs: /* GraphQL */ `
type Query {
hi: String
}
`,
resolvers: {
Query: {
hi: () => 'Hello from Elysia'
}
}
})
)
.listen(3000)
```
Accessing `/graphql` in the browser (GET request) would show you a GraphiQL instance for the GraphQL-enabled Elysia server.
optional: you can install a custom version of optional peer dependencies as well:
```bash
bun add graphql graphql-yoga
```
## Resolver
Elysia uses Mobius to infer type from **typeDefs** field automatically, allowing you to get full type-safety and auto-complete when typing **resolver** types.
## Context
You can add custom context to the resolver function by adding **context**
```ts
import { Elysia } from 'elysia'
import { yoga } from '@elysiajs/graphql-yoga'
const app = new Elysia()
.use(
yoga({
typeDefs: /* GraphQL */ `
type Query {
hi: String
}
`,
context: {
name: 'Mobius'
},
// If context is a function on this doesn't present
// for some reason it won't infer context type
useContext(_) {},
resolvers: {
Query: {
hi: async (parent, args, context) => context.name
}
}
})
)
.listen(3000)
```
## Config
This plugin extends [GraphQL Yoga's createYoga options, please refer to the GraphQL Yoga documentation](https://the-guild.dev/graphql/yoga-server/docs) with inlining `schema` config to root.
Below is a config which is accepted by the plugin
### path
@default `/graphql`
Endpoint to expose GraphQL handler

View File

@@ -0,0 +1,188 @@
# HTML
Allows you to use JSX and HTML with proper headers and support.
## Installation
```bash
bun add @elysiajs/html
```
## Basic Usage
```tsx twoslash
import React from 'react'
import { Elysia } from 'elysia'
import { html, Html } from '@elysiajs/html'
new Elysia()
.use(html())
.get(
'/html',
() => `
<html lang='en'>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>`
)
.get('/jsx', () => (
<html lang="en">
<head>
<title>Hello World</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
))
.listen(3000)
```
This plugin will automatically add `Content-Type: text/html; charset=utf8` header to the response, add `<!doctype html>`, and convert it into a Response object.
## JSX
Elysia can use JSX
1. Replace your file that needs to use JSX to end with affix **"x"**:
- .js -> .jsx
- .ts -> .tsx
2. Register the TypeScript type by append the following to **tsconfig.json**:
```jsonc
// tsconfig.json
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "Html.createElement",
"jsxFragmentFactory": "Html.Fragment"
}
}
```
3. Starts using JSX in your file
```tsx twoslash
import React from 'react'
import { Elysia } from 'elysia'
import { html, Html } from '@elysiajs/html'
new Elysia()
.use(html())
.get('/', () => (
<html lang="en">
<head>
<title>Hello World</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
))
.listen(3000)
```
If the error `Cannot find name 'Html'. Did you mean 'html'?` occurs, this import must be added to the JSX template:
```tsx
import { Html } from '@elysiajs/html'
```
It is important that it is written in uppercase.
## XSS
Elysia HTML is based use of the Kita HTML plugin to detect possible XSS attacks in compile time.
You can use a dedicated `safe` attribute to sanitize user value to prevent XSS vulnerability.
```tsx
import { Elysia, t } from 'elysia'
import { html, Html } from '@elysiajs/html'
new Elysia()
.use(html())
.post(
'/',
({ body }) => (
<html lang="en">
<head>
<title>Hello World</title>
</head>
<body>
<h1 safe>{body}</h1>
</body>
</html>
),
{
body: t.String()
}
)
.listen(3000)
```
However, when are building a large-scale app, it's best to have a type reminder to detect possible XSS vulnerabilities in your codebase.
To add a type-safe reminder, please install:
```sh
bun add @kitajs/ts-html-plugin
```
Then appends the following **tsconfig.json**
```jsonc
// tsconfig.json
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "Html.createElement",
"jsxFragmentFactory": "Html.Fragment",
"plugins": [{ "name": "@kitajs/ts-html-plugin" }]
}
}
```
## Config
Below is a config which is accepted by the plugin
### contentType
- Type: `string`
- Default: `'text/html; charset=utf8'`
The content-type of the response.
### autoDetect
- Type: `boolean`
- Default: `true`
Whether to automatically detect HTML content and set the content-type.
### autoDoctype
- Type: `boolean | 'full'`
- Default: `true`
Whether to automatically add `<!doctype html>` to a response starting with `<html>`, if not found.
Use `full` to also automatically add doctypes on responses returned without this plugin
```ts
// without the plugin
app.get('/', () => '<html></html>')
// With the plugin
app.get('/', ({ html }) => html('<html></html>'))
```
### isHtml
- Type: `(value: string) => boolean`
- Default: `isHtml` (exported function)
The function is used to detect if a string is a html or not. Default implementation if length is greater than 7, starts with `<` and ends with `>`.
Keep in mind there's no real way to validate HTML, so the default implementation is a best guess.

View File

@@ -0,0 +1,197 @@
# JWT Plugin
This plugin adds support for using JWT in Elysia handlers.
## Installation
```bash
bun add @elysiajs/jwt
```
## Basic Usage
```typescript [cookie]
import { Elysia } from 'elysia'
import { jwt } from '@elysiajs/jwt'
const app = new Elysia()
.use(
jwt({
name: 'jwt',
secret: 'Fischl von Luftschloss Narfidort'
})
)
.get('/sign/:name', async ({ jwt, params: { name }, cookie: { auth } }) => {
const value = await jwt.sign({ name })
auth.set({
value,
httpOnly: true,
maxAge: 7 * 86400,
path: '/profile',
})
return `Sign in as ${value}`
})
.get('/profile', async ({ jwt, status, cookie: { auth } }) => {
const profile = await jwt.verify(auth.value)
if (!profile)
return status(401, 'Unauthorized')
return `Hello ${profile.name}`
})
.listen(3000)
```
## Config
This plugin extends config from [jose](https://github.com/panva/jose).
Below is a config that is accepted by the plugin.
### name
Name to register `jwt` function as.
For example, `jwt` function will be registered with a custom name.
```typescript
new Elysia()
.use(
jwt({
name: 'myJWTNamespace',
secret: process.env.JWT_SECRETS!
})
)
.get('/sign/:name', ({ myJWTNamespace, params }) => {
return myJWTNamespace.sign(params)
})
```
Because some might need to use multiple `jwt` with different configs in a single server, explicitly registering the JWT function with a different name is needed.
### secret
The private key to sign JWT payload with.
### schema
Type strict validation for JWT payload.
### alg
@default `HS256`
Signing Algorithm to sign JWT payload with.
Possible properties for jose are:
HS256
HS384
HS512
PS256
PS384
PS512
RS256
RS384
RS512
ES256
ES256K
ES384
ES512
EdDSA
### iss
The issuer claim identifies the principal that issued the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.1)
TLDR; is usually (the domain) name of the signer.
### sub
The subject claim identifies the principal that is the subject of the JWT.
The claims in a JWT are normally statements about the subject as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2)
### aud
The audience claim identifies the recipients that the JWT is intended for.
Each principal intended to process the JWT MUST identify itself with a value in the audience claim as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3)
### jti
JWT ID claim provides a unique identifier for the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7)
### nbf
The "not before" claim identifies the time before which the JWT must not be accepted for processing as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.5)
### exp
The expiration time claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.4)
### iat
The "issued at" claim identifies the time at which the JWT was issued.
This claim can be used to determine the age of the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6)
### b64
This JWS Extension Header Parameter modifies the JWS Payload representation and the JWS Signing input computation as per [RFC7797](https://www.rfc-editor.org/rfc/rfc7797).
### kid
A hint indicating which key was used to secure the JWS.
This parameter allows originators to explicitly signal a change of key to recipients as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.4)
### x5t
(X.509 certificate SHA-1 thumbprint) header parameter is a base64url-encoded SHA-1 digest of the DER encoding of the X.509 certificate [RFC5280](https://www.rfc-editor.org/rfc/rfc5280) corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.7)
### x5c
(X.509 certificate chain) header parameter contains the X.509 public key certificate or certificate chain [RFC5280](https://www.rfc-editor.org/rfc/rfc5280) corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.6)
### x5u
(X.509 URL) header parameter is a URI [RFC3986](https://www.rfc-editor.org/rfc/rfc3986) that refers to a resource for the X.509 public key certificate or certificate chain [RFC5280] corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.5)
### jwk
The "jku" (JWK Set URL) Header Parameter is a URI [RFC3986] that refers to a resource for a set of JSON-encoded public keys, one of which corresponds to the key used to digitally sign the JWS.
The keys MUST be encoded as a JWK Set [JWK] as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.2)
### typ
The `typ` (type) Header Parameter is used by JWS applications to declare the media type [IANA.MediaTypes] of this complete JWS.
This is intended for use by the application when more than one kind of object could be present in an application data structure that can contain a JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.9)
### ctr
Content-Type parameter is used by JWS applications to declare the media type [IANA.MediaTypes] of the secured content (the payload).
This is intended for use by the application when more than one kind of object could be present in the JWS Payload as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.9)
## Handler
Below are the value added to the handler.
### jwt.sign
A dynamic object of collection related to use with JWT registered by the JWT plugin.
Type:
```typescript
sign: (payload: JWTPayloadSpec): Promise<string>
```
`JWTPayloadSpec` accepts the same value as [JWT config](#config)
### jwt.verify
Verify payload with the provided JWT config
Type:
```typescript
verify(payload: string) => Promise<JWTPayloadSpec | false>
```
`JWTPayloadSpec` accepts the same value as [JWT config](#config)
## Pattern
Below you can find the common patterns to use the plugin.
## Set JWT expiration date
By default, the config is passed to `setCookie` and inherits its value.
```typescript
const app = new Elysia()
.use(
jwt({
name: 'jwt',
secret: 'kunikuzushi',
exp: '7d'
})
)
.get('/sign/:name', async ({ jwt, params }) => jwt.sign(params))
```
This will sign JWT with an expiration date of the next 7 days.

View File

@@ -0,0 +1,246 @@
# OpenAPI Plugin
## Installation
```bash
bun add @elysiajs/openapi
```
## Basic Usage
```typescript
import { openapi } from '@elysiajs/openapi'
new Elysia()
.use(openapi())
.get('/', () => 'hello')
```
Docs at `/openapi`, spec at `/openapi/json`.
## Detail Object
Extends OpenAPI Operation Object:
```typescript
.get('/', () => 'hello', {
detail: {
title: 'Hello',
description: 'An example route',
summary: 'Short summary',
deprecated: false,
hide: true, // Hide from docs
tags: ['App']
}
})
```
### Documentation Config
```typescript
openapi({
documentation: {
info: {
title: 'API',
version: '1.0.0'
},
tags: [
{ name: 'App', description: 'General' }
],
components: {
securitySchemes: {
bearerAuth: { type: 'http', scheme: 'bearer' }
}
}
}
})
```
### Standard Schema Mapping
```typescript
mapJsonSchema: {
zod: z.toJSONSchema, // Zod 4
valibot: toJsonSchema,
effect: JSONSchema.make
}
```
Zod 3: `zodToJsonSchema` from `zod-to-json-schema`
## OpenAPI Type Gen
Generate docs from types:
```typescript
import { fromTypes } from '@elysiajs/openapi'
export const app = new Elysia()
.use(openapi({
references: fromTypes()
}))
```
### Production
Recommended to generate `.d.ts` file for production when using OpenAPI Type Gen
```typescript
references: fromTypes(
process.env.NODE_ENV === 'production'
? 'dist/index.d.ts'
: 'src/index.ts'
)
```
### Options
```typescript
fromTypes('src/index.ts', {
projectRoot: path.join('..', import.meta.dir),
tsconfigPath: 'tsconfig.dts.json'
})
```
### Caveat: Explicit Types
Use `Prettify` helper to inline when type is not showing:
```typescript
type Prettify<T> = { [K in keyof T]: T[K] } & {}
function getUser(): Prettify<User> { }
```
## Schema Description
```typescript
body: t.Object({
username: t.String(),
password: t.String({
minLength: 8,
description: 'Password (8+ chars)'
})
}, {
description: 'Expected username and password'
}),
detail: {
summary: 'Sign in user',
tags: ['auth']
}
```
## Response Headers
```typescript
import { withHeader } from '@elysiajs/openapi'
response: withHeader(
t.Literal('Hi'),
{ 'x-powered-by': t.Literal('Elysia') }
)
```
Annotation only - doesn't enforce. Set headers manually.
## Tags
Define + assign:
```typescript
.use(openapi({
documentation: {
tags: [
{ name: 'App', description: 'General' },
{ name: 'Auth', description: 'Auth' }
]
}
}))
.get('/', () => 'hello', {
detail: { tags: ['App'] }
})
```
### Instance Tags
```typescript
new Elysia({ tags: ['user'] })
.get('/user', 'user')
```
## Reference Models
Auto-generates schemas:
```typescript
.model({
User: t.Object({
id: t.Number(),
username: t.String()
})
})
.get('/user', () => ({ id: 1, username: 'x' }), {
response: { 200: 'User' },
detail: { tags: ['User'] }
})
```
## Guard
Apply to instance/group:
```typescript
.guard({
detail: {
description: 'Requires auth'
}
})
.get('/user', 'user')
```
## Security
```typescript
.use(openapi({
documentation: {
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
}
}
}
}))
new Elysia({
prefix: '/address',
detail: {
security: [{ bearerAuth: [] }]
}
})
```
Secures all routes under prefix.
## Config
Below is a config which is accepted by the `openapi({})`
### enabled
@default true
Enable/Disable the plugin
### documentation
OpenAPI documentation information
@see https://spec.openapis.org/oas/v3.0.3.html
### exclude
Configuration to exclude paths or methods from documentation
### exclude.methods
List of methods to exclude from documentation
### exclude.paths
List of paths to exclude from documentation
### exclude.staticFile
@default true
Exclude static file routes from documentation
### exclude.tags
List of tags to exclude from documentation
### mapJsonSchema
A custom mapping function from Standard schema to OpenAPI schema
### path
@default '/openapi'
The endpoint to expose OpenAPI documentation frontend
### provider
@default 'scalar'
OpenAPI documentation frontend between:
- Scalar
- SwaggerUI
- null: disable frontend

View File

@@ -0,0 +1,167 @@
# OpenTelemetry Plugin - SKILLS.md
## Installation
```bash
bun add @elysiajs/opentelemetry
```
## Basic Usage
```typescript
import { opentelemetry } from '@elysiajs/opentelemetry'
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
new Elysia()
.use(opentelemetry({
spanProcessors: [
new BatchSpanProcessor(new OTLPTraceExporter())
]
}))
```
Auto-collects spans from OpenTelemetry-compatible libraries. Parent/child spans applied automatically.
## Config
Extends OpenTelemetry SDK params:
- `autoDetectResources` (true) - Auto-detect from env
- `contextManager` (AsyncHooksContextManager) - Custom context
- `textMapPropagator` (CompositePropagator) - W3C Trace + Baggage
- `metricReader` - For MeterProvider
- `views` - Histogram bucket config
- `instrumentations` (getNodeAutoInstrumentations()) - Metapackage or individual
- `resource` - Custom resource
- `resourceDetectors` ([envDetector, processDetector, hostDetector]) - Auto-detect needs `autoDetectResources: true`
- `sampler` - Custom sampler (default: sample all)
- `serviceName` - Namespace identifier
- `spanProcessors` - Array for tracer provider
- `traceExporter` - Auto-setup OTLP/http/protobuf with BatchSpanProcessor if not set
- `spanLimits` - Tracing params
### Resource Detectors via Env
```bash
export OTEL_NODE_RESOURCE_DETECTORS="env,host"
# Options: env, host, os, process, serviceinstance, all, none
```
## Export to Backends
Example - Axiom:
```typescript
.use(opentelemetry({
spanProcessors: [
new BatchSpanProcessor(
new OTLPTraceExporter({
url: 'https://api.axiom.co/v1/traces',
headers: {
Authorization: `Bearer ${Bun.env.AXIOM_TOKEN}`,
'X-Axiom-Dataset': Bun.env.AXIOM_DATASET
}
})
)
]
}))
```
## OpenTelemetry SDK
Use SDK normally - runs under Elysia's request span, auto-appears in trace.
## Record Utility
Equivalent to `startActiveSpan` - auto-closes + captures exceptions:
```typescript
import { record } from '@elysiajs/opentelemetry'
.get('', () => {
return record('database.query', () => {
return db.query('SELECT * FROM users')
})
})
```
Label for code shown in trace.
## Function Naming
Elysia reads function names as span names:
```typescript
// ⚠️ Anonymous span
.derive(async ({ cookie: { session } }) => {
return { user: await getProfile(session) }
})
// ✅ Named span: "getProfile"
.derive(async function getProfile({ cookie: { session } }) {
return { user: await getProfile(session) }
})
```
## getCurrentSpan
Get current span outside handler (via AsyncLocalStorage):
```typescript
import { getCurrentSpan } from '@elysiajs/opentelemetry'
function utility() {
const span = getCurrentSpan()
span.setAttributes({ 'custom.attribute': 'value' })
}
```
## setAttributes
Sugar for `getCurrentSpan().setAttributes`:
```typescript
import { setAttributes } from '@elysiajs/opentelemetry'
function utility() {
setAttributes({ 'custom.attribute': 'value' })
}
```
## Instrumentations (Advanced)
SDK must run before importing instrumented module.
### Setup
1. Separate file:
```typescript
// src/instrumentation.ts
import { opentelemetry } from '@elysiajs/opentelemetry'
import { PgInstrumentation } from '@opentelemetry/instrumentation-pg'
export const instrumentation = opentelemetry({
instrumentations: [new PgInstrumentation()]
})
```
2. Apply:
```typescript
// src/index.ts
import { instrumentation } from './instrumentation'
new Elysia().use(instrumentation).listen(3000)
```
3. Preload:
```toml
# bunfig.toml
preload = ["./src/instrumentation.ts"]
```
### Production Deployment (Advanced)
OpenTelemetry monkey-patches `node_modules`. Exclude instrumented libs from bundling:
```bash
bun build --compile --external pg --outfile server src/index.ts
```
Package.json:
```json
{
"dependencies": { "pg": "^8.15.6" },
"devDependencies": {
"@elysiajs/opentelemetry": "^1.2.0",
"@opentelemetry/instrumentation-pg": "^0.52.0"
}
}
```
Production install:
```bash
bun install --production
```
Keeps `node_modules` with instrumented libs at runtime.

View File

@@ -0,0 +1,71 @@
# Server Timing Plugin
This plugin adds support for auditing performance bottlenecks with Server Timing API
## Installation
```bash
bun add @elysiajs/server-timing
```
## Basic Usage
```typescript twoslash
import { Elysia } from 'elysia'
import { serverTiming } from '@elysiajs/server-timing'
new Elysia()
.use(serverTiming())
.get('/', () => 'hello')
.listen(3000)
```
Server Timing then will append header 'Server-Timing' with log duration, function name, and detail for each life-cycle function.
To inspect, open browser developer tools > Network > [Request made through Elysia server] > Timing.
Now you can effortlessly audit the performance bottleneck of your server.
## Config
Below is a config which is accepted by the plugin
### enabled
@default `NODE_ENV !== 'production'`
Determine whether or not Server Timing should be enabled
### allow
@default `undefined`
A condition whether server timing should be log
### trace
@default `undefined`
Allow Server Timing to log specified life-cycle events:
Trace accepts objects of the following:
- request: capture duration from request
- parse: capture duration from parse
- transform: capture duration from transform
- beforeHandle: capture duration from beforeHandle
- handle: capture duration from the handle
- afterHandle: capture duration from afterHandle
- total: capture total duration from start to finish
## Pattern
Below you can find the common patterns to use the plugin.
## Allow Condition
You may disable Server Timing on specific routes via `allow` property
```ts twoslash
import { Elysia } from 'elysia'
import { serverTiming } from '@elysiajs/server-timing'
new Elysia()
.use(
serverTiming({
allow: ({ request }) => {
return new URL(request.url).pathname !== '/no-trace'
}
})
)
```

View File

@@ -0,0 +1,84 @@
# Static Plugin
This plugin can serve static files/folders for Elysia Server
## Installation
```bash
bun add @elysiajs/static
```
## Basic Usage
```typescript twoslash
import { Elysia } from 'elysia'
import { staticPlugin } from '@elysiajs/static'
new Elysia()
.use(staticPlugin())
.listen(3000)
```
By default, the static plugin default folder is `public`, and registered with `/public` prefix.
Suppose your project structure is:
```
| - src
| - index.ts
| - public
| - takodachi.png
| - nested
| - takodachi.png
```
The available path will become:
- /public/takodachi.png
- /public/nested/takodachi.png
## Config
Below is a config which is accepted by the plugin
### assets
@default `"public"`
Path to the folder to expose as static
### prefix
@default `"/public"`
Path prefix to register public files
### ignorePatterns
@default `[]`
List of files to ignore from serving as static files
### staticLimit
@default `1024`
By default, the static plugin will register paths to the Router with a static name, if the limits are exceeded, paths will be lazily added to the Router to reduce memory usage.
Tradeoff memory with performance.
### alwaysStatic
@default `false`
If set to true, static files path will be registered to Router skipping the `staticLimits`.
### headers
@default `{}`
Set response headers of files
### indexHTML
@default `false`
If set to true, the `index.html` file from the static directory will be served for any request that is matching neither a route nor any existing static file.
## Pattern
Below you can find the common patterns to use the plugin.
## Single file
Suppose you want to return just a single file, you can use `file` instead of using the static plugin
```typescript
import { Elysia, file } from 'elysia'
new Elysia()
.get('/file', file('public/takodachi.png'))
```

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)
}
})
```