More skills fixing
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Elysia, t } from 'elysia'
|
||||
|
||||
new Elysia()
|
||||
.get('/', 'Hello Elysia')
|
||||
.post('/', ({ body: { name } }) => name, {
|
||||
body: t.Object({
|
||||
name: t.String()
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
@@ -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}`)
|
||||
})
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Elysia } from 'elysia'
|
||||
|
||||
new Elysia()
|
||||
.get('/', () => 'Hi')
|
||||
.get('/redirect', ({ redirect }) => redirect('/'))
|
||||
.listen(3000)
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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)
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Elysia } from 'elysia'
|
||||
|
||||
new Elysia()
|
||||
.state('counter', 0)
|
||||
.get('/', ({ store }) => store.counter++)
|
||||
.listen(3000)
|
||||
@@ -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)
|
||||
@@ -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}`)
|
||||
})
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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 * * *` |
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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'
|
||||
}
|
||||
})
|
||||
)
|
||||
```
|
||||
@@ -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'))
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
```
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: svelte-code-writer
|
||||
description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating, editing or analyzing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results.
|
||||
---
|
||||
|
||||
# Svelte 5 Code Writer
|
||||
|
||||
## CLI Tools
|
||||
|
||||
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
|
||||
|
||||
### List Documentation Sections
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp list-sections
|
||||
```
|
||||
|
||||
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
|
||||
|
||||
### Get Documentation
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
|
||||
```
|
||||
|
||||
Retrieves full documentation for specified sections. Use after `list-sections` to fetch relevant docs.
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
|
||||
```
|
||||
|
||||
### Svelte Autofixer
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]
|
||||
```
|
||||
|
||||
Analyzes Svelte code and suggests fixes for common issues.
|
||||
|
||||
**Options:**
|
||||
|
||||
- `--async` - Enable async Svelte mode (default: false)
|
||||
- `--svelte-version` - Target version: 4 or 5 (default: 5)
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Analyze inline code (escape $ as \$)
|
||||
npx @sveltejs/mcp svelte-autofixer '<script>let count = \$state(0);</script>'
|
||||
|
||||
# Analyze a file
|
||||
npx @sveltejs/mcp svelte-autofixer ./src/lib/Component.svelte
|
||||
|
||||
# Target Svelte 4
|
||||
npx @sveltejs/mcp svelte-autofixer ./Component.svelte --svelte-version 4
|
||||
```
|
||||
|
||||
**Important:** When passing code with runes (`$state`, `$derived`, etc.) via the terminal, escape the `$` character as `\$` to prevent shell variable substitution.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Uncertain about syntax?** Run `list-sections` then `get-documentation` for relevant topics
|
||||
2. **Reviewing/debugging?** Run `svelte-autofixer` on the code to detect issues
|
||||
3. **Always validate** - Run `svelte-autofixer` before finalizing any Svelte component
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
name: svelte-core-bestpractices
|
||||
description: Guidance on writing fast, robust, modern Svelte code. Load this skill whenever in a Svelte project and asked to write/edit or analyze a Svelte component or module. Covers reactivity, event handling, styling, integration with libraries and more.
|
||||
---
|
||||
|
||||
## `$state`
|
||||
|
||||
Only use the `$state` rune for variables that should be _reactive_ — in other words, variables that cause an `$effect`, `$derived` or template expression to update. Everything else can be a normal variable.
|
||||
|
||||
Objects and arrays (`$state({...})` or `$state([...])`) are made deeply reactive, meaning mutation will trigger updates. This has a trade-off: in exchange for fine-grained reactivity, the objects must be proxied, which has performance overhead. In cases where you're dealing with large objects that are only ever reassigned (rather than mutated), use `$state.raw` instead. This is often the case with API responses, for example.
|
||||
|
||||
## `$derived`
|
||||
|
||||
To compute something from state, use `$derived` rather than `$effect`:
|
||||
|
||||
```js
|
||||
// do this
|
||||
let square = $derived(num * num);
|
||||
|
||||
// don't do this
|
||||
let square;
|
||||
|
||||
$effect(() => {
|
||||
square = num * num;
|
||||
});
|
||||
```
|
||||
|
||||
> [!NOTE] `$derived` is given an expression, _not_ a function. If you need to use a function (because the expression is complex, for example) use `$derived.by`.
|
||||
|
||||
Deriveds are writable — you can assign to them, just like `$state`, except that they will re-evaluate when their expression changes.
|
||||
|
||||
If the derived expression is an object or array, it will be returned as-is — it is _not_ made deeply reactive. You can, however, use `$state` inside `$derived.by` in the rare cases that you need this.
|
||||
|
||||
## `$effect`
|
||||
|
||||
Effects are an escape hatch and should mostly be avoided. In particular, avoid updating state inside effects.
|
||||
|
||||
- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/@attach.md)
|
||||
- If you need to run some code in response to user interaction, put the code directly in an event handler or use a [function binding](references/bind.md) as appropriate
|
||||
- If you need to log values for debugging purposes, use [`$inspect`](references/$inspect.md)
|
||||
- If you need to observe something external to Svelte, use [`createSubscriber`](references/svelte-reactivity.md)
|
||||
|
||||
Never wrap the contents of an effect in `if (browser) {...}` or similar — effects do not run on the server.
|
||||
|
||||
## `$props`
|
||||
|
||||
Treat props as though they will change. For example, values that depend on props should usually use `$derived`:
|
||||
|
||||
```js
|
||||
// @errors: 2451
|
||||
let { type } = $props();
|
||||
|
||||
// do this
|
||||
let color = $derived(type === 'danger' ? 'red' : 'green');
|
||||
|
||||
// don't do this — `color` will not update if `type` changes
|
||||
let color = type === 'danger' ? 'red' : 'green';
|
||||
```
|
||||
|
||||
## `$inspect.trace`
|
||||
|
||||
`$inspect.trace` is a debugging tool for reactivity. If something is not updating properly or running more than it should you can add `$inspect.trace(label)` as the first line of an `$effect` or `$derived.by` (or any function they call) to trace their dependencies and discover which one triggered an update.
|
||||
|
||||
## Events
|
||||
|
||||
Any element attribute starting with `on` is treated as an event listener:
|
||||
|
||||
```svelte
|
||||
<button onclick={() => {...}}>click me</button>
|
||||
|
||||
<!-- attribute shorthand also works -->
|
||||
<button {onclick}>...</button>
|
||||
|
||||
<!-- so do spread attributes -->
|
||||
<button {...props}>...</button>
|
||||
```
|
||||
|
||||
If you need to attach listeners to `window` or `document` you can use `<svelte:window>` and `<svelte:document>`:
|
||||
|
||||
```svelte
|
||||
<svelte:window onkeydown={...} />
|
||||
<svelte:document onvisibilitychange={...} />
|
||||
```
|
||||
|
||||
Avoid using `onMount` or `$effect` for this.
|
||||
|
||||
## Snippets
|
||||
|
||||
[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/@render.md) tag, or passed to components as props. They must be declared within the template.
|
||||
|
||||
```svelte
|
||||
{#snippet greeting(name)}
|
||||
<p>hello {name}!</p>
|
||||
{/snippet}
|
||||
|
||||
{@render greeting('world')}
|
||||
```
|
||||
|
||||
> [!NOTE] Snippets declared at the top level of a component (i.e. not inside elements or blocks) can be referenced inside `<script>`. A snippet that doesn't reference component state is also available in a `<script module>`, in which case it can be exported for use by other components.
|
||||
|
||||
## Each blocks
|
||||
|
||||
Prefer to use [keyed each blocks](references/each.md) — this improves performance by allowing Svelte to surgically insert or remove items rather than updating the DOM belonging to existing items.
|
||||
|
||||
> [!NOTE] The key _must_ uniquely identify the object. Do not use the index as a key.
|
||||
|
||||
Avoid destructuring if you need to mutate the item (with something like `bind:value={item.count}`, for example).
|
||||
|
||||
## Using JavaScript variables in CSS
|
||||
|
||||
If you have a JS variable that you want to use inside CSS you can set a CSS custom property with the `style:` directive.
|
||||
|
||||
```svelte
|
||||
<div style:--columns={columns}>...</div>
|
||||
```
|
||||
|
||||
You can then reference `var(--columns)` inside the component's `<style>`.
|
||||
|
||||
## Styling child components
|
||||
|
||||
The CSS in a component's `<style>` is scoped to that component. If a parent component needs to control the child's styles, the preferred way is to use CSS custom properties:
|
||||
|
||||
```svelte
|
||||
<!-- Parent.svelte -->
|
||||
<Child --color="red" />
|
||||
|
||||
<!-- Child.svelte -->
|
||||
<h1>Hello</h1>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
color: var(--color);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
If this is impossible (for example, the child component comes from a library) you can use `:global` to override styles:
|
||||
|
||||
```svelte
|
||||
<div>
|
||||
<Child />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div :global {
|
||||
h1 {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
Consider using context instead of declaring state in a shared module. This will scope the state to the part of the app that needs it, and eliminate the possibility of it leaking between users when server-side rendering.
|
||||
|
||||
Use `createContext` rather than `setContext` and `getContext`, as it provides type safety.
|
||||
|
||||
## Async Svelte
|
||||
|
||||
If using version 5.36 or higher, you can use [await expressions](references/await-expressions.md) and [hydratable](references/hydratable.md) to use promises directly inside components. Note that these require the `experimental.async` option to be enabled in `svelte.config.js` as they are not yet considered fully stable.
|
||||
|
||||
## Avoid legacy features
|
||||
|
||||
Always use runes mode for new code, and avoid features that have more modern replacements:
|
||||
|
||||
- use `$state` instead of implicit reactivity (e.g. `let count = 0; count += 1`)
|
||||
- use `$derived` and `$effect` instead of `$:` assignments and statements (but only use effects when there is no better solution)
|
||||
- use `$props` instead of `export let`, `$$props` and `$$restProps`
|
||||
- use `onclick={...}` instead of `on:click={...}`
|
||||
- use `{#snippet ...}` and `{@render ...}` instead of `<slot>` and `$$slots` and `<svelte:fragment>`
|
||||
- use `<DynamicComponent>` instead of `<svelte:component this={DynamicComponent}>`
|
||||
- use `import Self from './ThisComponent.svelte'` and `<Self>` instead of `<svelte:self>`
|
||||
- use classes with `$state` fields to share reactivity between components, instead of using stores
|
||||
- use `{@attach ...}` instead of `use:action`
|
||||
- use clsx-style arrays and objects in `class` attributes, instead of the `class:` directive
|
||||
@@ -0,0 +1,53 @@
|
||||
> [!NOTE] `$inspect` only works during development. In a production build it becomes a noop.
|
||||
|
||||
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire (demo:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
let message = $state('hello');
|
||||
|
||||
$inspect(count, message); // will console.log when `count` or `message` change
|
||||
</script>
|
||||
|
||||
<button onclick={() => count++}>Increment</button>
|
||||
<input bind:value={message} />
|
||||
```
|
||||
|
||||
On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations).
|
||||
|
||||
## $inspect(...).with
|
||||
|
||||
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` (demo:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
$inspect(count).with((type, count) => {
|
||||
if (type === 'update') {
|
||||
debugger; // or `console.trace`, or whatever you want
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<button onclick={() => count++}>Increment</button>
|
||||
```
|
||||
|
||||
## $inspect.trace(...)
|
||||
|
||||
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { doSomeWork } from './elsewhere';
|
||||
|
||||
$effect(() => {
|
||||
+++// $inspect.trace must be the first statement of a function body+++
|
||||
+++$inspect.trace();+++
|
||||
doSomeWork();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
`$inspect.trace` takes an optional first argument which will be used as the label.
|
||||
@@ -0,0 +1,166 @@
|
||||
Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
|
||||
|
||||
Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM.
|
||||
|
||||
> [!NOTE]
|
||||
> Attachments are available in Svelte 5.29 and newer.
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
/** @type {import('svelte/attachments').Attachment} */
|
||||
function myAttachment(element) {
|
||||
console.log(element.nodeName); // 'DIV'
|
||||
|
||||
return () => {
|
||||
console.log('cleaning up');
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div {@attach myAttachment}>...</div>
|
||||
```
|
||||
|
||||
An element can have any number of attachments.
|
||||
|
||||
## Attachment factories
|
||||
|
||||
A useful pattern is for a function, such as `tooltip` in this example, to _return_ an attachment (demo:
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import tippy from 'tippy.js';
|
||||
|
||||
let content = $state('Hello!');
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @returns {import('svelte/attachments').Attachment}
|
||||
*/
|
||||
function tooltip(content) {
|
||||
return (element) => {
|
||||
const tooltip = tippy(element, { content });
|
||||
return tooltip.destroy;
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<input bind:value={content} />
|
||||
|
||||
<button {@attach tooltip(content)}> Hover me </button>
|
||||
```
|
||||
|
||||
Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
|
||||
|
||||
## Inline attachments
|
||||
|
||||
Attachments can also be created inline (demo:
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<canvas
|
||||
width={32}
|
||||
height={32}
|
||||
{@attach (canvas) => {
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
$effect(() => {
|
||||
context.fillStyle = color;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
});
|
||||
}}
|
||||
></canvas>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state.
|
||||
|
||||
## Conditional attachments
|
||||
|
||||
Falsy values like `false` or `undefined` are treated as no attachment, enabling conditional usage:
|
||||
|
||||
```svelte
|
||||
<div {@attach enabled && myAttachment}>...</div>
|
||||
```
|
||||
|
||||
## Passing attachments to components
|
||||
|
||||
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.
|
||||
|
||||
This allows you to create _wrapper components_ that augment elements (demo:
|
||||
|
||||
```svelte
|
||||
<!--- file: Button.svelte --->
|
||||
<script>
|
||||
/** @type {import('svelte/elements').HTMLButtonAttributes} */
|
||||
let { children, ...props } = $props();
|
||||
</script>
|
||||
|
||||
<!-- `props` includes attachments -->
|
||||
<button {...props}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import tippy from 'tippy.js';
|
||||
import Button from './Button.svelte';
|
||||
|
||||
let content = $state('Hello!');
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @returns {import('svelte/attachments').Attachment}
|
||||
*/
|
||||
function tooltip(content) {
|
||||
return (element) => {
|
||||
const tooltip = tippy(element, { content });
|
||||
return tooltip.destroy;
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<input bind:value={content} />
|
||||
|
||||
<Button {@attach tooltip(content)}>Hover me</Button>
|
||||
```
|
||||
|
||||
## Controlling when attachments re-run
|
||||
|
||||
Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
|
||||
|
||||
```js
|
||||
// @errors: 7006 2304 2552
|
||||
function foo(bar) {
|
||||
return (node) => {
|
||||
veryExpensiveSetupWork(node);
|
||||
update(node, bar);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
In the rare case that this is a problem (for example, if `foo` does expensive and unavoidable setup work) consider passing the data inside a function and reading it in a child effect:
|
||||
|
||||
```js
|
||||
// @errors: 7006 2304 2552
|
||||
function foo(+++getBar+++) {
|
||||
return (node) => {
|
||||
veryExpensiveSetupWork(node);
|
||||
|
||||
+++ $effect(() => {
|
||||
update(node, getBar());
|
||||
});+++
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Creating attachments programmatically
|
||||
|
||||
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
|
||||
|
||||
## Converting actions to attachments
|
||||
|
||||
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components.
|
||||
@@ -0,0 +1,35 @@
|
||||
To render a [snippet](snippet), use a `{@render ...}` tag.
|
||||
|
||||
```svelte
|
||||
{#snippet sum(a, b)}
|
||||
<p>{a} + {b} = {a + b}</p>
|
||||
{/snippet}
|
||||
|
||||
{@render sum(1, 2)}
|
||||
{@render sum(3, 4)}
|
||||
{@render sum(5, 6)}
|
||||
```
|
||||
|
||||
The expression can be an identifier like `sum`, or an arbitrary JavaScript expression:
|
||||
|
||||
```svelte
|
||||
{@render (cool ? coolSnippet : lameSnippet)()}
|
||||
```
|
||||
|
||||
## Optional snippets
|
||||
|
||||
If the snippet is potentially undefined — for example, because it's an incoming prop — then you can use optional chaining to only render it when it _is_ defined:
|
||||
|
||||
```svelte
|
||||
{@render children?.()}
|
||||
```
|
||||
|
||||
Alternatively, use an [`{#if ...}`](if) block with an `:else` clause to render fallback content:
|
||||
|
||||
```svelte
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else}
|
||||
<p>fallback content</p>
|
||||
{/if}
|
||||
```
|
||||
@@ -0,0 +1,180 @@
|
||||
As of Svelte 5.36, you can use the `await` keyword inside your components in three places where it was previously unavailable:
|
||||
|
||||
- at the top level of your component's `<script>`
|
||||
- inside `$derived(...)` declarations
|
||||
- inside your markup
|
||||
|
||||
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](/docs/kit/configuration) Svelte, usually `svelte.config.js`:
|
||||
|
||||
```js
|
||||
/// file: svelte.config.js
|
||||
export default {
|
||||
compilerOptions: {
|
||||
experimental: {
|
||||
async: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
The experimental flag will be removed in Svelte 6.
|
||||
|
||||
## Synchronized updates
|
||||
|
||||
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like this...
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let a = $state(1);
|
||||
let b = $state(2);
|
||||
|
||||
async function add(a, b) {
|
||||
await new Promise((f) => setTimeout(f, 500)); // artificial delay
|
||||
return a + b;
|
||||
}
|
||||
</script>
|
||||
|
||||
<input type="number" bind:value={a} />
|
||||
<input type="number" bind:value={b} />
|
||||
|
||||
<p>{a} + {b} = {await add(a, b)}</p>
|
||||
```
|
||||
|
||||
...if you increment `a`, the contents of the `<p>` will _not_ immediately update to read this —
|
||||
|
||||
```html
|
||||
<p>2 + 2 = 3</p>
|
||||
```
|
||||
|
||||
— instead, the text will update to `2 + 2 = 4` when `add(a, b)` resolves.
|
||||
|
||||
Updates can overlap — a fast update will be reflected in the UI while an earlier slow update is still ongoing.
|
||||
|
||||
## Concurrency
|
||||
|
||||
Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup...
|
||||
|
||||
```svelte
|
||||
<p>{await one()}</p><p>{await two()}</p>
|
||||
```
|
||||
|
||||
...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential.
|
||||
|
||||
This does not apply to sequential `await` expressions inside your `<script>` or inside async functions — these run like any other asynchronous JavaScript. An exception is that independent `$derived` expressions will update independently, even though they will run sequentially when they are first created:
|
||||
|
||||
```js
|
||||
// these will run sequentially the first time,
|
||||
// but will update independently
|
||||
let a = $derived(await one());
|
||||
let b = $derived(await two());
|
||||
```
|
||||
|
||||
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](runtime-warnings#Client-warnings-await_waterfall) warning
|
||||
|
||||
## Indicating loading states
|
||||
|
||||
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](svelte-boundary#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
|
||||
|
||||
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`]($effect#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
|
||||
|
||||
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
|
||||
|
||||
```js
|
||||
import { tick, settled } from 'svelte';
|
||||
|
||||
async function onclick() {
|
||||
updating = true;
|
||||
|
||||
// without this, the change to `updating` will be
|
||||
// grouped with the other changes, meaning it
|
||||
// won't be reflected in the UI
|
||||
await tick();
|
||||
|
||||
color = 'octarine';
|
||||
answer = 42;
|
||||
|
||||
await settled();
|
||||
|
||||
// any updates affected by `color` or `answer`
|
||||
// have now been applied
|
||||
updating = false;
|
||||
}
|
||||
```
|
||||
|
||||
## Error handling
|
||||
|
||||
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
|
||||
|
||||
## Server-side rendering
|
||||
|
||||
Svelte supports asynchronous server-side rendering (SSR) with the `render(...)` API. To use it, simply await the return value:
|
||||
|
||||
```js
|
||||
/// file: server.js
|
||||
import { render } from 'svelte/server';
|
||||
import App from './App.svelte';
|
||||
|
||||
const { head, body } = +++await+++ render(App);
|
||||
```
|
||||
|
||||
> [!NOTE] If you're using a framework like SvelteKit, this is done on your behalf.
|
||||
|
||||
If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, that snippet will be rendered while the rest of the content is ignored. All `await` expressions encountered outside boundaries with `pending` snippets will resolve and render their contents prior to `await render(...)` returning.
|
||||
|
||||
> [!NOTE] In the future, we plan to add a streaming implementation that renders the content in the background.
|
||||
|
||||
## Forking
|
||||
|
||||
The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
import Menu from './Menu.svelte';
|
||||
|
||||
let open = $state(false);
|
||||
|
||||
/** @type {import('svelte').Fork | null} */
|
||||
let pending = null;
|
||||
|
||||
function preload() {
|
||||
pending ??= fork(() => {
|
||||
open = true;
|
||||
});
|
||||
}
|
||||
|
||||
function discard() {
|
||||
pending?.discard();
|
||||
pending = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
onfocusin={preload}
|
||||
onfocusout={discard}
|
||||
onpointerenter={preload}
|
||||
onpointerleave={discard}
|
||||
onclick={() => {
|
||||
pending?.commit();
|
||||
pending = null;
|
||||
|
||||
// in case `pending` didn't exist
|
||||
// (if it did, this is a no-op)
|
||||
open = true;
|
||||
}}>open menu</button
|
||||
>
|
||||
|
||||
{#if open}
|
||||
<!-- any async work inside this component will start
|
||||
as soon as the fork is created -->
|
||||
<Menu onclose={() => (open = false)} />
|
||||
{/if}
|
||||
```
|
||||
|
||||
## Caveats
|
||||
|
||||
As an experimental feature, the details of how `await` is handled (and related APIs like `$effect.pending()`) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum.
|
||||
|
||||
## Breaking changes
|
||||
|
||||
Effects run in a slightly different order when the `experimental.async` option is `true`. Specifically, _block_ effects like `{#if ...}` and `{#each ...}` now run before an `$effect.pre` or `beforeUpdate` in the same component, which means that in very rare situations.
|
||||
@@ -0,0 +1,16 @@
|
||||
## Function bindings
|
||||
|
||||
You can also use `bind:property={get, set}`, where `get` and `set` are functions, allowing you to perform validation and transformation:
|
||||
|
||||
```svelte
|
||||
<input bind:value={() => value, (v) => (value = v.toLowerCase())} />
|
||||
```
|
||||
|
||||
In the case of readonly bindings like [dimension bindings](#Dimensions), the `get` value should be `null`:
|
||||
|
||||
```svelte
|
||||
<div bind:clientWidth={null, redraw} bind:clientHeight={null, redraw}>...</div>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Function bindings are available in Svelte 5.9.0 and newer.
|
||||
@@ -0,0 +1,42 @@
|
||||
## Keyed each blocks
|
||||
|
||||
```svelte
|
||||
<!--- copy: false --->
|
||||
{#each expression as name (key)}...{/each}
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- copy: false --->
|
||||
{#each expression as name, index (key)}...{/each}
|
||||
```
|
||||
|
||||
If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to intelligently update the list when data changes by inserting, moving and deleting items, rather than adding or removing items at the end and updating the state in the middle.
|
||||
|
||||
The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.
|
||||
|
||||
```svelte
|
||||
{#each items as item (item.id)}
|
||||
<li>{item.name} x {item.qty}</li>
|
||||
{/each}
|
||||
|
||||
<!-- or with additional index value -->
|
||||
{#each items as item, i (item.id)}
|
||||
<li>{i + 1}: {item.name} x {item.qty}</li>
|
||||
{/each}
|
||||
```
|
||||
|
||||
You can freely use destructuring and rest patterns in each blocks.
|
||||
|
||||
```svelte
|
||||
{#each items as { id, name, qty }, i (id)}
|
||||
<li>{i + 1}: {name} x {qty}</li>
|
||||
{/each}
|
||||
|
||||
{#each objects as { id, ...rest }}
|
||||
<li><span>{id}</span><MyComponent {...rest} /></li>
|
||||
{/each}
|
||||
|
||||
{#each items as [id, ...rest]}
|
||||
<li><span>{id}</span><MyComponent values={rest} /></li>
|
||||
{/each}
|
||||
```
|
||||
@@ -0,0 +1,100 @@
|
||||
In Svelte, when you want to render asynchronous content data on the server, you can simply `await` it. This is great! However, it comes with a pitfall: when hydrating that content on the client, Svelte has to redo the asynchronous work, which blocks hydration for however long it takes:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { getUser } from 'my-database-library';
|
||||
|
||||
// This will get the user on the server, render the user's name into the h1,
|
||||
// and then, during hydration on the client, it will get the user _again_,
|
||||
// blocking hydration until it's done.
|
||||
const user = await getUser();
|
||||
</script>
|
||||
|
||||
<h1>{user.name}</h1>
|
||||
```
|
||||
|
||||
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
|
||||
|
||||
To fix the example above:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { hydratable } from 'svelte';
|
||||
import { getUser } from 'my-database-library';
|
||||
|
||||
// During server rendering, this will serialize and stash the result of `getUser`, associating
|
||||
// it with the provided key and baking it into the `head` content. During hydration, it will
|
||||
// look for the serialized version, returning it instead of running `getUser`. After hydration
|
||||
// is done, if it's called again, it'll simply invoke `getUser`.
|
||||
const user = await hydratable('user', () => getUser());
|
||||
</script>
|
||||
|
||||
<h1>{user.name}</h1>
|
||||
```
|
||||
|
||||
This API can also be used to provide access to random or time-based values that are stable between server rendering and hydration. For example, to get a random number that doesn't update on hydration:
|
||||
|
||||
```ts
|
||||
import { hydratable } from 'svelte';
|
||||
const rand = hydratable('random', () => Math.random());
|
||||
```
|
||||
|
||||
If you're a library author, be sure to prefix the keys of your `hydratable` values with the name of your library so that your keys don't conflict with other libraries.
|
||||
|
||||
## Serialization
|
||||
|
||||
All data returned from a `hydratable` function must be serializable. But this doesn't mean you're limited to JSON — Svelte uses [`devalue`](https://npmjs.com/package/devalue), which can serialize all sorts of things including `Map`, `Set`, `URL`, and `BigInt`. Check the documentation page for a full list. In addition to these, thanks to some Svelte magic, you can also fearlessly use promises:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { hydratable } from 'svelte';
|
||||
const promises = hydratable('random', () => {
|
||||
return {
|
||||
one: Promise.resolve(1),
|
||||
two: Promise.resolve(2),
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
{await promises.one}
|
||||
{await promises.two}
|
||||
```
|
||||
|
||||
## CSP
|
||||
|
||||
`hydratable` adds an inline `<script>` block to the `head` returned from `render`. If you're using [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) (CSP), this script will likely fail to run. You can provide a `nonce` to `render`:
|
||||
|
||||
```js
|
||||
const nonce = crypto.randomUUID();
|
||||
|
||||
const { head, body } = await render(App, {
|
||||
csp: { nonce },
|
||||
});
|
||||
```
|
||||
|
||||
This will add the `nonce` to the script block, on the assumption that you will later add the same nonce to the CSP header of the document that contains it:
|
||||
|
||||
```js
|
||||
response.headers.set('Content-Security-Policy', `script-src 'nonce-${nonce}'`);
|
||||
```
|
||||
|
||||
It's essential that a `nonce` — which, British slang definition aside, means 'number used once' — is only used when dynamically server rendering an individual response.
|
||||
|
||||
If instead you are generating static HTML ahead of time, you must use hashes instead:
|
||||
|
||||
```js
|
||||
const { head, body, hashes } = await render(App, {
|
||||
csp: { hash: true },
|
||||
});
|
||||
```
|
||||
|
||||
`hashes.script` will be an array of strings like `["sha256-abcd123"]`. As with `nonce`, the hashes should be used in your CSP header:
|
||||
|
||||
```js
|
||||
response.headers.set(
|
||||
'Content-Security-Policy',
|
||||
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`,
|
||||
);
|
||||
```
|
||||
|
||||
We recommend using `nonce` over hash if you can, as `hash` will interfere with streaming SSR in the future.
|
||||
@@ -0,0 +1,276 @@
|
||||
```svelte
|
||||
<!--- copy: false --->
|
||||
{#snippet name()}...{/snippet}
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- copy: false --->
|
||||
{#snippet name(param1, param2, paramN)}...{/snippet}
|
||||
```
|
||||
|
||||
Snippets, and [render tags](@render), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this...
|
||||
|
||||
```svelte
|
||||
{#each images as image}
|
||||
{#if image.href}
|
||||
<a href={image.href}>
|
||||
<figure>
|
||||
<img src={image.src} alt={image.caption} width={image.width} height={image.height} />
|
||||
<figcaption>{image.caption}</figcaption>
|
||||
</figure>
|
||||
</a>
|
||||
{:else}
|
||||
<figure>
|
||||
<img src={image.src} alt={image.caption} width={image.width} height={image.height} />
|
||||
<figcaption>{image.caption}</figcaption>
|
||||
</figure>
|
||||
{/if}
|
||||
{/each}
|
||||
```
|
||||
|
||||
...you can write this:
|
||||
|
||||
```svelte
|
||||
{#snippet figure(image)}
|
||||
<figure>
|
||||
<img src={image.src} alt={image.caption} width={image.width} height={image.height} />
|
||||
<figcaption>{image.caption}</figcaption>
|
||||
</figure>
|
||||
{/snippet}
|
||||
|
||||
{#each images as image}
|
||||
{#if image.href}
|
||||
<a href={image.href}>
|
||||
{@render figure(image)}
|
||||
</a>
|
||||
{:else}
|
||||
{@render figure(image)}
|
||||
{/if}
|
||||
{/each}
|
||||
```
|
||||
|
||||
Like function declarations, snippets can have an arbitrary number of parameters, which can have default values, and you can destructure each parameter. You cannot use rest parameters, however.
|
||||
|
||||
## Snippet scope
|
||||
|
||||
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks (demo...
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { message = `it's great to see you!` } = $props();
|
||||
</script>
|
||||
|
||||
{#snippet hello(name)}
|
||||
<p>hello {name}! {message}!</p>
|
||||
{/snippet}
|
||||
|
||||
{@render hello('alice')}
|
||||
{@render hello('bob')}
|
||||
```
|
||||
|
||||
...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
|
||||
|
||||
```svelte
|
||||
<div>
|
||||
{#snippet x()}
|
||||
{#snippet y()}...{/snippet}
|
||||
|
||||
<!-- this is fine -->
|
||||
{@render y()}
|
||||
{/snippet}
|
||||
|
||||
<!-- this will error, as `y` is not in scope -->
|
||||
{@render y()}
|
||||
</div>
|
||||
|
||||
<!-- this will also error, as `x` is not in scope -->
|
||||
{@render x()}
|
||||
```
|
||||
|
||||
Snippets can reference themselves and each other (demo:
|
||||
|
||||
```svelte
|
||||
{#snippet blastoff()}
|
||||
<span>🚀</span>
|
||||
{/snippet}
|
||||
|
||||
{#snippet countdown(n)}
|
||||
{#if n > 0}
|
||||
<span>{n}...</span>
|
||||
{@render countdown(n - 1)}
|
||||
{:else}
|
||||
{@render blastoff()}
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{@render countdown(10)}
|
||||
```
|
||||
|
||||
## Passing snippets to components
|
||||
|
||||
### Explicit props
|
||||
|
||||
Within the template, snippets are values just like any other. As such, they can be passed to components as props (demo:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import Table from './Table.svelte';
|
||||
|
||||
const fruits = [
|
||||
{ name: 'apples', qty: 5, price: 2 },
|
||||
{ name: 'bananas', qty: 10, price: 1 },
|
||||
{ name: 'cherries', qty: 20, price: 0.5 },
|
||||
];
|
||||
</script>
|
||||
|
||||
{#snippet header()}
|
||||
<th>fruit</th>
|
||||
<th>qty</th>
|
||||
<th>price</th>
|
||||
<th>total</th>
|
||||
{/snippet}
|
||||
|
||||
{#snippet row(d)}
|
||||
<td>{d.name}</td>
|
||||
<td>{d.qty}</td>
|
||||
<td>{d.price}</td>
|
||||
<td>{d.qty * d.price}</td>
|
||||
{/snippet}
|
||||
|
||||
<Table data={fruits} {header} {row} />
|
||||
```
|
||||
|
||||
Think about it like passing content instead of data to a component. The concept is similar to slots in web components.
|
||||
|
||||
### Implicit props
|
||||
|
||||
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component (demo:
|
||||
|
||||
```svelte
|
||||
<!-- this is semantically the same as the above -->
|
||||
<Table data={fruits}>
|
||||
{#snippet header()}
|
||||
<th>fruit</th>
|
||||
<th>qty</th>
|
||||
<th>price</th>
|
||||
<th>total</th>
|
||||
{/snippet}
|
||||
|
||||
{#snippet row(d)}
|
||||
<td>{d.name}</td>
|
||||
<td>{d.qty}</td>
|
||||
<td>{d.price}</td>
|
||||
<td>{d.qty * d.price}</td>
|
||||
{/snippet}
|
||||
</Table>
|
||||
```
|
||||
|
||||
### Implicit `children` snippet
|
||||
|
||||
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet (demo:
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<Button>click me</Button>
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- file: Button.svelte --->
|
||||
<script>
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<!-- result will be <button>click me</button> -->
|
||||
<button>{@render children()}</button>
|
||||
```
|
||||
|
||||
> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name
|
||||
|
||||
### Optional snippet props
|
||||
|
||||
You can declare snippet props as being optional. You can either use optional chaining to not render anything if the snippet isn't set...
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children?.()}
|
||||
```
|
||||
|
||||
...or use an `#if` block to render fallback content:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else}
|
||||
fallback content
|
||||
{/if}
|
||||
```
|
||||
|
||||
## Typing snippets
|
||||
|
||||
Snippets implement the `Snippet` interface imported from `'svelte'`:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
data: any[];
|
||||
children: Snippet;
|
||||
row: Snippet<[any]>;
|
||||
}
|
||||
|
||||
let { data, children, row }: Props = $props();
|
||||
</script>
|
||||
```
|
||||
|
||||
With this change, red squigglies will appear if you try and use the component without providing a `data` prop and a `row` snippet. Notice that the type argument provided to `Snippet` is a tuple, since snippets can have multiple parameters.
|
||||
|
||||
We can tighten things up further by declaring a generic, so that `data` and `row` refer to the same type:
|
||||
|
||||
```svelte
|
||||
<script lang="ts" generics="T">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let {
|
||||
data,
|
||||
children,
|
||||
row,
|
||||
}: {
|
||||
data: T[];
|
||||
children: Snippet;
|
||||
row: Snippet<[T]>;
|
||||
} = $props();
|
||||
</script>
|
||||
```
|
||||
|
||||
## Exporting snippets
|
||||
|
||||
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets) (demo:
|
||||
|
||||
```svelte
|
||||
<script module>
|
||||
export { add };
|
||||
</script>
|
||||
|
||||
{#snippet add(a, b)}
|
||||
{a} + {b} = {a + b}
|
||||
{/snippet}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> This requires Svelte 5.5.0 or newer
|
||||
|
||||
## Programmatic snippets
|
||||
|
||||
Snippets can be created programmatically with the [`createRawSnippet`](svelte#createRawSnippet) API. This is intended for advanced use cases.
|
||||
|
||||
## Snippets and slots
|
||||
|
||||
In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.
|
||||
@@ -0,0 +1,61 @@
|
||||
## createSubscriber
|
||||
|
||||
<blockquote class="since note">
|
||||
|
||||
Available since 5.7.0
|
||||
|
||||
</blockquote>
|
||||
|
||||
Returns a `subscribe` function that integrates external event-based systems with Svelte's reactivity.
|
||||
It's particularly useful for integrating with web APIs like `MediaQuery`, `IntersectionObserver`, or `WebSocket`.
|
||||
|
||||
If `subscribe` is called inside an effect (including indirectly, for example inside a getter),
|
||||
the `start` callback will be called with an `update` function. Whenever `update` is called, the effect re-runs.
|
||||
|
||||
If `start` returns a cleanup function, it will be called when the effect is destroyed.
|
||||
|
||||
If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects
|
||||
are active, and the returned teardown function will only be called when all effects are destroyed.
|
||||
|
||||
It's best understood with an example. Here's an implementation of [`MediaQuery`](/docs/svelte/svelte-reactivity#MediaQuery):
|
||||
|
||||
```js
|
||||
// @errors: 7031
|
||||
import { createSubscriber } from 'svelte/reactivity';
|
||||
import { on } from 'svelte/events';
|
||||
|
||||
export class MediaQuery {
|
||||
#query;
|
||||
#subscribe;
|
||||
|
||||
constructor(query) {
|
||||
this.#query = window.matchMedia(`(${query})`);
|
||||
|
||||
this.#subscribe = createSubscriber((update) => {
|
||||
// when the `change` event occurs, re-run any effects that read `this.current`
|
||||
const off = on(this.#query, 'change', update);
|
||||
|
||||
// stop listening when all the effects are destroyed
|
||||
return () => off();
|
||||
});
|
||||
}
|
||||
|
||||
get current() {
|
||||
// This makes the getter reactive, if read in an effect
|
||||
this.#subscribe();
|
||||
|
||||
// Return the current state of the query, whether or not we're in an effect
|
||||
return this.#query.matches;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<div class="ts-block">
|
||||
|
||||
```dts
|
||||
function createSubscriber(
|
||||
start: (update: () => void) => (() => void) | void
|
||||
): () => void;
|
||||
```
|
||||
|
||||
</div>
|
||||
Reference in New Issue
Block a user