set-up
This commit is contained in:
475
.agents/skills/elysiajs/SKILL.md
Normal file
475
.agents/skills/elysiajs/SKILL.md
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
---
|
||||||
|
name: elysiajs
|
||||||
|
description: Create backend with ElysiaJS, a type-safe, high-performance framework.
|
||||||
|
---
|
||||||
|
|
||||||
|
# ElysiaJS Development Skill
|
||||||
|
|
||||||
|
Always consult [elysiajs.com/llms.txt](https://elysiajs.com/llms.txt) for code examples and latest API.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
ElysiaJS is a TypeScript framework for building Bun-first (but not limited to Bun) type-safe, high-performance backend servers. This skill provides comprehensive guidance for developing with Elysia, including routing, validation, authentication, plugins, integrations, and deployment.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
Trigger this skill when the user asks to:
|
||||||
|
- Create or modify ElysiaJS routes, handlers, or servers
|
||||||
|
- Setup validation with TypeBox or other schema libraries (Zod, Valibot)
|
||||||
|
- Implement authentication (JWT, session-based, macros, guards)
|
||||||
|
- Add plugins (CORS, OpenAPI, Static files, JWT)
|
||||||
|
- Integrate with external services (Drizzle ORM, Better Auth, Next.js, Eden Treaty)
|
||||||
|
- Setup WebSocket endpoints for real-time features
|
||||||
|
- Create unit tests for Elysia instances
|
||||||
|
- Deploy Elysia servers to production
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
Quick scaffold:
|
||||||
|
```bash
|
||||||
|
bun create elysia app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic Server
|
||||||
|
```typescript
|
||||||
|
import { Elysia, t, status } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/', () => 'Hello World')
|
||||||
|
.post('/user', ({ body }) => body, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
age: t.Number()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get('/id/:id', ({ params: { id } }) => {
|
||||||
|
if(id > 1_000_000) return status(404, 'Not Found')
|
||||||
|
|
||||||
|
return id
|
||||||
|
}, {
|
||||||
|
params: t.Object({
|
||||||
|
id: t.Number({
|
||||||
|
minimum: 1
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: t.Number(),
|
||||||
|
404: t.Literal('Not Found')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### HTTP Methods
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', 'GET')
|
||||||
|
.post('/', 'POST')
|
||||||
|
.put('/', 'PUT')
|
||||||
|
.patch('/', 'PATCH')
|
||||||
|
.delete('/', 'DELETE')
|
||||||
|
.options('/', 'OPTIONS')
|
||||||
|
.head('/', 'HEAD')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Path Parameters
|
||||||
|
```typescript
|
||||||
|
.get('/user/:id', ({ params: { id } }) => id)
|
||||||
|
.get('/post/:id/:slug', ({ params }) => params)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Parameters
|
||||||
|
```typescript
|
||||||
|
.get('/search', ({ query }) => query.q)
|
||||||
|
// GET /search?q=elysia → "elysia"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request Body
|
||||||
|
```typescript
|
||||||
|
.post('/user', ({ body }) => body)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Headers
|
||||||
|
```typescript
|
||||||
|
.get('/', ({ headers }) => headers.authorization)
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeBox Validation
|
||||||
|
|
||||||
|
### Basic Types
|
||||||
|
```typescript
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
.post('/user', ({ body }) => body, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
age: t.Number(),
|
||||||
|
email: t.String({ format: 'email' }),
|
||||||
|
website: t.Optional(t.String({ format: 'uri' }))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nested Objects
|
||||||
|
```typescript
|
||||||
|
body: t.Object({
|
||||||
|
user: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
address: t.Object({
|
||||||
|
street: t.String(),
|
||||||
|
city: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arrays
|
||||||
|
```typescript
|
||||||
|
body: t.Object({
|
||||||
|
tags: t.Array(t.String()),
|
||||||
|
users: t.Array(t.Object({
|
||||||
|
id: t.String(),
|
||||||
|
name: t.String()
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Upload
|
||||||
|
```typescript
|
||||||
|
.post('/upload', ({ body }) => body.file, {
|
||||||
|
body: t.Object({
|
||||||
|
file: t.File({
|
||||||
|
type: 'image', // image/* mime types
|
||||||
|
maxSize: '5m' // 5 megabytes
|
||||||
|
}),
|
||||||
|
files: t.Files({ // Multiple files
|
||||||
|
type: ['image/png', 'image/jpeg']
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Validation
|
||||||
|
```typescript
|
||||||
|
.get('/user/:id', ({ params: { id } }) => ({
|
||||||
|
id,
|
||||||
|
name: 'John',
|
||||||
|
email: 'john@example.com'
|
||||||
|
}), {
|
||||||
|
params: t.Object({
|
||||||
|
id: t.Number()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: t.Object({
|
||||||
|
id: t.Number(),
|
||||||
|
name: t.String(),
|
||||||
|
email: t.String()
|
||||||
|
}),
|
||||||
|
404: t.String()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Standard Schema (Zod, Valibot, ArkType)
|
||||||
|
|
||||||
|
### Zod
|
||||||
|
```typescript
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
.post('/user', ({ body }) => body, {
|
||||||
|
body: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
age: z.number().min(0),
|
||||||
|
email: z.string().email()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.get('/user/:id', ({ params: { id }, status }) => {
|
||||||
|
const user = findUser(id)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return status(404, 'User not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Guards (Apply to Multiple Routes)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.guard({
|
||||||
|
params: t.Object({
|
||||||
|
id: t.Number()
|
||||||
|
})
|
||||||
|
}, app => app
|
||||||
|
.get('/user/:id', ({ params: { id } }) => id)
|
||||||
|
.delete('/user/:id', ({ params: { id } }) => id)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Macro
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.macro({
|
||||||
|
hi: (word: string) => ({
|
||||||
|
beforeHandle() { console.log(word) }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get('/', () => 'hi', { hi: 'Elysia' })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Project Structure (Recommended)
|
||||||
|
Elysia takes an unopinionated approach but based on user request. But without any specific preference, we recommend a feature-based and domain driven folder structure where each feature has its own folder containing controllers, services, and models.
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── index.ts # Main server entry
|
||||||
|
├── modules/
|
||||||
|
│ ├── auth/
|
||||||
|
│ │ ├── index.ts # Auth routes (Elysia instance)
|
||||||
|
│ │ ├── service.ts # Business logic
|
||||||
|
│ │ └── model.ts # TypeBox schemas/DTOs
|
||||||
|
│ └── user/
|
||||||
|
│ ├── index.ts
|
||||||
|
│ ├── service.ts
|
||||||
|
│ └── model.ts
|
||||||
|
└── plugins/
|
||||||
|
└── custom.ts
|
||||||
|
|
||||||
|
public/ # Static files (if using static plugin)
|
||||||
|
test/ # Unit tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Each file has its own responsibility as follows:
|
||||||
|
- **Controller (index.ts)**: Handle HTTP routing, request validation, and cookie.
|
||||||
|
- **Service (service.ts)**: Handle business logic, decoupled from Elysia controller if possible.
|
||||||
|
- **Model (model.ts)**: Define the data structure and validation for the request and response.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
Elysia is unopinionated on design pattern, but if not provided, we can relies on MVC pattern pair with feature based folder structure.
|
||||||
|
|
||||||
|
- Controller:
|
||||||
|
- Prefers Elysia as a controller for HTTP dependant controller
|
||||||
|
- For non HTTP dependent, prefers service instead unless explicitly asked
|
||||||
|
- Use `onError` to handle local custom errors
|
||||||
|
- Register Model to Elysia instance via `Elysia.models({ ...models })` and prefix model by namespace `Elysia.prefix('model', 'Namespace.')
|
||||||
|
- Prefers Reference Model by name provided by Elysia instead of using an actual `Model.name`
|
||||||
|
- Service:
|
||||||
|
- Prefers class (or abstract class if possible)
|
||||||
|
- Prefers interface/type derive from `Model`
|
||||||
|
- Return `status` (`import { status } from 'elysia'`) for error
|
||||||
|
- Prefers `return Error` instead of `throw Error`
|
||||||
|
- Models:
|
||||||
|
- Always export validation model and type of validation model
|
||||||
|
- Custom Error should be in contains in Model
|
||||||
|
|
||||||
|
## Elysia Key Concept
|
||||||
|
Elysia has a every important concepts/rules to understand before use.
|
||||||
|
|
||||||
|
## Encapsulation - Isolates by Default
|
||||||
|
|
||||||
|
Lifecycles (hooks, middleware) **don't leak** between instances unless scoped.
|
||||||
|
|
||||||
|
**Scope levels:**
|
||||||
|
- `local` (default) - current instance + descendants
|
||||||
|
- `scoped` - parent + current + descendants
|
||||||
|
- `global` - all instances
|
||||||
|
|
||||||
|
```ts
|
||||||
|
.onBeforeHandle(() => {}) // only local instance
|
||||||
|
.onBeforeHandle({ as: 'global' }, () => {}) // exports to all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Method Chaining - Required for Types
|
||||||
|
|
||||||
|
**Must chain**. Each method returns new type reference.
|
||||||
|
|
||||||
|
❌ Don't:
|
||||||
|
```ts
|
||||||
|
const app = new Elysia()
|
||||||
|
app.state('build', 1) // loses type
|
||||||
|
app.get('/', ({ store }) => store.build) // build doesn't exists
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ Do:
|
||||||
|
```ts
|
||||||
|
new Elysia()
|
||||||
|
.state('build', 1)
|
||||||
|
.get('/', ({ store }) => store.build)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Explicit Dependencies
|
||||||
|
|
||||||
|
Each instance independent. **Declare what you use.**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const auth = new Elysia()
|
||||||
|
.decorate('Auth', Auth)
|
||||||
|
.model(Auth.models)
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', ({ Auth }) => Auth.getProfile()) // Auth doesn't exists
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(auth) // must declare
|
||||||
|
.get('/', ({ Auth }) => Auth.getProfile())
|
||||||
|
```
|
||||||
|
|
||||||
|
**Global scope when:**
|
||||||
|
- No types added (cors, helmet)
|
||||||
|
- Global lifecycle (logging, tracing)
|
||||||
|
|
||||||
|
**Explicit when:**
|
||||||
|
- Adds types (state, models)
|
||||||
|
- Business logic (auth, db)
|
||||||
|
|
||||||
|
## Deduplication
|
||||||
|
|
||||||
|
Plugins re-execute unless named:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia() // rerun on `.use`
|
||||||
|
new Elysia({ name: 'ip' }) // runs once across all instances
|
||||||
|
```
|
||||||
|
|
||||||
|
## Order Matters
|
||||||
|
|
||||||
|
Events apply to routes **registered after** them.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
.onBeforeHandle(() => console.log('1'))
|
||||||
|
.get('/', () => 'hi') // has hook
|
||||||
|
.onBeforeHandle(() => console.log('2')) // doesn't affect '/'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Inference
|
||||||
|
|
||||||
|
**Inline functions only** for accurate types.
|
||||||
|
|
||||||
|
For controllers, destructure in inline wrapper:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
.post('/', ({ body }) => Controller.greet(body), {
|
||||||
|
body: t.Object({ name: t.String() })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Get type from schema:
|
||||||
|
```ts
|
||||||
|
type MyType = typeof MyType.static
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reference Model
|
||||||
|
Model can be reference by name, especially great for documenting an API
|
||||||
|
```ts
|
||||||
|
new Elysia()
|
||||||
|
.model({
|
||||||
|
book: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.post('/', ({ body }) => body.name, {
|
||||||
|
body: 'book'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Model can be renamed by using `.prefix` / `.suffix`
|
||||||
|
```ts
|
||||||
|
new Elysia()
|
||||||
|
.model({
|
||||||
|
book: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.prefix('model', 'Namespace')
|
||||||
|
.post('/', ({ body }) => body.name, {
|
||||||
|
body: 'Namespace.Book'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Once `prefix`, model name will be capitalized by default.
|
||||||
|
|
||||||
|
## Technical Terms
|
||||||
|
The following are technical terms that is use for Elysia:
|
||||||
|
- `OpenAPI Type Gen` - function name `fromTypes` from `@elysiajs/openapi` for generating OpenAPI from types, see `plugins/openapi.md`
|
||||||
|
- `Eden`, `Eden Treaty` - e2e type safe RPC client for share type from backend to frontend
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
Use the following references as needed.
|
||||||
|
|
||||||
|
It's recommended to checkout `route.md` for as it contains the most important foundation building blocks with examples.
|
||||||
|
|
||||||
|
`plugin.md` and `validation.md` is important as well but can be check as needed.
|
||||||
|
|
||||||
|
### references/
|
||||||
|
Detailed documentation split by topic:
|
||||||
|
- `bun-fullstack-dev-server.md` - Bun Fullstack Dev Server with HMR. React without bundler.
|
||||||
|
- `cookie.md` - Detailed documentation on cookie
|
||||||
|
- `deployment.md` - Production deployment guide / Docker
|
||||||
|
- `eden.md` - e2e type safe RPC client for share type from backend to frontend
|
||||||
|
- `guard.md` - Setting validation/lifecycle all at once
|
||||||
|
- `macro.md` - Compose multiple schema/lifecycle as a reusable Elysia via key-value (recommended for complex setup, eg. authentication, authorization, Role-based Access Check)
|
||||||
|
- `plugin.md` - Decouple part of Elysia into a standalone component
|
||||||
|
- `route.md` - Elysia foundation building block: Routing, Handler and Context
|
||||||
|
- `testing.md` - Unit tests with examples
|
||||||
|
- `validation.md` - Setup input/output validation and list of all custom validation rules
|
||||||
|
- `websocket.md` - Real-time features
|
||||||
|
|
||||||
|
### plugins/
|
||||||
|
Detailed documentation, usage and configuration reference for official Elysia plugin:
|
||||||
|
- `bearer.md` - Add bearer capability to Elysia (`@elysiajs/bearer`)
|
||||||
|
- `cors.md` - Out of box configuration for CORS (`@elysiajs/cors`)
|
||||||
|
- `cron.md` - Run cron job with access to Elysia context (`@elysiajs/cron`)
|
||||||
|
- `graphql-apollo.md` - Integration GraphQL Apollo (`@elysiajs/graphql-apollo`)
|
||||||
|
- `graphql-yoga.md` - Integration with GraphQL Yoga (`@elysiajs/graphql-yoga`)
|
||||||
|
- `html.md` - HTML and JSX plugin setup and usage (`@elysiajs/html`)
|
||||||
|
- `jwt.md` - JWT / JWK plugin (`@elysiajs/jwt`)
|
||||||
|
- `openapi.md` - OpenAPI documentation and OpenAPI Type Gen / OpenAPI from types (`@elysiajs/openapi`)
|
||||||
|
- `opentelemetry.md` - OpenTelemetry, instrumentation, and record span utilities (`@elysiajs/opentelemetry`)
|
||||||
|
- `server-timing.md` - Server Timing metric for debug (`@elysiajs/server-timing`)
|
||||||
|
- `static.md` - Serve static files/folders for Elysia Server (`@elysiajs/static`)
|
||||||
|
|
||||||
|
### integrations/
|
||||||
|
Guide to integrate Elysia with external library/runtime:
|
||||||
|
- `ai-sdk.md` - Using Vercel AI SDK with Elysia
|
||||||
|
- `astro.md` - Elysia in Astro API route
|
||||||
|
- `better-auth.md` - Integrate Elysia with better-auth
|
||||||
|
- `cloudflare-worker.md` - Elysia on Cloudflare Worker adapter
|
||||||
|
- `deno.md` - Elysia on Deno
|
||||||
|
- `drizzle.md` - Integrate Elysia with Drizzle ORM
|
||||||
|
- `expo.md` - Elysia in Expo API route
|
||||||
|
- `nextjs.md` - Elysia in Nextjs API route
|
||||||
|
- `nodejs.md` - Run Elysia on Node.js
|
||||||
|
- `nuxt.md` - Elysia on API route
|
||||||
|
- `prisma.md` - Integrate Elysia with Prisma
|
||||||
|
- `react-email.d` - Create and Send Email with React and Elysia
|
||||||
|
- `sveltekit.md` - Run Elysia on Svelte Kit API route
|
||||||
|
- `tanstack-start.md` - Run Elysia on Tanstack Start / React Query
|
||||||
|
- `vercel.md` - Deploy Elysia to Vercel
|
||||||
|
|
||||||
|
### examples/ (optional)
|
||||||
|
- `basic.ts` - Basic Elysia example
|
||||||
|
- `body-parser.ts` - Custom body parser example via `.onParse`
|
||||||
|
- `complex.ts` - Comprehensive usage of Elysia server
|
||||||
|
- `cookie.ts` - Setting cookie
|
||||||
|
- `error.ts` - Error handling
|
||||||
|
- `file.ts` - Returning local file from server
|
||||||
|
- `guard.ts` - Setting mulitple validation schema and lifecycle
|
||||||
|
- `map-response.ts` - Custom response mapper
|
||||||
|
- `redirect.ts` - Redirect response
|
||||||
|
- `rename.ts` - Rename context's property
|
||||||
|
- `schema.ts` - Setup validation
|
||||||
|
- `state.ts` - Setup global state
|
||||||
|
- `upload-file.ts` - File upload with validation
|
||||||
|
- `websocket.ts` - Web Socket for realtime communication
|
||||||
|
|
||||||
|
### patterns/ (optional)
|
||||||
|
- `patterns/mvc.md` - Detail guideline for using Elysia with MVC patterns
|
||||||
9
.agents/skills/elysiajs/examples/basic.ts
Normal file
9
.agents/skills/elysiajs/examples/basic.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', 'Hello Elysia')
|
||||||
|
.post('/', ({ body: { name } }) => name, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
33
.agents/skills/elysiajs/examples/body-parser.ts
Normal file
33
.agents/skills/elysiajs/examples/body-parser.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
// Add custom body parser
|
||||||
|
.onParse(async ({ request, contentType }) => {
|
||||||
|
switch (contentType) {
|
||||||
|
case 'application/Elysia':
|
||||||
|
return request.text()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post('/', ({ body: { username } }) => `Hi ${username}`, {
|
||||||
|
body: t.Object({
|
||||||
|
id: t.Number(),
|
||||||
|
username: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// Increase id by 1 from body before main handler
|
||||||
|
.post('/transform', ({ body }) => body, {
|
||||||
|
transform: ({ body }) => {
|
||||||
|
body.id = body.id + 1
|
||||||
|
},
|
||||||
|
body: t.Object({
|
||||||
|
id: t.Number(),
|
||||||
|
username: t.String()
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: 'A'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post('/mirror', ({ body }) => body)
|
||||||
|
.listen(3000)
|
||||||
|
|
||||||
|
console.log('🦊 Elysia is running at :8080')
|
||||||
112
.agents/skills/elysiajs/examples/complex.ts
Normal file
112
.agents/skills/elysiajs/examples/complex.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { Elysia, t, file } from 'elysia'
|
||||||
|
|
||||||
|
const loggerPlugin = new Elysia()
|
||||||
|
.get('/hi', () => 'Hi')
|
||||||
|
.decorate('log', () => 'A')
|
||||||
|
.decorate('date', () => new Date())
|
||||||
|
.state('fromPlugin', 'From Logger')
|
||||||
|
.use((app) => app.state('abc', 'abc'))
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.onRequest(({ set }) => {
|
||||||
|
set.headers = {
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onError(({ code }) => {
|
||||||
|
if (code === 'NOT_FOUND')
|
||||||
|
return 'Not Found :('
|
||||||
|
})
|
||||||
|
.use(loggerPlugin)
|
||||||
|
.state('build', Date.now())
|
||||||
|
.get('/', 'Elysia')
|
||||||
|
.get('/tako', file('./example/takodachi.png'))
|
||||||
|
.get('/json', () => ({
|
||||||
|
hi: 'world'
|
||||||
|
}))
|
||||||
|
.get('/root/plugin/log', ({ log, store: { build } }) => {
|
||||||
|
log()
|
||||||
|
|
||||||
|
return build
|
||||||
|
})
|
||||||
|
.get('/wildcard/*', () => 'Hi Wildcard')
|
||||||
|
.get('/query', () => 'Elysia', {
|
||||||
|
beforeHandle: ({ query }) => {
|
||||||
|
console.log('Name:', query?.name)
|
||||||
|
|
||||||
|
if (query?.name === 'aom') return 'Hi saltyaom'
|
||||||
|
},
|
||||||
|
query: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.post('/json', async ({ body }) => body, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
additional: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.post('/transform-body', async ({ body }) => body, {
|
||||||
|
beforeHandle: (ctx) => {
|
||||||
|
ctx.body = {
|
||||||
|
...ctx.body,
|
||||||
|
additional: 'Elysia'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
additional: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get('/id/:id', ({ params: { id } }) => id, {
|
||||||
|
transform({ params }) {
|
||||||
|
params.id = +params.id
|
||||||
|
},
|
||||||
|
params: t.Object({
|
||||||
|
id: t.Number()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.post('/new/:id', async ({ body, params }) => body, {
|
||||||
|
params: t.Object({
|
||||||
|
id: t.Number()
|
||||||
|
}),
|
||||||
|
body: t.Object({
|
||||||
|
username: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get('/trailing-slash', () => 'A')
|
||||||
|
.group('/group', (app) =>
|
||||||
|
app
|
||||||
|
.onBeforeHandle(({ query }) => {
|
||||||
|
if (query?.name === 'aom') return 'Hi saltyaom'
|
||||||
|
})
|
||||||
|
.get('/', () => 'From Group')
|
||||||
|
.get('/hi', () => 'HI GROUP')
|
||||||
|
.get('/elysia', () => 'Welcome to Elysian Realm')
|
||||||
|
.get('/fbk', () => 'FuBuKing')
|
||||||
|
)
|
||||||
|
.get('/response-header', ({ set }) => {
|
||||||
|
set.status = 404
|
||||||
|
set.headers['a'] = 'b'
|
||||||
|
|
||||||
|
return 'A'
|
||||||
|
})
|
||||||
|
.get('/this/is/my/deep/nested/root', () => 'Hi')
|
||||||
|
.get('/build', ({ store: { build } }) => build)
|
||||||
|
.get('/ref', ({ date }) => date())
|
||||||
|
.get('/response', () => new Response('Hi'))
|
||||||
|
.get('/error', () => new Error('Something went wrong'))
|
||||||
|
.get('/401', ({ set }) => {
|
||||||
|
set.status = 401
|
||||||
|
|
||||||
|
return 'Status should be 401'
|
||||||
|
})
|
||||||
|
.get('/timeout', async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||||
|
|
||||||
|
return 'A'
|
||||||
|
})
|
||||||
|
.all('/all', () => 'hi')
|
||||||
|
.listen(8080, ({ hostname, port }) => {
|
||||||
|
console.log(`🦊 Elysia is running at http://${hostname}:${port}`)
|
||||||
|
})
|
||||||
45
.agents/skills/elysiajs/examples/cookie.ts
Normal file
45
.agents/skills/elysiajs/examples/cookie.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia({
|
||||||
|
cookie: {
|
||||||
|
secrets: 'Fischl von Luftschloss Narfidort',
|
||||||
|
sign: ['name']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get(
|
||||||
|
'/council',
|
||||||
|
({ cookie: { council } }) =>
|
||||||
|
(council.value = [
|
||||||
|
{
|
||||||
|
name: 'Rin',
|
||||||
|
affilation: 'Administration'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
cookie: t.Cookie({
|
||||||
|
council: t.Array(
|
||||||
|
t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
affilation: t.String()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.get('/create', ({ cookie: { name } }) => (name.value = 'Himari'))
|
||||||
|
.get(
|
||||||
|
'/update',
|
||||||
|
({ cookie: { name } }) => {
|
||||||
|
name.value = 'seminar: Rio'
|
||||||
|
name.value = 'seminar: Himari'
|
||||||
|
name.maxAge = 86400
|
||||||
|
|
||||||
|
return name.value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cookie: t.Cookie({
|
||||||
|
name: t.Optional(t.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
38
.agents/skills/elysiajs/examples/error.ts
Normal file
38
.agents/skills/elysiajs/examples/error.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
class CustomError extends Error {
|
||||||
|
constructor(public name: string) {
|
||||||
|
super(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.error({
|
||||||
|
CUSTOM_ERROR: CustomError
|
||||||
|
})
|
||||||
|
// global handler
|
||||||
|
.onError(({ code, error, status }) => {
|
||||||
|
switch (code) {
|
||||||
|
case "CUSTOM_ERROR":
|
||||||
|
return status(401, { message: error.message })
|
||||||
|
|
||||||
|
case "NOT_FOUND":
|
||||||
|
return "Not found :("
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post('/', ({ body }) => body, {
|
||||||
|
body: t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String(),
|
||||||
|
nested: t.Optional(
|
||||||
|
t.Object({
|
||||||
|
hi: t.String()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
// local handler
|
||||||
|
error({ error }) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.listen(3000)
|
||||||
10
.agents/skills/elysiajs/examples/file.ts
Normal file
10
.agents/skills/elysiajs/examples/file.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Elysia, file } from 'elysia'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example of handle single static file
|
||||||
|
*
|
||||||
|
* @see https://github.com/elysiajs/elysia-static
|
||||||
|
*/
|
||||||
|
new Elysia()
|
||||||
|
.get('/tako', file('./example/takodachi.png'))
|
||||||
|
.listen(3000)
|
||||||
34
.agents/skills/elysiajs/examples/guard.ts
Normal file
34
.agents/skills/elysiajs/examples/guard.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.state('name', 'salt')
|
||||||
|
.get('/', ({ store: { name } }) => `Hi ${name}`, {
|
||||||
|
query: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// If query 'name' is not preset, skip the whole handler
|
||||||
|
.guard(
|
||||||
|
{
|
||||||
|
query: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
(app) =>
|
||||||
|
app
|
||||||
|
// Query type is inherited from guard
|
||||||
|
.get('/profile', ({ query }) => `Hi`)
|
||||||
|
// Store is inherited
|
||||||
|
.post('/name', ({ store: { name }, body, query }) => name, {
|
||||||
|
body: t.Object({
|
||||||
|
id: t.Number({
|
||||||
|
minimum: 5
|
||||||
|
}),
|
||||||
|
username: t.String(),
|
||||||
|
profile: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
15
.agents/skills/elysiajs/examples/map-response.ts
Normal file
15
.agents/skills/elysiajs/examples/map-response.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
const prettyJson = new Elysia()
|
||||||
|
.mapResponse(({ response }) => {
|
||||||
|
if (response instanceof Object)
|
||||||
|
return new Response(JSON.stringify(response, null, 4))
|
||||||
|
})
|
||||||
|
.as('scoped')
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(prettyJson)
|
||||||
|
.get('/', () => ({
|
||||||
|
hello: 'world'
|
||||||
|
}))
|
||||||
|
.listen(3000)
|
||||||
6
.agents/skills/elysiajs/examples/redirect.ts
Normal file
6
.agents/skills/elysiajs/examples/redirect.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', () => 'Hi')
|
||||||
|
.get('/redirect', ({ redirect }) => redirect('/'))
|
||||||
|
.listen(3000)
|
||||||
32
.agents/skills/elysiajs/examples/rename.ts
Normal file
32
.agents/skills/elysiajs/examples/rename.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
// ? Elysia#83 | Proposal: Standardized way of renaming third party plugin-scoped stuff
|
||||||
|
// this would be a plugin provided by a third party
|
||||||
|
const myPlugin = new Elysia()
|
||||||
|
.decorate('myProperty', 42)
|
||||||
|
.model('salt', t.String())
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(
|
||||||
|
myPlugin
|
||||||
|
// map decorator, rename "myProperty" to "renamedProperty"
|
||||||
|
.decorate(({ myProperty, ...decorators }) => ({
|
||||||
|
renamedProperty: myProperty,
|
||||||
|
...decorators
|
||||||
|
}))
|
||||||
|
// map model, rename "salt" to "pepper"
|
||||||
|
.model(({ salt, ...models }) => ({
|
||||||
|
...models,
|
||||||
|
pepper: t.String()
|
||||||
|
}))
|
||||||
|
// Add prefix
|
||||||
|
.prefix('decorator', 'unstable')
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
'/mapped',
|
||||||
|
({ unstableRenamedProperty }) => unstableRenamedProperty
|
||||||
|
)
|
||||||
|
.post('/pepper', ({ body }) => body, {
|
||||||
|
body: 'pepper',
|
||||||
|
// response: t.String()
|
||||||
|
})
|
||||||
61
.agents/skills/elysiajs/examples/schema.ts
Normal file
61
.agents/skills/elysiajs/examples/schema.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.model({
|
||||||
|
name: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
}),
|
||||||
|
b: t.Object({
|
||||||
|
response: t.Number()
|
||||||
|
}),
|
||||||
|
authorization: t.Object({
|
||||||
|
authorization: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// Strictly validate response
|
||||||
|
.get('/', () => 'hi')
|
||||||
|
// Strictly validate body and response
|
||||||
|
.post('/', ({ body, query }) => body.id, {
|
||||||
|
body: t.Object({
|
||||||
|
id: t.Number(),
|
||||||
|
username: t.String(),
|
||||||
|
profile: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// Strictly validate query, params, and body
|
||||||
|
.get('/query/:id', ({ query: { name }, params }) => name, {
|
||||||
|
query: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
}),
|
||||||
|
params: t.Object({
|
||||||
|
id: t.String()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: t.String(),
|
||||||
|
300: t.Object({
|
||||||
|
error: t.String()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.guard(
|
||||||
|
{
|
||||||
|
headers: 'authorization'
|
||||||
|
},
|
||||||
|
(app) =>
|
||||||
|
app
|
||||||
|
.derive(({ headers }) => ({
|
||||||
|
userId: headers.authorization
|
||||||
|
}))
|
||||||
|
.get('/', ({ userId }) => 'A')
|
||||||
|
.post('/id/:id', ({ query, body, params, userId }) => body, {
|
||||||
|
params: t.Object({
|
||||||
|
id: t.Number()
|
||||||
|
}),
|
||||||
|
transform({ params }) {
|
||||||
|
params.id = +params.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
6
.agents/skills/elysiajs/examples/state.ts
Normal file
6
.agents/skills/elysiajs/examples/state.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.state('counter', 0)
|
||||||
|
.get('/', ({ store }) => store.counter++)
|
||||||
|
.listen(3000)
|
||||||
20
.agents/skills/elysiajs/examples/upload-file.ts
Normal file
20
.agents/skills/elysiajs/examples/upload-file.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.post('/single', ({ body: { file } }) => file, {
|
||||||
|
body: t.Object({
|
||||||
|
file: t.File({
|
||||||
|
maxSize: '1m'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.post(
|
||||||
|
'/multiple',
|
||||||
|
({ body: { files } }) => files.reduce((a, b) => a + b.size, 0),
|
||||||
|
{
|
||||||
|
body: t.Object({
|
||||||
|
files: t.Files()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
25
.agents/skills/elysiajs/examples/websocket.ts
Normal file
25
.agents/skills/elysiajs/examples/websocket.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.state('start', 'here')
|
||||||
|
.ws('/ws', {
|
||||||
|
open(ws) {
|
||||||
|
ws.subscribe('asdf')
|
||||||
|
console.log('Open Connection:', ws.id)
|
||||||
|
},
|
||||||
|
close(ws) {
|
||||||
|
console.log('Closed Connection:', ws.id)
|
||||||
|
},
|
||||||
|
message(ws, message) {
|
||||||
|
ws.publish('asdf', message)
|
||||||
|
ws.send(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get('/publish/:publish', ({ params: { publish: text } }) => {
|
||||||
|
app.server!.publish('asdf', text)
|
||||||
|
|
||||||
|
return text
|
||||||
|
})
|
||||||
|
.listen(3000, (server) => {
|
||||||
|
console.log(`http://${server.hostname}:${server.port}`)
|
||||||
|
})
|
||||||
92
.agents/skills/elysiajs/integrations/ai-sdk.md
Normal file
92
.agents/skills/elysiajs/integrations/ai-sdk.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# AI SDK Integration
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Seamless integration with Vercel AI SDK via response streaming.
|
||||||
|
|
||||||
|
## Response Streaming
|
||||||
|
Return `ReadableStream` or `Response` directly:
|
||||||
|
```typescript
|
||||||
|
import { streamText } from 'ai'
|
||||||
|
import { openai } from '@ai-sdk/openai'
|
||||||
|
|
||||||
|
new Elysia().get('/', () => {
|
||||||
|
const stream = streamText({
|
||||||
|
model: openai('gpt-5'),
|
||||||
|
system: 'You are Yae Miko from Genshin Impact',
|
||||||
|
prompt: 'Hi! How are you doing?'
|
||||||
|
})
|
||||||
|
|
||||||
|
return stream.textStream // ReadableStream
|
||||||
|
// or
|
||||||
|
return stream.toUIMessageStream() // UI Message Stream
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Elysia auto-handles stream.
|
||||||
|
|
||||||
|
## Server-Sent Events
|
||||||
|
Wrap `ReadableStream` with `sse`:
|
||||||
|
```typescript
|
||||||
|
import { sse } from 'elysia'
|
||||||
|
|
||||||
|
.get('/', () => {
|
||||||
|
const stream = streamText({ /* ... */ })
|
||||||
|
|
||||||
|
return sse(stream.textStream)
|
||||||
|
// or
|
||||||
|
return sse(stream.toUIMessageStream())
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Each chunk → SSE.
|
||||||
|
|
||||||
|
## As Response
|
||||||
|
Return stream directly (no Eden type safety):
|
||||||
|
```typescript
|
||||||
|
.get('/', () => {
|
||||||
|
const stream = streamText({ /* ... */ })
|
||||||
|
|
||||||
|
return stream.toTextStreamResponse()
|
||||||
|
// or
|
||||||
|
return stream.toUIMessageStreamResponse() // Uses SSE
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Streaming
|
||||||
|
Generator function for control:
|
||||||
|
```typescript
|
||||||
|
import { sse } from 'elysia'
|
||||||
|
|
||||||
|
.get('/', async function* () {
|
||||||
|
const stream = streamText({ /* ... */ })
|
||||||
|
|
||||||
|
for await (const data of stream.textStream)
|
||||||
|
yield sse({ data, event: 'message' })
|
||||||
|
|
||||||
|
yield sse({ event: 'done' })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fetch for Unsupported Models
|
||||||
|
Direct fetch with streaming proxy:
|
||||||
|
```typescript
|
||||||
|
.get('/', () => {
|
||||||
|
return fetch('https://api.openai.com/v1/chat/completions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'gpt-5',
|
||||||
|
stream: true,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: 'You are Yae Miko' },
|
||||||
|
{ role: 'user', content: 'Hi! How are you doing?' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Elysia auto-proxies fetch response with streaming.
|
||||||
59
.agents/skills/elysiajs/integrations/astro.md
Normal file
59
.agents/skills/elysiajs/integrations/astro.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Astro Integration - SKILLS.md
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Run Elysia on Astro via Astro Endpoint.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Set output to server:
|
||||||
|
```javascript
|
||||||
|
// astro.config.mjs
|
||||||
|
export default defineConfig({
|
||||||
|
output: 'server'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create `pages/[...slugs].ts`
|
||||||
|
3. Define Elysia server + export handlers:
|
||||||
|
```typescript
|
||||||
|
// pages/[...slugs].ts
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/api', () => 'hi')
|
||||||
|
.post('/api', ({ body }) => body, {
|
||||||
|
body: t.Object({ name: t.String() })
|
||||||
|
})
|
||||||
|
|
||||||
|
const handle = ({ request }: { request: Request }) => app.handle(request)
|
||||||
|
|
||||||
|
export const GET = handle
|
||||||
|
export const POST = handle
|
||||||
|
```
|
||||||
|
|
||||||
|
WinterCG compliance - works normally.
|
||||||
|
|
||||||
|
Recommended: Run Astro on Bun (Elysia designed for Bun).
|
||||||
|
|
||||||
|
## Prefix for Non-Root
|
||||||
|
If placed in `pages/api/[...slugs].ts`, set prefix:
|
||||||
|
```typescript
|
||||||
|
// pages/api/[...slugs].ts
|
||||||
|
const app = new Elysia({ prefix: '/api' })
|
||||||
|
.get('/', () => 'hi')
|
||||||
|
|
||||||
|
const handle = ({ request }: { request: Request }) => app.handle(request)
|
||||||
|
|
||||||
|
export const GET = handle
|
||||||
|
export const POST = handle
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensures routing works in any location.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
Co-location of frontend + backend. End-to-end type safety with Eden.
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
117
.agents/skills/elysiajs/integrations/better-auth.md
Normal file
117
.agents/skills/elysiajs/integrations/better-auth.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Better Auth Integration
|
||||||
|
Elysia + Better Auth integration guide
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Framework-agnostic TypeScript auth/authz. Comprehensive features + plugin ecosystem.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
```typescript
|
||||||
|
import { betterAuth } from 'better-auth'
|
||||||
|
import { Pool } from 'pg'
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
database: new Pool()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handler Mounting
|
||||||
|
```typescript
|
||||||
|
import { auth } from './auth'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.mount(auth.handler) // http://localhost:3000/api/auth
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Endpoint
|
||||||
|
```typescript
|
||||||
|
// Mount with prefix
|
||||||
|
.mount('/auth', auth.handler) // http://localhost:3000/auth/api/auth
|
||||||
|
|
||||||
|
// Customize basePath
|
||||||
|
export const auth = betterAuth({
|
||||||
|
basePath: '/api' // http://localhost:3000/auth/api
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Cannot set `basePath` to empty or `/`.
|
||||||
|
|
||||||
|
## OpenAPI Integration
|
||||||
|
Extract docs from Better Auth:
|
||||||
|
```typescript
|
||||||
|
import { openAPI } from 'better-auth/plugins'
|
||||||
|
|
||||||
|
let _schema: ReturnType<typeof auth.api.generateOpenAPISchema>
|
||||||
|
const getSchema = async () => (_schema ??= auth.api.generateOpenAPISchema())
|
||||||
|
|
||||||
|
export const OpenAPI = {
|
||||||
|
getPaths: (prefix = '/auth/api') =>
|
||||||
|
getSchema().then(({ paths }) => {
|
||||||
|
const reference: typeof paths = Object.create(null)
|
||||||
|
|
||||||
|
for (const path of Object.keys(paths)) {
|
||||||
|
const key = prefix + path
|
||||||
|
reference[key] = paths[path]
|
||||||
|
|
||||||
|
for (const method of Object.keys(paths[path])) {
|
||||||
|
const operation = (reference[key] as any)[method]
|
||||||
|
operation.tags = ['Better Auth']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reference
|
||||||
|
}) as Promise<any>,
|
||||||
|
components: getSchema().then(({ components }) => components) as Promise<any>
|
||||||
|
} as const
|
||||||
|
```
|
||||||
|
|
||||||
|
Apply to Elysia:
|
||||||
|
```typescript
|
||||||
|
new Elysia().use(openapi({
|
||||||
|
documentation: {
|
||||||
|
components: await OpenAPI.components,
|
||||||
|
paths: await OpenAPI.getPaths()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
## CORS
|
||||||
|
```typescript
|
||||||
|
import { cors } from '@elysiajs/cors'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(cors({
|
||||||
|
origin: 'http://localhost:3001',
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
credentials: true,
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization']
|
||||||
|
}))
|
||||||
|
.mount(auth.handler)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Macro for Auth
|
||||||
|
Use macro + resolve for session/user:
|
||||||
|
```typescript
|
||||||
|
const betterAuth = new Elysia({ name: 'better-auth' })
|
||||||
|
.mount(auth.handler)
|
||||||
|
.macro({
|
||||||
|
auth: {
|
||||||
|
async resolve({ status, request: { headers } }) {
|
||||||
|
const session = await auth.api.getSession({ headers })
|
||||||
|
|
||||||
|
if (!session) return status(401)
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: session.user,
|
||||||
|
session: session.session
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(betterAuth)
|
||||||
|
.get('/user', ({ user }) => user, { auth: true })
|
||||||
|
```
|
||||||
|
|
||||||
|
Access `user` and `session` in all routes.
|
||||||
95
.agents/skills/elysiajs/integrations/cloudflare-worker.md
Normal file
95
.agents/skills/elysiajs/integrations/cloudflare-worker.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
|
||||||
|
# Cloudflare Worker Integration
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
**Experimental** Cloudflare Worker adapter for Elysia.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Install Wrangler:
|
||||||
|
```bash
|
||||||
|
wrangler init elysia-on-cloudflare
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Apply adapter + compile:
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { CloudflareAdapter } from 'elysia/adapter/cloudflare-worker'
|
||||||
|
|
||||||
|
export default new Elysia({
|
||||||
|
adapter: CloudflareAdapter
|
||||||
|
})
|
||||||
|
.get('/', () => 'Hello Cloudflare Worker!')
|
||||||
|
.compile() // Required
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Set compatibility date (min `2025-06-01`):
|
||||||
|
```json
|
||||||
|
// wrangler.json
|
||||||
|
{
|
||||||
|
"name": "elysia-on-cloudflare",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"compatibility_date": "2025-06-01"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Dev server:
|
||||||
|
```bash
|
||||||
|
wrangler dev
|
||||||
|
# http://localhost:8787
|
||||||
|
```
|
||||||
|
|
||||||
|
No `nodejs_compat` flag needed.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
1. `Elysia.file` + Static Plugin don't work (no `fs` module)
|
||||||
|
2. OpenAPI Type Gen doesn't work (no `fs` module)
|
||||||
|
3. Cannot define Response before server start
|
||||||
|
4. Cannot inline values:
|
||||||
|
```typescript
|
||||||
|
// ❌ Throws error
|
||||||
|
.get('/', 'Hello Elysia')
|
||||||
|
|
||||||
|
// ✅ Works
|
||||||
|
.get('/', () => 'Hello Elysia')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Static Files
|
||||||
|
Use Cloudflare's built-in static serving:
|
||||||
|
```json
|
||||||
|
// wrangler.json
|
||||||
|
{
|
||||||
|
"assets": { "directory": "public" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Structure:
|
||||||
|
```
|
||||||
|
├─ public
|
||||||
|
│ ├─ kyuukurarin.mp4
|
||||||
|
│ └─ static/mika.webp
|
||||||
|
```
|
||||||
|
|
||||||
|
Access:
|
||||||
|
- `http://localhost:8787/kyuukurarin.mp4`
|
||||||
|
- `http://localhost:8787/static/mika.webp`
|
||||||
|
|
||||||
|
## Binding
|
||||||
|
Import env from `cloudflare:workers`:
|
||||||
|
```typescript
|
||||||
|
import { env } from 'cloudflare:workers'
|
||||||
|
|
||||||
|
export default new Elysia({ adapter: CloudflareAdapter })
|
||||||
|
.get('/', () => `Hello ${await env.KV.get('my-key')}`)
|
||||||
|
.compile()
|
||||||
|
```
|
||||||
|
|
||||||
|
## AoT Compilation
|
||||||
|
As of Elysia 1.4.7, AoT works with Cloudflare Worker. Drop `aot: false` flag.
|
||||||
|
|
||||||
|
Cloudflare now supports Function compilation during startup.
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
34
.agents/skills/elysiajs/integrations/deno.md
Normal file
34
.agents/skills/elysiajs/integrations/deno.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Deno Integration
|
||||||
|
Run Elysia on Deno
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Run Elysia on Deno via Web Standard Request/Response.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
Wrap `Elysia.fetch` in `Deno.serve`:
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/', () => 'Hello Elysia')
|
||||||
|
.listen(3000)
|
||||||
|
|
||||||
|
Deno.serve(app.fetch)
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
deno serve --watch src/index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Port Config
|
||||||
|
```typescript
|
||||||
|
Deno.serve(app.fetch) // Default
|
||||||
|
Deno.serve({ port: 8787 }, app.fetch) // Custom port
|
||||||
|
```
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
[Inference] pnpm doesn't auto-install peer deps. Manual install required:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
258
.agents/skills/elysiajs/integrations/drizzle.md
Normal file
258
.agents/skills/elysiajs/integrations/drizzle.md
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
# Drizzle Integration
|
||||||
|
Elysia + Drizzle integration guide
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Headless TypeScript ORM. Convert Drizzle schema → Elysia validation models via `drizzle-typebox`.
|
||||||
|
|
||||||
|
## Flow
|
||||||
|
```
|
||||||
|
Drizzle → drizzle-typebox → Elysia validation → OpenAPI + Eden Treaty
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add drizzle-orm drizzle-typebox
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pin TypeBox Version
|
||||||
|
Prevent Symbol conflicts:
|
||||||
|
```bash
|
||||||
|
grep "@sinclair/typebox" node_modules/elysia/package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to `package.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"overrides": {
|
||||||
|
"@sinclair/typebox": "0.32.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Drizzle Schema
|
||||||
|
```typescript
|
||||||
|
// src/database/schema.ts
|
||||||
|
import { pgTable, varchar, timestamp } from 'drizzle-orm/pg-core'
|
||||||
|
import { createId } from '@paralleldrive/cuid2'
|
||||||
|
|
||||||
|
export const user = pgTable('user', {
|
||||||
|
id: varchar('id').$defaultFn(() => createId()).primaryKey(),
|
||||||
|
username: varchar('username').notNull().unique(),
|
||||||
|
password: varchar('password').notNull(),
|
||||||
|
email: varchar('email').notNull().unique(),
|
||||||
|
salt: varchar('salt', { length: 64 }).notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const table = { user } as const
|
||||||
|
export type Table = typeof table
|
||||||
|
```
|
||||||
|
|
||||||
|
## drizzle-typebox
|
||||||
|
```typescript
|
||||||
|
import { t } from 'elysia'
|
||||||
|
import { createInsertSchema } from 'drizzle-typebox'
|
||||||
|
import { table } from './database/schema'
|
||||||
|
|
||||||
|
const _createUser = createInsertSchema(table.user, {
|
||||||
|
email: t.String({ format: 'email' }) // Replace with Elysia type
|
||||||
|
})
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.post('/sign-up', ({ body }) => {}, {
|
||||||
|
body: t.Omit(_createUser, ['id', 'salt', 'createdAt'])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Instantiation Error
|
||||||
|
**Error**: "Type instantiation is possibly infinite"
|
||||||
|
|
||||||
|
**Cause**: Circular reference when nesting drizzle-typebox into Elysia schema.
|
||||||
|
|
||||||
|
**Fix**: Explicitly define type between them:
|
||||||
|
```typescript
|
||||||
|
// ✅ Works
|
||||||
|
const _createUser = createInsertSchema(table.user, {
|
||||||
|
email: t.String({ format: 'email' })
|
||||||
|
})
|
||||||
|
const createUser = t.Omit(_createUser, ['id', 'salt', 'createdAt'])
|
||||||
|
|
||||||
|
// ❌ Infinite loop
|
||||||
|
const createUser = t.Omit(
|
||||||
|
createInsertSchema(table.user, { email: t.String({ format: 'email' }) }),
|
||||||
|
['id', 'salt', 'createdAt']
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Always declare variable for drizzle-typebox then reference it.
|
||||||
|
|
||||||
|
## Utility Functions
|
||||||
|
Copy as-is for simplified usage:
|
||||||
|
```typescript
|
||||||
|
// src/database/utils.ts
|
||||||
|
/**
|
||||||
|
* @lastModified 2025-02-04
|
||||||
|
* @see https://elysiajs.com/recipe/drizzle.html#utility
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Kind, type TObject } from '@sinclair/typebox'
|
||||||
|
import {
|
||||||
|
createInsertSchema,
|
||||||
|
createSelectSchema,
|
||||||
|
BuildSchema,
|
||||||
|
} from 'drizzle-typebox'
|
||||||
|
|
||||||
|
import { table } from './schema'
|
||||||
|
import type { Table } from 'drizzle-orm'
|
||||||
|
|
||||||
|
type Spread<
|
||||||
|
T extends TObject | Table,
|
||||||
|
Mode extends 'select' | 'insert' | undefined,
|
||||||
|
> =
|
||||||
|
T extends TObject<infer Fields>
|
||||||
|
? {
|
||||||
|
[K in keyof Fields]: Fields[K]
|
||||||
|
}
|
||||||
|
: T extends Table
|
||||||
|
? Mode extends 'select'
|
||||||
|
? BuildSchema<
|
||||||
|
'select',
|
||||||
|
T['_']['columns'],
|
||||||
|
undefined
|
||||||
|
>['properties']
|
||||||
|
: Mode extends 'insert'
|
||||||
|
? BuildSchema<
|
||||||
|
'insert',
|
||||||
|
T['_']['columns'],
|
||||||
|
undefined
|
||||||
|
>['properties']
|
||||||
|
: {}
|
||||||
|
: {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spread a Drizzle schema into a plain object
|
||||||
|
*/
|
||||||
|
export const spread = <
|
||||||
|
T extends TObject | Table,
|
||||||
|
Mode extends 'select' | 'insert' | undefined,
|
||||||
|
>(
|
||||||
|
schema: T,
|
||||||
|
mode?: Mode,
|
||||||
|
): Spread<T, Mode> => {
|
||||||
|
const newSchema: Record<string, unknown> = {}
|
||||||
|
let table
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case 'insert':
|
||||||
|
case 'select':
|
||||||
|
if (Kind in schema) {
|
||||||
|
table = schema
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
table =
|
||||||
|
mode === 'insert'
|
||||||
|
? createInsertSchema(schema)
|
||||||
|
: createSelectSchema(schema)
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (!(Kind in schema)) throw new Error('Expect a schema')
|
||||||
|
table = schema
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(table.properties))
|
||||||
|
newSchema[key] = table.properties[key]
|
||||||
|
|
||||||
|
return newSchema as any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spread a Drizzle Table into a plain object
|
||||||
|
*
|
||||||
|
* If `mode` is 'insert', the schema will be refined for insert
|
||||||
|
* If `mode` is 'select', the schema will be refined for select
|
||||||
|
* If `mode` is undefined, the schema will be spread as is, models will need to be refined manually
|
||||||
|
*/
|
||||||
|
export const spreads = <
|
||||||
|
T extends Record<string, TObject | Table>,
|
||||||
|
Mode extends 'select' | 'insert' | undefined,
|
||||||
|
>(
|
||||||
|
models: T,
|
||||||
|
mode?: Mode,
|
||||||
|
): {
|
||||||
|
[K in keyof T]: Spread<T[K], Mode>
|
||||||
|
} => {
|
||||||
|
const newSchema: Record<string, unknown> = {}
|
||||||
|
const keys = Object.keys(models)
|
||||||
|
|
||||||
|
for (const key of keys) newSchema[key] = spread(models[key], mode)
|
||||||
|
|
||||||
|
return newSchema as any
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```typescript
|
||||||
|
// ✅ Using spread
|
||||||
|
const user = spread(table.user, 'insert')
|
||||||
|
const createUser = t.Object({
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
password: user.password
|
||||||
|
})
|
||||||
|
|
||||||
|
// ⚠️ Using t.Pick
|
||||||
|
const _createUser = createInsertSchema(table.user)
|
||||||
|
const createUser = t.Pick(_createUser, ['id', 'username', 'password'])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Table Singleton Pattern
|
||||||
|
```typescript
|
||||||
|
// src/database/model.ts
|
||||||
|
import { table } from './schema'
|
||||||
|
import { spreads } from './utils'
|
||||||
|
|
||||||
|
export const db = {
|
||||||
|
insert: spreads({ user: table.user }, 'insert'),
|
||||||
|
select: spreads({ user: table.user }, 'select')
|
||||||
|
} as const
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```typescript
|
||||||
|
// src/index.ts
|
||||||
|
import { db } from './database/model'
|
||||||
|
const { user } = db.insert
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.post('/sign-up', ({ body }) => {}, {
|
||||||
|
body: t.Object({
|
||||||
|
id: user.username,
|
||||||
|
username: user.username,
|
||||||
|
password: user.password
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Refinement
|
||||||
|
```typescript
|
||||||
|
// src/database/model.ts
|
||||||
|
import { createInsertSchema, createSelectSchema } from 'drizzle-typebox'
|
||||||
|
|
||||||
|
export const db = {
|
||||||
|
insert: spreads({
|
||||||
|
user: createInsertSchema(table.user, {
|
||||||
|
email: t.String({ format: 'email' })
|
||||||
|
})
|
||||||
|
}, 'insert'),
|
||||||
|
select: spreads({
|
||||||
|
user: createSelectSchema(table.user, {
|
||||||
|
email: t.String({ format: 'email' })
|
||||||
|
})
|
||||||
|
}, 'select')
|
||||||
|
} as const
|
||||||
|
```
|
||||||
|
|
||||||
|
`spread` skips refined schemas.
|
||||||
95
.agents/skills/elysiajs/integrations/expo.md
Normal file
95
.agents/skills/elysiajs/integrations/expo.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# Expo Integration
|
||||||
|
Run Elysia on Expo (React Native)
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Create API routes in Expo app (SDK 50+, App Router v3).
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Create `app/[...slugs]+api.ts`
|
||||||
|
2. Define Elysia server
|
||||||
|
3. Export `Elysia.fetch` as HTTP methods
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app/[...slugs]+api.ts
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/', 'hello Expo')
|
||||||
|
.post('/', ({ body }) => body, {
|
||||||
|
body: t.Object({ name: t.String() })
|
||||||
|
})
|
||||||
|
|
||||||
|
export const GET = app.fetch
|
||||||
|
export const POST = app.fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prefix for Non-Root
|
||||||
|
If placed in `app/api/[...slugs]+api.ts`, set prefix:
|
||||||
|
```typescript
|
||||||
|
const app = new Elysia({ prefix: '/api' })
|
||||||
|
.get('/', 'Hello Expo')
|
||||||
|
|
||||||
|
export const GET = app.fetch
|
||||||
|
export const POST = app.fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensures routing works in any location.
|
||||||
|
|
||||||
|
## Eden (End-to-End Type Safety)
|
||||||
|
1. Export type:
|
||||||
|
```typescript
|
||||||
|
// app/[...slugs]+api.ts
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/', 'Hello Nextjs')
|
||||||
|
.post('/user', ({ body }) => body, {
|
||||||
|
body: treaty.schema('User', { name: 'string' })
|
||||||
|
})
|
||||||
|
|
||||||
|
export type app = typeof app
|
||||||
|
|
||||||
|
export const GET = app.fetch
|
||||||
|
export const POST = app.fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create client:
|
||||||
|
```typescript
|
||||||
|
// lib/eden.ts
|
||||||
|
import { treaty } from '@elysiajs/eden'
|
||||||
|
import type { app } from '../app/[...slugs]+api'
|
||||||
|
|
||||||
|
export const api = treaty<app>('localhost:3000/api')
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Use in components:
|
||||||
|
```tsx
|
||||||
|
// app/page.tsx
|
||||||
|
import { api } from '../lib/eden'
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const message = await api.get()
|
||||||
|
return <h1>Hello, {message}</h1>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
- Deploy as normal Elysia app OR
|
||||||
|
- Use experimental Expo server runtime
|
||||||
|
|
||||||
|
With Expo runtime:
|
||||||
|
```bash
|
||||||
|
expo export
|
||||||
|
# Creates dist/server/_expo/functions/[...slugs]+api.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Edge function, not normal server (no port allocation).
|
||||||
|
|
||||||
|
### Adapters
|
||||||
|
- Express
|
||||||
|
- Netlify
|
||||||
|
- Vercel
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
103
.agents/skills/elysiajs/integrations/nextjs.md
Normal file
103
.agents/skills/elysiajs/integrations/nextjs.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
|
||||||
|
# Next.js Integration
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Run Elysia on Next.js App Router.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Create `app/api/[[...slugs]]/route.ts`
|
||||||
|
2. Define Elysia + export handlers:
|
||||||
|
```typescript
|
||||||
|
// app/api/[[...slugs]]/route.ts
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia({ prefix: '/api' })
|
||||||
|
.get('/', 'Hello Nextjs')
|
||||||
|
.post('/', ({ body }) => body, {
|
||||||
|
body: t.Object({ name: t.String() })
|
||||||
|
})
|
||||||
|
|
||||||
|
export const GET = app.fetch
|
||||||
|
export const POST = app.fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
WinterCG compliance - works as normal Next.js API route.
|
||||||
|
|
||||||
|
## Prefix for Non-Root
|
||||||
|
If placed in `app/user/[[...slugs]]/route.ts`, set prefix:
|
||||||
|
```typescript
|
||||||
|
const app = new Elysia({ prefix: '/user' })
|
||||||
|
.get('/', 'Hello Nextjs')
|
||||||
|
|
||||||
|
export const GET = app.fetch
|
||||||
|
export const POST = app.fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Eden (End-to-End Type Safety)
|
||||||
|
Isomorphic fetch pattern:
|
||||||
|
- Server: Direct calls (no network)
|
||||||
|
- Client: Network calls
|
||||||
|
|
||||||
|
1. Export type:
|
||||||
|
```typescript
|
||||||
|
// app/api/[[...slugs]]/route.ts
|
||||||
|
export const app = new Elysia({ prefix: '/api' })
|
||||||
|
.get('/', 'Hello Nextjs')
|
||||||
|
.post('/user', ({ body }) => body, {
|
||||||
|
body: treaty.schema('User', { name: 'string' })
|
||||||
|
})
|
||||||
|
|
||||||
|
export type app = typeof app
|
||||||
|
|
||||||
|
export const GET = app.fetch
|
||||||
|
export const POST = app.fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create client:
|
||||||
|
```typescript
|
||||||
|
// lib/eden.ts
|
||||||
|
import { treaty } from '@elysiajs/eden'
|
||||||
|
import type { app } from '../app/api/[[...slugs]]/route'
|
||||||
|
|
||||||
|
export const api =
|
||||||
|
typeof process !== 'undefined'
|
||||||
|
? treaty(app).api
|
||||||
|
: treaty<typeof app>('localhost:3000').api
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `typeof process` not `typeof window` (window undefined at build time → hydration error).
|
||||||
|
|
||||||
|
3. Use in components:
|
||||||
|
```tsx
|
||||||
|
// app/page.tsx
|
||||||
|
import { api } from '../lib/eden'
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const message = await api.get()
|
||||||
|
return <h1>Hello, {message}</h1>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Works with server/client components + ISR.
|
||||||
|
|
||||||
|
## React Query
|
||||||
|
```tsx
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const { data: response } = useQuery({
|
||||||
|
queryKey: ['get'],
|
||||||
|
queryFn: () => getTreaty().get()
|
||||||
|
})
|
||||||
|
|
||||||
|
return response?.data
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Works with all React Query features.
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
64
.agents/skills/elysiajs/integrations/nodejs.md
Normal file
64
.agents/skills/elysiajs/integrations/nodejs.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Node.js Integration
|
||||||
|
Run Elysia on Node.js
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Runtime adapter to run Elysia on Node.js.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add elysia @elysiajs/node
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
Apply node adapter:
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { node } from '@elysiajs/node'
|
||||||
|
|
||||||
|
const app = new Elysia({ adapter: node() })
|
||||||
|
.get('/', () => 'Hello Elysia')
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Setup (Recommended)
|
||||||
|
Install `tsx` for hot-reload:
|
||||||
|
```bash
|
||||||
|
bun add -d tsx @types/node typescript
|
||||||
|
```
|
||||||
|
|
||||||
|
Scripts in `package.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"build": "tsc src/index.ts --outDir dist",
|
||||||
|
"start": "NODE_ENV=production node dist/index.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **dev**: Hot-reload dev mode
|
||||||
|
- **build**: Production build
|
||||||
|
- **start**: Production server
|
||||||
|
|
||||||
|
Create `tsconfig.json`:
|
||||||
|
```bash
|
||||||
|
tsc --init
|
||||||
|
```
|
||||||
|
|
||||||
|
Update strict mode:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Provides hot-reload + JSX support similar to `bun dev`.
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
67
.agents/skills/elysiajs/integrations/nuxt.md
Normal file
67
.agents/skills/elysiajs/integrations/nuxt.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Nuxt Integration
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Community plugin `nuxt-elysia` for Nuxt API routes with Eden Treaty.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add elysia @elysiajs/eden
|
||||||
|
bun add -d nuxt-elysia
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Add to Nuxt config:
|
||||||
|
```typescript
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
modules: ['nuxt-elysia']
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create `api.ts` at project root:
|
||||||
|
```typescript
|
||||||
|
// api.ts
|
||||||
|
export default () => new Elysia()
|
||||||
|
.get('/hello', () => ({ message: 'Hello world!' }))
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Use Eden Treaty:
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<p>{{ data.message }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { $api } = useNuxtApp()
|
||||||
|
|
||||||
|
const { data } = await useAsyncData(async () => {
|
||||||
|
const { data, error } = await $api.hello.get()
|
||||||
|
|
||||||
|
if (error) throw new Error('Failed to call API')
|
||||||
|
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-setup on Nuxt API route.
|
||||||
|
|
||||||
|
## Prefix
|
||||||
|
Default: `/_api`. Customize:
|
||||||
|
```typescript
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
nuxtElysia: {
|
||||||
|
path: '/api'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Mounts on `/api` instead of `/_api`.
|
||||||
|
|
||||||
|
See [nuxt-elysia](https://github.com/tkesgar/nuxt-elysia) for more config.
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
93
.agents/skills/elysiajs/integrations/prisma.md
Normal file
93
.agents/skills/elysiajs/integrations/prisma.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
|
||||||
|
# Prisma Integration
|
||||||
|
Elysia + Prisma integration guide
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Type-safe ORM. Generate Elysia validation models from Prisma schema via `prismabox`.
|
||||||
|
|
||||||
|
## Flow
|
||||||
|
```
|
||||||
|
Prisma → prismabox → Elysia validation → OpenAPI + Eden Treaty
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @prisma/client prismabox && \
|
||||||
|
bun add -d prisma
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prisma Schema
|
||||||
|
Add `prismabox` generator:
|
||||||
|
```prisma
|
||||||
|
// prisma/schema.prisma
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client"
|
||||||
|
output = "../generated/prisma"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
generator prismabox {
|
||||||
|
provider = "prismabox"
|
||||||
|
typeboxImportDependencyName = "elysia"
|
||||||
|
typeboxImportVariableName = "t"
|
||||||
|
inputModel = true
|
||||||
|
output = "../generated/prismabox"
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
email String @unique
|
||||||
|
name String?
|
||||||
|
posts Post[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Post {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
title String
|
||||||
|
content String?
|
||||||
|
published Boolean @default(false)
|
||||||
|
author User @relation(fields: [authorId], references: [id])
|
||||||
|
authorId String
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Generates:
|
||||||
|
- `User` → `generated/prismabox/User.ts`
|
||||||
|
- `Post` → `generated/prismabox/Post.ts`
|
||||||
|
|
||||||
|
## Using Generated Models
|
||||||
|
```typescript
|
||||||
|
// src/index.ts
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
import { PrismaClient } from '../generated/prisma'
|
||||||
|
import { UserPlain, UserPlainInputCreate } from '../generated/prismabox/User'
|
||||||
|
|
||||||
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.put('/', async ({ body }) =>
|
||||||
|
prisma.user.create({ data: body }), {
|
||||||
|
body: UserPlainInputCreate,
|
||||||
|
response: UserPlain
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.get('/id/:id', async ({ params: { id }, status }) => {
|
||||||
|
const user = await prisma.user.findUnique({ where: { id } })
|
||||||
|
|
||||||
|
if (!user) return status(404, 'User not found')
|
||||||
|
|
||||||
|
return user
|
||||||
|
}, {
|
||||||
|
response: {
|
||||||
|
200: UserPlain,
|
||||||
|
404: t.String()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
Reuses DB schema in Elysia validation models.
|
||||||
134
.agents/skills/elysiajs/integrations/react-email.md
Normal file
134
.agents/skills/elysiajs/integrations/react-email.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# React Email Integration
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Use React components to create emails. Direct JSX import via Bun.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add -d react-email
|
||||||
|
bun add @react-email/components react react-dom
|
||||||
|
```
|
||||||
|
|
||||||
|
Script in `package.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"email": "email dev --dir src/emails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Email templates → `src/emails` directory.
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
Add to `tsconfig.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Email Template
|
||||||
|
```tsx
|
||||||
|
// src/emails/otp.tsx
|
||||||
|
import * as React from 'react'
|
||||||
|
import { Tailwind, Section, Text } from '@react-email/components'
|
||||||
|
|
||||||
|
export default function OTPEmail({ otp }: { otp: number }) {
|
||||||
|
return (
|
||||||
|
<Tailwind>
|
||||||
|
<Section className="flex justify-center items-center w-full min-h-screen font-sans">
|
||||||
|
<Section className="flex flex-col items-center w-76 rounded-2xl px-6 py-1 bg-gray-50">
|
||||||
|
<Text className="text-xs font-medium text-violet-500">
|
||||||
|
Verify your Email Address
|
||||||
|
</Text>
|
||||||
|
<Text className="text-gray-500 my-0">
|
||||||
|
Use the following code to verify your email address
|
||||||
|
</Text>
|
||||||
|
<Text className="text-5xl font-bold pt-2">{otp}</Text>
|
||||||
|
<Text className="text-gray-400 font-light text-xs pb-4">
|
||||||
|
This code is valid for 10 minutes
|
||||||
|
</Text>
|
||||||
|
<Text className="text-gray-600 text-xs">
|
||||||
|
Thank you for joining us
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</Section>
|
||||||
|
</Tailwind>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OTPEmail.PreviewProps = { otp: 123456 }
|
||||||
|
```
|
||||||
|
|
||||||
|
`@react-email/components` → email-client compatible (Gmail, Outlook). Tailwind support.
|
||||||
|
|
||||||
|
`PreviewProps` → playground only.
|
||||||
|
|
||||||
|
## Preview
|
||||||
|
```bash
|
||||||
|
bun email
|
||||||
|
```
|
||||||
|
|
||||||
|
Opens browser with preview.
|
||||||
|
|
||||||
|
## Send Email
|
||||||
|
Render with `react-dom/server`, submit via provider:
|
||||||
|
|
||||||
|
### Nodemailer
|
||||||
|
```typescript
|
||||||
|
import { renderToStaticMarkup } from 'react-dom/server'
|
||||||
|
import OTPEmail from './emails/otp'
|
||||||
|
import nodemailer from 'nodemailer'
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: 'smtp.gehenna.sh',
|
||||||
|
port: 465,
|
||||||
|
auth: { user: 'makoto', pass: '12345678' }
|
||||||
|
})
|
||||||
|
|
||||||
|
.get('/otp', async ({ body }) => {
|
||||||
|
const otp = ~~(Math.random() * 900_000) + 100_000
|
||||||
|
const html = renderToStaticMarkup(<OTPEmail otp={otp} />)
|
||||||
|
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: '[email protected]',
|
||||||
|
to: body,
|
||||||
|
subject: 'Verify your email address',
|
||||||
|
html
|
||||||
|
})
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
}, {
|
||||||
|
body: t.String({ format: 'email' })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resend
|
||||||
|
```typescript
|
||||||
|
import OTPEmail from './emails/otp'
|
||||||
|
import Resend from 'resend'
|
||||||
|
|
||||||
|
const resend = new Resend('re_123456789')
|
||||||
|
|
||||||
|
.get('/otp', ({ body }) => {
|
||||||
|
const otp = ~~(Math.random() * 900_000) + 100_000
|
||||||
|
|
||||||
|
await resend.emails.send({
|
||||||
|
from: '[email protected]',
|
||||||
|
to: body,
|
||||||
|
subject: 'Verify your email address',
|
||||||
|
html: <OTPEmail otp={otp} /> // Direct JSX
|
||||||
|
})
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Direct JSX import thanks to Bun.
|
||||||
|
|
||||||
|
Other providers: AWS SES, SendGrid.
|
||||||
|
|
||||||
|
See [React Email Integrations](https://react.email/docs/integrations/overview).
|
||||||
53
.agents/skills/elysiajs/integrations/sveltekit.md
Normal file
53
.agents/skills/elysiajs/integrations/sveltekit.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
|
||||||
|
# SvelteKit Integration
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Run Elysia on SvelteKit server routes.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Create `src/routes/[...slugs]/+server.ts`
|
||||||
|
2. Define Elysia server
|
||||||
|
3. Export fallback handler:
|
||||||
|
```typescript
|
||||||
|
// src/routes/[...slugs]/+server.ts
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/', 'hello SvelteKit')
|
||||||
|
.post('/', ({ body }) => body, {
|
||||||
|
body: t.Object({ name: t.String() })
|
||||||
|
})
|
||||||
|
|
||||||
|
interface WithRequest {
|
||||||
|
request: Request
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fallback = ({ request }: WithRequest) => app.handle(request)
|
||||||
|
```
|
||||||
|
|
||||||
|
Treat as normal SvelteKit server route.
|
||||||
|
|
||||||
|
## Prefix for Non-Root
|
||||||
|
If placed in `src/routes/api/[...slugs]/+server.ts`, set prefix:
|
||||||
|
```typescript
|
||||||
|
// src/routes/api/[...slugs]/+server.ts
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia({ prefix: '/api' })
|
||||||
|
.get('/', () => 'hi')
|
||||||
|
.post('/', ({ body }) => body, {
|
||||||
|
body: t.Object({ name: t.String() })
|
||||||
|
})
|
||||||
|
|
||||||
|
type RequestHandler = (v: { request: Request }) => Response | Promise<Response>
|
||||||
|
|
||||||
|
export const fallback: RequestHandler = ({ request }) => app.handle(request)
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensures routing works in any location.
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
87
.agents/skills/elysiajs/integrations/tanstack-start.md
Normal file
87
.agents/skills/elysiajs/integrations/tanstack-start.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Tanstack Start Integration
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Elysia runs inside Tanstack Start server routes.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Create `src/routes/api.$.ts`
|
||||||
|
2. Define Elysia server
|
||||||
|
3. Export handlers in `server.handlers`:
|
||||||
|
```typescript
|
||||||
|
// src/routes/api.$.ts
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import { createIsomorphicFn } from '@tanstack/react-start'
|
||||||
|
|
||||||
|
const app = new Elysia({
|
||||||
|
prefix: '/api'
|
||||||
|
}).get('/', 'Hello Elysia!')
|
||||||
|
|
||||||
|
const handle = ({ request }: { request: Request }) => app.fetch(request)
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/api/$')({
|
||||||
|
server: {
|
||||||
|
handlers: {
|
||||||
|
GET: handle,
|
||||||
|
POST: handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Runs on `/api`. Add methods to `server.handlers` as needed.
|
||||||
|
|
||||||
|
## Eden (End-to-End Type Safety)
|
||||||
|
Isomorphic pattern with `createIsomorphicFn`:
|
||||||
|
```typescript
|
||||||
|
// src/routes/api.$.ts
|
||||||
|
export const getTreaty = createIsomorphicFn()
|
||||||
|
.server(() => treaty(app).api)
|
||||||
|
.client(() => treaty<typeof app>('localhost:3000').api)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Server: Direct call (no HTTP overhead)
|
||||||
|
- Client: HTTP call
|
||||||
|
|
||||||
|
## Loader Data
|
||||||
|
Fetch before render:
|
||||||
|
```tsx
|
||||||
|
// src/routes/index.tsx
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import { getTreaty } from './api.$'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/a')({
|
||||||
|
component: App,
|
||||||
|
loader: () => getTreaty().get().then((res) => res.data)
|
||||||
|
})
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const data = Route.useLoaderData()
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Executed server-side during SSR. No HTTP overhead. Type-safe.
|
||||||
|
|
||||||
|
## React Query
|
||||||
|
```tsx
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { getTreaty } from './api.$'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const { data: response } = useQuery({
|
||||||
|
queryKey: ['get'],
|
||||||
|
queryFn: () => getTreaty().get()
|
||||||
|
})
|
||||||
|
|
||||||
|
return response?.data
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Works with all React Query features.
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
55
.agents/skills/elysiajs/integrations/vercel.md
Normal file
55
.agents/skills/elysiajs/integrations/vercel.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Vercel Integration
|
||||||
|
Deploy Elysia on Vercel
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Zero-config deployment on Vercel (Bun or Node runtime).
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Create/import Elysia server in `src/index.ts`
|
||||||
|
2. Export as default:
|
||||||
|
```typescript
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
export default new Elysia()
|
||||||
|
.get('/', () => 'Hello Vercel Function')
|
||||||
|
.post('/', ({ body }) => body, {
|
||||||
|
body: t.Object({ name: t.String() })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Develop locally:
|
||||||
|
```bash
|
||||||
|
vc dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Deploy:
|
||||||
|
```bash
|
||||||
|
vc deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Node.js Runtime
|
||||||
|
Set in `package.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "elysia-app",
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bun Runtime
|
||||||
|
Set in `vercel.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||||
|
"bunVersion": "1.x"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
Vercel has zero config for Elysia. For additional config, see [Vercel docs](https://vercel.com/docs/frameworks/backend/elysia).
|
||||||
380
.agents/skills/elysiajs/patterns/mvc.md
Normal file
380
.agents/skills/elysiajs/patterns/mvc.md
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
# MVC pattern
|
||||||
|
This file contains a guideline for using Elysia with MVC or Model View Controller patterns
|
||||||
|
|
||||||
|
- Controller:
|
||||||
|
- Prefers Elysia as a controller for HTTP dependant
|
||||||
|
- For non HTTP dependent, prefers service instead unless explicitly asked
|
||||||
|
- Use `onError` to handle local custom errors
|
||||||
|
- Register Model to Elysia instance via `Elysia.models({ ...models })` and prefix model by namespace `Elysia.prefix('model', 'Namespace.')
|
||||||
|
- Prefers Reference Model by name provided by Elysia instead of using an actual `Model.name`
|
||||||
|
- Service:
|
||||||
|
- Prefers class (or abstract class if possible)
|
||||||
|
- Prefers interface/type derive from `Model`
|
||||||
|
- Return `status` (`import { status } from 'elysia'`) for error
|
||||||
|
- Prefers `return Error` instead of `throw Error`
|
||||||
|
- Models:
|
||||||
|
- Always export validation model and type of validation model
|
||||||
|
- Custom Error should be in contains in Model
|
||||||
|
|
||||||
|
## Controller
|
||||||
|
Due to type soundness of Elysia, it's not recommended to use a traditional controller class that is tightly coupled with Elysia's `Context` because:
|
||||||
|
|
||||||
|
1. **Elysia type is complex** and heavily depends on plugin and multiple level of chaining.
|
||||||
|
2. **Hard to type**, Elysia type could change at anytime, especially with decorators, and store
|
||||||
|
3. **Loss of type integrity**, and inconsistency between types and runtime code.
|
||||||
|
|
||||||
|
We recommended one of the following approach to implement a controller in Elysia.
|
||||||
|
1. Use Elysia instance as a controller itself
|
||||||
|
2. Create a controller that is not tied with HTTP request or Elysia.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. Elysia instance as a controller
|
||||||
|
> 1 Elysia instance = 1 controller
|
||||||
|
|
||||||
|
Treat an Elysia instance as a controller, and define your routes directly on the Elysia instance.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Do
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { Service } from './service'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', ({ stuff }) => {
|
||||||
|
Service.doStuff(stuff)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach allows Elysia to infer the `Context` type automatically, ensuring type integrity and consistency between types and runtime code.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Don't
|
||||||
|
import { Elysia, t, type Context } from 'elysia'
|
||||||
|
|
||||||
|
abstract class Controller {
|
||||||
|
static root(context: Context) {
|
||||||
|
return Service.doStuff(context.stuff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', Controller.root)
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach makes it hard to type `Context` properly, and may lead to loss of type integrity.
|
||||||
|
|
||||||
|
### 2. Controller without HTTP request
|
||||||
|
If you want to create a controller class, we recommend creating a class that is not tied to HTTP request or Elysia at all.
|
||||||
|
|
||||||
|
This approach allows you to decouple the controller from Elysia, making it easier to test, reuse, and even swap a framework while still follows the MVC pattern.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
abstract class Controller {
|
||||||
|
static doStuff(stuff: string) {
|
||||||
|
return Service.doStuff(stuff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', ({ stuff }) => Controller.doStuff(stuff))
|
||||||
|
```
|
||||||
|
|
||||||
|
Tying the controller to Elysia Context may lead to:
|
||||||
|
1. Loss of type integrity
|
||||||
|
2. Make it harder to test and reuse
|
||||||
|
3. Lead to vendor lock-in
|
||||||
|
|
||||||
|
We recommended to keep the controller decoupled from Elysia as much as possible.
|
||||||
|
|
||||||
|
### Don't: Pass entire `Context` to a controller
|
||||||
|
**Context is a highly dynamic type** that can be inferred from Elysia instance.
|
||||||
|
|
||||||
|
Do not pass an entire `Context` to a controller, instead use object destructuring to extract what you need and pass it to the controller.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { Context } from 'elysia'
|
||||||
|
|
||||||
|
abstract class Controller {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
// Don't do this
|
||||||
|
static root(context: Context) {
|
||||||
|
return Service.doStuff(context.stuff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach makes it hard to type `Context` properly, and may lead to loss of type integrity.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
If you're using Elysia as a controller, you can test your controller using `handle` to directly call a function (and it's lifecycle)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { Service } from './service'
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'bun:test'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/', ({ stuff }) => {
|
||||||
|
Service.doStuff(stuff)
|
||||||
|
|
||||||
|
return 'ok'
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Controller', () => {
|
||||||
|
it('should work', async () => {
|
||||||
|
const response = await app
|
||||||
|
.handle(new Request('http://localhost/'))
|
||||||
|
.then((x) => x.text())
|
||||||
|
|
||||||
|
expect(response).toBe('ok')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You may find more information about testing in [Unit Test](/patterns/unit-test.html).
|
||||||
|
|
||||||
|
## Service
|
||||||
|
Service is a set of utility/helper functions decoupled as a business logic to use in a module/controller, in our case, an Elysia instance.
|
||||||
|
|
||||||
|
Any technical logic that can be decoupled from controller may live inside a **Service**.
|
||||||
|
|
||||||
|
There are 2 types of service in Elysia:
|
||||||
|
1. Non-request dependent service
|
||||||
|
2. Request dependent service
|
||||||
|
|
||||||
|
### 1. Abstract away Non-request dependent service
|
||||||
|
|
||||||
|
We recommend abstracting a service class/function away from Elysia.
|
||||||
|
|
||||||
|
If the service or function isn't tied to an HTTP request or doesn't access a `Context`, it's recommended to implement it as a static class or function.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
abstract class Service {
|
||||||
|
static fibo(number: number): number {
|
||||||
|
if(number < 2)
|
||||||
|
return number
|
||||||
|
|
||||||
|
return Service.fibo(number - 1) + Service.fibo(number - 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/fibo', ({ body }) => {
|
||||||
|
return Service.fibo(body)
|
||||||
|
}, {
|
||||||
|
body: t.Numeric()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
If your service doesn't need to store a property, you may use `abstract class` and `static` instead to avoid allocating class instance.
|
||||||
|
|
||||||
|
### 2. Request dependent service as Elysia instance
|
||||||
|
|
||||||
|
**If the service is a request-dependent service** or needs to process HTTP requests, we recommend abstracting it as an Elysia instance to ensure type integrity and inference:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
// Do
|
||||||
|
const AuthService = new Elysia({ name: 'Auth.Service' })
|
||||||
|
.macro({
|
||||||
|
isSignIn: {
|
||||||
|
resolve({ cookie, status }) {
|
||||||
|
if (!cookie.session.value) return status(401)
|
||||||
|
|
||||||
|
return {
|
||||||
|
session: cookie.session.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const UserController = new Elysia()
|
||||||
|
.use(AuthService)
|
||||||
|
.get('/profile', ({ Auth: { user } }) => user, {
|
||||||
|
isSignIn: true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Do: Decorate only request dependent property
|
||||||
|
|
||||||
|
It's recommended to `decorate` only request-dependent properties, such as `requestIP`, `requestTime`, or `session`.
|
||||||
|
|
||||||
|
Overusing decorators may tie your code to Elysia, making it harder to test and reuse.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.decorate('requestIP', ({ request }) => request.headers.get('x-forwarded-for') || request.ip)
|
||||||
|
.decorate('requestTime', () => Date.now())
|
||||||
|
.decorate('session', ({ cookie }) => cookie.session.value)
|
||||||
|
.get('/', ({ requestIP, requestTime, session }) => {
|
||||||
|
return { requestIP, requestTime, session }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Don't: Pass entire `Context` to a service
|
||||||
|
**Context is a highly dynamic type** that can be inferred from Elysia instance.
|
||||||
|
|
||||||
|
Do not pass an entire `Context` to a service, instead use object destructuring to extract what you need and pass it to the service.
|
||||||
|
```typescript
|
||||||
|
import type { Context } from 'elysia'
|
||||||
|
|
||||||
|
class AuthService {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
// Don't do this
|
||||||
|
isSignIn({ status, cookie: { session } }: Context) {
|
||||||
|
if (session.value)
|
||||||
|
return status(401)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As Elysia type is complex, and heavily depends on plugin and multiple level of chaining, it can be challenging to manually type as it's highly dynamic.
|
||||||
|
|
||||||
|
## Model
|
||||||
|
Model or [DTO (Data Transfer Object)](https://en.wikipedia.org/wiki/Data_transfer_object) is handle by [Elysia.t (Validation)](/essential/validation.html#elysia-type).
|
||||||
|
|
||||||
|
Elysia has a validation system built-in which can infers type from your code and validate it at runtime.
|
||||||
|
|
||||||
|
### Do: Use Elysia's validation system
|
||||||
|
|
||||||
|
Elysia strength is prioritizing a single source of truth for both type and runtime validation.
|
||||||
|
|
||||||
|
Instead of declaring an interface, reuse validation's model instead:
|
||||||
|
```typescript twoslash
|
||||||
|
// Do
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const customBody = t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Optional if you want to get the type of the model
|
||||||
|
// Usually if we didn't use the type, as it's already inferred by Elysia
|
||||||
|
type CustomBody = typeof customBody.static
|
||||||
|
|
||||||
|
export { customBody }
|
||||||
|
```
|
||||||
|
|
||||||
|
We can get type of model by using `typeof` with `.static` property from the model.
|
||||||
|
|
||||||
|
Then you can use the `CustomBody` type to infer the type of the request body.
|
||||||
|
|
||||||
|
```typescript twoslash
|
||||||
|
// Do
|
||||||
|
new Elysia()
|
||||||
|
.post('/login', ({ body }) => {
|
||||||
|
return body
|
||||||
|
}, {
|
||||||
|
body: customBody
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Don't: Declare a class instance as a model
|
||||||
|
|
||||||
|
Do not declare a class instance as a model:
|
||||||
|
```typescript
|
||||||
|
// Don't
|
||||||
|
class CustomBody {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
|
||||||
|
constructor(username: string, password: string) {
|
||||||
|
this.username = username
|
||||||
|
this.password = password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't
|
||||||
|
interface ICustomBody {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Don't: Declare type separate from the model
|
||||||
|
Do not declare a type separate from the model, instead use `typeof` with `.static` property to get the type of the model.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Don't
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const customBody = t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
type CustomBody = {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do
|
||||||
|
const customBody = t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
type CustomBody = typeof customBody.static
|
||||||
|
```
|
||||||
|
|
||||||
|
### Group
|
||||||
|
You can group multiple models into a single object to make it more organized.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
export const AuthModel = {
|
||||||
|
sign: t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const models = AuthModel.models
|
||||||
|
```
|
||||||
|
|
||||||
|
### Model Injection
|
||||||
|
Though this is optional, if you are strictly following MVC pattern, you may want to inject like a service into a controller. We recommended using Elysia reference model
|
||||||
|
|
||||||
|
Using Elysia's model reference
|
||||||
|
```typescript twoslash
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const customBody = t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
const AuthModel = new Elysia()
|
||||||
|
.model({
|
||||||
|
sign: customBody
|
||||||
|
})
|
||||||
|
|
||||||
|
const models = AuthModel.models
|
||||||
|
|
||||||
|
const UserController = new Elysia({ prefix: '/auth' })
|
||||||
|
.use(AuthModel)
|
||||||
|
.prefix('model', 'auth.')
|
||||||
|
.post('/sign-in', async ({ body, cookie: { session } }) => {
|
||||||
|
return true
|
||||||
|
}, {
|
||||||
|
body: 'auth.Sign'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach provide several benefits:
|
||||||
|
1. Allow us to name a model and provide auto-completion.
|
||||||
|
2. Modify schema for later usage, or perform a [remap](/essential/handler.html#remap).
|
||||||
|
3. Show up as "models" in OpenAPI compliance client, eg. OpenAPI.
|
||||||
|
4. Improve TypeScript inference speed as model type will be cached during registration.
|
||||||
30
.agents/skills/elysiajs/plugins/bearer.md
Normal file
30
.agents/skills/elysiajs/plugins/bearer.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Bearer
|
||||||
|
Plugin for Elysia for retrieving the Bearer token.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/bearer
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript twoslash
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { bearer } from '@elysiajs/bearer'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(bearer())
|
||||||
|
.get('/sign', ({ bearer }) => bearer, {
|
||||||
|
beforeHandle({ bearer, set, status }) {
|
||||||
|
if (!bearer) {
|
||||||
|
set.headers[
|
||||||
|
'WWW-Authenticate'
|
||||||
|
] = `Bearer realm='sign', error="invalid_request"`
|
||||||
|
|
||||||
|
return status(400, 'Unauthorized')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
This plugin is for retrieving a Bearer token specified in RFC6750
|
||||||
141
.agents/skills/elysiajs/plugins/cors.md
Normal file
141
.agents/skills/elysiajs/plugins/cors.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# CORS
|
||||||
|
|
||||||
|
Plugin for Elysia that adds support for customizing Cross-Origin Resource Sharing behavior.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/cors
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript twoslash
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { cors } from '@elysiajs/cors'
|
||||||
|
|
||||||
|
new Elysia().use(cors()).listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
This will set Elysia to accept requests from any origin.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
Below is a config which is accepted by the plugin
|
||||||
|
|
||||||
|
### origin
|
||||||
|
|
||||||
|
@default `true`
|
||||||
|
|
||||||
|
Indicates whether the response can be shared with the requesting code from the given origins.
|
||||||
|
|
||||||
|
Value can be one of the following:
|
||||||
|
|
||||||
|
- **string** - Name of origin which will directly assign to Access-Control-Allow-Origin header.
|
||||||
|
- **boolean** - If set to true, Access-Control-Allow-Origin will be set to `*` (any origins)
|
||||||
|
- **RegExp** - Pattern to match request's URL, allowed if matched.
|
||||||
|
- **Function** - Custom logic to allow resource sharing, allow if `true` is returned.
|
||||||
|
- Expected to have the type of:
|
||||||
|
```typescript
|
||||||
|
cors(context: Context) => boolean | void
|
||||||
|
```
|
||||||
|
- **Array<string | RegExp | Function>** - iterate through all cases above in order, allowed if any of the values are `true`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### methods
|
||||||
|
|
||||||
|
@default `*`
|
||||||
|
|
||||||
|
Allowed methods for cross-origin requests by assign `Access-Control-Allow-Methods` header.
|
||||||
|
|
||||||
|
Value can be one of the following:
|
||||||
|
- **undefined | null | ''** - Ignore all methods.
|
||||||
|
- **\*** - Allows all methods.
|
||||||
|
- **string** - Expects either a single method or a comma-delimited string
|
||||||
|
- (eg: `'GET, PUT, POST'`)
|
||||||
|
- **string[]** - Allow multiple HTTP methods.
|
||||||
|
- eg: `['GET', 'PUT', 'POST']`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### allowedHeaders
|
||||||
|
|
||||||
|
@default `*`
|
||||||
|
|
||||||
|
Allowed headers for an incoming request by assign `Access-Control-Allow-Headers` header.
|
||||||
|
|
||||||
|
Value can be one of the following:
|
||||||
|
- **string** - Expects either a single header or a comma-delimited string
|
||||||
|
- eg: `'Content-Type, Authorization'`.
|
||||||
|
- **string[]** - Allow multiple HTTP headers.
|
||||||
|
- eg: `['Content-Type', 'Authorization']`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### exposeHeaders
|
||||||
|
|
||||||
|
@default `*`
|
||||||
|
|
||||||
|
Response CORS with specified headers by sssign Access-Control-Expose-Headers header.
|
||||||
|
|
||||||
|
Value can be one of the following:
|
||||||
|
- **string** - Expects either a single header or a comma-delimited string.
|
||||||
|
- eg: `'Content-Type, X-Powered-By'`.
|
||||||
|
- **string[]** - Allow multiple HTTP headers.
|
||||||
|
- eg: `['Content-Type', 'X-Powered-By']`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### credentials
|
||||||
|
|
||||||
|
@default `true`
|
||||||
|
|
||||||
|
The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to the frontend JavaScript code when the request's credentials mode Request.credentials is `include`.
|
||||||
|
|
||||||
|
Credentials are cookies, authorization headers, or TLS client certificates by assign `Access-Control-Allow-Credentials` header.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### maxAge
|
||||||
|
|
||||||
|
@default `5`
|
||||||
|
|
||||||
|
Indicates how long the results of a preflight request that is the information contained in the `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` headers) can be cached.
|
||||||
|
|
||||||
|
Assign `Access-Control-Max-Age` header.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### preflight
|
||||||
|
|
||||||
|
The preflight request is a request sent to check if the CORS protocol is understood and if a server is aware of using specific methods and headers.
|
||||||
|
|
||||||
|
Response with **OPTIONS** request with 3 HTTP request headers:
|
||||||
|
- **Access-Control-Request-Method**
|
||||||
|
- **Access-Control-Request-Headers**
|
||||||
|
- **Origin**
|
||||||
|
|
||||||
|
This config indicates if the server should respond to preflight requests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pattern
|
||||||
|
|
||||||
|
Below you can find the common patterns to use the plugin.
|
||||||
|
|
||||||
|
## Allow CORS by top-level domain
|
||||||
|
|
||||||
|
```typescript twoslash
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { cors } from '@elysiajs/cors'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
cors({
|
||||||
|
origin: /.*\.saltyaom\.com$/
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.get('/', () => 'Hi')
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
This will allow requests from top-level domains with `saltyaom.com`
|
||||||
265
.agents/skills/elysiajs/plugins/cron.md
Normal file
265
.agents/skills/elysiajs/plugins/cron.md
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
# Cron Plugin
|
||||||
|
|
||||||
|
This plugin adds support for running cronjob to Elysia server.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/cron
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript twoslash
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { cron } from '@elysiajs/cron'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(
|
||||||
|
cron({
|
||||||
|
name: 'heartbeat',
|
||||||
|
pattern: '*/10 * * * * *',
|
||||||
|
run() {
|
||||||
|
console.log('Heartbeat')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
The above code will log `heartbeat` every 10 seconds.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
Below is a config which is accepted by the plugin
|
||||||
|
|
||||||
|
### cron
|
||||||
|
|
||||||
|
Create a cronjob for the Elysia server.
|
||||||
|
|
||||||
|
```
|
||||||
|
cron(config: CronConfig, callback: (Instance['store']) => void): this
|
||||||
|
```
|
||||||
|
|
||||||
|
`CronConfig` accepts the parameters specified below:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CronConfig.name
|
||||||
|
|
||||||
|
Job name to register to `store`.
|
||||||
|
|
||||||
|
This will register the cron instance to `store` with a specified name, which can be used to reference in later processes eg. stop the job.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CronConfig.pattern
|
||||||
|
|
||||||
|
Time to run the job as specified by cron syntax.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────── second (optional)
|
||||||
|
│ ┌──────────── minute
|
||||||
|
│ │ ┌────────── hour
|
||||||
|
│ │ │ ┌──────── day of the month
|
||||||
|
│ │ │ │ ┌────── month
|
||||||
|
│ │ │ │ │ ┌──── day of week
|
||||||
|
│ │ │ │ │ │
|
||||||
|
* * * * * *
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CronConfig.timezone
|
||||||
|
Time zone in Europe/Stockholm format
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CronConfig.startAt
|
||||||
|
Schedule start time for the job
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CronConfig.stopAt
|
||||||
|
Schedule stop time for the job
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CronConfig.maxRuns
|
||||||
|
Maximum number of executions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CronConfig.catch
|
||||||
|
Continue execution even if an unhandled error is thrown by a triggered function.
|
||||||
|
|
||||||
|
### CronConfig.interval
|
||||||
|
The minimum interval between executions, in seconds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CronConfig.Pattern
|
||||||
|
Below you can find the common patterns to use the plugin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pattern
|
||||||
|
|
||||||
|
Below you can find the common patterns to use the plugin.
|
||||||
|
|
||||||
|
## Stop cronjob
|
||||||
|
|
||||||
|
You can stop cronjob manually by accessing the cronjob name registered to `store`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { cron } from '@elysiajs/cron'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
cron({
|
||||||
|
name: 'heartbeat',
|
||||||
|
pattern: '*/1 * * * * *',
|
||||||
|
run() {
|
||||||
|
console.log('Heartbeat')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
'/stop',
|
||||||
|
({
|
||||||
|
store: {
|
||||||
|
cron: { heartbeat }
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
heartbeat.stop()
|
||||||
|
|
||||||
|
return 'Stop heartbeat'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Predefined patterns
|
||||||
|
|
||||||
|
You can use predefined patterns from `@elysiajs/cron/schedule`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { cron, Patterns } from '@elysiajs/cron'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
cron({
|
||||||
|
name: 'heartbeat',
|
||||||
|
pattern: Patterns.everySecond(),
|
||||||
|
run() {
|
||||||
|
console.log('Heartbeat')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
'/stop',
|
||||||
|
({
|
||||||
|
store: {
|
||||||
|
cron: { heartbeat }
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
heartbeat.stop()
|
||||||
|
|
||||||
|
return 'Stop heartbeat'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Functions
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
| ---------------------------------------- | ----------------------------------------------------- |
|
||||||
|
| `.everySeconds(2)` | Run the task every 2 seconds |
|
||||||
|
| `.everyMinutes(5)` | Run the task every 5 minutes |
|
||||||
|
| `.everyHours(3)` | Run the task every 3 hours |
|
||||||
|
| `.everyHoursAt(3, 15)` | Run the task every 3 hours at 15 minutes |
|
||||||
|
| `.everyDayAt('04:19')` | Run the task every day at 04:19 |
|
||||||
|
| `.everyWeekOn(Patterns.MONDAY, '19:30')` | Run the task every Monday at 19:30 |
|
||||||
|
| `.everyWeekdayAt('17:00')` | Run the task every day from Monday to Friday at 17:00 |
|
||||||
|
| `.everyWeekendAt('11:00')` | Run the task on Saturday and Sunday at 11:00 |
|
||||||
|
|
||||||
|
### Function aliases to constants
|
||||||
|
|
||||||
|
| Function | Constant |
|
||||||
|
| ----------------- | ---------------------------------- |
|
||||||
|
| `.everySecond()` | EVERY_SECOND |
|
||||||
|
| `.everyMinute()` | EVERY_MINUTE |
|
||||||
|
| `.hourly()` | EVERY_HOUR |
|
||||||
|
| `.daily()` | EVERY_DAY_AT_MIDNIGHT |
|
||||||
|
| `.everyWeekday()` | EVERY_WEEKDAY |
|
||||||
|
| `.everyWeekend()` | EVERY_WEEKEND |
|
||||||
|
| `.weekly()` | EVERY_WEEK |
|
||||||
|
| `.monthly()` | EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT |
|
||||||
|
| `.everyQuarter()` | EVERY_QUARTER |
|
||||||
|
| `.yearly()` | EVERY_YEAR |
|
||||||
|
|
||||||
|
### Constants
|
||||||
|
|
||||||
|
| Constant | Pattern |
|
||||||
|
| ---------------------------------------- | -------------------- |
|
||||||
|
| `.EVERY_SECOND` | `* * * * * *` |
|
||||||
|
| `.EVERY_5_SECONDS` | `*/5 * * * * *` |
|
||||||
|
| `.EVERY_10_SECONDS` | `*/10 * * * * *` |
|
||||||
|
| `.EVERY_30_SECONDS` | `*/30 * * * * *` |
|
||||||
|
| `.EVERY_MINUTE` | `*/1 * * * *` |
|
||||||
|
| `.EVERY_5_MINUTES` | `0 */5 * * * *` |
|
||||||
|
| `.EVERY_10_MINUTES` | `0 */10 * * * *` |
|
||||||
|
| `.EVERY_30_MINUTES` | `0 */30 * * * *` |
|
||||||
|
| `.EVERY_HOUR` | `0 0-23/1 * * *` |
|
||||||
|
| `.EVERY_2_HOURS` | `0 0-23/2 * * *` |
|
||||||
|
| `.EVERY_3_HOURS` | `0 0-23/3 * * *` |
|
||||||
|
| `.EVERY_4_HOURS` | `0 0-23/4 * * *` |
|
||||||
|
| `.EVERY_5_HOURS` | `0 0-23/5 * * *` |
|
||||||
|
| `.EVERY_6_HOURS` | `0 0-23/6 * * *` |
|
||||||
|
| `.EVERY_7_HOURS` | `0 0-23/7 * * *` |
|
||||||
|
| `.EVERY_8_HOURS` | `0 0-23/8 * * *` |
|
||||||
|
| `.EVERY_9_HOURS` | `0 0-23/9 * * *` |
|
||||||
|
| `.EVERY_10_HOURS` | `0 0-23/10 * * *` |
|
||||||
|
| `.EVERY_11_HOURS` | `0 0-23/11 * * *` |
|
||||||
|
| `.EVERY_12_HOURS` | `0 0-23/12 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_1AM` | `0 01 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_2AM` | `0 02 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_3AM` | `0 03 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_4AM` | `0 04 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_5AM` | `0 05 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_6AM` | `0 06 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_7AM` | `0 07 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_8AM` | `0 08 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_9AM` | `0 09 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_10AM` | `0 10 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_11AM` | `0 11 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_NOON` | `0 12 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_1PM` | `0 13 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_2PM` | `0 14 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_3PM` | `0 15 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_4PM` | `0 16 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_5PM` | `0 17 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_6PM` | `0 18 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_7PM` | `0 19 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_8PM` | `0 20 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_9PM` | `0 21 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_10PM` | `0 22 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_11PM` | `0 23 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_MIDNIGHT` | `0 0 * * *` |
|
||||||
|
| `.EVERY_WEEK` | `0 0 * * 0` |
|
||||||
|
| `.EVERY_WEEKDAY` | `0 0 * * 1-5` |
|
||||||
|
| `.EVERY_WEEKEND` | `0 0 * * 6,0` |
|
||||||
|
| `.EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT` | `0 0 1 * *` |
|
||||||
|
| `.EVERY_1ST_DAY_OF_MONTH_AT_NOON` | `0 12 1 * *` |
|
||||||
|
| `.EVERY_2ND_HOUR` | `0 */2 * * *` |
|
||||||
|
| `.EVERY_2ND_HOUR_FROM_1AM_THROUGH_11PM` | `0 1-23/2 * * *` |
|
||||||
|
| `.EVERY_2ND_MONTH` | `0 0 1 */2 *` |
|
||||||
|
| `.EVERY_QUARTER` | `0 0 1 */3 *` |
|
||||||
|
| `.EVERY_6_MONTHS` | `0 0 1 */6 *` |
|
||||||
|
| `.EVERY_YEAR` | `0 0 1 1 *` |
|
||||||
|
| `.EVERY_30_MINUTES_BETWEEN_9AM_AND_5PM` | `0 */30 9-17 * * *` |
|
||||||
|
| `.EVERY_30_MINUTES_BETWEEN_9AM_AND_6PM` | `0 */30 9-18 * * *` |
|
||||||
|
| `.EVERY_30_MINUTES_BETWEEN_10AM_AND_7PM` | `0 */30 10-19 * * *` |
|
||||||
90
.agents/skills/elysiajs/plugins/graphql-apollo.md
Normal file
90
.agents/skills/elysiajs/plugins/graphql-apollo.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# GraphQL Apollo
|
||||||
|
|
||||||
|
Plugin for Elysia to use GraphQL Apollo.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add graphql @elysiajs/apollo @apollo/server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { apollo, gql } from '@elysiajs/apollo'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
apollo({
|
||||||
|
typeDefs: gql`
|
||||||
|
type Book {
|
||||||
|
title: String
|
||||||
|
author: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
books: [Book]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
resolvers: {
|
||||||
|
Query: {
|
||||||
|
books: () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: 'Elysia',
|
||||||
|
author: 'saltyAom'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
Accessing `/graphql` should show Apollo GraphQL playground work with.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Because Elysia is based on Web Standard Request and Response which is different from Node's `HttpRequest` and `HttpResponse` that Express uses, results in `req, res` being undefined in context.
|
||||||
|
|
||||||
|
Because of this, Elysia replaces both with `context` like route parameters.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
apollo({
|
||||||
|
typeDefs,
|
||||||
|
resolvers,
|
||||||
|
context: async ({ request }) => {
|
||||||
|
const authorization = request.headers.get('Authorization')
|
||||||
|
|
||||||
|
return {
|
||||||
|
authorization
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
This plugin extends Apollo's [ServerRegistration](https://www.apollographql.com/docs/apollo-server/api/apollo-server/#options) (which is `ApolloServer`'s' constructor parameter).
|
||||||
|
|
||||||
|
Below are the extended parameters for configuring Apollo Server with Elysia.
|
||||||
|
|
||||||
|
### path
|
||||||
|
|
||||||
|
@default `"/graphql"`
|
||||||
|
|
||||||
|
Path to expose Apollo Server.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### enablePlayground
|
||||||
|
|
||||||
|
@default `process.env.ENV !== 'production'`
|
||||||
|
|
||||||
|
Determine whether should Apollo should provide Apollo Playground.
|
||||||
87
.agents/skills/elysiajs/plugins/graphql-yoga.md
Normal file
87
.agents/skills/elysiajs/plugins/graphql-yoga.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# GraphQL Yoga
|
||||||
|
|
||||||
|
This plugin integrates GraphQL yoga with Elysia
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/graphql-yoga
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { yoga } from '@elysiajs/graphql-yoga'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
yoga({
|
||||||
|
typeDefs: /* GraphQL */ `
|
||||||
|
type Query {
|
||||||
|
hi: String
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
resolvers: {
|
||||||
|
Query: {
|
||||||
|
hi: () => 'Hello from Elysia'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
Accessing `/graphql` in the browser (GET request) would show you a GraphiQL instance for the GraphQL-enabled Elysia server.
|
||||||
|
|
||||||
|
optional: you can install a custom version of optional peer dependencies as well:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add graphql graphql-yoga
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resolver
|
||||||
|
|
||||||
|
Elysia uses Mobius to infer type from **typeDefs** field automatically, allowing you to get full type-safety and auto-complete when typing **resolver** types.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
You can add custom context to the resolver function by adding **context**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { yoga } from '@elysiajs/graphql-yoga'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
yoga({
|
||||||
|
typeDefs: /* GraphQL */ `
|
||||||
|
type Query {
|
||||||
|
hi: String
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
context: {
|
||||||
|
name: 'Mobius'
|
||||||
|
},
|
||||||
|
// If context is a function on this doesn't present
|
||||||
|
// for some reason it won't infer context type
|
||||||
|
useContext(_) {},
|
||||||
|
resolvers: {
|
||||||
|
Query: {
|
||||||
|
hi: async (parent, args, context) => context.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
This plugin extends [GraphQL Yoga's createYoga options, please refer to the GraphQL Yoga documentation](https://the-guild.dev/graphql/yoga-server/docs) with inlining `schema` config to root.
|
||||||
|
|
||||||
|
Below is a config which is accepted by the plugin
|
||||||
|
|
||||||
|
### path
|
||||||
|
|
||||||
|
@default `/graphql`
|
||||||
|
|
||||||
|
Endpoint to expose GraphQL handler
|
||||||
188
.agents/skills/elysiajs/plugins/html.md
Normal file
188
.agents/skills/elysiajs/plugins/html.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# HTML
|
||||||
|
|
||||||
|
Allows you to use JSX and HTML with proper headers and support.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```tsx twoslash
|
||||||
|
import React from 'react'
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { html, Html } from '@elysiajs/html'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(html())
|
||||||
|
.get(
|
||||||
|
'/html',
|
||||||
|
() => `
|
||||||
|
<html lang='en'>
|
||||||
|
<head>
|
||||||
|
<title>Hello World</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello World</h1>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
)
|
||||||
|
.get('/jsx', () => (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Hello World</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello World</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
))
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
This plugin will automatically add `Content-Type: text/html; charset=utf8` header to the response, add `<!doctype html>`, and convert it into a Response object.
|
||||||
|
|
||||||
|
## JSX
|
||||||
|
Elysia can use JSX
|
||||||
|
|
||||||
|
1. Replace your file that needs to use JSX to end with affix **"x"**:
|
||||||
|
- .js -> .jsx
|
||||||
|
- .ts -> .tsx
|
||||||
|
|
||||||
|
2. Register the TypeScript type by append the following to **tsconfig.json**:
|
||||||
|
```jsonc
|
||||||
|
// tsconfig.json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react",
|
||||||
|
"jsxFactory": "Html.createElement",
|
||||||
|
"jsxFragmentFactory": "Html.Fragment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Starts using JSX in your file
|
||||||
|
```tsx twoslash
|
||||||
|
import React from 'react'
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { html, Html } from '@elysiajs/html'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(html())
|
||||||
|
.get('/', () => (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Hello World</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello World</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
))
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
If the error `Cannot find name 'Html'. Did you mean 'html'?` occurs, this import must be added to the JSX template:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Html } from '@elysiajs/html'
|
||||||
|
```
|
||||||
|
|
||||||
|
It is important that it is written in uppercase.
|
||||||
|
|
||||||
|
## XSS
|
||||||
|
|
||||||
|
Elysia HTML is based use of the Kita HTML plugin to detect possible XSS attacks in compile time.
|
||||||
|
|
||||||
|
You can use a dedicated `safe` attribute to sanitize user value to prevent XSS vulnerability.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
import { html, Html } from '@elysiajs/html'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(html())
|
||||||
|
.post(
|
||||||
|
'/',
|
||||||
|
({ body }) => (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Hello World</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 safe>{body}</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
body: t.String()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
However, when are building a large-scale app, it's best to have a type reminder to detect possible XSS vulnerabilities in your codebase.
|
||||||
|
|
||||||
|
To add a type-safe reminder, please install:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun add @kitajs/ts-html-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
Then appends the following **tsconfig.json**
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// tsconfig.json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react",
|
||||||
|
"jsxFactory": "Html.createElement",
|
||||||
|
"jsxFragmentFactory": "Html.Fragment",
|
||||||
|
"plugins": [{ "name": "@kitajs/ts-html-plugin" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Config
|
||||||
|
Below is a config which is accepted by the plugin
|
||||||
|
|
||||||
|
### contentType
|
||||||
|
|
||||||
|
- Type: `string`
|
||||||
|
- Default: `'text/html; charset=utf8'`
|
||||||
|
|
||||||
|
The content-type of the response.
|
||||||
|
|
||||||
|
### autoDetect
|
||||||
|
|
||||||
|
- Type: `boolean`
|
||||||
|
- Default: `true`
|
||||||
|
|
||||||
|
Whether to automatically detect HTML content and set the content-type.
|
||||||
|
|
||||||
|
### autoDoctype
|
||||||
|
|
||||||
|
- Type: `boolean | 'full'`
|
||||||
|
- Default: `true`
|
||||||
|
|
||||||
|
Whether to automatically add `<!doctype html>` to a response starting with `<html>`, if not found.
|
||||||
|
|
||||||
|
Use `full` to also automatically add doctypes on responses returned without this plugin
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// without the plugin
|
||||||
|
app.get('/', () => '<html></html>')
|
||||||
|
|
||||||
|
// With the plugin
|
||||||
|
app.get('/', ({ html }) => html('<html></html>'))
|
||||||
|
```
|
||||||
|
|
||||||
|
### isHtml
|
||||||
|
|
||||||
|
- Type: `(value: string) => boolean`
|
||||||
|
- Default: `isHtml` (exported function)
|
||||||
|
|
||||||
|
The function is used to detect if a string is a html or not. Default implementation if length is greater than 7, starts with `<` and ends with `>`.
|
||||||
|
|
||||||
|
Keep in mind there's no real way to validate HTML, so the default implementation is a best guess.
|
||||||
197
.agents/skills/elysiajs/plugins/jwt.md
Normal file
197
.agents/skills/elysiajs/plugins/jwt.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# JWT Plugin
|
||||||
|
This plugin adds support for using JWT in Elysia handlers.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/jwt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript [cookie]
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { jwt } from '@elysiajs/jwt'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
jwt({
|
||||||
|
name: 'jwt',
|
||||||
|
secret: 'Fischl von Luftschloss Narfidort'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.get('/sign/:name', async ({ jwt, params: { name }, cookie: { auth } }) => {
|
||||||
|
const value = await jwt.sign({ name })
|
||||||
|
|
||||||
|
auth.set({
|
||||||
|
value,
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 7 * 86400,
|
||||||
|
path: '/profile',
|
||||||
|
})
|
||||||
|
|
||||||
|
return `Sign in as ${value}`
|
||||||
|
})
|
||||||
|
.get('/profile', async ({ jwt, status, cookie: { auth } }) => {
|
||||||
|
const profile = await jwt.verify(auth.value)
|
||||||
|
|
||||||
|
if (!profile)
|
||||||
|
return status(401, 'Unauthorized')
|
||||||
|
|
||||||
|
return `Hello ${profile.name}`
|
||||||
|
})
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Config
|
||||||
|
This plugin extends config from [jose](https://github.com/panva/jose).
|
||||||
|
|
||||||
|
Below is a config that is accepted by the plugin.
|
||||||
|
|
||||||
|
### name
|
||||||
|
Name to register `jwt` function as.
|
||||||
|
|
||||||
|
For example, `jwt` function will be registered with a custom name.
|
||||||
|
```typescript
|
||||||
|
new Elysia()
|
||||||
|
.use(
|
||||||
|
jwt({
|
||||||
|
name: 'myJWTNamespace',
|
||||||
|
secret: process.env.JWT_SECRETS!
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.get('/sign/:name', ({ myJWTNamespace, params }) => {
|
||||||
|
return myJWTNamespace.sign(params)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Because some might need to use multiple `jwt` with different configs in a single server, explicitly registering the JWT function with a different name is needed.
|
||||||
|
|
||||||
|
### secret
|
||||||
|
The private key to sign JWT payload with.
|
||||||
|
|
||||||
|
### schema
|
||||||
|
Type strict validation for JWT payload.
|
||||||
|
|
||||||
|
### alg
|
||||||
|
@default `HS256`
|
||||||
|
|
||||||
|
Signing Algorithm to sign JWT payload with.
|
||||||
|
|
||||||
|
Possible properties for jose are:
|
||||||
|
HS256
|
||||||
|
HS384
|
||||||
|
HS512
|
||||||
|
PS256
|
||||||
|
PS384
|
||||||
|
PS512
|
||||||
|
RS256
|
||||||
|
RS384
|
||||||
|
RS512
|
||||||
|
ES256
|
||||||
|
ES256K
|
||||||
|
ES384
|
||||||
|
ES512
|
||||||
|
EdDSA
|
||||||
|
|
||||||
|
### iss
|
||||||
|
The issuer claim identifies the principal that issued the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.1)
|
||||||
|
|
||||||
|
TLDR; is usually (the domain) name of the signer.
|
||||||
|
|
||||||
|
### sub
|
||||||
|
The subject claim identifies the principal that is the subject of the JWT.
|
||||||
|
|
||||||
|
The claims in a JWT are normally statements about the subject as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2)
|
||||||
|
|
||||||
|
### aud
|
||||||
|
The audience claim identifies the recipients that the JWT is intended for.
|
||||||
|
|
||||||
|
Each principal intended to process the JWT MUST identify itself with a value in the audience claim as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3)
|
||||||
|
|
||||||
|
### jti
|
||||||
|
JWT ID claim provides a unique identifier for the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7)
|
||||||
|
|
||||||
|
### nbf
|
||||||
|
The "not before" claim identifies the time before which the JWT must not be accepted for processing as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.5)
|
||||||
|
|
||||||
|
### exp
|
||||||
|
The expiration time claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.4)
|
||||||
|
|
||||||
|
### iat
|
||||||
|
The "issued at" claim identifies the time at which the JWT was issued.
|
||||||
|
|
||||||
|
This claim can be used to determine the age of the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6)
|
||||||
|
|
||||||
|
### b64
|
||||||
|
This JWS Extension Header Parameter modifies the JWS Payload representation and the JWS Signing input computation as per [RFC7797](https://www.rfc-editor.org/rfc/rfc7797).
|
||||||
|
|
||||||
|
### kid
|
||||||
|
A hint indicating which key was used to secure the JWS.
|
||||||
|
|
||||||
|
This parameter allows originators to explicitly signal a change of key to recipients as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.4)
|
||||||
|
|
||||||
|
### x5t
|
||||||
|
(X.509 certificate SHA-1 thumbprint) header parameter is a base64url-encoded SHA-1 digest of the DER encoding of the X.509 certificate [RFC5280](https://www.rfc-editor.org/rfc/rfc5280) corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.7)
|
||||||
|
|
||||||
|
### x5c
|
||||||
|
(X.509 certificate chain) header parameter contains the X.509 public key certificate or certificate chain [RFC5280](https://www.rfc-editor.org/rfc/rfc5280) corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.6)
|
||||||
|
|
||||||
|
### x5u
|
||||||
|
(X.509 URL) header parameter is a URI [RFC3986](https://www.rfc-editor.org/rfc/rfc3986) that refers to a resource for the X.509 public key certificate or certificate chain [RFC5280] corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.5)
|
||||||
|
|
||||||
|
### jwk
|
||||||
|
The "jku" (JWK Set URL) Header Parameter is a URI [RFC3986] that refers to a resource for a set of JSON-encoded public keys, one of which corresponds to the key used to digitally sign the JWS.
|
||||||
|
|
||||||
|
The keys MUST be encoded as a JWK Set [JWK] as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.2)
|
||||||
|
|
||||||
|
### typ
|
||||||
|
The `typ` (type) Header Parameter is used by JWS applications to declare the media type [IANA.MediaTypes] of this complete JWS.
|
||||||
|
|
||||||
|
This is intended for use by the application when more than one kind of object could be present in an application data structure that can contain a JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.9)
|
||||||
|
|
||||||
|
### ctr
|
||||||
|
Content-Type parameter is used by JWS applications to declare the media type [IANA.MediaTypes] of the secured content (the payload).
|
||||||
|
|
||||||
|
This is intended for use by the application when more than one kind of object could be present in the JWS Payload as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.9)
|
||||||
|
|
||||||
|
## Handler
|
||||||
|
Below are the value added to the handler.
|
||||||
|
|
||||||
|
### jwt.sign
|
||||||
|
A dynamic object of collection related to use with JWT registered by the JWT plugin.
|
||||||
|
|
||||||
|
Type:
|
||||||
|
```typescript
|
||||||
|
sign: (payload: JWTPayloadSpec): Promise<string>
|
||||||
|
```
|
||||||
|
|
||||||
|
`JWTPayloadSpec` accepts the same value as [JWT config](#config)
|
||||||
|
|
||||||
|
### jwt.verify
|
||||||
|
Verify payload with the provided JWT config
|
||||||
|
|
||||||
|
Type:
|
||||||
|
```typescript
|
||||||
|
verify(payload: string) => Promise<JWTPayloadSpec | false>
|
||||||
|
```
|
||||||
|
|
||||||
|
`JWTPayloadSpec` accepts the same value as [JWT config](#config)
|
||||||
|
|
||||||
|
## Pattern
|
||||||
|
Below you can find the common patterns to use the plugin.
|
||||||
|
|
||||||
|
## Set JWT expiration date
|
||||||
|
By default, the config is passed to `setCookie` and inherits its value.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
jwt({
|
||||||
|
name: 'jwt',
|
||||||
|
secret: 'kunikuzushi',
|
||||||
|
exp: '7d'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.get('/sign/:name', async ({ jwt, params }) => jwt.sign(params))
|
||||||
|
```
|
||||||
|
|
||||||
|
This will sign JWT with an expiration date of the next 7 days.
|
||||||
246
.agents/skills/elysiajs/plugins/openapi.md
Normal file
246
.agents/skills/elysiajs/plugins/openapi.md
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
# OpenAPI Plugin
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/openapi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript
|
||||||
|
import { openapi } from '@elysiajs/openapi'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(openapi())
|
||||||
|
.get('/', () => 'hello')
|
||||||
|
```
|
||||||
|
|
||||||
|
Docs at `/openapi`, spec at `/openapi/json`.
|
||||||
|
|
||||||
|
## Detail Object
|
||||||
|
Extends OpenAPI Operation Object:
|
||||||
|
```typescript
|
||||||
|
.get('/', () => 'hello', {
|
||||||
|
detail: {
|
||||||
|
title: 'Hello',
|
||||||
|
description: 'An example route',
|
||||||
|
summary: 'Short summary',
|
||||||
|
deprecated: false,
|
||||||
|
hide: true, // Hide from docs
|
||||||
|
tags: ['App']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Documentation Config
|
||||||
|
```typescript
|
||||||
|
openapi({
|
||||||
|
documentation: {
|
||||||
|
info: {
|
||||||
|
title: 'API',
|
||||||
|
version: '1.0.0'
|
||||||
|
},
|
||||||
|
tags: [
|
||||||
|
{ name: 'App', description: 'General' }
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
bearerAuth: { type: 'http', scheme: 'bearer' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Standard Schema Mapping
|
||||||
|
```typescript
|
||||||
|
mapJsonSchema: {
|
||||||
|
zod: z.toJSONSchema, // Zod 4
|
||||||
|
valibot: toJsonSchema,
|
||||||
|
effect: JSONSchema.make
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Zod 3: `zodToJsonSchema` from `zod-to-json-schema`
|
||||||
|
|
||||||
|
## OpenAPI Type Gen
|
||||||
|
Generate docs from types:
|
||||||
|
```typescript
|
||||||
|
import { fromTypes } from '@elysiajs/openapi'
|
||||||
|
|
||||||
|
export const app = new Elysia()
|
||||||
|
.use(openapi({
|
||||||
|
references: fromTypes()
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
Recommended to generate `.d.ts` file for production when using OpenAPI Type Gen
|
||||||
|
```typescript
|
||||||
|
references: fromTypes(
|
||||||
|
process.env.NODE_ENV === 'production'
|
||||||
|
? 'dist/index.d.ts'
|
||||||
|
: 'src/index.ts'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
```typescript
|
||||||
|
fromTypes('src/index.ts', {
|
||||||
|
projectRoot: path.join('..', import.meta.dir),
|
||||||
|
tsconfigPath: 'tsconfig.dts.json'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caveat: Explicit Types
|
||||||
|
Use `Prettify` helper to inline when type is not showing:
|
||||||
|
```typescript
|
||||||
|
type Prettify<T> = { [K in keyof T]: T[K] } & {}
|
||||||
|
|
||||||
|
function getUser(): Prettify<User> { }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema Description
|
||||||
|
```typescript
|
||||||
|
body: t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String({
|
||||||
|
minLength: 8,
|
||||||
|
description: 'Password (8+ chars)'
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
description: 'Expected username and password'
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: 'Sign in user',
|
||||||
|
tags: ['auth']
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Headers
|
||||||
|
```typescript
|
||||||
|
import { withHeader } from '@elysiajs/openapi'
|
||||||
|
|
||||||
|
response: withHeader(
|
||||||
|
t.Literal('Hi'),
|
||||||
|
{ 'x-powered-by': t.Literal('Elysia') }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Annotation only - doesn't enforce. Set headers manually.
|
||||||
|
|
||||||
|
## Tags
|
||||||
|
Define + assign:
|
||||||
|
```typescript
|
||||||
|
.use(openapi({
|
||||||
|
documentation: {
|
||||||
|
tags: [
|
||||||
|
{ name: 'App', description: 'General' },
|
||||||
|
{ name: 'Auth', description: 'Auth' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.get('/', () => 'hello', {
|
||||||
|
detail: { tags: ['App'] }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Instance Tags
|
||||||
|
```typescript
|
||||||
|
new Elysia({ tags: ['user'] })
|
||||||
|
.get('/user', 'user')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reference Models
|
||||||
|
Auto-generates schemas:
|
||||||
|
```typescript
|
||||||
|
.model({
|
||||||
|
User: t.Object({
|
||||||
|
id: t.Number(),
|
||||||
|
username: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get('/user', () => ({ id: 1, username: 'x' }), {
|
||||||
|
response: { 200: 'User' },
|
||||||
|
detail: { tags: ['User'] }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Guard
|
||||||
|
Apply to instance/group:
|
||||||
|
```typescript
|
||||||
|
.guard({
|
||||||
|
detail: {
|
||||||
|
description: 'Requires auth'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get('/user', 'user')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
```typescript
|
||||||
|
.use(openapi({
|
||||||
|
documentation: {
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
bearerAuth: {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
bearerFormat: 'JWT'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
new Elysia({
|
||||||
|
prefix: '/address',
|
||||||
|
detail: {
|
||||||
|
security: [{ bearerAuth: [] }]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Secures all routes under prefix.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
Below is a config which is accepted by the `openapi({})`
|
||||||
|
|
||||||
|
### enabled
|
||||||
|
@default true
|
||||||
|
Enable/Disable the plugin
|
||||||
|
|
||||||
|
### documentation
|
||||||
|
OpenAPI documentation information
|
||||||
|
@see https://spec.openapis.org/oas/v3.0.3.html
|
||||||
|
|
||||||
|
### exclude
|
||||||
|
Configuration to exclude paths or methods from documentation
|
||||||
|
|
||||||
|
### exclude.methods
|
||||||
|
List of methods to exclude from documentation
|
||||||
|
|
||||||
|
### exclude.paths
|
||||||
|
List of paths to exclude from documentation
|
||||||
|
|
||||||
|
### exclude.staticFile
|
||||||
|
@default true
|
||||||
|
|
||||||
|
Exclude static file routes from documentation
|
||||||
|
|
||||||
|
### exclude.tags
|
||||||
|
List of tags to exclude from documentation
|
||||||
|
|
||||||
|
### mapJsonSchema
|
||||||
|
A custom mapping function from Standard schema to OpenAPI schema
|
||||||
|
|
||||||
|
### path
|
||||||
|
@default '/openapi'
|
||||||
|
The endpoint to expose OpenAPI documentation frontend
|
||||||
|
|
||||||
|
### provider
|
||||||
|
@default 'scalar'
|
||||||
|
|
||||||
|
OpenAPI documentation frontend between:
|
||||||
|
- Scalar
|
||||||
|
- SwaggerUI
|
||||||
|
- null: disable frontend
|
||||||
167
.agents/skills/elysiajs/plugins/opentelemetry.md
Normal file
167
.agents/skills/elysiajs/plugins/opentelemetry.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# OpenTelemetry Plugin - SKILLS.md
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/opentelemetry
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript
|
||||||
|
import { opentelemetry } from '@elysiajs/opentelemetry'
|
||||||
|
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'
|
||||||
|
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(opentelemetry({
|
||||||
|
spanProcessors: [
|
||||||
|
new BatchSpanProcessor(new OTLPTraceExporter())
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-collects spans from OpenTelemetry-compatible libraries. Parent/child spans applied automatically.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
Extends OpenTelemetry SDK params:
|
||||||
|
|
||||||
|
- `autoDetectResources` (true) - Auto-detect from env
|
||||||
|
- `contextManager` (AsyncHooksContextManager) - Custom context
|
||||||
|
- `textMapPropagator` (CompositePropagator) - W3C Trace + Baggage
|
||||||
|
- `metricReader` - For MeterProvider
|
||||||
|
- `views` - Histogram bucket config
|
||||||
|
- `instrumentations` (getNodeAutoInstrumentations()) - Metapackage or individual
|
||||||
|
- `resource` - Custom resource
|
||||||
|
- `resourceDetectors` ([envDetector, processDetector, hostDetector]) - Auto-detect needs `autoDetectResources: true`
|
||||||
|
- `sampler` - Custom sampler (default: sample all)
|
||||||
|
- `serviceName` - Namespace identifier
|
||||||
|
- `spanProcessors` - Array for tracer provider
|
||||||
|
- `traceExporter` - Auto-setup OTLP/http/protobuf with BatchSpanProcessor if not set
|
||||||
|
- `spanLimits` - Tracing params
|
||||||
|
|
||||||
|
### Resource Detectors via Env
|
||||||
|
```bash
|
||||||
|
export OTEL_NODE_RESOURCE_DETECTORS="env,host"
|
||||||
|
# Options: env, host, os, process, serviceinstance, all, none
|
||||||
|
```
|
||||||
|
|
||||||
|
## Export to Backends
|
||||||
|
Example - Axiom:
|
||||||
|
```typescript
|
||||||
|
.use(opentelemetry({
|
||||||
|
spanProcessors: [
|
||||||
|
new BatchSpanProcessor(
|
||||||
|
new OTLPTraceExporter({
|
||||||
|
url: 'https://api.axiom.co/v1/traces',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${Bun.env.AXIOM_TOKEN}`,
|
||||||
|
'X-Axiom-Dataset': Bun.env.AXIOM_DATASET
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
## OpenTelemetry SDK
|
||||||
|
Use SDK normally - runs under Elysia's request span, auto-appears in trace.
|
||||||
|
|
||||||
|
## Record Utility
|
||||||
|
Equivalent to `startActiveSpan` - auto-closes + captures exceptions:
|
||||||
|
```typescript
|
||||||
|
import { record } from '@elysiajs/opentelemetry'
|
||||||
|
|
||||||
|
.get('', () => {
|
||||||
|
return record('database.query', () => {
|
||||||
|
return db.query('SELECT * FROM users')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Label for code shown in trace.
|
||||||
|
|
||||||
|
## Function Naming
|
||||||
|
Elysia reads function names as span names:
|
||||||
|
```typescript
|
||||||
|
// ⚠️ Anonymous span
|
||||||
|
.derive(async ({ cookie: { session } }) => {
|
||||||
|
return { user: await getProfile(session) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ Named span: "getProfile"
|
||||||
|
.derive(async function getProfile({ cookie: { session } }) {
|
||||||
|
return { user: await getProfile(session) }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## getCurrentSpan
|
||||||
|
Get current span outside handler (via AsyncLocalStorage):
|
||||||
|
```typescript
|
||||||
|
import { getCurrentSpan } from '@elysiajs/opentelemetry'
|
||||||
|
|
||||||
|
function utility() {
|
||||||
|
const span = getCurrentSpan()
|
||||||
|
span.setAttributes({ 'custom.attribute': 'value' })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## setAttributes
|
||||||
|
Sugar for `getCurrentSpan().setAttributes`:
|
||||||
|
```typescript
|
||||||
|
import { setAttributes } from '@elysiajs/opentelemetry'
|
||||||
|
|
||||||
|
function utility() {
|
||||||
|
setAttributes({ 'custom.attribute': 'value' })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Instrumentations (Advanced)
|
||||||
|
SDK must run before importing instrumented module.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
1. Separate file:
|
||||||
|
```typescript
|
||||||
|
// src/instrumentation.ts
|
||||||
|
import { opentelemetry } from '@elysiajs/opentelemetry'
|
||||||
|
import { PgInstrumentation } from '@opentelemetry/instrumentation-pg'
|
||||||
|
|
||||||
|
export const instrumentation = opentelemetry({
|
||||||
|
instrumentations: [new PgInstrumentation()]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Apply:
|
||||||
|
```typescript
|
||||||
|
// src/index.ts
|
||||||
|
import { instrumentation } from './instrumentation'
|
||||||
|
new Elysia().use(instrumentation).listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Preload:
|
||||||
|
```toml
|
||||||
|
# bunfig.toml
|
||||||
|
preload = ["./src/instrumentation.ts"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Deployment (Advanced)
|
||||||
|
OpenTelemetry monkey-patches `node_modules`. Exclude instrumented libs from bundling:
|
||||||
|
```bash
|
||||||
|
bun build --compile --external pg --outfile server src/index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Package.json:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dependencies": { "pg": "^8.15.6" },
|
||||||
|
"devDependencies": {
|
||||||
|
"@elysiajs/opentelemetry": "^1.2.0",
|
||||||
|
"@opentelemetry/instrumentation-pg": "^0.52.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Production install:
|
||||||
|
```bash
|
||||||
|
bun install --production
|
||||||
|
```
|
||||||
|
|
||||||
|
Keeps `node_modules` with instrumented libs at runtime.
|
||||||
71
.agents/skills/elysiajs/plugins/server-timing.md
Normal file
71
.agents/skills/elysiajs/plugins/server-timing.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Server Timing Plugin
|
||||||
|
This plugin adds support for auditing performance bottlenecks with Server Timing API
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/server-timing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript twoslash
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { serverTiming } from '@elysiajs/server-timing'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(serverTiming())
|
||||||
|
.get('/', () => 'hello')
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
Server Timing then will append header 'Server-Timing' with log duration, function name, and detail for each life-cycle function.
|
||||||
|
|
||||||
|
To inspect, open browser developer tools > Network > [Request made through Elysia server] > Timing.
|
||||||
|
|
||||||
|
Now you can effortlessly audit the performance bottleneck of your server.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
Below is a config which is accepted by the plugin
|
||||||
|
|
||||||
|
### enabled
|
||||||
|
@default `NODE_ENV !== 'production'`
|
||||||
|
|
||||||
|
Determine whether or not Server Timing should be enabled
|
||||||
|
|
||||||
|
### allow
|
||||||
|
@default `undefined`
|
||||||
|
|
||||||
|
A condition whether server timing should be log
|
||||||
|
|
||||||
|
### trace
|
||||||
|
@default `undefined`
|
||||||
|
|
||||||
|
Allow Server Timing to log specified life-cycle events:
|
||||||
|
|
||||||
|
Trace accepts objects of the following:
|
||||||
|
- request: capture duration from request
|
||||||
|
- parse: capture duration from parse
|
||||||
|
- transform: capture duration from transform
|
||||||
|
- beforeHandle: capture duration from beforeHandle
|
||||||
|
- handle: capture duration from the handle
|
||||||
|
- afterHandle: capture duration from afterHandle
|
||||||
|
- total: capture total duration from start to finish
|
||||||
|
|
||||||
|
## Pattern
|
||||||
|
Below you can find the common patterns to use the plugin.
|
||||||
|
|
||||||
|
## Allow Condition
|
||||||
|
You may disable Server Timing on specific routes via `allow` property
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { serverTiming } from '@elysiajs/server-timing'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(
|
||||||
|
serverTiming({
|
||||||
|
allow: ({ request }) => {
|
||||||
|
return new URL(request.url).pathname !== '/no-trace'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
```
|
||||||
84
.agents/skills/elysiajs/plugins/static.md
Normal file
84
.agents/skills/elysiajs/plugins/static.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Static Plugin
|
||||||
|
This plugin can serve static files/folders for Elysia Server
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/static
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript twoslash
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { staticPlugin } from '@elysiajs/static'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(staticPlugin())
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, the static plugin default folder is `public`, and registered with `/public` prefix.
|
||||||
|
|
||||||
|
Suppose your project structure is:
|
||||||
|
```
|
||||||
|
| - src
|
||||||
|
| - index.ts
|
||||||
|
| - public
|
||||||
|
| - takodachi.png
|
||||||
|
| - nested
|
||||||
|
| - takodachi.png
|
||||||
|
```
|
||||||
|
|
||||||
|
The available path will become:
|
||||||
|
- /public/takodachi.png
|
||||||
|
- /public/nested/takodachi.png
|
||||||
|
|
||||||
|
## Config
|
||||||
|
Below is a config which is accepted by the plugin
|
||||||
|
|
||||||
|
### assets
|
||||||
|
@default `"public"`
|
||||||
|
|
||||||
|
Path to the folder to expose as static
|
||||||
|
|
||||||
|
### prefix
|
||||||
|
@default `"/public"`
|
||||||
|
|
||||||
|
Path prefix to register public files
|
||||||
|
|
||||||
|
### ignorePatterns
|
||||||
|
@default `[]`
|
||||||
|
|
||||||
|
List of files to ignore from serving as static files
|
||||||
|
|
||||||
|
### staticLimit
|
||||||
|
@default `1024`
|
||||||
|
|
||||||
|
By default, the static plugin will register paths to the Router with a static name, if the limits are exceeded, paths will be lazily added to the Router to reduce memory usage.
|
||||||
|
Tradeoff memory with performance.
|
||||||
|
|
||||||
|
### alwaysStatic
|
||||||
|
@default `false`
|
||||||
|
|
||||||
|
If set to true, static files path will be registered to Router skipping the `staticLimits`.
|
||||||
|
|
||||||
|
### headers
|
||||||
|
@default `{}`
|
||||||
|
|
||||||
|
Set response headers of files
|
||||||
|
|
||||||
|
### indexHTML
|
||||||
|
@default `false`
|
||||||
|
|
||||||
|
If set to true, the `index.html` file from the static directory will be served for any request that is matching neither a route nor any existing static file.
|
||||||
|
|
||||||
|
## Pattern
|
||||||
|
Below you can find the common patterns to use the plugin.
|
||||||
|
|
||||||
|
## Single file
|
||||||
|
Suppose you want to return just a single file, you can use `file` instead of using the static plugin
|
||||||
|
```typescript
|
||||||
|
import { Elysia, file } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/file', file('public/takodachi.png'))
|
||||||
|
```
|
||||||
129
.agents/skills/elysiajs/references/bun-fullstack-dev-server.md
Normal file
129
.agents/skills/elysiajs/references/bun-fullstack-dev-server.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Fullstack Dev Server
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Bun 1.3 Fullstack Dev Server with HMR. React without bundler (no Vite/Webpack).
|
||||||
|
|
||||||
|
Example: [elysia-fullstack-example](https://github.com/saltyaom/elysia-fullstack-example)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Install + use Elysia Static:
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { staticPlugin } from '@elysiajs/static'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(await staticPlugin()) // await required for HMR hooks
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create `public/index.html` + `public/index.tsx`:
|
||||||
|
```html
|
||||||
|
<!-- public/index.html -->
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Elysia React App</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// public/index.tsx
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
const increase = () => setCount((c) => c + 1)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h2>{count}</h2>
|
||||||
|
<button onClick={increase}>Increase</button>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = createRoot(document.getElementById('root')!)
|
||||||
|
root.render(<App />)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Enable JSX in `tsconfig.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Navigate to `http://localhost:3000/public`.
|
||||||
|
|
||||||
|
Frontend + backend in single project. No bundler. Works with HMR, Tailwind, Tanstack Query, Eden Treaty, path alias.
|
||||||
|
|
||||||
|
## Custom Prefix
|
||||||
|
```typescript
|
||||||
|
.use(await staticPlugin({ prefix: '/' }))
|
||||||
|
```
|
||||||
|
|
||||||
|
Serves at `/` instead of `/public`.
|
||||||
|
|
||||||
|
## Tailwind CSS
|
||||||
|
1. Install:
|
||||||
|
```bash
|
||||||
|
bun add tailwindcss@4
|
||||||
|
bun add -d bun-plugin-tailwind
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create `bunfig.toml`:
|
||||||
|
```toml
|
||||||
|
[serve.static]
|
||||||
|
plugins = ["bun-plugin-tailwind"]
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create `public/global.css`:
|
||||||
|
```css
|
||||||
|
@tailwind base;
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Add to HTML or TS:
|
||||||
|
```html
|
||||||
|
<link rel="stylesheet" href="tailwindcss">
|
||||||
|
```
|
||||||
|
Or:
|
||||||
|
```tsx
|
||||||
|
import './global.css'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Path Alias
|
||||||
|
1. Add to `tsconfig.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@public/*": ["public/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Use:
|
||||||
|
```tsx
|
||||||
|
import '@public/global.css'
|
||||||
|
```
|
||||||
|
|
||||||
|
Works out of box.
|
||||||
|
|
||||||
|
## Production Build
|
||||||
|
```bash
|
||||||
|
bun build --compile --target bun --outfile server src/index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates single executable `server`. Include `public` folder when running.
|
||||||
187
.agents/skills/elysiajs/references/cookie.md
Normal file
187
.agents/skills/elysiajs/references/cookie.md
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# Cookie
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Reactive mutable signal for cookie interaction. Auto-encodes/decodes objects.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
No get/set - direct value access:
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', ({ cookie: { name } }) => {
|
||||||
|
// Get
|
||||||
|
name.value
|
||||||
|
|
||||||
|
// Set
|
||||||
|
name.value = "New Value"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-encodes/decodes objects. Just works.
|
||||||
|
|
||||||
|
## Reactivity
|
||||||
|
Signal-like approach. Single source of truth. Auto-sets headers, syncs values.
|
||||||
|
|
||||||
|
Cookie jar = Proxy object. Extract value always `Cookie<unknown>`, never `undefined`. Access via `.value`.
|
||||||
|
|
||||||
|
Iterate over cookie jar → only existing cookies.
|
||||||
|
|
||||||
|
## Cookie Attributes
|
||||||
|
|
||||||
|
### Direct Property Assignment
|
||||||
|
```typescript
|
||||||
|
.get('/', ({ cookie: { name } }) => {
|
||||||
|
// Get
|
||||||
|
name.domain
|
||||||
|
|
||||||
|
// Set
|
||||||
|
name.domain = 'millennium.sh'
|
||||||
|
name.httpOnly = true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### set - Reset All Properties
|
||||||
|
```typescript
|
||||||
|
.get('/', ({ cookie: { name } }) => {
|
||||||
|
name.set({
|
||||||
|
domain: 'millennium.sh',
|
||||||
|
httpOnly: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Overwrites all properties.
|
||||||
|
|
||||||
|
### add - Update Specific Properties
|
||||||
|
Like `set` but only overwrites defined properties.
|
||||||
|
|
||||||
|
## Remove Cookie
|
||||||
|
```typescript
|
||||||
|
.get('/', ({ cookie, cookie: { name } }) => {
|
||||||
|
name.remove()
|
||||||
|
// or
|
||||||
|
delete cookie.name
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cookie Schema
|
||||||
|
Strict validation + type inference with `t.Cookie`:
|
||||||
|
```typescript
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', ({ cookie: { name } }) => {
|
||||||
|
name.value = {
|
||||||
|
id: 617,
|
||||||
|
name: 'Summoning 101'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
cookie: t.Cookie({
|
||||||
|
name: t.Object({
|
||||||
|
id: t.Numeric(),
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nullable Cookie
|
||||||
|
```typescript
|
||||||
|
cookie: t.Cookie({
|
||||||
|
name: t.Optional(
|
||||||
|
t.Object({
|
||||||
|
id: t.Numeric(),
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cookie Signature
|
||||||
|
Cryptographic hash for verification. Prevents malicious modification.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new Elysia()
|
||||||
|
.get('/', ({ cookie: { profile } }) => {
|
||||||
|
profile.value = { id: 617, name: 'Summoning 101' }
|
||||||
|
}, {
|
||||||
|
cookie: t.Cookie({
|
||||||
|
profile: t.Object({
|
||||||
|
id: t.Numeric(),
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
secrets: 'Fischl von Luftschloss Narfidort',
|
||||||
|
sign: ['profile']
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-signs/unsigns.
|
||||||
|
|
||||||
|
### Global Config
|
||||||
|
```typescript
|
||||||
|
new Elysia({
|
||||||
|
cookie: {
|
||||||
|
secrets: 'Fischl von Luftschloss Narfidort',
|
||||||
|
sign: ['profile']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cookie Rotation
|
||||||
|
Auto-handles secret rotation. Old signature verification + new signature signing.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new Elysia({
|
||||||
|
cookie: {
|
||||||
|
secrets: ['Vengeance will be mine', 'Fischl von Luftschloss Narfidort']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Array = key rotation (retire old, replace with new).
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
### secrets
|
||||||
|
Secret key for signing/unsigning. Array = key rotation.
|
||||||
|
|
||||||
|
### domain
|
||||||
|
Domain Set-Cookie attribute. Default: none (current domain only).
|
||||||
|
|
||||||
|
### encode
|
||||||
|
Function to encode value. Default: `encodeURIComponent`.
|
||||||
|
|
||||||
|
### expires
|
||||||
|
Date for Expires attribute. Default: none (non-persistent, deleted on browser exit).
|
||||||
|
|
||||||
|
If both `expires` and `maxAge` set, `maxAge` takes precedence (spec-compliant clients).
|
||||||
|
|
||||||
|
### httpOnly (false)
|
||||||
|
HttpOnly attribute. If true, JS can't access via `document.cookie`.
|
||||||
|
|
||||||
|
### maxAge (undefined)
|
||||||
|
Seconds for Max-Age attribute. Rounded down to integer.
|
||||||
|
|
||||||
|
If both `expires` and `maxAge` set, `maxAge` takes precedence (spec-compliant clients).
|
||||||
|
|
||||||
|
### path
|
||||||
|
Path attribute. Default: handler path.
|
||||||
|
|
||||||
|
### priority
|
||||||
|
Priority attribute: `low` | `medium` | `high`. Not fully standardized.
|
||||||
|
|
||||||
|
### sameSite
|
||||||
|
SameSite attribute:
|
||||||
|
- `true` = Strict
|
||||||
|
- `false` = not set
|
||||||
|
- `'lax'` = Lax
|
||||||
|
- `'none'` = None (explicit cross-site)
|
||||||
|
- `'strict'` = Strict
|
||||||
|
|
||||||
|
Not fully standardized.
|
||||||
|
|
||||||
|
### secure
|
||||||
|
Secure attribute. If true, only HTTPS. Clients won't send over HTTP.
|
||||||
413
.agents/skills/elysiajs/references/deployment.md
Normal file
413
.agents/skills/elysiajs/references/deployment.md
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
# Deployment
|
||||||
|
|
||||||
|
## Production Build
|
||||||
|
|
||||||
|
### Compile to Binary (Recommended)
|
||||||
|
```bash
|
||||||
|
bun build \
|
||||||
|
--compile \
|
||||||
|
--minify-whitespace \
|
||||||
|
--minify-syntax \
|
||||||
|
--target bun \
|
||||||
|
--outfile server \
|
||||||
|
src/index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- No runtime needed on deployment server
|
||||||
|
- Smaller memory footprint (2-3x reduction)
|
||||||
|
- Faster startup
|
||||||
|
- Single portable executable
|
||||||
|
|
||||||
|
**Run the binary:**
|
||||||
|
```bash
|
||||||
|
./server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile to JavaScript
|
||||||
|
```bash
|
||||||
|
bun build \
|
||||||
|
--minify-whitespace \
|
||||||
|
--minify-syntax \
|
||||||
|
--outfile ./dist/index.js \
|
||||||
|
src/index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run:**
|
||||||
|
```bash
|
||||||
|
NODE_ENV=production bun ./dist/index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
### Basic Dockerfile
|
||||||
|
```dockerfile
|
||||||
|
FROM oven/bun:1 AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Cache dependencies
|
||||||
|
COPY package.json bun.lock ./
|
||||||
|
RUN bun install
|
||||||
|
|
||||||
|
COPY ./src ./src
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
RUN bun build \
|
||||||
|
--compile \
|
||||||
|
--minify-whitespace \
|
||||||
|
--minify-syntax \
|
||||||
|
--outfile server \
|
||||||
|
src/index.ts
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/base
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=build /app/server server
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
CMD ["./server"]
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build and Run
|
||||||
|
```bash
|
||||||
|
docker build -t my-elysia-app .
|
||||||
|
docker run -p 3000:3000 my-elysia-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Environment Variables
|
||||||
|
```dockerfile
|
||||||
|
FROM gcr.io/distroless/base
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=build /app/server server
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV DATABASE_URL=""
|
||||||
|
ENV JWT_SECRET=""
|
||||||
|
|
||||||
|
CMD ["./server"]
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cluster Mode (Multiple CPU Cores)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/index.ts
|
||||||
|
import cluster from 'node:cluster'
|
||||||
|
import os from 'node:os'
|
||||||
|
import process from 'node:process'
|
||||||
|
|
||||||
|
if (cluster.isPrimary) {
|
||||||
|
for (let i = 0; i < os.availableParallelism(); i++) {
|
||||||
|
cluster.fork()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await import('./server')
|
||||||
|
console.log(`Worker ${process.pid} started`)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/server.ts
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', () => 'Hello World!')
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
### .env File
|
||||||
|
```env
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
DATABASE_URL=postgresql://user:password@localhost:5432/db
|
||||||
|
JWT_SECRET=your-secret-key
|
||||||
|
CORS_ORIGIN=https://example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Load in App
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/env', () => ({
|
||||||
|
env: process.env.NODE_ENV,
|
||||||
|
port: process.env.PORT
|
||||||
|
}))
|
||||||
|
.listen(parseInt(process.env.PORT || '3000'))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Platform-Specific Deployments
|
||||||
|
|
||||||
|
### Railway
|
||||||
|
```typescript
|
||||||
|
// Railway assigns random PORT via env variable
|
||||||
|
new Elysia()
|
||||||
|
.get('/', () => 'Hello Railway')
|
||||||
|
.listen(process.env.PORT ?? 3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vercel
|
||||||
|
```typescript
|
||||||
|
// src/index.ts
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
export default new Elysia()
|
||||||
|
.get('/', () => 'Hello Vercel')
|
||||||
|
|
||||||
|
export const GET = app.fetch
|
||||||
|
export const POST = app.fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
// vercel.json
|
||||||
|
{
|
||||||
|
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||||
|
"bunVersion": "1.x"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cloudflare Workers
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { CloudflareAdapter } from 'elysia/adapter/cloudflare-worker'
|
||||||
|
|
||||||
|
export default new Elysia({
|
||||||
|
adapter: CloudflareAdapter
|
||||||
|
})
|
||||||
|
.get('/', () => 'Hello Cloudflare!')
|
||||||
|
.compile()
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# wrangler.toml
|
||||||
|
name = "elysia-app"
|
||||||
|
main = "src/index.ts"
|
||||||
|
compatibility_date = "2025-06-01"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node.js Adapter
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { node } from '@elysiajs/node'
|
||||||
|
|
||||||
|
const app = new Elysia({ adapter: node() })
|
||||||
|
.get('/', () => 'Hello Node.js')
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Enable AoT Compilation
|
||||||
|
```typescript
|
||||||
|
new Elysia({
|
||||||
|
aot: true // Ahead-of-time compilation
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Native Static Response
|
||||||
|
```typescript
|
||||||
|
new Elysia({
|
||||||
|
nativeStaticResponse: true
|
||||||
|
})
|
||||||
|
.get('/version', 1) // Optimized for Bun.serve.static
|
||||||
|
```
|
||||||
|
|
||||||
|
### Precompile Routes
|
||||||
|
```typescript
|
||||||
|
new Elysia({
|
||||||
|
precompile: true // Compile all routes ahead of time
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Health Checks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new Elysia()
|
||||||
|
.get('/health', () => ({
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: Date.now()
|
||||||
|
}))
|
||||||
|
.get('/ready', ({ db }) => {
|
||||||
|
// Check database connection
|
||||||
|
const isDbReady = checkDbConnection()
|
||||||
|
|
||||||
|
if (!isDbReady) {
|
||||||
|
return status(503, { status: 'not ready' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: 'ready' }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Graceful Shutdown
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/', () => 'Hello')
|
||||||
|
.listen(3000)
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
console.log('SIGTERM received, shutting down gracefully')
|
||||||
|
app.stop()
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('SIGINT received, shutting down gracefully')
|
||||||
|
app.stop()
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
### OpenTelemetry
|
||||||
|
```typescript
|
||||||
|
import { opentelemetry } from '@elysiajs/opentelemetry'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(opentelemetry({
|
||||||
|
serviceName: 'my-service',
|
||||||
|
endpoint: 'http://localhost:4318'
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Logging
|
||||||
|
```typescript
|
||||||
|
.onRequest(({ request }) => {
|
||||||
|
console.log(`[${new Date().toISOString()}] ${request.method} ${request.url}`)
|
||||||
|
})
|
||||||
|
.onAfterResponse(({ request, set }) => {
|
||||||
|
console.log(`[${new Date().toISOString()}] ${request.method} ${request.url} - ${set.status}`)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## SSL/TLS (HTTPS)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia, file } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia({
|
||||||
|
serve: {
|
||||||
|
tls: {
|
||||||
|
cert: file('cert.pem'),
|
||||||
|
key: file('key.pem')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get('/', () => 'Hello HTTPS')
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Always compile to binary for production**
|
||||||
|
- Reduces memory usage
|
||||||
|
- Smaller deployment size
|
||||||
|
- No runtime needed
|
||||||
|
|
||||||
|
2. **Use environment variables**
|
||||||
|
- Never hardcode secrets
|
||||||
|
- Use different configs per environment
|
||||||
|
|
||||||
|
3. **Enable health checks**
|
||||||
|
- Essential for load balancers
|
||||||
|
- K8s/Docker orchestration
|
||||||
|
|
||||||
|
4. **Implement graceful shutdown**
|
||||||
|
- Handle SIGTERM/SIGINT
|
||||||
|
- Close connections properly
|
||||||
|
|
||||||
|
5. **Use cluster mode**
|
||||||
|
- Utilize all CPU cores
|
||||||
|
- Better performance under load
|
||||||
|
|
||||||
|
6. **Monitor your app**
|
||||||
|
- Use OpenTelemetry
|
||||||
|
- Log requests/responses
|
||||||
|
- Track errors
|
||||||
|
|
||||||
|
## Example Production Setup
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/server.ts
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { cors } from '@elysiajs/cors'
|
||||||
|
import { opentelemetry } from '@elysiajs/opentelemetry'
|
||||||
|
|
||||||
|
export const app = new Elysia({
|
||||||
|
aot: true,
|
||||||
|
nativeStaticResponse: true
|
||||||
|
})
|
||||||
|
.use(cors({
|
||||||
|
origin: process.env.CORS_ORIGIN || 'http://localhost:3000'
|
||||||
|
}))
|
||||||
|
.use(opentelemetry({
|
||||||
|
serviceName: 'my-service'
|
||||||
|
}))
|
||||||
|
.get('/health', () => ({ status: 'ok' }))
|
||||||
|
.get('/', () => 'Hello Production')
|
||||||
|
.listen(parseInt(process.env.PORT || '3000'))
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
app.stop()
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/index.ts (cluster)
|
||||||
|
import cluster from 'node:cluster'
|
||||||
|
import os from 'node:os'
|
||||||
|
|
||||||
|
if (cluster.isPrimary) {
|
||||||
|
for (let i = 0; i < os.availableParallelism(); i++) {
|
||||||
|
cluster.fork()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await import('./server')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Dockerfile
|
||||||
|
FROM oven/bun:1 AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json bun.lock ./
|
||||||
|
RUN bun install
|
||||||
|
|
||||||
|
COPY ./src ./src
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
RUN bun build --compile --outfile server src/index.ts
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/base
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=build /app/server server
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
CMD ["./server"]
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
```
|
||||||
158
.agents/skills/elysiajs/references/eden.md
Normal file
158
.agents/skills/elysiajs/references/eden.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# Eden Treaty
|
||||||
|
e2e type safe RPC client for share type from backend to frontend.
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Type-safe object representation for Elysia server. Auto-completion + error handling.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/eden
|
||||||
|
bun add -d elysia
|
||||||
|
```
|
||||||
|
|
||||||
|
Export Elysia server type:
|
||||||
|
```typescript
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/', () => 'Hi Elysia')
|
||||||
|
.get('/id/:id', ({ params: { id } }) => id)
|
||||||
|
.post('/mirror', ({ body }) => body, {
|
||||||
|
body: t.Object({
|
||||||
|
id: t.Number(),
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.listen(3000)
|
||||||
|
|
||||||
|
export type App = typeof app
|
||||||
|
```
|
||||||
|
|
||||||
|
Consume on client side:
|
||||||
|
```typescript
|
||||||
|
import { treaty } from '@elysiajs/eden'
|
||||||
|
import type { App } from './server'
|
||||||
|
|
||||||
|
const client = treaty<App>('localhost:3000')
|
||||||
|
|
||||||
|
// response: Hi Elysia
|
||||||
|
const { data: index } = await client.get()
|
||||||
|
|
||||||
|
// response: 1895
|
||||||
|
const { data: id } = await client.id({ id: 1895 }).get()
|
||||||
|
|
||||||
|
// response: { id: 1895, name: 'Skadi' }
|
||||||
|
const { data: nendoroid } = await client.mirror.post({
|
||||||
|
id: 1895,
|
||||||
|
name: 'Skadi'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Errors & Fixes
|
||||||
|
- **Strict mode**: Enable in tsconfig
|
||||||
|
- **Version mismatch**: `npm why elysia` - must match server/client
|
||||||
|
- **TypeScript**: Min 5.0
|
||||||
|
- **Method chaining**: Required on server
|
||||||
|
- **Bun types**: `bun add -d @types/bun` if using Bun APIs
|
||||||
|
- **Path alias**: Must resolve same on frontend/backend
|
||||||
|
|
||||||
|
### Monorepo Path Alias
|
||||||
|
Must resolve to same file on frontend/backend
|
||||||
|
|
||||||
|
```json
|
||||||
|
// tsconfig.json at root
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@frontend/*": ["./apps/frontend/src/*"],
|
||||||
|
"@backend/*": ["./apps/backend/src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Syntax Mapping
|
||||||
|
| Path | Method | Treaty |
|
||||||
|
|----------------|--------|-------------------------------|
|
||||||
|
| / | GET | `.get()` |
|
||||||
|
| /hi | GET | `.hi.get()` |
|
||||||
|
| /deep/nested | POST | `.deep.nested.post()` |
|
||||||
|
| /item/:name | GET | `.item({ name: 'x' }).get()` |
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
### With body (POST/PUT/PATCH/DELETE):
|
||||||
|
```typescript
|
||||||
|
.user.post(
|
||||||
|
{ name: 'Elysia' }, // body
|
||||||
|
{ headers: {}, query: {}, fetch: {} } // optional
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### No body (GET/HEAD):
|
||||||
|
```typescript
|
||||||
|
.hello.get({ headers: {}, query: {}, fetch: {} })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Empty body with query/headers:
|
||||||
|
```typescript
|
||||||
|
.user.post(null, { query: { name: 'Ely' } })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fetch options:
|
||||||
|
```typescript
|
||||||
|
.hello.get({ fetch: { signal: controller.signal } })
|
||||||
|
```
|
||||||
|
|
||||||
|
### File upload:
|
||||||
|
```typescript
|
||||||
|
// Accepts: File | File[] | FileList | Blob
|
||||||
|
.image.post({
|
||||||
|
title: 'Title',
|
||||||
|
image: fileInput.files!
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response
|
||||||
|
```typescript
|
||||||
|
const { data, error, response, status, headers } = await api.user.post({ name: 'x' })
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
switch (error.status) {
|
||||||
|
case 400: throw error.value
|
||||||
|
default: throw error.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// data unwrapped after error handling
|
||||||
|
return data
|
||||||
|
```
|
||||||
|
|
||||||
|
status >= 300 → `data = null`, `error` has value
|
||||||
|
|
||||||
|
## Stream/SSE
|
||||||
|
Interpreted as `AsyncGenerator`:
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await treaty(app).ok.get()
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
for await (const chunk of data) console.log(chunk)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utility Types
|
||||||
|
```typescript
|
||||||
|
import { Treaty } from '@elysiajs/eden'
|
||||||
|
|
||||||
|
type UserData = Treaty.Data<typeof api.user.post>
|
||||||
|
type UserError = Treaty.Error<typeof api.user.post>
|
||||||
|
```
|
||||||
|
|
||||||
|
## WebSocket
|
||||||
|
```typescript
|
||||||
|
const chat = api.chat.subscribe()
|
||||||
|
|
||||||
|
chat.subscribe((message) => console.log('got', message))
|
||||||
|
chat.on('open', () => chat.send('hello'))
|
||||||
|
|
||||||
|
// Native access: chat.raw
|
||||||
|
```
|
||||||
|
|
||||||
|
`.subscribe()` accepts same params as `get`/`head`
|
||||||
198
.agents/skills/elysiajs/references/lifecycle.md
Normal file
198
.agents/skills/elysiajs/references/lifecycle.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# Lifecycle
|
||||||
|
|
||||||
|
Instead of a sequential process, Elysia's request handling is divided into multiple stages called lifecycle events.
|
||||||
|
|
||||||
|
It's designed to separate the process into distinct phases based on their responsibility without interfering with each others.
|
||||||
|
|
||||||
|
### List of events in order
|
||||||
|
|
||||||
|
1. **request** - early, global
|
||||||
|
2. **parse** - body parsing
|
||||||
|
3. **transform** / **derive** - mutate context pre validation
|
||||||
|
4. **beforeHandle** / **resolve** - auth/guard logic
|
||||||
|
5. **handler** - your business code
|
||||||
|
6. **afterHandle** - tweak response, set headers
|
||||||
|
7. **mapResponse** - turn anything into a proper `Response`
|
||||||
|
8. **onError** - centralized error handling
|
||||||
|
9. **onAfterResponse** - post response/cleanup tasks
|
||||||
|
|
||||||
|
## Request (`onRequest`)
|
||||||
|
|
||||||
|
Runs first for every incoming request.
|
||||||
|
|
||||||
|
- Ideal for **caching, rate limiting, CORS, adding global headers**.
|
||||||
|
- If the hook returns a value, the whole lifecycle stops and that value becomes the response.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().onRequest(({ ip, set }) => {
|
||||||
|
if (blocked(ip)) return (set.status = 429)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parse (`onParse`)
|
||||||
|
|
||||||
|
_Body parsing stage._
|
||||||
|
|
||||||
|
- Handles `text/plain`, `application/json`, `multipart/form-data`, `application/x www-form-urlencoded` by default.
|
||||||
|
- Use to add **custom parsers** or support extra `Content Type`s.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().onParse(({ request, contentType }) => {
|
||||||
|
if (contentType === 'application/custom') return request.text()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Transform (`onTransform`)
|
||||||
|
|
||||||
|
_Runs **just before validation**; can mutate the request context._
|
||||||
|
|
||||||
|
- Perfect for **type coercion**, trimming strings, or adding temporary fields that validation will use.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().onTransform(({ params }) => {
|
||||||
|
params.id = Number(params.id)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Derive
|
||||||
|
|
||||||
|
_Runs along with `onTransform` **but before validation**; adds per request values to the context._
|
||||||
|
|
||||||
|
- Useful for extracting info from headers, cookies, query, etc., that you want to reuse in handlers.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().derive(({ headers }) => ({
|
||||||
|
bearer: headers.authorization?.replace(/^Bearer /, '')
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Before Handle (`onBeforeHandle`)
|
||||||
|
|
||||||
|
_Executed after validation, right before the route handler._
|
||||||
|
|
||||||
|
- Great for **auth checks, permission gating, custom pre validation logic**.
|
||||||
|
- Returning a value skips the handler.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().get('/', () => 'hi', {
|
||||||
|
beforeHandle({ cookie, status }) {
|
||||||
|
if (!cookie.session) return status(401)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resolve
|
||||||
|
|
||||||
|
_Like `derive` but runs **after validation** along "Before Handle" (so you can rely on validated data)._
|
||||||
|
|
||||||
|
- Usually placed inside a `guard` because it isn't available as a local hook.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().guard(
|
||||||
|
{ headers: t.Object({ authorization: t.String() }) },
|
||||||
|
(app) =>
|
||||||
|
app
|
||||||
|
.resolve(({ headers }) => ({
|
||||||
|
bearer: headers.authorization.split(' ')[1]
|
||||||
|
}))
|
||||||
|
.get('/', ({ bearer }) => bearer)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## After Handle (`onAfterHandle`)
|
||||||
|
|
||||||
|
_Runs after the handler finishes._
|
||||||
|
|
||||||
|
- Can **modify response headers**, wrap the result in a `Response`, or transform the payload.
|
||||||
|
- Returning a value **replaces** the handler’s result, but the next `afterHandle` hooks still run.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().get('/', () => '<h1>Hello</h1>', {
|
||||||
|
afterHandle({ response, set }) {
|
||||||
|
if (isHtml(response)) {
|
||||||
|
set.headers['content-type'] = 'text/html; charset=utf-8'
|
||||||
|
return new Response(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Map Response (`mapResponse`)
|
||||||
|
|
||||||
|
_Runs right after all `afterHandle` hooks; maps **any** value to a Web standard `Response`._
|
||||||
|
|
||||||
|
- Ideal for **compression, custom content type mapping, streaming**.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().mapResponse(({ responseValue, set }) => {
|
||||||
|
const body =
|
||||||
|
typeof responseValue === 'object'
|
||||||
|
? JSON.stringify(responseValue)
|
||||||
|
: String(responseValue ?? '')
|
||||||
|
|
||||||
|
set.headers['content-encoding'] = 'gzip'
|
||||||
|
return new Response(Bun.gzipSync(new TextEncoder().encode(body)), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type':
|
||||||
|
typeof responseValue === 'object'
|
||||||
|
? 'application/json'
|
||||||
|
: 'text/plain'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## On Error (`onError`)
|
||||||
|
|
||||||
|
_Caught whenever an error bubbles up from any lifecycle stage._
|
||||||
|
|
||||||
|
- Use to **customize error messages**, **handle 404**, **log**, or **retry**.
|
||||||
|
- Must be registered **before** the routes it should protect.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().onError(({ code, status }) => {
|
||||||
|
if (code === 'NOT_FOUND') return status(404, 'â“ Not found')
|
||||||
|
return new Response('Oops', { status: 500 })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## After Response (`onAfterResponse`)
|
||||||
|
|
||||||
|
_Runs **after** the response has been sent to the client._
|
||||||
|
|
||||||
|
- Perfect for **logging, metrics, cleanup**.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().onAfterResponse(() =>
|
||||||
|
console.log('✅ response sent at', Date.now())
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hook Types
|
||||||
|
|
||||||
|
| Type | Scope | How to add |
|
||||||
|
| -------------------- | --------------------------------- | --------------------------------------------------------- |
|
||||||
|
| **Local Hook** | Single route | Inside route options (`afterHandle`, `beforeHandle`, …) |
|
||||||
|
| **Interceptor Hook** | Whole instance (and later routes) | `.onXxx(cb)` or `.use(plugin)` |
|
||||||
|
|
||||||
|
> **Remember:** Hooks only affect routes **defined after** they are registered, except `onRequest` which is global because it runs before route matching.
|
||||||
83
.agents/skills/elysiajs/references/macro.md
Normal file
83
.agents/skills/elysiajs/references/macro.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Macro
|
||||||
|
|
||||||
|
Composable Elysia function for controlling lifecycle/schema/context with full type safety. Available in hook after definition control by key-value label.
|
||||||
|
|
||||||
|
## Basic Pattern
|
||||||
|
```typescript
|
||||||
|
.macro({
|
||||||
|
hi: (word: string) => ({
|
||||||
|
beforeHandle() { console.log(word) }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get('/', () => 'hi', { hi: 'Elysia' })
|
||||||
|
```
|
||||||
|
|
||||||
|
## Property Shorthand
|
||||||
|
Object → function accepting boolean:
|
||||||
|
```typescript
|
||||||
|
.macro({
|
||||||
|
// These equivalent:
|
||||||
|
isAuth: { resolve: () => ({ user: 'saltyaom' }) },
|
||||||
|
isAuth(enabled: boolean) { if(enabled) return { resolve() {...} } }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
Return `status`, don't throw:
|
||||||
|
```typescript
|
||||||
|
.macro({
|
||||||
|
auth: {
|
||||||
|
resolve({ headers }) {
|
||||||
|
if(!headers.authorization) return status(401, 'Unauthorized')
|
||||||
|
return { user: 'SaltyAom' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resolve - Add Context Props
|
||||||
|
```typescript
|
||||||
|
.macro({
|
||||||
|
user: (enabled: true) => ({
|
||||||
|
resolve: () => ({ user: 'Pardofelis' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get('/', ({ user }) => user, { user: true })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Named Macro for Type Inference
|
||||||
|
TypeScript limitation workaround:
|
||||||
|
```typescript
|
||||||
|
.macro('user', { resolve: () => ({ user: 'lilith' }) })
|
||||||
|
.macro('user2', { user: true, resolve: ({ user }) => {} })
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
Auto-validates, infers types, stacks with other schemas:
|
||||||
|
```typescript
|
||||||
|
.macro({
|
||||||
|
withFriends: {
|
||||||
|
body: t.Object({ friends: t.Tuple([...]) })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Use named single macro for lifecycle type inference within same macro.
|
||||||
|
|
||||||
|
## Extension
|
||||||
|
Stack macros:
|
||||||
|
```typescript
|
||||||
|
.macro({
|
||||||
|
sartre: { body: t.Object({...}) },
|
||||||
|
fouco: { body: t.Object({...}) },
|
||||||
|
lilith: { fouco: true, sartre: true, body: t.Object({...}) }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deduplication
|
||||||
|
Auto-dedupes by property value. Custom seed:
|
||||||
|
```typescript
|
||||||
|
.macro({ sartre: (role: string) => ({ seed: role, ... }) })
|
||||||
|
```
|
||||||
|
|
||||||
|
Max stack: 16 (prevents infinite loops)
|
||||||
207
.agents/skills/elysiajs/references/plugin.md
Normal file
207
.agents/skills/elysiajs/references/plugin.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# Plugins
|
||||||
|
|
||||||
|
## Plugin = Decoupled Elysia Instance
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const plugin = new Elysia()
|
||||||
|
.decorate('plugin', 'hi')
|
||||||
|
.get('/plugin', ({ plugin }) => plugin)
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(plugin) // inherit properties
|
||||||
|
.get('/', ({ plugin }) => plugin)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Inherits**: state, decorate
|
||||||
|
**Does NOT inherit**: lifecycle (isolated by default)
|
||||||
|
|
||||||
|
## Dependency
|
||||||
|
|
||||||
|
Each instance runs independently like microservice. **Must explicitly declare dependencies**.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const auth = new Elysia()
|
||||||
|
.decorate('Auth', Auth)
|
||||||
|
|
||||||
|
// ❌ Missing dependency
|
||||||
|
const main = new Elysia()
|
||||||
|
.get('/', ({ Auth }) => Auth.getProfile())
|
||||||
|
|
||||||
|
// ✅ Declare dependency
|
||||||
|
const main = new Elysia()
|
||||||
|
.use(auth) // required for Auth
|
||||||
|
.get('/', ({ Auth }) => Auth.getProfile())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deduplication
|
||||||
|
|
||||||
|
**Every plugin re-executes by default**. Use `name` + optional `seed` to deduplicate:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const ip = new Elysia({ name: 'ip' }) // unique identifier
|
||||||
|
.derive({ as: 'global' }, ({ server, request }) => ({
|
||||||
|
ip: server?.requestIP(request)
|
||||||
|
}))
|
||||||
|
|
||||||
|
const router1 = new Elysia().use(ip)
|
||||||
|
const router2 = new Elysia().use(ip)
|
||||||
|
const server = new Elysia().use(router1).use(router2)
|
||||||
|
// `ip` only executes once due to deduplication
|
||||||
|
```
|
||||||
|
|
||||||
|
## Global vs Explicit Dependency
|
||||||
|
|
||||||
|
**Global plugin** (rare, apply everywhere):
|
||||||
|
- Doesn't add types - cors, compress, helmet
|
||||||
|
- Global lifecycle no instance controls - tracing, logging
|
||||||
|
- Examples: OpenAPI docs, OpenTelemetry, logging
|
||||||
|
|
||||||
|
**Explicit dependency** (default, recommended):
|
||||||
|
- Adds types - macro, state, model
|
||||||
|
- Business logic instances interact with - Auth, DB
|
||||||
|
- Examples: state management, ORM, auth, features
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
**Lifecycle isolated by default**. Must specify scope to export.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// ❌ NOT inherited by app
|
||||||
|
const profile = new Elysia()
|
||||||
|
.onBeforeHandle(({ cookie }) => throwIfNotSignIn(cookie))
|
||||||
|
.get('/profile', () => 'Hi')
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(profile)
|
||||||
|
.patch('/rename', ({ body }) => updateProfile(body)) // No sign-in check
|
||||||
|
|
||||||
|
// ✅ Exported to app
|
||||||
|
const profile = new Elysia()
|
||||||
|
.onBeforeHandle({ as: 'global' }, ({ cookie }) => throwIfNotSignIn(cookie))
|
||||||
|
.get('/profile', () => 'Hi')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scope Levels
|
||||||
|
|
||||||
|
1. **local** (default) - current + descendants only
|
||||||
|
2. **scoped** - parent + current + descendants
|
||||||
|
3. **global** - all instances (all parents, current, descendants)
|
||||||
|
|
||||||
|
Example with `.onBeforeHandle({ as: 'local' }, ...)`:
|
||||||
|
|
||||||
|
| type | child | current | parent | main |
|
||||||
|
|------|-------|---------|--------|------|
|
||||||
|
| local | ✅ | ✅ | ❌ | ❌ |
|
||||||
|
| scoped | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| global | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Instance factory with config
|
||||||
|
const version = (v = 1) => new Elysia()
|
||||||
|
.get('/version', v)
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(version(1))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Functional Callback (not recommended)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Harder to handle scope/encapsulation
|
||||||
|
const plugin = (app: Elysia) => app
|
||||||
|
.state('counter', 0)
|
||||||
|
.get('/plugin', () => 'Hi')
|
||||||
|
|
||||||
|
// Prefer new instance (better type inference, no perf diff)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Guard (Apply to Multiple Routes)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
.guard(
|
||||||
|
{ body: t.Object({ username: t.String(), password: t.String() }) },
|
||||||
|
(app) =>
|
||||||
|
app.post('/sign-up', ({ body }) => signUp(body))
|
||||||
|
.post('/sign-in', ({ body }) => signIn(body))
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Grouped guard** (merge group + guard):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
.group(
|
||||||
|
'/v1',
|
||||||
|
{ body: t.Literal('Rikuhachima Aru') }, // guard here
|
||||||
|
(app) => app.post('/student', ({ body }) => body)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scope Casting
|
||||||
|
|
||||||
|
**3 methods to apply hook to parent**:
|
||||||
|
|
||||||
|
1. **Inline as** (single hook):
|
||||||
|
```ts
|
||||||
|
.derive({ as: 'scoped' }, () => ({ hi: 'ok' }))
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Guard as** (multiple hooks, no derive/resolve):
|
||||||
|
```ts
|
||||||
|
.guard({
|
||||||
|
as: 'scoped',
|
||||||
|
response: t.String(),
|
||||||
|
beforeHandle() { console.log('ok') }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Instance as** (all hooks + schema):
|
||||||
|
```ts
|
||||||
|
const plugin = new Elysia()
|
||||||
|
.derive(() => ({ hi: 'ok' }))
|
||||||
|
.get('/child', ({ hi }) => hi)
|
||||||
|
.as('scoped') // lift scope up
|
||||||
|
```
|
||||||
|
|
||||||
|
`.as()` lifts scope: local → scoped → global
|
||||||
|
|
||||||
|
## Lazy Load
|
||||||
|
|
||||||
|
**Deferred module** (async plugin, non-blocking startup):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// plugin.ts
|
||||||
|
export const loadStatic = async (app: Elysia) => {
|
||||||
|
const files = await loadAllFiles()
|
||||||
|
files.forEach((asset) => app.get(asset, file(asset)))
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
// main.ts
|
||||||
|
const app = new Elysia().use(loadStatic)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Lazy-load module** (dynamic import):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(import('./plugin')) // loaded after startup
|
||||||
|
```
|
||||||
|
|
||||||
|
**Testing** (wait for modules):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await app.modules // ensure all deferred/lazy modules loaded
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
[Inference] Based on docs patterns:
|
||||||
|
- Use inline values for static resources (performance optimization)
|
||||||
|
- Group routes by prefix for organization
|
||||||
|
- Extend context minimally (separation of concerns)
|
||||||
|
- Use `status()` over `set.status` for type safety
|
||||||
|
- Prefer `resolve()` over `derive()` when type integrity matters
|
||||||
|
- Plugins isolated by default (must declare scope explicitly)
|
||||||
|
- Use `name` for deduplication when plugin used multiple times
|
||||||
|
- Prefer explicit dependency over global (better modularity/tracking)
|
||||||
331
.agents/skills/elysiajs/references/route.md
Normal file
331
.agents/skills/elysiajs/references/route.md
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
# ElysiaJS: Routing, Handlers & Context
|
||||||
|
|
||||||
|
## Routing
|
||||||
|
|
||||||
|
### Path Types
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia()
|
||||||
|
.get('/static', 'static path') // exact match
|
||||||
|
.get('/id/:id', 'dynamic path') // captures segment
|
||||||
|
.get('/id/*', 'wildcard path') // captures rest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Path Priority**: static > dynamic > wildcard
|
||||||
|
|
||||||
|
### Dynamic Paths
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia()
|
||||||
|
.get('/id/:id', ({ params: { id } }) => id)
|
||||||
|
.get('/id/:id/:name', ({ params: { id, name } }) => id + ' ' + name)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Optional params**: `.get('/id/:id?', ...)`
|
||||||
|
|
||||||
|
### HTTP Verbs
|
||||||
|
|
||||||
|
- `.get()` - retrieve data
|
||||||
|
- `.post()` - submit/create
|
||||||
|
- `.put()` - replace
|
||||||
|
- `.patch()` - partial update
|
||||||
|
- `.delete()` - remove
|
||||||
|
- `.all()` - any method
|
||||||
|
- `.route(method, path, handler)` - custom verb
|
||||||
|
|
||||||
|
### Grouping Routes
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia()
|
||||||
|
.group('/user', { body: t.Literal('auth') }, (app) =>
|
||||||
|
app.post('/sign-in', ...)
|
||||||
|
.post('/sign-up', ...)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Or use prefix in constructor
|
||||||
|
new Elysia({ prefix: '/user' })
|
||||||
|
.post('/sign-in', ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handlers
|
||||||
|
|
||||||
|
### Handler = function accepting HTTP request, returning response
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Inline value (compiled ahead, optimized)
|
||||||
|
.get('/', 'Hello Elysia')
|
||||||
|
.get('/video', file('video.mp4'))
|
||||||
|
|
||||||
|
// Function handler
|
||||||
|
.get('/', () => 'hello')
|
||||||
|
.get('/', ({ params, query, body }) => {...})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Context Properties
|
||||||
|
|
||||||
|
- `body` - HTTP message/form/file
|
||||||
|
- `query` - query string as object
|
||||||
|
- `params` - path parameters
|
||||||
|
- `headers` - HTTP headers
|
||||||
|
- `cookie` - mutable signal for cookies
|
||||||
|
- `store` - global mutable state
|
||||||
|
- `request` - Web Standard Request
|
||||||
|
- `server` - Bun server instance
|
||||||
|
- `path` - request pathname
|
||||||
|
|
||||||
|
### Context Utilities
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { redirect, form } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia().get('/', ({ status, set, form }) => {
|
||||||
|
// Status code (type-safe)
|
||||||
|
status(418, "I'm a teapot")
|
||||||
|
|
||||||
|
// Set response props
|
||||||
|
set.headers['x-custom'] = 'value'
|
||||||
|
set.status = 418 // legacy, no type inference
|
||||||
|
|
||||||
|
// Redirect
|
||||||
|
return redirect('https://...', 302)
|
||||||
|
|
||||||
|
// Cookies (mutable signal, no get/set)
|
||||||
|
cookie.name.value // get
|
||||||
|
cookie.name.value = 'new' // set
|
||||||
|
|
||||||
|
// FormData response
|
||||||
|
return form({ name: 'Party', images: [file('a.jpg')] })
|
||||||
|
|
||||||
|
// Single file
|
||||||
|
return file('document.pdf')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Streaming
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia()
|
||||||
|
.get('/stream', function* () {
|
||||||
|
yield 1
|
||||||
|
yield 2
|
||||||
|
yield 3
|
||||||
|
})
|
||||||
|
// Server-Sent Events
|
||||||
|
.get('/sse', function* () {
|
||||||
|
yield sse('hello')
|
||||||
|
yield sse({ event: 'msg', data: {...} })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Headers only settable before first yield
|
||||||
|
|
||||||
|
**Conditional stream**: returning without yield converts to normal response
|
||||||
|
|
||||||
|
## Context Extension
|
||||||
|
|
||||||
|
[Inference] Extend when property is:
|
||||||
|
|
||||||
|
- Global mutable (use `state`)
|
||||||
|
- Request/response related (use `decorate`)
|
||||||
|
- Derived from existing props (use `derive`/`resolve`)
|
||||||
|
|
||||||
|
### state() - Global Mutable
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia()
|
||||||
|
`.state('version', 1)
|
||||||
|
.get('/', ({ store: { version } }) => version)
|
||||||
|
// Multiple
|
||||||
|
.state({ counter: 0, visits: 0 })
|
||||||
|
|
||||||
|
// Remap (create new from existing)
|
||||||
|
.state(({ version, ...store }) => ({
|
||||||
|
...store,
|
||||||
|
apiVersion: version
|
||||||
|
}))
|
||||||
|
````
|
||||||
|
|
||||||
|
**Gotcha**: Use reference not value
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia()
|
||||||
|
// ✅ Correct
|
||||||
|
.get('/', ({ store }) => store.counter++)
|
||||||
|
|
||||||
|
// ❌ Wrong - loses reference
|
||||||
|
.get('/', ({ store: { counter } }) => counter++)
|
||||||
|
```
|
||||||
|
|
||||||
|
### decorate() - Additional Context Props
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia()
|
||||||
|
.decorate('logger', new Logger())
|
||||||
|
.get('/', ({ logger }) => logger.log('hi'))
|
||||||
|
|
||||||
|
// Multiple
|
||||||
|
.decorate({ logger: new Logger(), db: connection })
|
||||||
|
```
|
||||||
|
|
||||||
|
**When**: constant/readonly values, classes with internal state, singletons
|
||||||
|
|
||||||
|
### derive() - Create from Existing (Transform Lifecycle)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia()
|
||||||
|
.derive(({ headers }) => ({
|
||||||
|
bearer: headers.authorization?.startsWith('Bearer ')
|
||||||
|
? headers.authorization.slice(7)
|
||||||
|
: null
|
||||||
|
}))
|
||||||
|
.get('/', ({ bearer }) => bearer)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Timing**: runs at transform (before validation)
|
||||||
|
**Type safety**: request props typed as `unknown`
|
||||||
|
|
||||||
|
### resolve() - Type-Safe Derive (beforeHandle Lifecycle)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia()
|
||||||
|
.guard({
|
||||||
|
headers: t.Object({
|
||||||
|
bearer: t.String({ pattern: '^Bearer .+$' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.resolve(({ headers }) => ({
|
||||||
|
bearer: headers.bearer.slice(7) // typed correctly
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Timing**: runs at beforeHandle (after validation)
|
||||||
|
**Type safety**: request props fully typed
|
||||||
|
|
||||||
|
### Error from derive/resolve
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia()
|
||||||
|
.derive(({ headers, status }) => {
|
||||||
|
if (!headers.authorization) return status(400)
|
||||||
|
return { bearer: ... }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns early if error returned
|
||||||
|
|
||||||
|
## Patterns
|
||||||
|
|
||||||
|
### Affix (Bulk Remap)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const plugin = new Elysia({ name: 'setup' }).decorate({
|
||||||
|
argon: 'a',
|
||||||
|
boron: 'b'
|
||||||
|
})
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(plugin)
|
||||||
|
.prefix('decorator', 'setup') // setupArgon, setupBoron
|
||||||
|
.prefix('all', 'setup') // remap everything
|
||||||
|
```
|
||||||
|
|
||||||
|
### Assignment Patterns
|
||||||
|
|
||||||
|
1. **key-value**: `.state('key', value)`
|
||||||
|
2. **object**: `.state({ k1: v1, k2: v2 })`
|
||||||
|
3. **remap**: `.state(({old}) => ({new}))`
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const app = new Elysia().get('/', 'hi')
|
||||||
|
|
||||||
|
// Programmatic test
|
||||||
|
app.handle(new Request('http://localhost/'))
|
||||||
|
```
|
||||||
|
|
||||||
|
## To Throw or Return
|
||||||
|
|
||||||
|
Most of an error handling in Elysia can be done by throwing an error and will be handle in `onError`.
|
||||||
|
|
||||||
|
But for `status` it can be a little bit confusing, since it can be used both as a return value or throw an error.
|
||||||
|
|
||||||
|
It could either be **return** or **throw** based on your specific needs.
|
||||||
|
|
||||||
|
- If an `status` is **throw**, it will be caught by `onError` middleware.
|
||||||
|
- If an `status` is **return**, it will be **NOT** caught by `onError` middleware.
|
||||||
|
|
||||||
|
See the following code:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia, file } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.onError(({ code, error, path }) => {
|
||||||
|
if (code === 418) return 'caught'
|
||||||
|
})
|
||||||
|
.get('/throw', ({ status }) => {
|
||||||
|
// This will be caught by onError
|
||||||
|
throw status(418)
|
||||||
|
})
|
||||||
|
.get('/return', ({ status }) => {
|
||||||
|
// This will NOT be caught by onError
|
||||||
|
return status(418)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## To Throw or Return
|
||||||
|
|
||||||
|
Elysia provide a `status` function for returning HTTP status code, prefers over `set.status`.
|
||||||
|
|
||||||
|
`status` can be import from Elysia but preferably extract from route handler Context for type safety.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Elysia, status } from 'elysia'
|
||||||
|
|
||||||
|
function doThing() {
|
||||||
|
if (Math.random() > 0.33) return status(418, "I'm a teapot")
|
||||||
|
}
|
||||||
|
|
||||||
|
new Elysia().get('/', ({ status }) => {
|
||||||
|
if (Math.random() > 0.33) return status(418)
|
||||||
|
|
||||||
|
return 'ok'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Error Handling in Elysia can be done by throwing an error and will be handle in `onError`.
|
||||||
|
|
||||||
|
Status could either be **return** or **throw** based on your specific needs.
|
||||||
|
|
||||||
|
- If an `status` is **throw**, it will be caught by `onError` middleware.
|
||||||
|
- If an `status` is **return**, it will be **NOT** caught by `onError` middleware.
|
||||||
|
|
||||||
|
See the following code:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia, file } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.onError(({ code, error, path }) => {
|
||||||
|
if (code === 418) return 'caught'
|
||||||
|
})
|
||||||
|
.get('/throw', ({ status }) => {
|
||||||
|
// This will be caught by onError
|
||||||
|
throw status(418)
|
||||||
|
})
|
||||||
|
.get('/return', ({ status }) => {
|
||||||
|
// This will NOT be caught by onError
|
||||||
|
return status(418)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
[Inference] Based on docs patterns:
|
||||||
|
|
||||||
|
- Use inline values for static resources (performance optimization)
|
||||||
|
- Group routes by prefix for organization
|
||||||
|
- Extend context minimally (separation of concerns)
|
||||||
|
- Use `status()` over `set.status` for type safety
|
||||||
|
- Prefer `resolve()` over `derive()` when type integrity matters
|
||||||
385
.agents/skills/elysiajs/references/testing.md
Normal file
385
.agents/skills/elysiajs/references/testing.md
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
# Unit Testing
|
||||||
|
|
||||||
|
## Basic Test Setup
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
```bash
|
||||||
|
bun add -d @elysiajs/eden
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic Test
|
||||||
|
```typescript
|
||||||
|
// test/app.test.ts
|
||||||
|
import { describe, expect, it } from 'bun:test'
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
describe('Elysia App', () => {
|
||||||
|
it('should return hello world', async () => {
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/', () => 'Hello World')
|
||||||
|
|
||||||
|
const res = await app.handle(
|
||||||
|
new Request('http://localhost/')
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.text()).toBe('Hello World')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Routes
|
||||||
|
|
||||||
|
### GET Request
|
||||||
|
```typescript
|
||||||
|
it('should get user by id', async () => {
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/user/:id', ({ params: { id } }) => ({
|
||||||
|
id,
|
||||||
|
name: 'John Doe'
|
||||||
|
}))
|
||||||
|
|
||||||
|
const res = await app.handle(
|
||||||
|
new Request('http://localhost/user/123')
|
||||||
|
)
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(data).toEqual({
|
||||||
|
id: '123',
|
||||||
|
name: 'John Doe'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST Request
|
||||||
|
```typescript
|
||||||
|
it('should create user', async () => {
|
||||||
|
const app = new Elysia()
|
||||||
|
.post('/user', ({ body }) => body)
|
||||||
|
|
||||||
|
const res = await app.handle(
|
||||||
|
new Request('http://localhost/user', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'Jane Doe',
|
||||||
|
email: 'jane@example.com'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(data.name).toBe('Jane Doe')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Module/Plugin
|
||||||
|
|
||||||
|
### Module Structure
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── modules/
|
||||||
|
│ └── auth/
|
||||||
|
│ ├── index.ts # Elysia instance
|
||||||
|
│ ├── service.ts
|
||||||
|
│ └── model.ts
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth Module
|
||||||
|
```typescript
|
||||||
|
// src/modules/auth/index.ts
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
export const authModule = new Elysia({ prefix: '/auth' })
|
||||||
|
.post('/login', ({ body, cookie: { session } }) => {
|
||||||
|
if (body.username === 'admin' && body.password === 'password') {
|
||||||
|
session.value = 'valid-session'
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
return { success: false }
|
||||||
|
}, {
|
||||||
|
body: t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get('/profile', ({ cookie: { session }, status }) => {
|
||||||
|
if (!session.value) {
|
||||||
|
return status(401, { error: 'Unauthorized' })
|
||||||
|
}
|
||||||
|
return { username: 'admin' }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth Module Test
|
||||||
|
```typescript
|
||||||
|
// test/auth.test.ts
|
||||||
|
import { describe, expect, it } from 'bun:test'
|
||||||
|
import { authModule } from '../src/modules/auth'
|
||||||
|
|
||||||
|
describe('Auth Module', () => {
|
||||||
|
it('should login successfully', async () => {
|
||||||
|
const res = await authModule.handle(
|
||||||
|
new Request('http://localhost/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: 'admin',
|
||||||
|
password: 'password'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(data.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reject invalid credentials', async () => {
|
||||||
|
const res = await authModule.handle(
|
||||||
|
new Request('http://localhost/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: 'wrong',
|
||||||
|
password: 'wrong'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
expect(data.success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return 401 for unauthenticated profile request', async () => {
|
||||||
|
const res = await authModule.handle(
|
||||||
|
new Request('http://localhost/auth/profile')
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toBe(401)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Eden Treaty Testing
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
```typescript
|
||||||
|
import { treaty } from '@elysiajs/eden'
|
||||||
|
import { app } from '../src/modules/auth'
|
||||||
|
|
||||||
|
const api = treaty(app)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Eden Tests
|
||||||
|
```typescript
|
||||||
|
describe('Auth Module with Eden', () => {
|
||||||
|
it('should login with Eden', async () => {
|
||||||
|
const { data, error } = await api.auth.login.post({
|
||||||
|
username: 'admin',
|
||||||
|
password: 'password'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(error).toBeNull()
|
||||||
|
expect(data?.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get profile with Eden', async () => {
|
||||||
|
// First login
|
||||||
|
await api.auth.login.post({
|
||||||
|
username: 'admin',
|
||||||
|
password: 'password'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then get profile
|
||||||
|
const { data, error } = await api.auth.profile.get()
|
||||||
|
|
||||||
|
expect(error).toBeNull()
|
||||||
|
expect(data?.username).toBe('admin')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mocking Dependencies
|
||||||
|
|
||||||
|
### With Decorators
|
||||||
|
```typescript
|
||||||
|
// app.ts
|
||||||
|
export const app = new Elysia()
|
||||||
|
.decorate('db', realDatabase)
|
||||||
|
.get('/users', ({ db }) => db.getUsers())
|
||||||
|
|
||||||
|
// test
|
||||||
|
import { app } from '../src/app'
|
||||||
|
|
||||||
|
describe('App with mocked DB', () => {
|
||||||
|
it('should use mock database', async () => {
|
||||||
|
const mockDb = {
|
||||||
|
getUsers: () => [{ id: 1, name: 'Test User' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const testApp = app.decorate('db', mockDb)
|
||||||
|
|
||||||
|
const res = await testApp.handle(
|
||||||
|
new Request('http://localhost/users')
|
||||||
|
)
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
expect(data).toEqual([{ id: 1, name: 'Test User' }])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing with Headers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
it('should require authorization', async () => {
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/protected', ({ headers, status }) => {
|
||||||
|
if (!headers.authorization) {
|
||||||
|
return status(401)
|
||||||
|
}
|
||||||
|
return { data: 'secret' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await app.handle(
|
||||||
|
new Request('http://localhost/protected', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer token123'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
it('should validate request body', async () => {
|
||||||
|
const app = new Elysia()
|
||||||
|
.post('/user', ({ body }) => body, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
age: t.Number({ minimum: 0 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Valid request
|
||||||
|
const validRes = await app.handle(
|
||||||
|
new Request('http://localhost/user', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'John',
|
||||||
|
age: 25
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
expect(validRes.status).toBe(200)
|
||||||
|
|
||||||
|
// Invalid request (negative age)
|
||||||
|
const invalidRes = await app.handle(
|
||||||
|
new Request('http://localhost/user', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'John',
|
||||||
|
age: -5
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
expect(invalidRes.status).toBe(400)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing WebSocket
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
it('should handle websocket connection', (done) => {
|
||||||
|
const app = new Elysia()
|
||||||
|
.ws('/chat', {
|
||||||
|
message(ws, message) {
|
||||||
|
ws.send('Echo: ' + message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const ws = new WebSocket('ws://localhost:3000/chat')
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
ws.send('Hello')
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
expect(event.data).toBe('Echo: Hello')
|
||||||
|
ws.close()
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/modules/auth/index.ts
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
export const authModule = new Elysia({ prefix: '/auth' })
|
||||||
|
.post('/login', ({ body, cookie: { session } }) => {
|
||||||
|
if (body.username === 'admin' && body.password === 'password') {
|
||||||
|
session.value = 'valid-session'
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
return { success: false }
|
||||||
|
}, {
|
||||||
|
body: t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get('/profile', ({ cookie: { session }, status }) => {
|
||||||
|
if (!session.value) {
|
||||||
|
return status(401)
|
||||||
|
}
|
||||||
|
return { username: 'admin' }
|
||||||
|
})
|
||||||
|
|
||||||
|
// test/auth.test.ts
|
||||||
|
import { describe, expect, it } from 'bun:test'
|
||||||
|
import { treaty } from '@elysiajs/eden'
|
||||||
|
import { authModule } from '../src/modules/auth'
|
||||||
|
|
||||||
|
const api = treaty(authModule)
|
||||||
|
|
||||||
|
describe('Auth Module', () => {
|
||||||
|
it('should login successfully', async () => {
|
||||||
|
const { data, error } = await api.auth.login.post({
|
||||||
|
username: 'admin',
|
||||||
|
password: 'password'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(error).toBeNull()
|
||||||
|
expect(data?.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return 401 for unauthorized access', async () => {
|
||||||
|
const { error } = await api.auth.profile.get()
|
||||||
|
|
||||||
|
expect(error?.status).toBe(401)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
491
.agents/skills/elysiajs/references/validation.md
Normal file
491
.agents/skills/elysiajs/references/validation.md
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
# Validation Schema - SKILLS.md
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Runtime validation + type inference + OpenAPI schema from single source. TypeBox-based with Standard Schema support.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/id/:id', ({ params: { id } }) => id, {
|
||||||
|
params: t.Object({ id: t.Number({ minimum: 1 }) }),
|
||||||
|
response: {
|
||||||
|
200: t.Number(),
|
||||||
|
404: t.Literal('Not Found')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema Types
|
||||||
|
Third parameter of HTTP method:
|
||||||
|
- **body** - HTTP message
|
||||||
|
- **query** - URL query params
|
||||||
|
- **params** - Path params
|
||||||
|
- **headers** - Request headers
|
||||||
|
- **cookie** - Request cookies
|
||||||
|
- **response** - Response (per status)
|
||||||
|
|
||||||
|
## Standard Schema Support
|
||||||
|
Use Zod, Valibot, ArkType, Effect, Yup, Joi:
|
||||||
|
```typescript
|
||||||
|
import { z } from 'zod'
|
||||||
|
import * as v from 'valibot'
|
||||||
|
|
||||||
|
.get('/', ({ params, query }) => params.id, {
|
||||||
|
params: z.object({ id: z.coerce.number() }),
|
||||||
|
query: v.object({ name: v.literal('Lilith') })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Mix validators in same handler.
|
||||||
|
|
||||||
|
## Body
|
||||||
|
```typescript
|
||||||
|
body: t.Object({ name: t.String() })
|
||||||
|
```
|
||||||
|
|
||||||
|
GET/HEAD: body-parser disabled by default (RFC2616).
|
||||||
|
|
||||||
|
### File Upload
|
||||||
|
```typescript
|
||||||
|
body: t.Object({
|
||||||
|
file: t.File({ format: 'image/*' }),
|
||||||
|
multipleFiles: t.Files()
|
||||||
|
})
|
||||||
|
// Auto-assumes multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### File (Standard Schema)
|
||||||
|
```typescript
|
||||||
|
import { fileType } from 'elysia'
|
||||||
|
|
||||||
|
body: z.object({
|
||||||
|
file: z.file().refine((file) => fileType(file, 'image/jpeg'))
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `fileType` for security (validates magic number, not just MIME).
|
||||||
|
|
||||||
|
## Query
|
||||||
|
```typescript
|
||||||
|
query: t.Object({ name: t.String() })
|
||||||
|
// /?name=Elysia
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-coerces to specified type.
|
||||||
|
|
||||||
|
### Arrays
|
||||||
|
```typescript
|
||||||
|
query: t.Object({ name: t.Array(t.String()) })
|
||||||
|
```
|
||||||
|
|
||||||
|
Formats supported:
|
||||||
|
- **nuqs**: `?name=a,b,c` (comma delimiter)
|
||||||
|
- **HTML form**: `?name=a&name=b&name=c` (multiple keys)
|
||||||
|
|
||||||
|
## Params
|
||||||
|
```typescript
|
||||||
|
params: t.Object({ id: t.Number() })
|
||||||
|
// /id/1
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-inferred as string if schema not provided.
|
||||||
|
|
||||||
|
## Headers
|
||||||
|
```typescript
|
||||||
|
headers: t.Object({ authorization: t.String() })
|
||||||
|
```
|
||||||
|
|
||||||
|
`additionalProperties: true` by default. Always lowercase keys.
|
||||||
|
|
||||||
|
## Cookie
|
||||||
|
```typescript
|
||||||
|
cookie: t.Cookie({
|
||||||
|
name: t.String()
|
||||||
|
}, {
|
||||||
|
secure: true,
|
||||||
|
httpOnly: true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use `t.Object`. `additionalProperties: true` by default.
|
||||||
|
|
||||||
|
## Response
|
||||||
|
```typescript
|
||||||
|
response: t.Object({ name: t.String() })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Per Status
|
||||||
|
```typescript
|
||||||
|
response: {
|
||||||
|
200: t.Object({ name: t.String() }),
|
||||||
|
400: t.Object({ error: t.String() })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Inline Error Property
|
||||||
|
```typescript
|
||||||
|
body: t.Object({
|
||||||
|
x: t.Number({ error: 'x must be number' })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Or function:
|
||||||
|
```typescript
|
||||||
|
x: t.Number({
|
||||||
|
error({ errors, type, validation, value }) {
|
||||||
|
return 'Expected x to be number'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### onError Hook
|
||||||
|
```typescript
|
||||||
|
.onError(({ code, error }) => {
|
||||||
|
if (code === 'VALIDATION')
|
||||||
|
return error.message // or error.all[0].message
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`error.all` - list all error causes. `error.all.find(x => x.path === '/name')` - find specific field.
|
||||||
|
|
||||||
|
## Reference Models
|
||||||
|
Name + reuse models:
|
||||||
|
```typescript
|
||||||
|
.model({
|
||||||
|
sign: t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.post('/sign-in', ({ body }) => body, {
|
||||||
|
body: 'sign',
|
||||||
|
response: 'sign'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract to plugin:
|
||||||
|
```typescript
|
||||||
|
// auth.model.ts
|
||||||
|
export const authModel = new Elysia().model({ sign: t.Object({...}) })
|
||||||
|
|
||||||
|
// main.ts
|
||||||
|
new Elysia().use(authModel).post('/', ..., { body: 'sign' })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Naming Convention
|
||||||
|
Prevent duplicates with namespaces:
|
||||||
|
```typescript
|
||||||
|
.model({
|
||||||
|
'auth.admin': t.Object({...}),
|
||||||
|
'auth.user': t.Object({...})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use `prefix` / `suffix` to rename models in current instance
|
||||||
|
```typescript
|
||||||
|
.model({ sign: t.Object({...}) })
|
||||||
|
.prefix('model', 'auth')
|
||||||
|
.post('/', () => '', {
|
||||||
|
body: 'auth.User'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Models with `prefix` will be capitalized.
|
||||||
|
|
||||||
|
## TypeScript Types
|
||||||
|
```typescript
|
||||||
|
const MyType = t.Object({ hello: t.Literal('Elysia') })
|
||||||
|
type MyType = typeof MyType.static
|
||||||
|
```
|
||||||
|
|
||||||
|
Single schema → runtime validation + coercion + TypeScript type + OpenAPI.
|
||||||
|
|
||||||
|
## Guard
|
||||||
|
Apply schema to multiple handlers. Affects all handlers after definition.
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
```typescript
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/none', ({ query }) => 'hi')
|
||||||
|
.guard({
|
||||||
|
query: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get('/query', ({ query }) => query)
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensures `query.name` string required for all handlers after guard.
|
||||||
|
|
||||||
|
### Behavior
|
||||||
|
| Path | Response |
|
||||||
|
|---------------|----------|
|
||||||
|
| /none | hi |
|
||||||
|
| /none?name=a | hi |
|
||||||
|
| /query | error |
|
||||||
|
| /query?name=a | a |
|
||||||
|
|
||||||
|
### Precedence
|
||||||
|
- Multiple global schemas: latest wins
|
||||||
|
- Global vs local: local wins
|
||||||
|
|
||||||
|
### Schema Types
|
||||||
|
|
||||||
|
1. override (default)
|
||||||
|
Latest schema overrides collided schema.
|
||||||
|
```typescript
|
||||||
|
.guard({ query: t.Object({ name: t.String() }) })
|
||||||
|
.guard({ query: t.Object({ id: t.Number() }) })
|
||||||
|
// Only id required, name overridden
|
||||||
|
```
|
||||||
|
|
||||||
|
2. standalone
|
||||||
|
Both schemas run independently. Both validated.
|
||||||
|
```typescript
|
||||||
|
.guard({ query: t.Object({ name: t.String() }) }, { type: 'standalone' })
|
||||||
|
.guard({ query: t.Object({ id: t.Number() }) }, { type: 'standalone' })
|
||||||
|
// Both name AND id required
|
||||||
|
```
|
||||||
|
|
||||||
|
# Typebox Validation (Elysia.t)
|
||||||
|
|
||||||
|
Elysia.t = TypeBox with server-side pre-configuration + HTTP-specific types
|
||||||
|
|
||||||
|
**TypeBox API mirrors TypeScript syntax** but provides runtime validation
|
||||||
|
|
||||||
|
## Basic Types
|
||||||
|
|
||||||
|
| TypeBox | TypeScript | Example Value |
|
||||||
|
|---------|------------|---------------|
|
||||||
|
| `t.String()` | `string` | `"hello"` |
|
||||||
|
| `t.Number()` | `number` | `42` |
|
||||||
|
| `t.Boolean()` | `boolean` | `true` |
|
||||||
|
| `t.Array(t.Number())` | `number[]` | `[1, 2, 3]` |
|
||||||
|
| `t.Object({ x: t.Number() })` | `{ x: number }` | `{ x: 10 }` |
|
||||||
|
| `t.Null()` | `null` | `null` |
|
||||||
|
| `t.Literal(42)` | `42` | `42` |
|
||||||
|
|
||||||
|
## Attributes (JSON Schema 7)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Email format
|
||||||
|
t.String({ format: 'email' })
|
||||||
|
|
||||||
|
// Number constraints
|
||||||
|
t.Number({ minimum: 10, maximum: 100 })
|
||||||
|
|
||||||
|
// Array constraints
|
||||||
|
t.Array(t.Number(), {
|
||||||
|
minItems: 1, // min items
|
||||||
|
maxItems: 5 // max items
|
||||||
|
})
|
||||||
|
|
||||||
|
// Object - allow extra properties
|
||||||
|
t.Object(
|
||||||
|
{ x: t.Number() },
|
||||||
|
{ additionalProperties: true } // default: false
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Union (Multiple Types)
|
||||||
|
```ts
|
||||||
|
t.Union([t.String(), t.Number()])
|
||||||
|
// type: string | number
|
||||||
|
// values: "Hello" or 123
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional (Field Optional)
|
||||||
|
```ts
|
||||||
|
t.Object({
|
||||||
|
x: t.Number(),
|
||||||
|
y: t.Optional(t.Number()) // can be undefined
|
||||||
|
})
|
||||||
|
// type: { x: number, y?: number }
|
||||||
|
// value: { x: 123 } or { x: 123, y: 456 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Partial (All Fields Optional)
|
||||||
|
```ts
|
||||||
|
t.Partial(t.Object({
|
||||||
|
x: t.Number(),
|
||||||
|
y: t.Number()
|
||||||
|
}))
|
||||||
|
// type: { x?: number, y?: number }
|
||||||
|
// value: {} or { y: 123 } or { x: 1, y: 2 }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Elysia-Specific Types
|
||||||
|
|
||||||
|
### UnionEnum (One of Values)
|
||||||
|
```ts
|
||||||
|
t.UnionEnum(['rapi', 'anis', 1, true, false])
|
||||||
|
```
|
||||||
|
|
||||||
|
### File (Single File Upload)
|
||||||
|
```ts
|
||||||
|
t.File({
|
||||||
|
type: 'image', // or ['image', 'video']
|
||||||
|
minSize: '1k', // 1024 bytes
|
||||||
|
maxSize: '5m' // 5242880 bytes
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**File unit suffixes**:
|
||||||
|
- `m` = MegaByte (1048576 bytes)
|
||||||
|
- `k` = KiloByte (1024 bytes)
|
||||||
|
|
||||||
|
### Files (Multiple Files)
|
||||||
|
```ts
|
||||||
|
t.Files() // extends File + array
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cookie (Cookie Jar)
|
||||||
|
```ts
|
||||||
|
t.Cookie({
|
||||||
|
name: t.String()
|
||||||
|
}, {
|
||||||
|
secrets: 'secret-key' // or ['key1', 'key2'] for rotation
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nullable (Allow null)
|
||||||
|
```ts
|
||||||
|
t.Nullable(t.String())
|
||||||
|
// type: string | null
|
||||||
|
```
|
||||||
|
|
||||||
|
### MaybeEmpty (Allow null + undefined)
|
||||||
|
```ts
|
||||||
|
t.MaybeEmpty(t.String())
|
||||||
|
// type: string | null | undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
### Form (FormData Validation)
|
||||||
|
```ts
|
||||||
|
t.Form({
|
||||||
|
someValue: t.File()
|
||||||
|
})
|
||||||
|
// Syntax sugar for t.Object with FormData support
|
||||||
|
```
|
||||||
|
|
||||||
|
### UInt8Array (Buffer → Uint8Array)
|
||||||
|
```ts
|
||||||
|
t.UInt8Array()
|
||||||
|
// For binary file uploads with arrayBuffer parser
|
||||||
|
```
|
||||||
|
|
||||||
|
### ArrayBuffer (Buffer → ArrayBuffer)
|
||||||
|
```ts
|
||||||
|
t.ArrayBuffer()
|
||||||
|
// For binary file uploads with arrayBuffer parser
|
||||||
|
```
|
||||||
|
|
||||||
|
### ObjectString (String → Object)
|
||||||
|
```ts
|
||||||
|
t.ObjectString()
|
||||||
|
// Accepts: '{"x":1}' → parses to { x: 1 }
|
||||||
|
// Use in: query string, headers, FormData
|
||||||
|
```
|
||||||
|
|
||||||
|
### BooleanString (String → Boolean)
|
||||||
|
```ts
|
||||||
|
t.BooleanString()
|
||||||
|
// Accepts: 'true'/'false' → parses to boolean
|
||||||
|
// Use in: query string, headers, FormData
|
||||||
|
```
|
||||||
|
|
||||||
|
### Numeric (String/Number → Number)
|
||||||
|
```ts
|
||||||
|
t.Numeric()
|
||||||
|
// Accepts: '123' or 123 → transforms to 123
|
||||||
|
// Use in: path params, query string
|
||||||
|
```
|
||||||
|
|
||||||
|
## Elysia Behavior Differences from TypeBox
|
||||||
|
|
||||||
|
### 1. Optional Behavior
|
||||||
|
|
||||||
|
In Elysia, `t.Optional` makes **entire route parameter** optional (not object field):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
.get('/optional', ({ query }) => query, {
|
||||||
|
query: t.Optional( // makes query itself optional
|
||||||
|
t.Object({ name: t.String() })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Different from TypeBox**: TypeBox uses Optional for object fields only
|
||||||
|
|
||||||
|
### 2. Number → Numeric Auto-Conversion
|
||||||
|
|
||||||
|
**Route schema only** (not nested objects):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
.get('/:id', ({ id }) => id, {
|
||||||
|
params: t.Object({
|
||||||
|
id: t.Number() // ✅ Auto-converts to t.Numeric()
|
||||||
|
}),
|
||||||
|
body: t.Object({
|
||||||
|
id: t.Number() // ❌ NOT converted (stays t.Number())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Outside route schema
|
||||||
|
t.Number() // ❌ NOT converted
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: HTTP headers/query/params always strings. Auto-conversion parses numeric strings.
|
||||||
|
|
||||||
|
### 3. Boolean → BooleanString Auto-Conversion
|
||||||
|
|
||||||
|
Same as Number → Numeric:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
.get('/:active', ({ active }) => active, {
|
||||||
|
params: t.Object({
|
||||||
|
active: t.Boolean() // ✅ Auto-converts to t.BooleanString()
|
||||||
|
}),
|
||||||
|
body: t.Object({
|
||||||
|
active: t.Boolean() // ❌ NOT converted
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Pattern
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.post('/', ({ body }) => `Hello ${body}`, {
|
||||||
|
body: t.String() // validates body is string
|
||||||
|
})
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation flow**:
|
||||||
|
1. Request arrives
|
||||||
|
2. Schema validates against HTTP body/params/query/headers
|
||||||
|
3. If valid → handler executes
|
||||||
|
4. If invalid → Error Life Cycle
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
[Inference] Based on docs:
|
||||||
|
- TypeBox mirrors TypeScript but adds runtime validation
|
||||||
|
- Elysia.t extends TypeBox with HTTP-specific types
|
||||||
|
- Auto-conversion (Number→Numeric, Boolean→BooleanString) only for route schemas
|
||||||
|
- Use `t.Optional` for optional route params (different from TypeBox behavior)
|
||||||
|
- File validation supports unit suffixes ('1k', '5m')
|
||||||
|
- ObjectString/BooleanString for parsing strings in query/headers
|
||||||
|
- Cookie supports key rotation with array of secrets
|
||||||
250
.agents/skills/elysiajs/references/websocket.md
Normal file
250
.agents/skills/elysiajs/references/websocket.md
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
# WebSocket
|
||||||
|
|
||||||
|
## Basic WebSocket
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.ws('/chat', {
|
||||||
|
message(ws, message) {
|
||||||
|
ws.send(message) // Echo back
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## With Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
.ws('/chat', {
|
||||||
|
body: t.Object({
|
||||||
|
message: t.String(),
|
||||||
|
username: t.String()
|
||||||
|
}),
|
||||||
|
response: t.Object({
|
||||||
|
message: t.String(),
|
||||||
|
timestamp: t.Number()
|
||||||
|
}),
|
||||||
|
message(ws, body) {
|
||||||
|
ws.send({
|
||||||
|
message: body.message,
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle Events
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.ws('/chat', {
|
||||||
|
open(ws) {
|
||||||
|
console.log('Client connected')
|
||||||
|
},
|
||||||
|
message(ws, message) {
|
||||||
|
console.log('Received:', message)
|
||||||
|
ws.send('Echo: ' + message)
|
||||||
|
},
|
||||||
|
close(ws) {
|
||||||
|
console.log('Client disconnected')
|
||||||
|
},
|
||||||
|
error(ws, error) {
|
||||||
|
console.error('Error:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Broadcasting
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const connections = new Set<any>()
|
||||||
|
|
||||||
|
.ws('/chat', {
|
||||||
|
open(ws) {
|
||||||
|
connections.add(ws)
|
||||||
|
},
|
||||||
|
message(ws, message) {
|
||||||
|
// Broadcast to all connected clients
|
||||||
|
for (const client of connections) {
|
||||||
|
client.send(message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close(ws) {
|
||||||
|
connections.delete(ws)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## With Authentication
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.ws('/chat', {
|
||||||
|
beforeHandle({ headers, status }) {
|
||||||
|
const token = headers.authorization?.replace('Bearer ', '')
|
||||||
|
if (!verifyToken(token)) {
|
||||||
|
return status(401)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
message(ws, message) {
|
||||||
|
ws.send(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Room-Based Chat
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const rooms = new Map<string, Set<any>>()
|
||||||
|
|
||||||
|
.ws('/chat/:room', {
|
||||||
|
open(ws) {
|
||||||
|
const room = ws.data.params.room
|
||||||
|
if (!rooms.has(room)) {
|
||||||
|
rooms.set(room, new Set())
|
||||||
|
}
|
||||||
|
rooms.get(room)!.add(ws)
|
||||||
|
},
|
||||||
|
message(ws, message) {
|
||||||
|
const room = ws.data.params.room
|
||||||
|
const clients = rooms.get(room)
|
||||||
|
|
||||||
|
if (clients) {
|
||||||
|
for (const client of clients) {
|
||||||
|
client.send(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close(ws) {
|
||||||
|
const room = ws.data.params.room
|
||||||
|
const clients = rooms.get(room)
|
||||||
|
|
||||||
|
if (clients) {
|
||||||
|
clients.delete(ws)
|
||||||
|
if (clients.size === 0) {
|
||||||
|
rooms.delete(room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## With State/Context
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.ws('/chat', {
|
||||||
|
open(ws) {
|
||||||
|
ws.data.userId = generateUserId()
|
||||||
|
ws.data.joinedAt = Date.now()
|
||||||
|
},
|
||||||
|
message(ws, message) {
|
||||||
|
const response = {
|
||||||
|
userId: ws.data.userId,
|
||||||
|
message,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
ws.send(response)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client Usage (Browser)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const ws = new WebSocket('ws://localhost:3000/chat')
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('Connected')
|
||||||
|
ws.send('Hello Server!')
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
console.log('Received:', event.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('Error:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('Disconnected')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Eden Treaty WebSocket
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Server
|
||||||
|
export const app = new Elysia()
|
||||||
|
.ws('/chat', {
|
||||||
|
message(ws, message) {
|
||||||
|
ws.send(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export type App = typeof app
|
||||||
|
|
||||||
|
// Client
|
||||||
|
import { treaty } from '@elysiajs/eden'
|
||||||
|
import type { App } from './server'
|
||||||
|
|
||||||
|
const api = treaty<App>('localhost:3000')
|
||||||
|
const chat = api.chat.subscribe()
|
||||||
|
|
||||||
|
chat.subscribe((message) => {
|
||||||
|
console.log('Received:', message)
|
||||||
|
})
|
||||||
|
|
||||||
|
chat.send('Hello!')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Headers in WebSocket
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.ws('/chat', {
|
||||||
|
header: t.Object({
|
||||||
|
authorization: t.String()
|
||||||
|
}),
|
||||||
|
beforeHandle({ headers, status }) {
|
||||||
|
const token = headers.authorization?.replace('Bearer ', '')
|
||||||
|
if (!token) return status(401)
|
||||||
|
},
|
||||||
|
message(ws, message) {
|
||||||
|
ws.send(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Query Parameters
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.ws('/chat', {
|
||||||
|
query: t.Object({
|
||||||
|
username: t.String()
|
||||||
|
}),
|
||||||
|
message(ws, message) {
|
||||||
|
const username = ws.data.query.username
|
||||||
|
ws.send(`${username}: ${message}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Client
|
||||||
|
const ws = new WebSocket('ws://localhost:3000/chat?username=john')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compression
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new Elysia({
|
||||||
|
websocket: {
|
||||||
|
perMessageDeflate: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ws('/chat', {
|
||||||
|
message(ws, message) {
|
||||||
|
ws.send(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
475
.kilocode/skills/elysiajs/SKILL.md
Normal file
475
.kilocode/skills/elysiajs/SKILL.md
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
---
|
||||||
|
name: elysiajs
|
||||||
|
description: Create backend with ElysiaJS, a type-safe, high-performance framework.
|
||||||
|
---
|
||||||
|
|
||||||
|
# ElysiaJS Development Skill
|
||||||
|
|
||||||
|
Always consult [elysiajs.com/llms.txt](https://elysiajs.com/llms.txt) for code examples and latest API.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
ElysiaJS is a TypeScript framework for building Bun-first (but not limited to Bun) type-safe, high-performance backend servers. This skill provides comprehensive guidance for developing with Elysia, including routing, validation, authentication, plugins, integrations, and deployment.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
Trigger this skill when the user asks to:
|
||||||
|
- Create or modify ElysiaJS routes, handlers, or servers
|
||||||
|
- Setup validation with TypeBox or other schema libraries (Zod, Valibot)
|
||||||
|
- Implement authentication (JWT, session-based, macros, guards)
|
||||||
|
- Add plugins (CORS, OpenAPI, Static files, JWT)
|
||||||
|
- Integrate with external services (Drizzle ORM, Better Auth, Next.js, Eden Treaty)
|
||||||
|
- Setup WebSocket endpoints for real-time features
|
||||||
|
- Create unit tests for Elysia instances
|
||||||
|
- Deploy Elysia servers to production
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
Quick scaffold:
|
||||||
|
```bash
|
||||||
|
bun create elysia app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic Server
|
||||||
|
```typescript
|
||||||
|
import { Elysia, t, status } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/', () => 'Hello World')
|
||||||
|
.post('/user', ({ body }) => body, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
age: t.Number()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get('/id/:id', ({ params: { id } }) => {
|
||||||
|
if(id > 1_000_000) return status(404, 'Not Found')
|
||||||
|
|
||||||
|
return id
|
||||||
|
}, {
|
||||||
|
params: t.Object({
|
||||||
|
id: t.Number({
|
||||||
|
minimum: 1
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: t.Number(),
|
||||||
|
404: t.Literal('Not Found')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### HTTP Methods
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', 'GET')
|
||||||
|
.post('/', 'POST')
|
||||||
|
.put('/', 'PUT')
|
||||||
|
.patch('/', 'PATCH')
|
||||||
|
.delete('/', 'DELETE')
|
||||||
|
.options('/', 'OPTIONS')
|
||||||
|
.head('/', 'HEAD')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Path Parameters
|
||||||
|
```typescript
|
||||||
|
.get('/user/:id', ({ params: { id } }) => id)
|
||||||
|
.get('/post/:id/:slug', ({ params }) => params)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Parameters
|
||||||
|
```typescript
|
||||||
|
.get('/search', ({ query }) => query.q)
|
||||||
|
// GET /search?q=elysia → "elysia"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request Body
|
||||||
|
```typescript
|
||||||
|
.post('/user', ({ body }) => body)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Headers
|
||||||
|
```typescript
|
||||||
|
.get('/', ({ headers }) => headers.authorization)
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeBox Validation
|
||||||
|
|
||||||
|
### Basic Types
|
||||||
|
```typescript
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
.post('/user', ({ body }) => body, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
age: t.Number(),
|
||||||
|
email: t.String({ format: 'email' }),
|
||||||
|
website: t.Optional(t.String({ format: 'uri' }))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nested Objects
|
||||||
|
```typescript
|
||||||
|
body: t.Object({
|
||||||
|
user: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
address: t.Object({
|
||||||
|
street: t.String(),
|
||||||
|
city: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arrays
|
||||||
|
```typescript
|
||||||
|
body: t.Object({
|
||||||
|
tags: t.Array(t.String()),
|
||||||
|
users: t.Array(t.Object({
|
||||||
|
id: t.String(),
|
||||||
|
name: t.String()
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Upload
|
||||||
|
```typescript
|
||||||
|
.post('/upload', ({ body }) => body.file, {
|
||||||
|
body: t.Object({
|
||||||
|
file: t.File({
|
||||||
|
type: 'image', // image/* mime types
|
||||||
|
maxSize: '5m' // 5 megabytes
|
||||||
|
}),
|
||||||
|
files: t.Files({ // Multiple files
|
||||||
|
type: ['image/png', 'image/jpeg']
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Validation
|
||||||
|
```typescript
|
||||||
|
.get('/user/:id', ({ params: { id } }) => ({
|
||||||
|
id,
|
||||||
|
name: 'John',
|
||||||
|
email: 'john@example.com'
|
||||||
|
}), {
|
||||||
|
params: t.Object({
|
||||||
|
id: t.Number()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: t.Object({
|
||||||
|
id: t.Number(),
|
||||||
|
name: t.String(),
|
||||||
|
email: t.String()
|
||||||
|
}),
|
||||||
|
404: t.String()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Standard Schema (Zod, Valibot, ArkType)
|
||||||
|
|
||||||
|
### Zod
|
||||||
|
```typescript
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
.post('/user', ({ body }) => body, {
|
||||||
|
body: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
age: z.number().min(0),
|
||||||
|
email: z.string().email()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.get('/user/:id', ({ params: { id }, status }) => {
|
||||||
|
const user = findUser(id)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return status(404, 'User not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Guards (Apply to Multiple Routes)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.guard({
|
||||||
|
params: t.Object({
|
||||||
|
id: t.Number()
|
||||||
|
})
|
||||||
|
}, app => app
|
||||||
|
.get('/user/:id', ({ params: { id } }) => id)
|
||||||
|
.delete('/user/:id', ({ params: { id } }) => id)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Macro
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
.macro({
|
||||||
|
hi: (word: string) => ({
|
||||||
|
beforeHandle() { console.log(word) }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get('/', () => 'hi', { hi: 'Elysia' })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Project Structure (Recommended)
|
||||||
|
Elysia takes an unopinionated approach but based on user request. But without any specific preference, we recommend a feature-based and domain driven folder structure where each feature has its own folder containing controllers, services, and models.
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── index.ts # Main server entry
|
||||||
|
├── modules/
|
||||||
|
│ ├── auth/
|
||||||
|
│ │ ├── index.ts # Auth routes (Elysia instance)
|
||||||
|
│ │ ├── service.ts # Business logic
|
||||||
|
│ │ └── model.ts # TypeBox schemas/DTOs
|
||||||
|
│ └── user/
|
||||||
|
│ ├── index.ts
|
||||||
|
│ ├── service.ts
|
||||||
|
│ └── model.ts
|
||||||
|
└── plugins/
|
||||||
|
└── custom.ts
|
||||||
|
|
||||||
|
public/ # Static files (if using static plugin)
|
||||||
|
test/ # Unit tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Each file has its own responsibility as follows:
|
||||||
|
- **Controller (index.ts)**: Handle HTTP routing, request validation, and cookie.
|
||||||
|
- **Service (service.ts)**: Handle business logic, decoupled from Elysia controller if possible.
|
||||||
|
- **Model (model.ts)**: Define the data structure and validation for the request and response.
|
||||||
|
|
||||||
|
## Best Practice
|
||||||
|
Elysia is unopinionated on design pattern, but if not provided, we can relies on MVC pattern pair with feature based folder structure.
|
||||||
|
|
||||||
|
- Controller:
|
||||||
|
- Prefers Elysia as a controller for HTTP dependant controller
|
||||||
|
- For non HTTP dependent, prefers service instead unless explicitly asked
|
||||||
|
- Use `onError` to handle local custom errors
|
||||||
|
- Register Model to Elysia instance via `Elysia.models({ ...models })` and prefix model by namespace `Elysia.prefix('model', 'Namespace.')
|
||||||
|
- Prefers Reference Model by name provided by Elysia instead of using an actual `Model.name`
|
||||||
|
- Service:
|
||||||
|
- Prefers class (or abstract class if possible)
|
||||||
|
- Prefers interface/type derive from `Model`
|
||||||
|
- Return `status` (`import { status } from 'elysia'`) for error
|
||||||
|
- Prefers `return Error` instead of `throw Error`
|
||||||
|
- Models:
|
||||||
|
- Always export validation model and type of validation model
|
||||||
|
- Custom Error should be in contains in Model
|
||||||
|
|
||||||
|
## Elysia Key Concept
|
||||||
|
Elysia has a every important concepts/rules to understand before use.
|
||||||
|
|
||||||
|
## Encapsulation - Isolates by Default
|
||||||
|
|
||||||
|
Lifecycles (hooks, middleware) **don't leak** between instances unless scoped.
|
||||||
|
|
||||||
|
**Scope levels:**
|
||||||
|
- `local` (default) - current instance + descendants
|
||||||
|
- `scoped` - parent + current + descendants
|
||||||
|
- `global` - all instances
|
||||||
|
|
||||||
|
```ts
|
||||||
|
.onBeforeHandle(() => {}) // only local instance
|
||||||
|
.onBeforeHandle({ as: 'global' }, () => {}) // exports to all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Method Chaining - Required for Types
|
||||||
|
|
||||||
|
**Must chain**. Each method returns new type reference.
|
||||||
|
|
||||||
|
❌ Don't:
|
||||||
|
```ts
|
||||||
|
const app = new Elysia()
|
||||||
|
app.state('build', 1) // loses type
|
||||||
|
app.get('/', ({ store }) => store.build) // build doesn't exists
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ Do:
|
||||||
|
```ts
|
||||||
|
new Elysia()
|
||||||
|
.state('build', 1)
|
||||||
|
.get('/', ({ store }) => store.build)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Explicit Dependencies
|
||||||
|
|
||||||
|
Each instance independent. **Declare what you use.**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const auth = new Elysia()
|
||||||
|
.decorate('Auth', Auth)
|
||||||
|
.model(Auth.models)
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', ({ Auth }) => Auth.getProfile()) // Auth doesn't exists
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(auth) // must declare
|
||||||
|
.get('/', ({ Auth }) => Auth.getProfile())
|
||||||
|
```
|
||||||
|
|
||||||
|
**Global scope when:**
|
||||||
|
- No types added (cors, helmet)
|
||||||
|
- Global lifecycle (logging, tracing)
|
||||||
|
|
||||||
|
**Explicit when:**
|
||||||
|
- Adds types (state, models)
|
||||||
|
- Business logic (auth, db)
|
||||||
|
|
||||||
|
## Deduplication
|
||||||
|
|
||||||
|
Plugins re-execute unless named:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia() // rerun on `.use`
|
||||||
|
new Elysia({ name: 'ip' }) // runs once across all instances
|
||||||
|
```
|
||||||
|
|
||||||
|
## Order Matters
|
||||||
|
|
||||||
|
Events apply to routes **registered after** them.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
.onBeforeHandle(() => console.log('1'))
|
||||||
|
.get('/', () => 'hi') // has hook
|
||||||
|
.onBeforeHandle(() => console.log('2')) // doesn't affect '/'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Inference
|
||||||
|
|
||||||
|
**Inline functions only** for accurate types.
|
||||||
|
|
||||||
|
For controllers, destructure in inline wrapper:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
.post('/', ({ body }) => Controller.greet(body), {
|
||||||
|
body: t.Object({ name: t.String() })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Get type from schema:
|
||||||
|
```ts
|
||||||
|
type MyType = typeof MyType.static
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reference Model
|
||||||
|
Model can be reference by name, especially great for documenting an API
|
||||||
|
```ts
|
||||||
|
new Elysia()
|
||||||
|
.model({
|
||||||
|
book: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.post('/', ({ body }) => body.name, {
|
||||||
|
body: 'book'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Model can be renamed by using `.prefix` / `.suffix`
|
||||||
|
```ts
|
||||||
|
new Elysia()
|
||||||
|
.model({
|
||||||
|
book: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.prefix('model', 'Namespace')
|
||||||
|
.post('/', ({ body }) => body.name, {
|
||||||
|
body: 'Namespace.Book'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Once `prefix`, model name will be capitalized by default.
|
||||||
|
|
||||||
|
## Technical Terms
|
||||||
|
The following are technical terms that is use for Elysia:
|
||||||
|
- `OpenAPI Type Gen` - function name `fromTypes` from `@elysiajs/openapi` for generating OpenAPI from types, see `plugins/openapi.md`
|
||||||
|
- `Eden`, `Eden Treaty` - e2e type safe RPC client for share type from backend to frontend
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
Use the following references as needed.
|
||||||
|
|
||||||
|
It's recommended to checkout `route.md` for as it contains the most important foundation building blocks with examples.
|
||||||
|
|
||||||
|
`plugin.md` and `validation.md` is important as well but can be check as needed.
|
||||||
|
|
||||||
|
### references/
|
||||||
|
Detailed documentation split by topic:
|
||||||
|
- `bun-fullstack-dev-server.md` - Bun Fullstack Dev Server with HMR. React without bundler.
|
||||||
|
- `cookie.md` - Detailed documentation on cookie
|
||||||
|
- `deployment.md` - Production deployment guide / Docker
|
||||||
|
- `eden.md` - e2e type safe RPC client for share type from backend to frontend
|
||||||
|
- `guard.md` - Setting validation/lifecycle all at once
|
||||||
|
- `macro.md` - Compose multiple schema/lifecycle as a reusable Elysia via key-value (recommended for complex setup, eg. authentication, authorization, Role-based Access Check)
|
||||||
|
- `plugin.md` - Decouple part of Elysia into a standalone component
|
||||||
|
- `route.md` - Elysia foundation building block: Routing, Handler and Context
|
||||||
|
- `testing.md` - Unit tests with examples
|
||||||
|
- `validation.md` - Setup input/output validation and list of all custom validation rules
|
||||||
|
- `websocket.md` - Real-time features
|
||||||
|
|
||||||
|
### plugins/
|
||||||
|
Detailed documentation, usage and configuration reference for official Elysia plugin:
|
||||||
|
- `bearer.md` - Add bearer capability to Elysia (`@elysiajs/bearer`)
|
||||||
|
- `cors.md` - Out of box configuration for CORS (`@elysiajs/cors`)
|
||||||
|
- `cron.md` - Run cron job with access to Elysia context (`@elysiajs/cron`)
|
||||||
|
- `graphql-apollo.md` - Integration GraphQL Apollo (`@elysiajs/graphql-apollo`)
|
||||||
|
- `graphql-yoga.md` - Integration with GraphQL Yoga (`@elysiajs/graphql-yoga`)
|
||||||
|
- `html.md` - HTML and JSX plugin setup and usage (`@elysiajs/html`)
|
||||||
|
- `jwt.md` - JWT / JWK plugin (`@elysiajs/jwt`)
|
||||||
|
- `openapi.md` - OpenAPI documentation and OpenAPI Type Gen / OpenAPI from types (`@elysiajs/openapi`)
|
||||||
|
- `opentelemetry.md` - OpenTelemetry, instrumentation, and record span utilities (`@elysiajs/opentelemetry`)
|
||||||
|
- `server-timing.md` - Server Timing metric for debug (`@elysiajs/server-timing`)
|
||||||
|
- `static.md` - Serve static files/folders for Elysia Server (`@elysiajs/static`)
|
||||||
|
|
||||||
|
### integrations/
|
||||||
|
Guide to integrate Elysia with external library/runtime:
|
||||||
|
- `ai-sdk.md` - Using Vercel AI SDK with Elysia
|
||||||
|
- `astro.md` - Elysia in Astro API route
|
||||||
|
- `better-auth.md` - Integrate Elysia with better-auth
|
||||||
|
- `cloudflare-worker.md` - Elysia on Cloudflare Worker adapter
|
||||||
|
- `deno.md` - Elysia on Deno
|
||||||
|
- `drizzle.md` - Integrate Elysia with Drizzle ORM
|
||||||
|
- `expo.md` - Elysia in Expo API route
|
||||||
|
- `nextjs.md` - Elysia in Nextjs API route
|
||||||
|
- `nodejs.md` - Run Elysia on Node.js
|
||||||
|
- `nuxt.md` - Elysia on API route
|
||||||
|
- `prisma.md` - Integrate Elysia with Prisma
|
||||||
|
- `react-email.d` - Create and Send Email with React and Elysia
|
||||||
|
- `sveltekit.md` - Run Elysia on Svelte Kit API route
|
||||||
|
- `tanstack-start.md` - Run Elysia on Tanstack Start / React Query
|
||||||
|
- `vercel.md` - Deploy Elysia to Vercel
|
||||||
|
|
||||||
|
### examples/ (optional)
|
||||||
|
- `basic.ts` - Basic Elysia example
|
||||||
|
- `body-parser.ts` - Custom body parser example via `.onParse`
|
||||||
|
- `complex.ts` - Comprehensive usage of Elysia server
|
||||||
|
- `cookie.ts` - Setting cookie
|
||||||
|
- `error.ts` - Error handling
|
||||||
|
- `file.ts` - Returning local file from server
|
||||||
|
- `guard.ts` - Setting mulitple validation schema and lifecycle
|
||||||
|
- `map-response.ts` - Custom response mapper
|
||||||
|
- `redirect.ts` - Redirect response
|
||||||
|
- `rename.ts` - Rename context's property
|
||||||
|
- `schema.ts` - Setup validation
|
||||||
|
- `state.ts` - Setup global state
|
||||||
|
- `upload-file.ts` - File upload with validation
|
||||||
|
- `websocket.ts` - Web Socket for realtime communication
|
||||||
|
|
||||||
|
### patterns/ (optional)
|
||||||
|
- `patterns/mvc.md` - Detail guideline for using Elysia with MVC patterns
|
||||||
9
.kilocode/skills/elysiajs/examples/basic.ts
Normal file
9
.kilocode/skills/elysiajs/examples/basic.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', 'Hello Elysia')
|
||||||
|
.post('/', ({ body: { name } }) => name, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
33
.kilocode/skills/elysiajs/examples/body-parser.ts
Normal file
33
.kilocode/skills/elysiajs/examples/body-parser.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
// Add custom body parser
|
||||||
|
.onParse(async ({ request, contentType }) => {
|
||||||
|
switch (contentType) {
|
||||||
|
case 'application/Elysia':
|
||||||
|
return request.text()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post('/', ({ body: { username } }) => `Hi ${username}`, {
|
||||||
|
body: t.Object({
|
||||||
|
id: t.Number(),
|
||||||
|
username: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// Increase id by 1 from body before main handler
|
||||||
|
.post('/transform', ({ body }) => body, {
|
||||||
|
transform: ({ body }) => {
|
||||||
|
body.id = body.id + 1
|
||||||
|
},
|
||||||
|
body: t.Object({
|
||||||
|
id: t.Number(),
|
||||||
|
username: t.String()
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: 'A'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post('/mirror', ({ body }) => body)
|
||||||
|
.listen(3000)
|
||||||
|
|
||||||
|
console.log('🦊 Elysia is running at :8080')
|
||||||
112
.kilocode/skills/elysiajs/examples/complex.ts
Normal file
112
.kilocode/skills/elysiajs/examples/complex.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { Elysia, t, file } from 'elysia'
|
||||||
|
|
||||||
|
const loggerPlugin = new Elysia()
|
||||||
|
.get('/hi', () => 'Hi')
|
||||||
|
.decorate('log', () => 'A')
|
||||||
|
.decorate('date', () => new Date())
|
||||||
|
.state('fromPlugin', 'From Logger')
|
||||||
|
.use((app) => app.state('abc', 'abc'))
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.onRequest(({ set }) => {
|
||||||
|
set.headers = {
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onError(({ code }) => {
|
||||||
|
if (code === 'NOT_FOUND')
|
||||||
|
return 'Not Found :('
|
||||||
|
})
|
||||||
|
.use(loggerPlugin)
|
||||||
|
.state('build', Date.now())
|
||||||
|
.get('/', 'Elysia')
|
||||||
|
.get('/tako', file('./example/takodachi.png'))
|
||||||
|
.get('/json', () => ({
|
||||||
|
hi: 'world'
|
||||||
|
}))
|
||||||
|
.get('/root/plugin/log', ({ log, store: { build } }) => {
|
||||||
|
log()
|
||||||
|
|
||||||
|
return build
|
||||||
|
})
|
||||||
|
.get('/wildcard/*', () => 'Hi Wildcard')
|
||||||
|
.get('/query', () => 'Elysia', {
|
||||||
|
beforeHandle: ({ query }) => {
|
||||||
|
console.log('Name:', query?.name)
|
||||||
|
|
||||||
|
if (query?.name === 'aom') return 'Hi saltyaom'
|
||||||
|
},
|
||||||
|
query: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.post('/json', async ({ body }) => body, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
additional: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.post('/transform-body', async ({ body }) => body, {
|
||||||
|
beforeHandle: (ctx) => {
|
||||||
|
ctx.body = {
|
||||||
|
...ctx.body,
|
||||||
|
additional: 'Elysia'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
additional: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get('/id/:id', ({ params: { id } }) => id, {
|
||||||
|
transform({ params }) {
|
||||||
|
params.id = +params.id
|
||||||
|
},
|
||||||
|
params: t.Object({
|
||||||
|
id: t.Number()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.post('/new/:id', async ({ body, params }) => body, {
|
||||||
|
params: t.Object({
|
||||||
|
id: t.Number()
|
||||||
|
}),
|
||||||
|
body: t.Object({
|
||||||
|
username: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get('/trailing-slash', () => 'A')
|
||||||
|
.group('/group', (app) =>
|
||||||
|
app
|
||||||
|
.onBeforeHandle(({ query }) => {
|
||||||
|
if (query?.name === 'aom') return 'Hi saltyaom'
|
||||||
|
})
|
||||||
|
.get('/', () => 'From Group')
|
||||||
|
.get('/hi', () => 'HI GROUP')
|
||||||
|
.get('/elysia', () => 'Welcome to Elysian Realm')
|
||||||
|
.get('/fbk', () => 'FuBuKing')
|
||||||
|
)
|
||||||
|
.get('/response-header', ({ set }) => {
|
||||||
|
set.status = 404
|
||||||
|
set.headers['a'] = 'b'
|
||||||
|
|
||||||
|
return 'A'
|
||||||
|
})
|
||||||
|
.get('/this/is/my/deep/nested/root', () => 'Hi')
|
||||||
|
.get('/build', ({ store: { build } }) => build)
|
||||||
|
.get('/ref', ({ date }) => date())
|
||||||
|
.get('/response', () => new Response('Hi'))
|
||||||
|
.get('/error', () => new Error('Something went wrong'))
|
||||||
|
.get('/401', ({ set }) => {
|
||||||
|
set.status = 401
|
||||||
|
|
||||||
|
return 'Status should be 401'
|
||||||
|
})
|
||||||
|
.get('/timeout', async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||||
|
|
||||||
|
return 'A'
|
||||||
|
})
|
||||||
|
.all('/all', () => 'hi')
|
||||||
|
.listen(8080, ({ hostname, port }) => {
|
||||||
|
console.log(`🦊 Elysia is running at http://${hostname}:${port}`)
|
||||||
|
})
|
||||||
45
.kilocode/skills/elysiajs/examples/cookie.ts
Normal file
45
.kilocode/skills/elysiajs/examples/cookie.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia({
|
||||||
|
cookie: {
|
||||||
|
secrets: 'Fischl von Luftschloss Narfidort',
|
||||||
|
sign: ['name']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get(
|
||||||
|
'/council',
|
||||||
|
({ cookie: { council } }) =>
|
||||||
|
(council.value = [
|
||||||
|
{
|
||||||
|
name: 'Rin',
|
||||||
|
affilation: 'Administration'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
cookie: t.Cookie({
|
||||||
|
council: t.Array(
|
||||||
|
t.Object({
|
||||||
|
name: t.String(),
|
||||||
|
affilation: t.String()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.get('/create', ({ cookie: { name } }) => (name.value = 'Himari'))
|
||||||
|
.get(
|
||||||
|
'/update',
|
||||||
|
({ cookie: { name } }) => {
|
||||||
|
name.value = 'seminar: Rio'
|
||||||
|
name.value = 'seminar: Himari'
|
||||||
|
name.maxAge = 86400
|
||||||
|
|
||||||
|
return name.value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cookie: t.Cookie({
|
||||||
|
name: t.Optional(t.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
38
.kilocode/skills/elysiajs/examples/error.ts
Normal file
38
.kilocode/skills/elysiajs/examples/error.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
class CustomError extends Error {
|
||||||
|
constructor(public name: string) {
|
||||||
|
super(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.error({
|
||||||
|
CUSTOM_ERROR: CustomError
|
||||||
|
})
|
||||||
|
// global handler
|
||||||
|
.onError(({ code, error, status }) => {
|
||||||
|
switch (code) {
|
||||||
|
case "CUSTOM_ERROR":
|
||||||
|
return status(401, { message: error.message })
|
||||||
|
|
||||||
|
case "NOT_FOUND":
|
||||||
|
return "Not found :("
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post('/', ({ body }) => body, {
|
||||||
|
body: t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String(),
|
||||||
|
nested: t.Optional(
|
||||||
|
t.Object({
|
||||||
|
hi: t.String()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
// local handler
|
||||||
|
error({ error }) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.listen(3000)
|
||||||
10
.kilocode/skills/elysiajs/examples/file.ts
Normal file
10
.kilocode/skills/elysiajs/examples/file.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Elysia, file } from 'elysia'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example of handle single static file
|
||||||
|
*
|
||||||
|
* @see https://github.com/elysiajs/elysia-static
|
||||||
|
*/
|
||||||
|
new Elysia()
|
||||||
|
.get('/tako', file('./example/takodachi.png'))
|
||||||
|
.listen(3000)
|
||||||
34
.kilocode/skills/elysiajs/examples/guard.ts
Normal file
34
.kilocode/skills/elysiajs/examples/guard.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.state('name', 'salt')
|
||||||
|
.get('/', ({ store: { name } }) => `Hi ${name}`, {
|
||||||
|
query: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// If query 'name' is not preset, skip the whole handler
|
||||||
|
.guard(
|
||||||
|
{
|
||||||
|
query: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
(app) =>
|
||||||
|
app
|
||||||
|
// Query type is inherited from guard
|
||||||
|
.get('/profile', ({ query }) => `Hi`)
|
||||||
|
// Store is inherited
|
||||||
|
.post('/name', ({ store: { name }, body, query }) => name, {
|
||||||
|
body: t.Object({
|
||||||
|
id: t.Number({
|
||||||
|
minimum: 5
|
||||||
|
}),
|
||||||
|
username: t.String(),
|
||||||
|
profile: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
15
.kilocode/skills/elysiajs/examples/map-response.ts
Normal file
15
.kilocode/skills/elysiajs/examples/map-response.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
const prettyJson = new Elysia()
|
||||||
|
.mapResponse(({ response }) => {
|
||||||
|
if (response instanceof Object)
|
||||||
|
return new Response(JSON.stringify(response, null, 4))
|
||||||
|
})
|
||||||
|
.as('scoped')
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(prettyJson)
|
||||||
|
.get('/', () => ({
|
||||||
|
hello: 'world'
|
||||||
|
}))
|
||||||
|
.listen(3000)
|
||||||
6
.kilocode/skills/elysiajs/examples/redirect.ts
Normal file
6
.kilocode/skills/elysiajs/examples/redirect.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', () => 'Hi')
|
||||||
|
.get('/redirect', ({ redirect }) => redirect('/'))
|
||||||
|
.listen(3000)
|
||||||
32
.kilocode/skills/elysiajs/examples/rename.ts
Normal file
32
.kilocode/skills/elysiajs/examples/rename.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
// ? Elysia#83 | Proposal: Standardized way of renaming third party plugin-scoped stuff
|
||||||
|
// this would be a plugin provided by a third party
|
||||||
|
const myPlugin = new Elysia()
|
||||||
|
.decorate('myProperty', 42)
|
||||||
|
.model('salt', t.String())
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(
|
||||||
|
myPlugin
|
||||||
|
// map decorator, rename "myProperty" to "renamedProperty"
|
||||||
|
.decorate(({ myProperty, ...decorators }) => ({
|
||||||
|
renamedProperty: myProperty,
|
||||||
|
...decorators
|
||||||
|
}))
|
||||||
|
// map model, rename "salt" to "pepper"
|
||||||
|
.model(({ salt, ...models }) => ({
|
||||||
|
...models,
|
||||||
|
pepper: t.String()
|
||||||
|
}))
|
||||||
|
// Add prefix
|
||||||
|
.prefix('decorator', 'unstable')
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
'/mapped',
|
||||||
|
({ unstableRenamedProperty }) => unstableRenamedProperty
|
||||||
|
)
|
||||||
|
.post('/pepper', ({ body }) => body, {
|
||||||
|
body: 'pepper',
|
||||||
|
// response: t.String()
|
||||||
|
})
|
||||||
61
.kilocode/skills/elysiajs/examples/schema.ts
Normal file
61
.kilocode/skills/elysiajs/examples/schema.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.model({
|
||||||
|
name: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
}),
|
||||||
|
b: t.Object({
|
||||||
|
response: t.Number()
|
||||||
|
}),
|
||||||
|
authorization: t.Object({
|
||||||
|
authorization: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// Strictly validate response
|
||||||
|
.get('/', () => 'hi')
|
||||||
|
// Strictly validate body and response
|
||||||
|
.post('/', ({ body, query }) => body.id, {
|
||||||
|
body: t.Object({
|
||||||
|
id: t.Number(),
|
||||||
|
username: t.String(),
|
||||||
|
profile: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// Strictly validate query, params, and body
|
||||||
|
.get('/query/:id', ({ query: { name }, params }) => name, {
|
||||||
|
query: t.Object({
|
||||||
|
name: t.String()
|
||||||
|
}),
|
||||||
|
params: t.Object({
|
||||||
|
id: t.String()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: t.String(),
|
||||||
|
300: t.Object({
|
||||||
|
error: t.String()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.guard(
|
||||||
|
{
|
||||||
|
headers: 'authorization'
|
||||||
|
},
|
||||||
|
(app) =>
|
||||||
|
app
|
||||||
|
.derive(({ headers }) => ({
|
||||||
|
userId: headers.authorization
|
||||||
|
}))
|
||||||
|
.get('/', ({ userId }) => 'A')
|
||||||
|
.post('/id/:id', ({ query, body, params, userId }) => body, {
|
||||||
|
params: t.Object({
|
||||||
|
id: t.Number()
|
||||||
|
}),
|
||||||
|
transform({ params }) {
|
||||||
|
params.id = +params.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
6
.kilocode/skills/elysiajs/examples/state.ts
Normal file
6
.kilocode/skills/elysiajs/examples/state.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.state('counter', 0)
|
||||||
|
.get('/', ({ store }) => store.counter++)
|
||||||
|
.listen(3000)
|
||||||
20
.kilocode/skills/elysiajs/examples/upload-file.ts
Normal file
20
.kilocode/skills/elysiajs/examples/upload-file.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.post('/single', ({ body: { file } }) => file, {
|
||||||
|
body: t.Object({
|
||||||
|
file: t.File({
|
||||||
|
maxSize: '1m'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.post(
|
||||||
|
'/multiple',
|
||||||
|
({ body: { files } }) => files.reduce((a, b) => a + b.size, 0),
|
||||||
|
{
|
||||||
|
body: t.Object({
|
||||||
|
files: t.Files()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
25
.kilocode/skills/elysiajs/examples/websocket.ts
Normal file
25
.kilocode/skills/elysiajs/examples/websocket.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.state('start', 'here')
|
||||||
|
.ws('/ws', {
|
||||||
|
open(ws) {
|
||||||
|
ws.subscribe('asdf')
|
||||||
|
console.log('Open Connection:', ws.id)
|
||||||
|
},
|
||||||
|
close(ws) {
|
||||||
|
console.log('Closed Connection:', ws.id)
|
||||||
|
},
|
||||||
|
message(ws, message) {
|
||||||
|
ws.publish('asdf', message)
|
||||||
|
ws.send(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get('/publish/:publish', ({ params: { publish: text } }) => {
|
||||||
|
app.server!.publish('asdf', text)
|
||||||
|
|
||||||
|
return text
|
||||||
|
})
|
||||||
|
.listen(3000, (server) => {
|
||||||
|
console.log(`http://${server.hostname}:${server.port}`)
|
||||||
|
})
|
||||||
92
.kilocode/skills/elysiajs/integrations/ai-sdk.md
Normal file
92
.kilocode/skills/elysiajs/integrations/ai-sdk.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# AI SDK Integration
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Seamless integration with Vercel AI SDK via response streaming.
|
||||||
|
|
||||||
|
## Response Streaming
|
||||||
|
Return `ReadableStream` or `Response` directly:
|
||||||
|
```typescript
|
||||||
|
import { streamText } from 'ai'
|
||||||
|
import { openai } from '@ai-sdk/openai'
|
||||||
|
|
||||||
|
new Elysia().get('/', () => {
|
||||||
|
const stream = streamText({
|
||||||
|
model: openai('gpt-5'),
|
||||||
|
system: 'You are Yae Miko from Genshin Impact',
|
||||||
|
prompt: 'Hi! How are you doing?'
|
||||||
|
})
|
||||||
|
|
||||||
|
return stream.textStream // ReadableStream
|
||||||
|
// or
|
||||||
|
return stream.toUIMessageStream() // UI Message Stream
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Elysia auto-handles stream.
|
||||||
|
|
||||||
|
## Server-Sent Events
|
||||||
|
Wrap `ReadableStream` with `sse`:
|
||||||
|
```typescript
|
||||||
|
import { sse } from 'elysia'
|
||||||
|
|
||||||
|
.get('/', () => {
|
||||||
|
const stream = streamText({ /* ... */ })
|
||||||
|
|
||||||
|
return sse(stream.textStream)
|
||||||
|
// or
|
||||||
|
return sse(stream.toUIMessageStream())
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Each chunk → SSE.
|
||||||
|
|
||||||
|
## As Response
|
||||||
|
Return stream directly (no Eden type safety):
|
||||||
|
```typescript
|
||||||
|
.get('/', () => {
|
||||||
|
const stream = streamText({ /* ... */ })
|
||||||
|
|
||||||
|
return stream.toTextStreamResponse()
|
||||||
|
// or
|
||||||
|
return stream.toUIMessageStreamResponse() // Uses SSE
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Streaming
|
||||||
|
Generator function for control:
|
||||||
|
```typescript
|
||||||
|
import { sse } from 'elysia'
|
||||||
|
|
||||||
|
.get('/', async function* () {
|
||||||
|
const stream = streamText({ /* ... */ })
|
||||||
|
|
||||||
|
for await (const data of stream.textStream)
|
||||||
|
yield sse({ data, event: 'message' })
|
||||||
|
|
||||||
|
yield sse({ event: 'done' })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fetch for Unsupported Models
|
||||||
|
Direct fetch with streaming proxy:
|
||||||
|
```typescript
|
||||||
|
.get('/', () => {
|
||||||
|
return fetch('https://api.openai.com/v1/chat/completions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'gpt-5',
|
||||||
|
stream: true,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: 'You are Yae Miko' },
|
||||||
|
{ role: 'user', content: 'Hi! How are you doing?' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Elysia auto-proxies fetch response with streaming.
|
||||||
59
.kilocode/skills/elysiajs/integrations/astro.md
Normal file
59
.kilocode/skills/elysiajs/integrations/astro.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Astro Integration - SKILLS.md
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Run Elysia on Astro via Astro Endpoint.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Set output to server:
|
||||||
|
```javascript
|
||||||
|
// astro.config.mjs
|
||||||
|
export default defineConfig({
|
||||||
|
output: 'server'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create `pages/[...slugs].ts`
|
||||||
|
3. Define Elysia server + export handlers:
|
||||||
|
```typescript
|
||||||
|
// pages/[...slugs].ts
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/api', () => 'hi')
|
||||||
|
.post('/api', ({ body }) => body, {
|
||||||
|
body: t.Object({ name: t.String() })
|
||||||
|
})
|
||||||
|
|
||||||
|
const handle = ({ request }: { request: Request }) => app.handle(request)
|
||||||
|
|
||||||
|
export const GET = handle
|
||||||
|
export const POST = handle
|
||||||
|
```
|
||||||
|
|
||||||
|
WinterCG compliance - works normally.
|
||||||
|
|
||||||
|
Recommended: Run Astro on Bun (Elysia designed for Bun).
|
||||||
|
|
||||||
|
## Prefix for Non-Root
|
||||||
|
If placed in `pages/api/[...slugs].ts`, set prefix:
|
||||||
|
```typescript
|
||||||
|
// pages/api/[...slugs].ts
|
||||||
|
const app = new Elysia({ prefix: '/api' })
|
||||||
|
.get('/', () => 'hi')
|
||||||
|
|
||||||
|
const handle = ({ request }: { request: Request }) => app.handle(request)
|
||||||
|
|
||||||
|
export const GET = handle
|
||||||
|
export const POST = handle
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensures routing works in any location.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
Co-location of frontend + backend. End-to-end type safety with Eden.
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
117
.kilocode/skills/elysiajs/integrations/better-auth.md
Normal file
117
.kilocode/skills/elysiajs/integrations/better-auth.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Better Auth Integration
|
||||||
|
Elysia + Better Auth integration guide
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Framework-agnostic TypeScript auth/authz. Comprehensive features + plugin ecosystem.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
```typescript
|
||||||
|
import { betterAuth } from 'better-auth'
|
||||||
|
import { Pool } from 'pg'
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
database: new Pool()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handler Mounting
|
||||||
|
```typescript
|
||||||
|
import { auth } from './auth'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.mount(auth.handler) // http://localhost:3000/api/auth
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Endpoint
|
||||||
|
```typescript
|
||||||
|
// Mount with prefix
|
||||||
|
.mount('/auth', auth.handler) // http://localhost:3000/auth/api/auth
|
||||||
|
|
||||||
|
// Customize basePath
|
||||||
|
export const auth = betterAuth({
|
||||||
|
basePath: '/api' // http://localhost:3000/auth/api
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Cannot set `basePath` to empty or `/`.
|
||||||
|
|
||||||
|
## OpenAPI Integration
|
||||||
|
Extract docs from Better Auth:
|
||||||
|
```typescript
|
||||||
|
import { openAPI } from 'better-auth/plugins'
|
||||||
|
|
||||||
|
let _schema: ReturnType<typeof auth.api.generateOpenAPISchema>
|
||||||
|
const getSchema = async () => (_schema ??= auth.api.generateOpenAPISchema())
|
||||||
|
|
||||||
|
export const OpenAPI = {
|
||||||
|
getPaths: (prefix = '/auth/api') =>
|
||||||
|
getSchema().then(({ paths }) => {
|
||||||
|
const reference: typeof paths = Object.create(null)
|
||||||
|
|
||||||
|
for (const path of Object.keys(paths)) {
|
||||||
|
const key = prefix + path
|
||||||
|
reference[key] = paths[path]
|
||||||
|
|
||||||
|
for (const method of Object.keys(paths[path])) {
|
||||||
|
const operation = (reference[key] as any)[method]
|
||||||
|
operation.tags = ['Better Auth']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reference
|
||||||
|
}) as Promise<any>,
|
||||||
|
components: getSchema().then(({ components }) => components) as Promise<any>
|
||||||
|
} as const
|
||||||
|
```
|
||||||
|
|
||||||
|
Apply to Elysia:
|
||||||
|
```typescript
|
||||||
|
new Elysia().use(openapi({
|
||||||
|
documentation: {
|
||||||
|
components: await OpenAPI.components,
|
||||||
|
paths: await OpenAPI.getPaths()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
## CORS
|
||||||
|
```typescript
|
||||||
|
import { cors } from '@elysiajs/cors'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(cors({
|
||||||
|
origin: 'http://localhost:3001',
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
credentials: true,
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization']
|
||||||
|
}))
|
||||||
|
.mount(auth.handler)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Macro for Auth
|
||||||
|
Use macro + resolve for session/user:
|
||||||
|
```typescript
|
||||||
|
const betterAuth = new Elysia({ name: 'better-auth' })
|
||||||
|
.mount(auth.handler)
|
||||||
|
.macro({
|
||||||
|
auth: {
|
||||||
|
async resolve({ status, request: { headers } }) {
|
||||||
|
const session = await auth.api.getSession({ headers })
|
||||||
|
|
||||||
|
if (!session) return status(401)
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: session.user,
|
||||||
|
session: session.session
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(betterAuth)
|
||||||
|
.get('/user', ({ user }) => user, { auth: true })
|
||||||
|
```
|
||||||
|
|
||||||
|
Access `user` and `session` in all routes.
|
||||||
95
.kilocode/skills/elysiajs/integrations/cloudflare-worker.md
Normal file
95
.kilocode/skills/elysiajs/integrations/cloudflare-worker.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
|
||||||
|
# Cloudflare Worker Integration
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
**Experimental** Cloudflare Worker adapter for Elysia.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Install Wrangler:
|
||||||
|
```bash
|
||||||
|
wrangler init elysia-on-cloudflare
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Apply adapter + compile:
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { CloudflareAdapter } from 'elysia/adapter/cloudflare-worker'
|
||||||
|
|
||||||
|
export default new Elysia({
|
||||||
|
adapter: CloudflareAdapter
|
||||||
|
})
|
||||||
|
.get('/', () => 'Hello Cloudflare Worker!')
|
||||||
|
.compile() // Required
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Set compatibility date (min `2025-06-01`):
|
||||||
|
```json
|
||||||
|
// wrangler.json
|
||||||
|
{
|
||||||
|
"name": "elysia-on-cloudflare",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"compatibility_date": "2025-06-01"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Dev server:
|
||||||
|
```bash
|
||||||
|
wrangler dev
|
||||||
|
# http://localhost:8787
|
||||||
|
```
|
||||||
|
|
||||||
|
No `nodejs_compat` flag needed.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
1. `Elysia.file` + Static Plugin don't work (no `fs` module)
|
||||||
|
2. OpenAPI Type Gen doesn't work (no `fs` module)
|
||||||
|
3. Cannot define Response before server start
|
||||||
|
4. Cannot inline values:
|
||||||
|
```typescript
|
||||||
|
// ❌ Throws error
|
||||||
|
.get('/', 'Hello Elysia')
|
||||||
|
|
||||||
|
// ✅ Works
|
||||||
|
.get('/', () => 'Hello Elysia')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Static Files
|
||||||
|
Use Cloudflare's built-in static serving:
|
||||||
|
```json
|
||||||
|
// wrangler.json
|
||||||
|
{
|
||||||
|
"assets": { "directory": "public" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Structure:
|
||||||
|
```
|
||||||
|
├─ public
|
||||||
|
│ ├─ kyuukurarin.mp4
|
||||||
|
│ └─ static/mika.webp
|
||||||
|
```
|
||||||
|
|
||||||
|
Access:
|
||||||
|
- `http://localhost:8787/kyuukurarin.mp4`
|
||||||
|
- `http://localhost:8787/static/mika.webp`
|
||||||
|
|
||||||
|
## Binding
|
||||||
|
Import env from `cloudflare:workers`:
|
||||||
|
```typescript
|
||||||
|
import { env } from 'cloudflare:workers'
|
||||||
|
|
||||||
|
export default new Elysia({ adapter: CloudflareAdapter })
|
||||||
|
.get('/', () => `Hello ${await env.KV.get('my-key')}`)
|
||||||
|
.compile()
|
||||||
|
```
|
||||||
|
|
||||||
|
## AoT Compilation
|
||||||
|
As of Elysia 1.4.7, AoT works with Cloudflare Worker. Drop `aot: false` flag.
|
||||||
|
|
||||||
|
Cloudflare now supports Function compilation during startup.
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
34
.kilocode/skills/elysiajs/integrations/deno.md
Normal file
34
.kilocode/skills/elysiajs/integrations/deno.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Deno Integration
|
||||||
|
Run Elysia on Deno
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Run Elysia on Deno via Web Standard Request/Response.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
Wrap `Elysia.fetch` in `Deno.serve`:
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/', () => 'Hello Elysia')
|
||||||
|
.listen(3000)
|
||||||
|
|
||||||
|
Deno.serve(app.fetch)
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
deno serve --watch src/index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Port Config
|
||||||
|
```typescript
|
||||||
|
Deno.serve(app.fetch) // Default
|
||||||
|
Deno.serve({ port: 8787 }, app.fetch) // Custom port
|
||||||
|
```
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
[Inference] pnpm doesn't auto-install peer deps. Manual install required:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
258
.kilocode/skills/elysiajs/integrations/drizzle.md
Normal file
258
.kilocode/skills/elysiajs/integrations/drizzle.md
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
# Drizzle Integration
|
||||||
|
Elysia + Drizzle integration guide
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Headless TypeScript ORM. Convert Drizzle schema → Elysia validation models via `drizzle-typebox`.
|
||||||
|
|
||||||
|
## Flow
|
||||||
|
```
|
||||||
|
Drizzle → drizzle-typebox → Elysia validation → OpenAPI + Eden Treaty
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add drizzle-orm drizzle-typebox
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pin TypeBox Version
|
||||||
|
Prevent Symbol conflicts:
|
||||||
|
```bash
|
||||||
|
grep "@sinclair/typebox" node_modules/elysia/package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to `package.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"overrides": {
|
||||||
|
"@sinclair/typebox": "0.32.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Drizzle Schema
|
||||||
|
```typescript
|
||||||
|
// src/database/schema.ts
|
||||||
|
import { pgTable, varchar, timestamp } from 'drizzle-orm/pg-core'
|
||||||
|
import { createId } from '@paralleldrive/cuid2'
|
||||||
|
|
||||||
|
export const user = pgTable('user', {
|
||||||
|
id: varchar('id').$defaultFn(() => createId()).primaryKey(),
|
||||||
|
username: varchar('username').notNull().unique(),
|
||||||
|
password: varchar('password').notNull(),
|
||||||
|
email: varchar('email').notNull().unique(),
|
||||||
|
salt: varchar('salt', { length: 64 }).notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const table = { user } as const
|
||||||
|
export type Table = typeof table
|
||||||
|
```
|
||||||
|
|
||||||
|
## drizzle-typebox
|
||||||
|
```typescript
|
||||||
|
import { t } from 'elysia'
|
||||||
|
import { createInsertSchema } from 'drizzle-typebox'
|
||||||
|
import { table } from './database/schema'
|
||||||
|
|
||||||
|
const _createUser = createInsertSchema(table.user, {
|
||||||
|
email: t.String({ format: 'email' }) // Replace with Elysia type
|
||||||
|
})
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.post('/sign-up', ({ body }) => {}, {
|
||||||
|
body: t.Omit(_createUser, ['id', 'salt', 'createdAt'])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Instantiation Error
|
||||||
|
**Error**: "Type instantiation is possibly infinite"
|
||||||
|
|
||||||
|
**Cause**: Circular reference when nesting drizzle-typebox into Elysia schema.
|
||||||
|
|
||||||
|
**Fix**: Explicitly define type between them:
|
||||||
|
```typescript
|
||||||
|
// ✅ Works
|
||||||
|
const _createUser = createInsertSchema(table.user, {
|
||||||
|
email: t.String({ format: 'email' })
|
||||||
|
})
|
||||||
|
const createUser = t.Omit(_createUser, ['id', 'salt', 'createdAt'])
|
||||||
|
|
||||||
|
// ❌ Infinite loop
|
||||||
|
const createUser = t.Omit(
|
||||||
|
createInsertSchema(table.user, { email: t.String({ format: 'email' }) }),
|
||||||
|
['id', 'salt', 'createdAt']
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Always declare variable for drizzle-typebox then reference it.
|
||||||
|
|
||||||
|
## Utility Functions
|
||||||
|
Copy as-is for simplified usage:
|
||||||
|
```typescript
|
||||||
|
// src/database/utils.ts
|
||||||
|
/**
|
||||||
|
* @lastModified 2025-02-04
|
||||||
|
* @see https://elysiajs.com/recipe/drizzle.html#utility
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Kind, type TObject } from '@sinclair/typebox'
|
||||||
|
import {
|
||||||
|
createInsertSchema,
|
||||||
|
createSelectSchema,
|
||||||
|
BuildSchema,
|
||||||
|
} from 'drizzle-typebox'
|
||||||
|
|
||||||
|
import { table } from './schema'
|
||||||
|
import type { Table } from 'drizzle-orm'
|
||||||
|
|
||||||
|
type Spread<
|
||||||
|
T extends TObject | Table,
|
||||||
|
Mode extends 'select' | 'insert' | undefined,
|
||||||
|
> =
|
||||||
|
T extends TObject<infer Fields>
|
||||||
|
? {
|
||||||
|
[K in keyof Fields]: Fields[K]
|
||||||
|
}
|
||||||
|
: T extends Table
|
||||||
|
? Mode extends 'select'
|
||||||
|
? BuildSchema<
|
||||||
|
'select',
|
||||||
|
T['_']['columns'],
|
||||||
|
undefined
|
||||||
|
>['properties']
|
||||||
|
: Mode extends 'insert'
|
||||||
|
? BuildSchema<
|
||||||
|
'insert',
|
||||||
|
T['_']['columns'],
|
||||||
|
undefined
|
||||||
|
>['properties']
|
||||||
|
: {}
|
||||||
|
: {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spread a Drizzle schema into a plain object
|
||||||
|
*/
|
||||||
|
export const spread = <
|
||||||
|
T extends TObject | Table,
|
||||||
|
Mode extends 'select' | 'insert' | undefined,
|
||||||
|
>(
|
||||||
|
schema: T,
|
||||||
|
mode?: Mode,
|
||||||
|
): Spread<T, Mode> => {
|
||||||
|
const newSchema: Record<string, unknown> = {}
|
||||||
|
let table
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case 'insert':
|
||||||
|
case 'select':
|
||||||
|
if (Kind in schema) {
|
||||||
|
table = schema
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
table =
|
||||||
|
mode === 'insert'
|
||||||
|
? createInsertSchema(schema)
|
||||||
|
: createSelectSchema(schema)
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (!(Kind in schema)) throw new Error('Expect a schema')
|
||||||
|
table = schema
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(table.properties))
|
||||||
|
newSchema[key] = table.properties[key]
|
||||||
|
|
||||||
|
return newSchema as any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spread a Drizzle Table into a plain object
|
||||||
|
*
|
||||||
|
* If `mode` is 'insert', the schema will be refined for insert
|
||||||
|
* If `mode` is 'select', the schema will be refined for select
|
||||||
|
* If `mode` is undefined, the schema will be spread as is, models will need to be refined manually
|
||||||
|
*/
|
||||||
|
export const spreads = <
|
||||||
|
T extends Record<string, TObject | Table>,
|
||||||
|
Mode extends 'select' | 'insert' | undefined,
|
||||||
|
>(
|
||||||
|
models: T,
|
||||||
|
mode?: Mode,
|
||||||
|
): {
|
||||||
|
[K in keyof T]: Spread<T[K], Mode>
|
||||||
|
} => {
|
||||||
|
const newSchema: Record<string, unknown> = {}
|
||||||
|
const keys = Object.keys(models)
|
||||||
|
|
||||||
|
for (const key of keys) newSchema[key] = spread(models[key], mode)
|
||||||
|
|
||||||
|
return newSchema as any
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```typescript
|
||||||
|
// ✅ Using spread
|
||||||
|
const user = spread(table.user, 'insert')
|
||||||
|
const createUser = t.Object({
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
password: user.password
|
||||||
|
})
|
||||||
|
|
||||||
|
// ⚠️ Using t.Pick
|
||||||
|
const _createUser = createInsertSchema(table.user)
|
||||||
|
const createUser = t.Pick(_createUser, ['id', 'username', 'password'])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Table Singleton Pattern
|
||||||
|
```typescript
|
||||||
|
// src/database/model.ts
|
||||||
|
import { table } from './schema'
|
||||||
|
import { spreads } from './utils'
|
||||||
|
|
||||||
|
export const db = {
|
||||||
|
insert: spreads({ user: table.user }, 'insert'),
|
||||||
|
select: spreads({ user: table.user }, 'select')
|
||||||
|
} as const
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```typescript
|
||||||
|
// src/index.ts
|
||||||
|
import { db } from './database/model'
|
||||||
|
const { user } = db.insert
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.post('/sign-up', ({ body }) => {}, {
|
||||||
|
body: t.Object({
|
||||||
|
id: user.username,
|
||||||
|
username: user.username,
|
||||||
|
password: user.password
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Refinement
|
||||||
|
```typescript
|
||||||
|
// src/database/model.ts
|
||||||
|
import { createInsertSchema, createSelectSchema } from 'drizzle-typebox'
|
||||||
|
|
||||||
|
export const db = {
|
||||||
|
insert: spreads({
|
||||||
|
user: createInsertSchema(table.user, {
|
||||||
|
email: t.String({ format: 'email' })
|
||||||
|
})
|
||||||
|
}, 'insert'),
|
||||||
|
select: spreads({
|
||||||
|
user: createSelectSchema(table.user, {
|
||||||
|
email: t.String({ format: 'email' })
|
||||||
|
})
|
||||||
|
}, 'select')
|
||||||
|
} as const
|
||||||
|
```
|
||||||
|
|
||||||
|
`spread` skips refined schemas.
|
||||||
95
.kilocode/skills/elysiajs/integrations/expo.md
Normal file
95
.kilocode/skills/elysiajs/integrations/expo.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# Expo Integration
|
||||||
|
Run Elysia on Expo (React Native)
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Create API routes in Expo app (SDK 50+, App Router v3).
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Create `app/[...slugs]+api.ts`
|
||||||
|
2. Define Elysia server
|
||||||
|
3. Export `Elysia.fetch` as HTTP methods
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app/[...slugs]+api.ts
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/', 'hello Expo')
|
||||||
|
.post('/', ({ body }) => body, {
|
||||||
|
body: t.Object({ name: t.String() })
|
||||||
|
})
|
||||||
|
|
||||||
|
export const GET = app.fetch
|
||||||
|
export const POST = app.fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prefix for Non-Root
|
||||||
|
If placed in `app/api/[...slugs]+api.ts`, set prefix:
|
||||||
|
```typescript
|
||||||
|
const app = new Elysia({ prefix: '/api' })
|
||||||
|
.get('/', 'Hello Expo')
|
||||||
|
|
||||||
|
export const GET = app.fetch
|
||||||
|
export const POST = app.fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensures routing works in any location.
|
||||||
|
|
||||||
|
## Eden (End-to-End Type Safety)
|
||||||
|
1. Export type:
|
||||||
|
```typescript
|
||||||
|
// app/[...slugs]+api.ts
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/', 'Hello Nextjs')
|
||||||
|
.post('/user', ({ body }) => body, {
|
||||||
|
body: treaty.schema('User', { name: 'string' })
|
||||||
|
})
|
||||||
|
|
||||||
|
export type app = typeof app
|
||||||
|
|
||||||
|
export const GET = app.fetch
|
||||||
|
export const POST = app.fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create client:
|
||||||
|
```typescript
|
||||||
|
// lib/eden.ts
|
||||||
|
import { treaty } from '@elysiajs/eden'
|
||||||
|
import type { app } from '../app/[...slugs]+api'
|
||||||
|
|
||||||
|
export const api = treaty<app>('localhost:3000/api')
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Use in components:
|
||||||
|
```tsx
|
||||||
|
// app/page.tsx
|
||||||
|
import { api } from '../lib/eden'
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const message = await api.get()
|
||||||
|
return <h1>Hello, {message}</h1>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
- Deploy as normal Elysia app OR
|
||||||
|
- Use experimental Expo server runtime
|
||||||
|
|
||||||
|
With Expo runtime:
|
||||||
|
```bash
|
||||||
|
expo export
|
||||||
|
# Creates dist/server/_expo/functions/[...slugs]+api.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Edge function, not normal server (no port allocation).
|
||||||
|
|
||||||
|
### Adapters
|
||||||
|
- Express
|
||||||
|
- Netlify
|
||||||
|
- Vercel
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
103
.kilocode/skills/elysiajs/integrations/nextjs.md
Normal file
103
.kilocode/skills/elysiajs/integrations/nextjs.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
|
||||||
|
# Next.js Integration
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Run Elysia on Next.js App Router.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Create `app/api/[[...slugs]]/route.ts`
|
||||||
|
2. Define Elysia + export handlers:
|
||||||
|
```typescript
|
||||||
|
// app/api/[[...slugs]]/route.ts
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia({ prefix: '/api' })
|
||||||
|
.get('/', 'Hello Nextjs')
|
||||||
|
.post('/', ({ body }) => body, {
|
||||||
|
body: t.Object({ name: t.String() })
|
||||||
|
})
|
||||||
|
|
||||||
|
export const GET = app.fetch
|
||||||
|
export const POST = app.fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
WinterCG compliance - works as normal Next.js API route.
|
||||||
|
|
||||||
|
## Prefix for Non-Root
|
||||||
|
If placed in `app/user/[[...slugs]]/route.ts`, set prefix:
|
||||||
|
```typescript
|
||||||
|
const app = new Elysia({ prefix: '/user' })
|
||||||
|
.get('/', 'Hello Nextjs')
|
||||||
|
|
||||||
|
export const GET = app.fetch
|
||||||
|
export const POST = app.fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Eden (End-to-End Type Safety)
|
||||||
|
Isomorphic fetch pattern:
|
||||||
|
- Server: Direct calls (no network)
|
||||||
|
- Client: Network calls
|
||||||
|
|
||||||
|
1. Export type:
|
||||||
|
```typescript
|
||||||
|
// app/api/[[...slugs]]/route.ts
|
||||||
|
export const app = new Elysia({ prefix: '/api' })
|
||||||
|
.get('/', 'Hello Nextjs')
|
||||||
|
.post('/user', ({ body }) => body, {
|
||||||
|
body: treaty.schema('User', { name: 'string' })
|
||||||
|
})
|
||||||
|
|
||||||
|
export type app = typeof app
|
||||||
|
|
||||||
|
export const GET = app.fetch
|
||||||
|
export const POST = app.fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create client:
|
||||||
|
```typescript
|
||||||
|
// lib/eden.ts
|
||||||
|
import { treaty } from '@elysiajs/eden'
|
||||||
|
import type { app } from '../app/api/[[...slugs]]/route'
|
||||||
|
|
||||||
|
export const api =
|
||||||
|
typeof process !== 'undefined'
|
||||||
|
? treaty(app).api
|
||||||
|
: treaty<typeof app>('localhost:3000').api
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `typeof process` not `typeof window` (window undefined at build time → hydration error).
|
||||||
|
|
||||||
|
3. Use in components:
|
||||||
|
```tsx
|
||||||
|
// app/page.tsx
|
||||||
|
import { api } from '../lib/eden'
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const message = await api.get()
|
||||||
|
return <h1>Hello, {message}</h1>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Works with server/client components + ISR.
|
||||||
|
|
||||||
|
## React Query
|
||||||
|
```tsx
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const { data: response } = useQuery({
|
||||||
|
queryKey: ['get'],
|
||||||
|
queryFn: () => getTreaty().get()
|
||||||
|
})
|
||||||
|
|
||||||
|
return response?.data
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Works with all React Query features.
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
64
.kilocode/skills/elysiajs/integrations/nodejs.md
Normal file
64
.kilocode/skills/elysiajs/integrations/nodejs.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Node.js Integration
|
||||||
|
Run Elysia on Node.js
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Runtime adapter to run Elysia on Node.js.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add elysia @elysiajs/node
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
Apply node adapter:
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { node } from '@elysiajs/node'
|
||||||
|
|
||||||
|
const app = new Elysia({ adapter: node() })
|
||||||
|
.get('/', () => 'Hello Elysia')
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Setup (Recommended)
|
||||||
|
Install `tsx` for hot-reload:
|
||||||
|
```bash
|
||||||
|
bun add -d tsx @types/node typescript
|
||||||
|
```
|
||||||
|
|
||||||
|
Scripts in `package.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"build": "tsc src/index.ts --outDir dist",
|
||||||
|
"start": "NODE_ENV=production node dist/index.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **dev**: Hot-reload dev mode
|
||||||
|
- **build**: Production build
|
||||||
|
- **start**: Production server
|
||||||
|
|
||||||
|
Create `tsconfig.json`:
|
||||||
|
```bash
|
||||||
|
tsc --init
|
||||||
|
```
|
||||||
|
|
||||||
|
Update strict mode:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Provides hot-reload + JSX support similar to `bun dev`.
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
67
.kilocode/skills/elysiajs/integrations/nuxt.md
Normal file
67
.kilocode/skills/elysiajs/integrations/nuxt.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Nuxt Integration
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Community plugin `nuxt-elysia` for Nuxt API routes with Eden Treaty.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add elysia @elysiajs/eden
|
||||||
|
bun add -d nuxt-elysia
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Add to Nuxt config:
|
||||||
|
```typescript
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
modules: ['nuxt-elysia']
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create `api.ts` at project root:
|
||||||
|
```typescript
|
||||||
|
// api.ts
|
||||||
|
export default () => new Elysia()
|
||||||
|
.get('/hello', () => ({ message: 'Hello world!' }))
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Use Eden Treaty:
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<p>{{ data.message }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { $api } = useNuxtApp()
|
||||||
|
|
||||||
|
const { data } = await useAsyncData(async () => {
|
||||||
|
const { data, error } = await $api.hello.get()
|
||||||
|
|
||||||
|
if (error) throw new Error('Failed to call API')
|
||||||
|
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-setup on Nuxt API route.
|
||||||
|
|
||||||
|
## Prefix
|
||||||
|
Default: `/_api`. Customize:
|
||||||
|
```typescript
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
nuxtElysia: {
|
||||||
|
path: '/api'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Mounts on `/api` instead of `/_api`.
|
||||||
|
|
||||||
|
See [nuxt-elysia](https://github.com/tkesgar/nuxt-elysia) for more config.
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
93
.kilocode/skills/elysiajs/integrations/prisma.md
Normal file
93
.kilocode/skills/elysiajs/integrations/prisma.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
|
||||||
|
# Prisma Integration
|
||||||
|
Elysia + Prisma integration guide
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Type-safe ORM. Generate Elysia validation models from Prisma schema via `prismabox`.
|
||||||
|
|
||||||
|
## Flow
|
||||||
|
```
|
||||||
|
Prisma → prismabox → Elysia validation → OpenAPI + Eden Treaty
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @prisma/client prismabox && \
|
||||||
|
bun add -d prisma
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prisma Schema
|
||||||
|
Add `prismabox` generator:
|
||||||
|
```prisma
|
||||||
|
// prisma/schema.prisma
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client"
|
||||||
|
output = "../generated/prisma"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
generator prismabox {
|
||||||
|
provider = "prismabox"
|
||||||
|
typeboxImportDependencyName = "elysia"
|
||||||
|
typeboxImportVariableName = "t"
|
||||||
|
inputModel = true
|
||||||
|
output = "../generated/prismabox"
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
email String @unique
|
||||||
|
name String?
|
||||||
|
posts Post[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Post {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
title String
|
||||||
|
content String?
|
||||||
|
published Boolean @default(false)
|
||||||
|
author User @relation(fields: [authorId], references: [id])
|
||||||
|
authorId String
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Generates:
|
||||||
|
- `User` → `generated/prismabox/User.ts`
|
||||||
|
- `Post` → `generated/prismabox/Post.ts`
|
||||||
|
|
||||||
|
## Using Generated Models
|
||||||
|
```typescript
|
||||||
|
// src/index.ts
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
import { PrismaClient } from '../generated/prisma'
|
||||||
|
import { UserPlain, UserPlainInputCreate } from '../generated/prismabox/User'
|
||||||
|
|
||||||
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.put('/', async ({ body }) =>
|
||||||
|
prisma.user.create({ data: body }), {
|
||||||
|
body: UserPlainInputCreate,
|
||||||
|
response: UserPlain
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.get('/id/:id', async ({ params: { id }, status }) => {
|
||||||
|
const user = await prisma.user.findUnique({ where: { id } })
|
||||||
|
|
||||||
|
if (!user) return status(404, 'User not found')
|
||||||
|
|
||||||
|
return user
|
||||||
|
}, {
|
||||||
|
response: {
|
||||||
|
200: UserPlain,
|
||||||
|
404: t.String()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
Reuses DB schema in Elysia validation models.
|
||||||
134
.kilocode/skills/elysiajs/integrations/react-email.md
Normal file
134
.kilocode/skills/elysiajs/integrations/react-email.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# React Email Integration
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Use React components to create emails. Direct JSX import via Bun.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add -d react-email
|
||||||
|
bun add @react-email/components react react-dom
|
||||||
|
```
|
||||||
|
|
||||||
|
Script in `package.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"email": "email dev --dir src/emails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Email templates → `src/emails` directory.
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
Add to `tsconfig.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Email Template
|
||||||
|
```tsx
|
||||||
|
// src/emails/otp.tsx
|
||||||
|
import * as React from 'react'
|
||||||
|
import { Tailwind, Section, Text } from '@react-email/components'
|
||||||
|
|
||||||
|
export default function OTPEmail({ otp }: { otp: number }) {
|
||||||
|
return (
|
||||||
|
<Tailwind>
|
||||||
|
<Section className="flex justify-center items-center w-full min-h-screen font-sans">
|
||||||
|
<Section className="flex flex-col items-center w-76 rounded-2xl px-6 py-1 bg-gray-50">
|
||||||
|
<Text className="text-xs font-medium text-violet-500">
|
||||||
|
Verify your Email Address
|
||||||
|
</Text>
|
||||||
|
<Text className="text-gray-500 my-0">
|
||||||
|
Use the following code to verify your email address
|
||||||
|
</Text>
|
||||||
|
<Text className="text-5xl font-bold pt-2">{otp}</Text>
|
||||||
|
<Text className="text-gray-400 font-light text-xs pb-4">
|
||||||
|
This code is valid for 10 minutes
|
||||||
|
</Text>
|
||||||
|
<Text className="text-gray-600 text-xs">
|
||||||
|
Thank you for joining us
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</Section>
|
||||||
|
</Tailwind>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OTPEmail.PreviewProps = { otp: 123456 }
|
||||||
|
```
|
||||||
|
|
||||||
|
`@react-email/components` → email-client compatible (Gmail, Outlook). Tailwind support.
|
||||||
|
|
||||||
|
`PreviewProps` → playground only.
|
||||||
|
|
||||||
|
## Preview
|
||||||
|
```bash
|
||||||
|
bun email
|
||||||
|
```
|
||||||
|
|
||||||
|
Opens browser with preview.
|
||||||
|
|
||||||
|
## Send Email
|
||||||
|
Render with `react-dom/server`, submit via provider:
|
||||||
|
|
||||||
|
### Nodemailer
|
||||||
|
```typescript
|
||||||
|
import { renderToStaticMarkup } from 'react-dom/server'
|
||||||
|
import OTPEmail from './emails/otp'
|
||||||
|
import nodemailer from 'nodemailer'
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: 'smtp.gehenna.sh',
|
||||||
|
port: 465,
|
||||||
|
auth: { user: 'makoto', pass: '12345678' }
|
||||||
|
})
|
||||||
|
|
||||||
|
.get('/otp', async ({ body }) => {
|
||||||
|
const otp = ~~(Math.random() * 900_000) + 100_000
|
||||||
|
const html = renderToStaticMarkup(<OTPEmail otp={otp} />)
|
||||||
|
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: '[email protected]',
|
||||||
|
to: body,
|
||||||
|
subject: 'Verify your email address',
|
||||||
|
html
|
||||||
|
})
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
}, {
|
||||||
|
body: t.String({ format: 'email' })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resend
|
||||||
|
```typescript
|
||||||
|
import OTPEmail from './emails/otp'
|
||||||
|
import Resend from 'resend'
|
||||||
|
|
||||||
|
const resend = new Resend('re_123456789')
|
||||||
|
|
||||||
|
.get('/otp', ({ body }) => {
|
||||||
|
const otp = ~~(Math.random() * 900_000) + 100_000
|
||||||
|
|
||||||
|
await resend.emails.send({
|
||||||
|
from: '[email protected]',
|
||||||
|
to: body,
|
||||||
|
subject: 'Verify your email address',
|
||||||
|
html: <OTPEmail otp={otp} /> // Direct JSX
|
||||||
|
})
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Direct JSX import thanks to Bun.
|
||||||
|
|
||||||
|
Other providers: AWS SES, SendGrid.
|
||||||
|
|
||||||
|
See [React Email Integrations](https://react.email/docs/integrations/overview).
|
||||||
53
.kilocode/skills/elysiajs/integrations/sveltekit.md
Normal file
53
.kilocode/skills/elysiajs/integrations/sveltekit.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
|
||||||
|
# SvelteKit Integration
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Run Elysia on SvelteKit server routes.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Create `src/routes/[...slugs]/+server.ts`
|
||||||
|
2. Define Elysia server
|
||||||
|
3. Export fallback handler:
|
||||||
|
```typescript
|
||||||
|
// src/routes/[...slugs]/+server.ts
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/', 'hello SvelteKit')
|
||||||
|
.post('/', ({ body }) => body, {
|
||||||
|
body: t.Object({ name: t.String() })
|
||||||
|
})
|
||||||
|
|
||||||
|
interface WithRequest {
|
||||||
|
request: Request
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fallback = ({ request }: WithRequest) => app.handle(request)
|
||||||
|
```
|
||||||
|
|
||||||
|
Treat as normal SvelteKit server route.
|
||||||
|
|
||||||
|
## Prefix for Non-Root
|
||||||
|
If placed in `src/routes/api/[...slugs]/+server.ts`, set prefix:
|
||||||
|
```typescript
|
||||||
|
// src/routes/api/[...slugs]/+server.ts
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia({ prefix: '/api' })
|
||||||
|
.get('/', () => 'hi')
|
||||||
|
.post('/', ({ body }) => body, {
|
||||||
|
body: t.Object({ name: t.String() })
|
||||||
|
})
|
||||||
|
|
||||||
|
type RequestHandler = (v: { request: Request }) => Response | Promise<Response>
|
||||||
|
|
||||||
|
export const fallback: RequestHandler = ({ request }) => app.handle(request)
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensures routing works in any location.
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
87
.kilocode/skills/elysiajs/integrations/tanstack-start.md
Normal file
87
.kilocode/skills/elysiajs/integrations/tanstack-start.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Tanstack Start Integration
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Elysia runs inside Tanstack Start server routes.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Create `src/routes/api.$.ts`
|
||||||
|
2. Define Elysia server
|
||||||
|
3. Export handlers in `server.handlers`:
|
||||||
|
```typescript
|
||||||
|
// src/routes/api.$.ts
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import { createIsomorphicFn } from '@tanstack/react-start'
|
||||||
|
|
||||||
|
const app = new Elysia({
|
||||||
|
prefix: '/api'
|
||||||
|
}).get('/', 'Hello Elysia!')
|
||||||
|
|
||||||
|
const handle = ({ request }: { request: Request }) => app.fetch(request)
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/api/$')({
|
||||||
|
server: {
|
||||||
|
handlers: {
|
||||||
|
GET: handle,
|
||||||
|
POST: handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Runs on `/api`. Add methods to `server.handlers` as needed.
|
||||||
|
|
||||||
|
## Eden (End-to-End Type Safety)
|
||||||
|
Isomorphic pattern with `createIsomorphicFn`:
|
||||||
|
```typescript
|
||||||
|
// src/routes/api.$.ts
|
||||||
|
export const getTreaty = createIsomorphicFn()
|
||||||
|
.server(() => treaty(app).api)
|
||||||
|
.client(() => treaty<typeof app>('localhost:3000').api)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Server: Direct call (no HTTP overhead)
|
||||||
|
- Client: HTTP call
|
||||||
|
|
||||||
|
## Loader Data
|
||||||
|
Fetch before render:
|
||||||
|
```tsx
|
||||||
|
// src/routes/index.tsx
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import { getTreaty } from './api.$'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/a')({
|
||||||
|
component: App,
|
||||||
|
loader: () => getTreaty().get().then((res) => res.data)
|
||||||
|
})
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const data = Route.useLoaderData()
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Executed server-side during SSR. No HTTP overhead. Type-safe.
|
||||||
|
|
||||||
|
## React Query
|
||||||
|
```tsx
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { getTreaty } from './api.$'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const { data: response } = useQuery({
|
||||||
|
queryKey: ['get'],
|
||||||
|
queryFn: () => getTreaty().get()
|
||||||
|
})
|
||||||
|
|
||||||
|
return response?.data
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Works with all React Query features.
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
55
.kilocode/skills/elysiajs/integrations/vercel.md
Normal file
55
.kilocode/skills/elysiajs/integrations/vercel.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Vercel Integration
|
||||||
|
Deploy Elysia on Vercel
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Zero-config deployment on Vercel (Bun or Node runtime).
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Create/import Elysia server in `src/index.ts`
|
||||||
|
2. Export as default:
|
||||||
|
```typescript
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
export default new Elysia()
|
||||||
|
.get('/', () => 'Hello Vercel Function')
|
||||||
|
.post('/', ({ body }) => body, {
|
||||||
|
body: t.Object({ name: t.String() })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Develop locally:
|
||||||
|
```bash
|
||||||
|
vc dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Deploy:
|
||||||
|
```bash
|
||||||
|
vc deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Node.js Runtime
|
||||||
|
Set in `package.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "elysia-app",
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bun Runtime
|
||||||
|
Set in `vercel.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||||
|
"bunVersion": "1.x"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## pnpm
|
||||||
|
Manual install:
|
||||||
|
```bash
|
||||||
|
pnpm add @sinclair/typebox openapi-types
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
Vercel has zero config for Elysia. For additional config, see [Vercel docs](https://vercel.com/docs/frameworks/backend/elysia).
|
||||||
380
.kilocode/skills/elysiajs/patterns/mvc.md
Normal file
380
.kilocode/skills/elysiajs/patterns/mvc.md
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
# MVC pattern
|
||||||
|
This file contains a guideline for using Elysia with MVC or Model View Controller patterns
|
||||||
|
|
||||||
|
- Controller:
|
||||||
|
- Prefers Elysia as a controller for HTTP dependant
|
||||||
|
- For non HTTP dependent, prefers service instead unless explicitly asked
|
||||||
|
- Use `onError` to handle local custom errors
|
||||||
|
- Register Model to Elysia instance via `Elysia.models({ ...models })` and prefix model by namespace `Elysia.prefix('model', 'Namespace.')
|
||||||
|
- Prefers Reference Model by name provided by Elysia instead of using an actual `Model.name`
|
||||||
|
- Service:
|
||||||
|
- Prefers class (or abstract class if possible)
|
||||||
|
- Prefers interface/type derive from `Model`
|
||||||
|
- Return `status` (`import { status } from 'elysia'`) for error
|
||||||
|
- Prefers `return Error` instead of `throw Error`
|
||||||
|
- Models:
|
||||||
|
- Always export validation model and type of validation model
|
||||||
|
- Custom Error should be in contains in Model
|
||||||
|
|
||||||
|
## Controller
|
||||||
|
Due to type soundness of Elysia, it's not recommended to use a traditional controller class that is tightly coupled with Elysia's `Context` because:
|
||||||
|
|
||||||
|
1. **Elysia type is complex** and heavily depends on plugin and multiple level of chaining.
|
||||||
|
2. **Hard to type**, Elysia type could change at anytime, especially with decorators, and store
|
||||||
|
3. **Loss of type integrity**, and inconsistency between types and runtime code.
|
||||||
|
|
||||||
|
We recommended one of the following approach to implement a controller in Elysia.
|
||||||
|
1. Use Elysia instance as a controller itself
|
||||||
|
2. Create a controller that is not tied with HTTP request or Elysia.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. Elysia instance as a controller
|
||||||
|
> 1 Elysia instance = 1 controller
|
||||||
|
|
||||||
|
Treat an Elysia instance as a controller, and define your routes directly on the Elysia instance.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Do
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { Service } from './service'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', ({ stuff }) => {
|
||||||
|
Service.doStuff(stuff)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach allows Elysia to infer the `Context` type automatically, ensuring type integrity and consistency between types and runtime code.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Don't
|
||||||
|
import { Elysia, t, type Context } from 'elysia'
|
||||||
|
|
||||||
|
abstract class Controller {
|
||||||
|
static root(context: Context) {
|
||||||
|
return Service.doStuff(context.stuff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', Controller.root)
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach makes it hard to type `Context` properly, and may lead to loss of type integrity.
|
||||||
|
|
||||||
|
### 2. Controller without HTTP request
|
||||||
|
If you want to create a controller class, we recommend creating a class that is not tied to HTTP request or Elysia at all.
|
||||||
|
|
||||||
|
This approach allows you to decouple the controller from Elysia, making it easier to test, reuse, and even swap a framework while still follows the MVC pattern.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
abstract class Controller {
|
||||||
|
static doStuff(stuff: string) {
|
||||||
|
return Service.doStuff(stuff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', ({ stuff }) => Controller.doStuff(stuff))
|
||||||
|
```
|
||||||
|
|
||||||
|
Tying the controller to Elysia Context may lead to:
|
||||||
|
1. Loss of type integrity
|
||||||
|
2. Make it harder to test and reuse
|
||||||
|
3. Lead to vendor lock-in
|
||||||
|
|
||||||
|
We recommended to keep the controller decoupled from Elysia as much as possible.
|
||||||
|
|
||||||
|
### Don't: Pass entire `Context` to a controller
|
||||||
|
**Context is a highly dynamic type** that can be inferred from Elysia instance.
|
||||||
|
|
||||||
|
Do not pass an entire `Context` to a controller, instead use object destructuring to extract what you need and pass it to the controller.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { Context } from 'elysia'
|
||||||
|
|
||||||
|
abstract class Controller {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
// Don't do this
|
||||||
|
static root(context: Context) {
|
||||||
|
return Service.doStuff(context.stuff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach makes it hard to type `Context` properly, and may lead to loss of type integrity.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
If you're using Elysia as a controller, you can test your controller using `handle` to directly call a function (and it's lifecycle)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { Service } from './service'
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'bun:test'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/', ({ stuff }) => {
|
||||||
|
Service.doStuff(stuff)
|
||||||
|
|
||||||
|
return 'ok'
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Controller', () => {
|
||||||
|
it('should work', async () => {
|
||||||
|
const response = await app
|
||||||
|
.handle(new Request('http://localhost/'))
|
||||||
|
.then((x) => x.text())
|
||||||
|
|
||||||
|
expect(response).toBe('ok')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You may find more information about testing in [Unit Test](/patterns/unit-test.html).
|
||||||
|
|
||||||
|
## Service
|
||||||
|
Service is a set of utility/helper functions decoupled as a business logic to use in a module/controller, in our case, an Elysia instance.
|
||||||
|
|
||||||
|
Any technical logic that can be decoupled from controller may live inside a **Service**.
|
||||||
|
|
||||||
|
There are 2 types of service in Elysia:
|
||||||
|
1. Non-request dependent service
|
||||||
|
2. Request dependent service
|
||||||
|
|
||||||
|
### 1. Abstract away Non-request dependent service
|
||||||
|
|
||||||
|
We recommend abstracting a service class/function away from Elysia.
|
||||||
|
|
||||||
|
If the service or function isn't tied to an HTTP request or doesn't access a `Context`, it's recommended to implement it as a static class or function.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
abstract class Service {
|
||||||
|
static fibo(number: number): number {
|
||||||
|
if(number < 2)
|
||||||
|
return number
|
||||||
|
|
||||||
|
return Service.fibo(number - 1) + Service.fibo(number - 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/fibo', ({ body }) => {
|
||||||
|
return Service.fibo(body)
|
||||||
|
}, {
|
||||||
|
body: t.Numeric()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
If your service doesn't need to store a property, you may use `abstract class` and `static` instead to avoid allocating class instance.
|
||||||
|
|
||||||
|
### 2. Request dependent service as Elysia instance
|
||||||
|
|
||||||
|
**If the service is a request-dependent service** or needs to process HTTP requests, we recommend abstracting it as an Elysia instance to ensure type integrity and inference:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
// Do
|
||||||
|
const AuthService = new Elysia({ name: 'Auth.Service' })
|
||||||
|
.macro({
|
||||||
|
isSignIn: {
|
||||||
|
resolve({ cookie, status }) {
|
||||||
|
if (!cookie.session.value) return status(401)
|
||||||
|
|
||||||
|
return {
|
||||||
|
session: cookie.session.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const UserController = new Elysia()
|
||||||
|
.use(AuthService)
|
||||||
|
.get('/profile', ({ Auth: { user } }) => user, {
|
||||||
|
isSignIn: true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Do: Decorate only request dependent property
|
||||||
|
|
||||||
|
It's recommended to `decorate` only request-dependent properties, such as `requestIP`, `requestTime`, or `session`.
|
||||||
|
|
||||||
|
Overusing decorators may tie your code to Elysia, making it harder to test and reuse.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.decorate('requestIP', ({ request }) => request.headers.get('x-forwarded-for') || request.ip)
|
||||||
|
.decorate('requestTime', () => Date.now())
|
||||||
|
.decorate('session', ({ cookie }) => cookie.session.value)
|
||||||
|
.get('/', ({ requestIP, requestTime, session }) => {
|
||||||
|
return { requestIP, requestTime, session }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Don't: Pass entire `Context` to a service
|
||||||
|
**Context is a highly dynamic type** that can be inferred from Elysia instance.
|
||||||
|
|
||||||
|
Do not pass an entire `Context` to a service, instead use object destructuring to extract what you need and pass it to the service.
|
||||||
|
```typescript
|
||||||
|
import type { Context } from 'elysia'
|
||||||
|
|
||||||
|
class AuthService {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
// Don't do this
|
||||||
|
isSignIn({ status, cookie: { session } }: Context) {
|
||||||
|
if (session.value)
|
||||||
|
return status(401)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As Elysia type is complex, and heavily depends on plugin and multiple level of chaining, it can be challenging to manually type as it's highly dynamic.
|
||||||
|
|
||||||
|
## Model
|
||||||
|
Model or [DTO (Data Transfer Object)](https://en.wikipedia.org/wiki/Data_transfer_object) is handle by [Elysia.t (Validation)](/essential/validation.html#elysia-type).
|
||||||
|
|
||||||
|
Elysia has a validation system built-in which can infers type from your code and validate it at runtime.
|
||||||
|
|
||||||
|
### Do: Use Elysia's validation system
|
||||||
|
|
||||||
|
Elysia strength is prioritizing a single source of truth for both type and runtime validation.
|
||||||
|
|
||||||
|
Instead of declaring an interface, reuse validation's model instead:
|
||||||
|
```typescript twoslash
|
||||||
|
// Do
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const customBody = t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Optional if you want to get the type of the model
|
||||||
|
// Usually if we didn't use the type, as it's already inferred by Elysia
|
||||||
|
type CustomBody = typeof customBody.static
|
||||||
|
|
||||||
|
export { customBody }
|
||||||
|
```
|
||||||
|
|
||||||
|
We can get type of model by using `typeof` with `.static` property from the model.
|
||||||
|
|
||||||
|
Then you can use the `CustomBody` type to infer the type of the request body.
|
||||||
|
|
||||||
|
```typescript twoslash
|
||||||
|
// Do
|
||||||
|
new Elysia()
|
||||||
|
.post('/login', ({ body }) => {
|
||||||
|
return body
|
||||||
|
}, {
|
||||||
|
body: customBody
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Don't: Declare a class instance as a model
|
||||||
|
|
||||||
|
Do not declare a class instance as a model:
|
||||||
|
```typescript
|
||||||
|
// Don't
|
||||||
|
class CustomBody {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
|
||||||
|
constructor(username: string, password: string) {
|
||||||
|
this.username = username
|
||||||
|
this.password = password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't
|
||||||
|
interface ICustomBody {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Don't: Declare type separate from the model
|
||||||
|
Do not declare a type separate from the model, instead use `typeof` with `.static` property to get the type of the model.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Don't
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const customBody = t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
type CustomBody = {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do
|
||||||
|
const customBody = t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
type CustomBody = typeof customBody.static
|
||||||
|
```
|
||||||
|
|
||||||
|
### Group
|
||||||
|
You can group multiple models into a single object to make it more organized.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
export const AuthModel = {
|
||||||
|
sign: t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const models = AuthModel.models
|
||||||
|
```
|
||||||
|
|
||||||
|
### Model Injection
|
||||||
|
Though this is optional, if you are strictly following MVC pattern, you may want to inject like a service into a controller. We recommended using Elysia reference model
|
||||||
|
|
||||||
|
Using Elysia's model reference
|
||||||
|
```typescript twoslash
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
const customBody = t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
const AuthModel = new Elysia()
|
||||||
|
.model({
|
||||||
|
sign: customBody
|
||||||
|
})
|
||||||
|
|
||||||
|
const models = AuthModel.models
|
||||||
|
|
||||||
|
const UserController = new Elysia({ prefix: '/auth' })
|
||||||
|
.use(AuthModel)
|
||||||
|
.prefix('model', 'auth.')
|
||||||
|
.post('/sign-in', async ({ body, cookie: { session } }) => {
|
||||||
|
return true
|
||||||
|
}, {
|
||||||
|
body: 'auth.Sign'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach provide several benefits:
|
||||||
|
1. Allow us to name a model and provide auto-completion.
|
||||||
|
2. Modify schema for later usage, or perform a [remap](/essential/handler.html#remap).
|
||||||
|
3. Show up as "models" in OpenAPI compliance client, eg. OpenAPI.
|
||||||
|
4. Improve TypeScript inference speed as model type will be cached during registration.
|
||||||
30
.kilocode/skills/elysiajs/plugins/bearer.md
Normal file
30
.kilocode/skills/elysiajs/plugins/bearer.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Bearer
|
||||||
|
Plugin for Elysia for retrieving the Bearer token.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/bearer
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript twoslash
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { bearer } from '@elysiajs/bearer'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(bearer())
|
||||||
|
.get('/sign', ({ bearer }) => bearer, {
|
||||||
|
beforeHandle({ bearer, set, status }) {
|
||||||
|
if (!bearer) {
|
||||||
|
set.headers[
|
||||||
|
'WWW-Authenticate'
|
||||||
|
] = `Bearer realm='sign', error="invalid_request"`
|
||||||
|
|
||||||
|
return status(400, 'Unauthorized')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
This plugin is for retrieving a Bearer token specified in RFC6750
|
||||||
141
.kilocode/skills/elysiajs/plugins/cors.md
Normal file
141
.kilocode/skills/elysiajs/plugins/cors.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# CORS
|
||||||
|
|
||||||
|
Plugin for Elysia that adds support for customizing Cross-Origin Resource Sharing behavior.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/cors
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript twoslash
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { cors } from '@elysiajs/cors'
|
||||||
|
|
||||||
|
new Elysia().use(cors()).listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
This will set Elysia to accept requests from any origin.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
Below is a config which is accepted by the plugin
|
||||||
|
|
||||||
|
### origin
|
||||||
|
|
||||||
|
@default `true`
|
||||||
|
|
||||||
|
Indicates whether the response can be shared with the requesting code from the given origins.
|
||||||
|
|
||||||
|
Value can be one of the following:
|
||||||
|
|
||||||
|
- **string** - Name of origin which will directly assign to Access-Control-Allow-Origin header.
|
||||||
|
- **boolean** - If set to true, Access-Control-Allow-Origin will be set to `*` (any origins)
|
||||||
|
- **RegExp** - Pattern to match request's URL, allowed if matched.
|
||||||
|
- **Function** - Custom logic to allow resource sharing, allow if `true` is returned.
|
||||||
|
- Expected to have the type of:
|
||||||
|
```typescript
|
||||||
|
cors(context: Context) => boolean | void
|
||||||
|
```
|
||||||
|
- **Array<string | RegExp | Function>** - iterate through all cases above in order, allowed if any of the values are `true`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### methods
|
||||||
|
|
||||||
|
@default `*`
|
||||||
|
|
||||||
|
Allowed methods for cross-origin requests by assign `Access-Control-Allow-Methods` header.
|
||||||
|
|
||||||
|
Value can be one of the following:
|
||||||
|
- **undefined | null | ''** - Ignore all methods.
|
||||||
|
- **\*** - Allows all methods.
|
||||||
|
- **string** - Expects either a single method or a comma-delimited string
|
||||||
|
- (eg: `'GET, PUT, POST'`)
|
||||||
|
- **string[]** - Allow multiple HTTP methods.
|
||||||
|
- eg: `['GET', 'PUT', 'POST']`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### allowedHeaders
|
||||||
|
|
||||||
|
@default `*`
|
||||||
|
|
||||||
|
Allowed headers for an incoming request by assign `Access-Control-Allow-Headers` header.
|
||||||
|
|
||||||
|
Value can be one of the following:
|
||||||
|
- **string** - Expects either a single header or a comma-delimited string
|
||||||
|
- eg: `'Content-Type, Authorization'`.
|
||||||
|
- **string[]** - Allow multiple HTTP headers.
|
||||||
|
- eg: `['Content-Type', 'Authorization']`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### exposeHeaders
|
||||||
|
|
||||||
|
@default `*`
|
||||||
|
|
||||||
|
Response CORS with specified headers by sssign Access-Control-Expose-Headers header.
|
||||||
|
|
||||||
|
Value can be one of the following:
|
||||||
|
- **string** - Expects either a single header or a comma-delimited string.
|
||||||
|
- eg: `'Content-Type, X-Powered-By'`.
|
||||||
|
- **string[]** - Allow multiple HTTP headers.
|
||||||
|
- eg: `['Content-Type', 'X-Powered-By']`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### credentials
|
||||||
|
|
||||||
|
@default `true`
|
||||||
|
|
||||||
|
The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to the frontend JavaScript code when the request's credentials mode Request.credentials is `include`.
|
||||||
|
|
||||||
|
Credentials are cookies, authorization headers, or TLS client certificates by assign `Access-Control-Allow-Credentials` header.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### maxAge
|
||||||
|
|
||||||
|
@default `5`
|
||||||
|
|
||||||
|
Indicates how long the results of a preflight request that is the information contained in the `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` headers) can be cached.
|
||||||
|
|
||||||
|
Assign `Access-Control-Max-Age` header.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### preflight
|
||||||
|
|
||||||
|
The preflight request is a request sent to check if the CORS protocol is understood and if a server is aware of using specific methods and headers.
|
||||||
|
|
||||||
|
Response with **OPTIONS** request with 3 HTTP request headers:
|
||||||
|
- **Access-Control-Request-Method**
|
||||||
|
- **Access-Control-Request-Headers**
|
||||||
|
- **Origin**
|
||||||
|
|
||||||
|
This config indicates if the server should respond to preflight requests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pattern
|
||||||
|
|
||||||
|
Below you can find the common patterns to use the plugin.
|
||||||
|
|
||||||
|
## Allow CORS by top-level domain
|
||||||
|
|
||||||
|
```typescript twoslash
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { cors } from '@elysiajs/cors'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
cors({
|
||||||
|
origin: /.*\.saltyaom\.com$/
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.get('/', () => 'Hi')
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
This will allow requests from top-level domains with `saltyaom.com`
|
||||||
265
.kilocode/skills/elysiajs/plugins/cron.md
Normal file
265
.kilocode/skills/elysiajs/plugins/cron.md
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
# Cron Plugin
|
||||||
|
|
||||||
|
This plugin adds support for running cronjob to Elysia server.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/cron
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript twoslash
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { cron } from '@elysiajs/cron'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(
|
||||||
|
cron({
|
||||||
|
name: 'heartbeat',
|
||||||
|
pattern: '*/10 * * * * *',
|
||||||
|
run() {
|
||||||
|
console.log('Heartbeat')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
The above code will log `heartbeat` every 10 seconds.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
Below is a config which is accepted by the plugin
|
||||||
|
|
||||||
|
### cron
|
||||||
|
|
||||||
|
Create a cronjob for the Elysia server.
|
||||||
|
|
||||||
|
```
|
||||||
|
cron(config: CronConfig, callback: (Instance['store']) => void): this
|
||||||
|
```
|
||||||
|
|
||||||
|
`CronConfig` accepts the parameters specified below:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CronConfig.name
|
||||||
|
|
||||||
|
Job name to register to `store`.
|
||||||
|
|
||||||
|
This will register the cron instance to `store` with a specified name, which can be used to reference in later processes eg. stop the job.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CronConfig.pattern
|
||||||
|
|
||||||
|
Time to run the job as specified by cron syntax.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────── second (optional)
|
||||||
|
│ ┌──────────── minute
|
||||||
|
│ │ ┌────────── hour
|
||||||
|
│ │ │ ┌──────── day of the month
|
||||||
|
│ │ │ │ ┌────── month
|
||||||
|
│ │ │ │ │ ┌──── day of week
|
||||||
|
│ │ │ │ │ │
|
||||||
|
* * * * * *
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CronConfig.timezone
|
||||||
|
Time zone in Europe/Stockholm format
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CronConfig.startAt
|
||||||
|
Schedule start time for the job
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CronConfig.stopAt
|
||||||
|
Schedule stop time for the job
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CronConfig.maxRuns
|
||||||
|
Maximum number of executions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CronConfig.catch
|
||||||
|
Continue execution even if an unhandled error is thrown by a triggered function.
|
||||||
|
|
||||||
|
### CronConfig.interval
|
||||||
|
The minimum interval between executions, in seconds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CronConfig.Pattern
|
||||||
|
Below you can find the common patterns to use the plugin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pattern
|
||||||
|
|
||||||
|
Below you can find the common patterns to use the plugin.
|
||||||
|
|
||||||
|
## Stop cronjob
|
||||||
|
|
||||||
|
You can stop cronjob manually by accessing the cronjob name registered to `store`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { cron } from '@elysiajs/cron'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
cron({
|
||||||
|
name: 'heartbeat',
|
||||||
|
pattern: '*/1 * * * * *',
|
||||||
|
run() {
|
||||||
|
console.log('Heartbeat')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
'/stop',
|
||||||
|
({
|
||||||
|
store: {
|
||||||
|
cron: { heartbeat }
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
heartbeat.stop()
|
||||||
|
|
||||||
|
return 'Stop heartbeat'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Predefined patterns
|
||||||
|
|
||||||
|
You can use predefined patterns from `@elysiajs/cron/schedule`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { cron, Patterns } from '@elysiajs/cron'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
cron({
|
||||||
|
name: 'heartbeat',
|
||||||
|
pattern: Patterns.everySecond(),
|
||||||
|
run() {
|
||||||
|
console.log('Heartbeat')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
'/stop',
|
||||||
|
({
|
||||||
|
store: {
|
||||||
|
cron: { heartbeat }
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
heartbeat.stop()
|
||||||
|
|
||||||
|
return 'Stop heartbeat'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Functions
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
| ---------------------------------------- | ----------------------------------------------------- |
|
||||||
|
| `.everySeconds(2)` | Run the task every 2 seconds |
|
||||||
|
| `.everyMinutes(5)` | Run the task every 5 minutes |
|
||||||
|
| `.everyHours(3)` | Run the task every 3 hours |
|
||||||
|
| `.everyHoursAt(3, 15)` | Run the task every 3 hours at 15 minutes |
|
||||||
|
| `.everyDayAt('04:19')` | Run the task every day at 04:19 |
|
||||||
|
| `.everyWeekOn(Patterns.MONDAY, '19:30')` | Run the task every Monday at 19:30 |
|
||||||
|
| `.everyWeekdayAt('17:00')` | Run the task every day from Monday to Friday at 17:00 |
|
||||||
|
| `.everyWeekendAt('11:00')` | Run the task on Saturday and Sunday at 11:00 |
|
||||||
|
|
||||||
|
### Function aliases to constants
|
||||||
|
|
||||||
|
| Function | Constant |
|
||||||
|
| ----------------- | ---------------------------------- |
|
||||||
|
| `.everySecond()` | EVERY_SECOND |
|
||||||
|
| `.everyMinute()` | EVERY_MINUTE |
|
||||||
|
| `.hourly()` | EVERY_HOUR |
|
||||||
|
| `.daily()` | EVERY_DAY_AT_MIDNIGHT |
|
||||||
|
| `.everyWeekday()` | EVERY_WEEKDAY |
|
||||||
|
| `.everyWeekend()` | EVERY_WEEKEND |
|
||||||
|
| `.weekly()` | EVERY_WEEK |
|
||||||
|
| `.monthly()` | EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT |
|
||||||
|
| `.everyQuarter()` | EVERY_QUARTER |
|
||||||
|
| `.yearly()` | EVERY_YEAR |
|
||||||
|
|
||||||
|
### Constants
|
||||||
|
|
||||||
|
| Constant | Pattern |
|
||||||
|
| ---------------------------------------- | -------------------- |
|
||||||
|
| `.EVERY_SECOND` | `* * * * * *` |
|
||||||
|
| `.EVERY_5_SECONDS` | `*/5 * * * * *` |
|
||||||
|
| `.EVERY_10_SECONDS` | `*/10 * * * * *` |
|
||||||
|
| `.EVERY_30_SECONDS` | `*/30 * * * * *` |
|
||||||
|
| `.EVERY_MINUTE` | `*/1 * * * *` |
|
||||||
|
| `.EVERY_5_MINUTES` | `0 */5 * * * *` |
|
||||||
|
| `.EVERY_10_MINUTES` | `0 */10 * * * *` |
|
||||||
|
| `.EVERY_30_MINUTES` | `0 */30 * * * *` |
|
||||||
|
| `.EVERY_HOUR` | `0 0-23/1 * * *` |
|
||||||
|
| `.EVERY_2_HOURS` | `0 0-23/2 * * *` |
|
||||||
|
| `.EVERY_3_HOURS` | `0 0-23/3 * * *` |
|
||||||
|
| `.EVERY_4_HOURS` | `0 0-23/4 * * *` |
|
||||||
|
| `.EVERY_5_HOURS` | `0 0-23/5 * * *` |
|
||||||
|
| `.EVERY_6_HOURS` | `0 0-23/6 * * *` |
|
||||||
|
| `.EVERY_7_HOURS` | `0 0-23/7 * * *` |
|
||||||
|
| `.EVERY_8_HOURS` | `0 0-23/8 * * *` |
|
||||||
|
| `.EVERY_9_HOURS` | `0 0-23/9 * * *` |
|
||||||
|
| `.EVERY_10_HOURS` | `0 0-23/10 * * *` |
|
||||||
|
| `.EVERY_11_HOURS` | `0 0-23/11 * * *` |
|
||||||
|
| `.EVERY_12_HOURS` | `0 0-23/12 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_1AM` | `0 01 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_2AM` | `0 02 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_3AM` | `0 03 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_4AM` | `0 04 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_5AM` | `0 05 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_6AM` | `0 06 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_7AM` | `0 07 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_8AM` | `0 08 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_9AM` | `0 09 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_10AM` | `0 10 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_11AM` | `0 11 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_NOON` | `0 12 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_1PM` | `0 13 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_2PM` | `0 14 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_3PM` | `0 15 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_4PM` | `0 16 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_5PM` | `0 17 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_6PM` | `0 18 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_7PM` | `0 19 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_8PM` | `0 20 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_9PM` | `0 21 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_10PM` | `0 22 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_11PM` | `0 23 * * *` |
|
||||||
|
| `.EVERY_DAY_AT_MIDNIGHT` | `0 0 * * *` |
|
||||||
|
| `.EVERY_WEEK` | `0 0 * * 0` |
|
||||||
|
| `.EVERY_WEEKDAY` | `0 0 * * 1-5` |
|
||||||
|
| `.EVERY_WEEKEND` | `0 0 * * 6,0` |
|
||||||
|
| `.EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT` | `0 0 1 * *` |
|
||||||
|
| `.EVERY_1ST_DAY_OF_MONTH_AT_NOON` | `0 12 1 * *` |
|
||||||
|
| `.EVERY_2ND_HOUR` | `0 */2 * * *` |
|
||||||
|
| `.EVERY_2ND_HOUR_FROM_1AM_THROUGH_11PM` | `0 1-23/2 * * *` |
|
||||||
|
| `.EVERY_2ND_MONTH` | `0 0 1 */2 *` |
|
||||||
|
| `.EVERY_QUARTER` | `0 0 1 */3 *` |
|
||||||
|
| `.EVERY_6_MONTHS` | `0 0 1 */6 *` |
|
||||||
|
| `.EVERY_YEAR` | `0 0 1 1 *` |
|
||||||
|
| `.EVERY_30_MINUTES_BETWEEN_9AM_AND_5PM` | `0 */30 9-17 * * *` |
|
||||||
|
| `.EVERY_30_MINUTES_BETWEEN_9AM_AND_6PM` | `0 */30 9-18 * * *` |
|
||||||
|
| `.EVERY_30_MINUTES_BETWEEN_10AM_AND_7PM` | `0 */30 10-19 * * *` |
|
||||||
90
.kilocode/skills/elysiajs/plugins/graphql-apollo.md
Normal file
90
.kilocode/skills/elysiajs/plugins/graphql-apollo.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# GraphQL Apollo
|
||||||
|
|
||||||
|
Plugin for Elysia to use GraphQL Apollo.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add graphql @elysiajs/apollo @apollo/server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { apollo, gql } from '@elysiajs/apollo'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
apollo({
|
||||||
|
typeDefs: gql`
|
||||||
|
type Book {
|
||||||
|
title: String
|
||||||
|
author: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
books: [Book]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
resolvers: {
|
||||||
|
Query: {
|
||||||
|
books: () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: 'Elysia',
|
||||||
|
author: 'saltyAom'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
Accessing `/graphql` should show Apollo GraphQL playground work with.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Because Elysia is based on Web Standard Request and Response which is different from Node's `HttpRequest` and `HttpResponse` that Express uses, results in `req, res` being undefined in context.
|
||||||
|
|
||||||
|
Because of this, Elysia replaces both with `context` like route parameters.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
apollo({
|
||||||
|
typeDefs,
|
||||||
|
resolvers,
|
||||||
|
context: async ({ request }) => {
|
||||||
|
const authorization = request.headers.get('Authorization')
|
||||||
|
|
||||||
|
return {
|
||||||
|
authorization
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
This plugin extends Apollo's [ServerRegistration](https://www.apollographql.com/docs/apollo-server/api/apollo-server/#options) (which is `ApolloServer`'s' constructor parameter).
|
||||||
|
|
||||||
|
Below are the extended parameters for configuring Apollo Server with Elysia.
|
||||||
|
|
||||||
|
### path
|
||||||
|
|
||||||
|
@default `"/graphql"`
|
||||||
|
|
||||||
|
Path to expose Apollo Server.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### enablePlayground
|
||||||
|
|
||||||
|
@default `process.env.ENV !== 'production'`
|
||||||
|
|
||||||
|
Determine whether should Apollo should provide Apollo Playground.
|
||||||
87
.kilocode/skills/elysiajs/plugins/graphql-yoga.md
Normal file
87
.kilocode/skills/elysiajs/plugins/graphql-yoga.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# GraphQL Yoga
|
||||||
|
|
||||||
|
This plugin integrates GraphQL yoga with Elysia
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/graphql-yoga
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { yoga } from '@elysiajs/graphql-yoga'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
yoga({
|
||||||
|
typeDefs: /* GraphQL */ `
|
||||||
|
type Query {
|
||||||
|
hi: String
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
resolvers: {
|
||||||
|
Query: {
|
||||||
|
hi: () => 'Hello from Elysia'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
Accessing `/graphql` in the browser (GET request) would show you a GraphiQL instance for the GraphQL-enabled Elysia server.
|
||||||
|
|
||||||
|
optional: you can install a custom version of optional peer dependencies as well:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add graphql graphql-yoga
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resolver
|
||||||
|
|
||||||
|
Elysia uses Mobius to infer type from **typeDefs** field automatically, allowing you to get full type-safety and auto-complete when typing **resolver** types.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
You can add custom context to the resolver function by adding **context**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { yoga } from '@elysiajs/graphql-yoga'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
yoga({
|
||||||
|
typeDefs: /* GraphQL */ `
|
||||||
|
type Query {
|
||||||
|
hi: String
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
context: {
|
||||||
|
name: 'Mobius'
|
||||||
|
},
|
||||||
|
// If context is a function on this doesn't present
|
||||||
|
// for some reason it won't infer context type
|
||||||
|
useContext(_) {},
|
||||||
|
resolvers: {
|
||||||
|
Query: {
|
||||||
|
hi: async (parent, args, context) => context.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
This plugin extends [GraphQL Yoga's createYoga options, please refer to the GraphQL Yoga documentation](https://the-guild.dev/graphql/yoga-server/docs) with inlining `schema` config to root.
|
||||||
|
|
||||||
|
Below is a config which is accepted by the plugin
|
||||||
|
|
||||||
|
### path
|
||||||
|
|
||||||
|
@default `/graphql`
|
||||||
|
|
||||||
|
Endpoint to expose GraphQL handler
|
||||||
188
.kilocode/skills/elysiajs/plugins/html.md
Normal file
188
.kilocode/skills/elysiajs/plugins/html.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# HTML
|
||||||
|
|
||||||
|
Allows you to use JSX and HTML with proper headers and support.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```tsx twoslash
|
||||||
|
import React from 'react'
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { html, Html } from '@elysiajs/html'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(html())
|
||||||
|
.get(
|
||||||
|
'/html',
|
||||||
|
() => `
|
||||||
|
<html lang='en'>
|
||||||
|
<head>
|
||||||
|
<title>Hello World</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello World</h1>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
)
|
||||||
|
.get('/jsx', () => (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Hello World</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello World</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
))
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
This plugin will automatically add `Content-Type: text/html; charset=utf8` header to the response, add `<!doctype html>`, and convert it into a Response object.
|
||||||
|
|
||||||
|
## JSX
|
||||||
|
Elysia can use JSX
|
||||||
|
|
||||||
|
1. Replace your file that needs to use JSX to end with affix **"x"**:
|
||||||
|
- .js -> .jsx
|
||||||
|
- .ts -> .tsx
|
||||||
|
|
||||||
|
2. Register the TypeScript type by append the following to **tsconfig.json**:
|
||||||
|
```jsonc
|
||||||
|
// tsconfig.json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react",
|
||||||
|
"jsxFactory": "Html.createElement",
|
||||||
|
"jsxFragmentFactory": "Html.Fragment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Starts using JSX in your file
|
||||||
|
```tsx twoslash
|
||||||
|
import React from 'react'
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { html, Html } from '@elysiajs/html'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(html())
|
||||||
|
.get('/', () => (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Hello World</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello World</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
))
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
If the error `Cannot find name 'Html'. Did you mean 'html'?` occurs, this import must be added to the JSX template:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Html } from '@elysiajs/html'
|
||||||
|
```
|
||||||
|
|
||||||
|
It is important that it is written in uppercase.
|
||||||
|
|
||||||
|
## XSS
|
||||||
|
|
||||||
|
Elysia HTML is based use of the Kita HTML plugin to detect possible XSS attacks in compile time.
|
||||||
|
|
||||||
|
You can use a dedicated `safe` attribute to sanitize user value to prevent XSS vulnerability.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
import { html, Html } from '@elysiajs/html'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(html())
|
||||||
|
.post(
|
||||||
|
'/',
|
||||||
|
({ body }) => (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Hello World</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 safe>{body}</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
body: t.String()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
However, when are building a large-scale app, it's best to have a type reminder to detect possible XSS vulnerabilities in your codebase.
|
||||||
|
|
||||||
|
To add a type-safe reminder, please install:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun add @kitajs/ts-html-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
Then appends the following **tsconfig.json**
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// tsconfig.json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react",
|
||||||
|
"jsxFactory": "Html.createElement",
|
||||||
|
"jsxFragmentFactory": "Html.Fragment",
|
||||||
|
"plugins": [{ "name": "@kitajs/ts-html-plugin" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Config
|
||||||
|
Below is a config which is accepted by the plugin
|
||||||
|
|
||||||
|
### contentType
|
||||||
|
|
||||||
|
- Type: `string`
|
||||||
|
- Default: `'text/html; charset=utf8'`
|
||||||
|
|
||||||
|
The content-type of the response.
|
||||||
|
|
||||||
|
### autoDetect
|
||||||
|
|
||||||
|
- Type: `boolean`
|
||||||
|
- Default: `true`
|
||||||
|
|
||||||
|
Whether to automatically detect HTML content and set the content-type.
|
||||||
|
|
||||||
|
### autoDoctype
|
||||||
|
|
||||||
|
- Type: `boolean | 'full'`
|
||||||
|
- Default: `true`
|
||||||
|
|
||||||
|
Whether to automatically add `<!doctype html>` to a response starting with `<html>`, if not found.
|
||||||
|
|
||||||
|
Use `full` to also automatically add doctypes on responses returned without this plugin
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// without the plugin
|
||||||
|
app.get('/', () => '<html></html>')
|
||||||
|
|
||||||
|
// With the plugin
|
||||||
|
app.get('/', ({ html }) => html('<html></html>'))
|
||||||
|
```
|
||||||
|
|
||||||
|
### isHtml
|
||||||
|
|
||||||
|
- Type: `(value: string) => boolean`
|
||||||
|
- Default: `isHtml` (exported function)
|
||||||
|
|
||||||
|
The function is used to detect if a string is a html or not. Default implementation if length is greater than 7, starts with `<` and ends with `>`.
|
||||||
|
|
||||||
|
Keep in mind there's no real way to validate HTML, so the default implementation is a best guess.
|
||||||
197
.kilocode/skills/elysiajs/plugins/jwt.md
Normal file
197
.kilocode/skills/elysiajs/plugins/jwt.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# JWT Plugin
|
||||||
|
This plugin adds support for using JWT in Elysia handlers.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/jwt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript [cookie]
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { jwt } from '@elysiajs/jwt'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
jwt({
|
||||||
|
name: 'jwt',
|
||||||
|
secret: 'Fischl von Luftschloss Narfidort'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.get('/sign/:name', async ({ jwt, params: { name }, cookie: { auth } }) => {
|
||||||
|
const value = await jwt.sign({ name })
|
||||||
|
|
||||||
|
auth.set({
|
||||||
|
value,
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 7 * 86400,
|
||||||
|
path: '/profile',
|
||||||
|
})
|
||||||
|
|
||||||
|
return `Sign in as ${value}`
|
||||||
|
})
|
||||||
|
.get('/profile', async ({ jwt, status, cookie: { auth } }) => {
|
||||||
|
const profile = await jwt.verify(auth.value)
|
||||||
|
|
||||||
|
if (!profile)
|
||||||
|
return status(401, 'Unauthorized')
|
||||||
|
|
||||||
|
return `Hello ${profile.name}`
|
||||||
|
})
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Config
|
||||||
|
This plugin extends config from [jose](https://github.com/panva/jose).
|
||||||
|
|
||||||
|
Below is a config that is accepted by the plugin.
|
||||||
|
|
||||||
|
### name
|
||||||
|
Name to register `jwt` function as.
|
||||||
|
|
||||||
|
For example, `jwt` function will be registered with a custom name.
|
||||||
|
```typescript
|
||||||
|
new Elysia()
|
||||||
|
.use(
|
||||||
|
jwt({
|
||||||
|
name: 'myJWTNamespace',
|
||||||
|
secret: process.env.JWT_SECRETS!
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.get('/sign/:name', ({ myJWTNamespace, params }) => {
|
||||||
|
return myJWTNamespace.sign(params)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Because some might need to use multiple `jwt` with different configs in a single server, explicitly registering the JWT function with a different name is needed.
|
||||||
|
|
||||||
|
### secret
|
||||||
|
The private key to sign JWT payload with.
|
||||||
|
|
||||||
|
### schema
|
||||||
|
Type strict validation for JWT payload.
|
||||||
|
|
||||||
|
### alg
|
||||||
|
@default `HS256`
|
||||||
|
|
||||||
|
Signing Algorithm to sign JWT payload with.
|
||||||
|
|
||||||
|
Possible properties for jose are:
|
||||||
|
HS256
|
||||||
|
HS384
|
||||||
|
HS512
|
||||||
|
PS256
|
||||||
|
PS384
|
||||||
|
PS512
|
||||||
|
RS256
|
||||||
|
RS384
|
||||||
|
RS512
|
||||||
|
ES256
|
||||||
|
ES256K
|
||||||
|
ES384
|
||||||
|
ES512
|
||||||
|
EdDSA
|
||||||
|
|
||||||
|
### iss
|
||||||
|
The issuer claim identifies the principal that issued the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.1)
|
||||||
|
|
||||||
|
TLDR; is usually (the domain) name of the signer.
|
||||||
|
|
||||||
|
### sub
|
||||||
|
The subject claim identifies the principal that is the subject of the JWT.
|
||||||
|
|
||||||
|
The claims in a JWT are normally statements about the subject as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2)
|
||||||
|
|
||||||
|
### aud
|
||||||
|
The audience claim identifies the recipients that the JWT is intended for.
|
||||||
|
|
||||||
|
Each principal intended to process the JWT MUST identify itself with a value in the audience claim as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3)
|
||||||
|
|
||||||
|
### jti
|
||||||
|
JWT ID claim provides a unique identifier for the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7)
|
||||||
|
|
||||||
|
### nbf
|
||||||
|
The "not before" claim identifies the time before which the JWT must not be accepted for processing as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.5)
|
||||||
|
|
||||||
|
### exp
|
||||||
|
The expiration time claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.4)
|
||||||
|
|
||||||
|
### iat
|
||||||
|
The "issued at" claim identifies the time at which the JWT was issued.
|
||||||
|
|
||||||
|
This claim can be used to determine the age of the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6)
|
||||||
|
|
||||||
|
### b64
|
||||||
|
This JWS Extension Header Parameter modifies the JWS Payload representation and the JWS Signing input computation as per [RFC7797](https://www.rfc-editor.org/rfc/rfc7797).
|
||||||
|
|
||||||
|
### kid
|
||||||
|
A hint indicating which key was used to secure the JWS.
|
||||||
|
|
||||||
|
This parameter allows originators to explicitly signal a change of key to recipients as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.4)
|
||||||
|
|
||||||
|
### x5t
|
||||||
|
(X.509 certificate SHA-1 thumbprint) header parameter is a base64url-encoded SHA-1 digest of the DER encoding of the X.509 certificate [RFC5280](https://www.rfc-editor.org/rfc/rfc5280) corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.7)
|
||||||
|
|
||||||
|
### x5c
|
||||||
|
(X.509 certificate chain) header parameter contains the X.509 public key certificate or certificate chain [RFC5280](https://www.rfc-editor.org/rfc/rfc5280) corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.6)
|
||||||
|
|
||||||
|
### x5u
|
||||||
|
(X.509 URL) header parameter is a URI [RFC3986](https://www.rfc-editor.org/rfc/rfc3986) that refers to a resource for the X.509 public key certificate or certificate chain [RFC5280] corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.5)
|
||||||
|
|
||||||
|
### jwk
|
||||||
|
The "jku" (JWK Set URL) Header Parameter is a URI [RFC3986] that refers to a resource for a set of JSON-encoded public keys, one of which corresponds to the key used to digitally sign the JWS.
|
||||||
|
|
||||||
|
The keys MUST be encoded as a JWK Set [JWK] as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.2)
|
||||||
|
|
||||||
|
### typ
|
||||||
|
The `typ` (type) Header Parameter is used by JWS applications to declare the media type [IANA.MediaTypes] of this complete JWS.
|
||||||
|
|
||||||
|
This is intended for use by the application when more than one kind of object could be present in an application data structure that can contain a JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.9)
|
||||||
|
|
||||||
|
### ctr
|
||||||
|
Content-Type parameter is used by JWS applications to declare the media type [IANA.MediaTypes] of the secured content (the payload).
|
||||||
|
|
||||||
|
This is intended for use by the application when more than one kind of object could be present in the JWS Payload as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.9)
|
||||||
|
|
||||||
|
## Handler
|
||||||
|
Below are the value added to the handler.
|
||||||
|
|
||||||
|
### jwt.sign
|
||||||
|
A dynamic object of collection related to use with JWT registered by the JWT plugin.
|
||||||
|
|
||||||
|
Type:
|
||||||
|
```typescript
|
||||||
|
sign: (payload: JWTPayloadSpec): Promise<string>
|
||||||
|
```
|
||||||
|
|
||||||
|
`JWTPayloadSpec` accepts the same value as [JWT config](#config)
|
||||||
|
|
||||||
|
### jwt.verify
|
||||||
|
Verify payload with the provided JWT config
|
||||||
|
|
||||||
|
Type:
|
||||||
|
```typescript
|
||||||
|
verify(payload: string) => Promise<JWTPayloadSpec | false>
|
||||||
|
```
|
||||||
|
|
||||||
|
`JWTPayloadSpec` accepts the same value as [JWT config](#config)
|
||||||
|
|
||||||
|
## Pattern
|
||||||
|
Below you can find the common patterns to use the plugin.
|
||||||
|
|
||||||
|
## Set JWT expiration date
|
||||||
|
By default, the config is passed to `setCookie` and inherits its value.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const app = new Elysia()
|
||||||
|
.use(
|
||||||
|
jwt({
|
||||||
|
name: 'jwt',
|
||||||
|
secret: 'kunikuzushi',
|
||||||
|
exp: '7d'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.get('/sign/:name', async ({ jwt, params }) => jwt.sign(params))
|
||||||
|
```
|
||||||
|
|
||||||
|
This will sign JWT with an expiration date of the next 7 days.
|
||||||
246
.kilocode/skills/elysiajs/plugins/openapi.md
Normal file
246
.kilocode/skills/elysiajs/plugins/openapi.md
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
# OpenAPI Plugin
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/openapi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript
|
||||||
|
import { openapi } from '@elysiajs/openapi'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(openapi())
|
||||||
|
.get('/', () => 'hello')
|
||||||
|
```
|
||||||
|
|
||||||
|
Docs at `/openapi`, spec at `/openapi/json`.
|
||||||
|
|
||||||
|
## Detail Object
|
||||||
|
Extends OpenAPI Operation Object:
|
||||||
|
```typescript
|
||||||
|
.get('/', () => 'hello', {
|
||||||
|
detail: {
|
||||||
|
title: 'Hello',
|
||||||
|
description: 'An example route',
|
||||||
|
summary: 'Short summary',
|
||||||
|
deprecated: false,
|
||||||
|
hide: true, // Hide from docs
|
||||||
|
tags: ['App']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Documentation Config
|
||||||
|
```typescript
|
||||||
|
openapi({
|
||||||
|
documentation: {
|
||||||
|
info: {
|
||||||
|
title: 'API',
|
||||||
|
version: '1.0.0'
|
||||||
|
},
|
||||||
|
tags: [
|
||||||
|
{ name: 'App', description: 'General' }
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
bearerAuth: { type: 'http', scheme: 'bearer' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Standard Schema Mapping
|
||||||
|
```typescript
|
||||||
|
mapJsonSchema: {
|
||||||
|
zod: z.toJSONSchema, // Zod 4
|
||||||
|
valibot: toJsonSchema,
|
||||||
|
effect: JSONSchema.make
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Zod 3: `zodToJsonSchema` from `zod-to-json-schema`
|
||||||
|
|
||||||
|
## OpenAPI Type Gen
|
||||||
|
Generate docs from types:
|
||||||
|
```typescript
|
||||||
|
import { fromTypes } from '@elysiajs/openapi'
|
||||||
|
|
||||||
|
export const app = new Elysia()
|
||||||
|
.use(openapi({
|
||||||
|
references: fromTypes()
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
Recommended to generate `.d.ts` file for production when using OpenAPI Type Gen
|
||||||
|
```typescript
|
||||||
|
references: fromTypes(
|
||||||
|
process.env.NODE_ENV === 'production'
|
||||||
|
? 'dist/index.d.ts'
|
||||||
|
: 'src/index.ts'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
```typescript
|
||||||
|
fromTypes('src/index.ts', {
|
||||||
|
projectRoot: path.join('..', import.meta.dir),
|
||||||
|
tsconfigPath: 'tsconfig.dts.json'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caveat: Explicit Types
|
||||||
|
Use `Prettify` helper to inline when type is not showing:
|
||||||
|
```typescript
|
||||||
|
type Prettify<T> = { [K in keyof T]: T[K] } & {}
|
||||||
|
|
||||||
|
function getUser(): Prettify<User> { }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema Description
|
||||||
|
```typescript
|
||||||
|
body: t.Object({
|
||||||
|
username: t.String(),
|
||||||
|
password: t.String({
|
||||||
|
minLength: 8,
|
||||||
|
description: 'Password (8+ chars)'
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
description: 'Expected username and password'
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: 'Sign in user',
|
||||||
|
tags: ['auth']
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Headers
|
||||||
|
```typescript
|
||||||
|
import { withHeader } from '@elysiajs/openapi'
|
||||||
|
|
||||||
|
response: withHeader(
|
||||||
|
t.Literal('Hi'),
|
||||||
|
{ 'x-powered-by': t.Literal('Elysia') }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Annotation only - doesn't enforce. Set headers manually.
|
||||||
|
|
||||||
|
## Tags
|
||||||
|
Define + assign:
|
||||||
|
```typescript
|
||||||
|
.use(openapi({
|
||||||
|
documentation: {
|
||||||
|
tags: [
|
||||||
|
{ name: 'App', description: 'General' },
|
||||||
|
{ name: 'Auth', description: 'Auth' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.get('/', () => 'hello', {
|
||||||
|
detail: { tags: ['App'] }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Instance Tags
|
||||||
|
```typescript
|
||||||
|
new Elysia({ tags: ['user'] })
|
||||||
|
.get('/user', 'user')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reference Models
|
||||||
|
Auto-generates schemas:
|
||||||
|
```typescript
|
||||||
|
.model({
|
||||||
|
User: t.Object({
|
||||||
|
id: t.Number(),
|
||||||
|
username: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get('/user', () => ({ id: 1, username: 'x' }), {
|
||||||
|
response: { 200: 'User' },
|
||||||
|
detail: { tags: ['User'] }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Guard
|
||||||
|
Apply to instance/group:
|
||||||
|
```typescript
|
||||||
|
.guard({
|
||||||
|
detail: {
|
||||||
|
description: 'Requires auth'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get('/user', 'user')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
```typescript
|
||||||
|
.use(openapi({
|
||||||
|
documentation: {
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
bearerAuth: {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
bearerFormat: 'JWT'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
new Elysia({
|
||||||
|
prefix: '/address',
|
||||||
|
detail: {
|
||||||
|
security: [{ bearerAuth: [] }]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Secures all routes under prefix.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
Below is a config which is accepted by the `openapi({})`
|
||||||
|
|
||||||
|
### enabled
|
||||||
|
@default true
|
||||||
|
Enable/Disable the plugin
|
||||||
|
|
||||||
|
### documentation
|
||||||
|
OpenAPI documentation information
|
||||||
|
@see https://spec.openapis.org/oas/v3.0.3.html
|
||||||
|
|
||||||
|
### exclude
|
||||||
|
Configuration to exclude paths or methods from documentation
|
||||||
|
|
||||||
|
### exclude.methods
|
||||||
|
List of methods to exclude from documentation
|
||||||
|
|
||||||
|
### exclude.paths
|
||||||
|
List of paths to exclude from documentation
|
||||||
|
|
||||||
|
### exclude.staticFile
|
||||||
|
@default true
|
||||||
|
|
||||||
|
Exclude static file routes from documentation
|
||||||
|
|
||||||
|
### exclude.tags
|
||||||
|
List of tags to exclude from documentation
|
||||||
|
|
||||||
|
### mapJsonSchema
|
||||||
|
A custom mapping function from Standard schema to OpenAPI schema
|
||||||
|
|
||||||
|
### path
|
||||||
|
@default '/openapi'
|
||||||
|
The endpoint to expose OpenAPI documentation frontend
|
||||||
|
|
||||||
|
### provider
|
||||||
|
@default 'scalar'
|
||||||
|
|
||||||
|
OpenAPI documentation frontend between:
|
||||||
|
- Scalar
|
||||||
|
- SwaggerUI
|
||||||
|
- null: disable frontend
|
||||||
167
.kilocode/skills/elysiajs/plugins/opentelemetry.md
Normal file
167
.kilocode/skills/elysiajs/plugins/opentelemetry.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# OpenTelemetry Plugin - SKILLS.md
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/opentelemetry
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript
|
||||||
|
import { opentelemetry } from '@elysiajs/opentelemetry'
|
||||||
|
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'
|
||||||
|
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(opentelemetry({
|
||||||
|
spanProcessors: [
|
||||||
|
new BatchSpanProcessor(new OTLPTraceExporter())
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-collects spans from OpenTelemetry-compatible libraries. Parent/child spans applied automatically.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
Extends OpenTelemetry SDK params:
|
||||||
|
|
||||||
|
- `autoDetectResources` (true) - Auto-detect from env
|
||||||
|
- `contextManager` (AsyncHooksContextManager) - Custom context
|
||||||
|
- `textMapPropagator` (CompositePropagator) - W3C Trace + Baggage
|
||||||
|
- `metricReader` - For MeterProvider
|
||||||
|
- `views` - Histogram bucket config
|
||||||
|
- `instrumentations` (getNodeAutoInstrumentations()) - Metapackage or individual
|
||||||
|
- `resource` - Custom resource
|
||||||
|
- `resourceDetectors` ([envDetector, processDetector, hostDetector]) - Auto-detect needs `autoDetectResources: true`
|
||||||
|
- `sampler` - Custom sampler (default: sample all)
|
||||||
|
- `serviceName` - Namespace identifier
|
||||||
|
- `spanProcessors` - Array for tracer provider
|
||||||
|
- `traceExporter` - Auto-setup OTLP/http/protobuf with BatchSpanProcessor if not set
|
||||||
|
- `spanLimits` - Tracing params
|
||||||
|
|
||||||
|
### Resource Detectors via Env
|
||||||
|
```bash
|
||||||
|
export OTEL_NODE_RESOURCE_DETECTORS="env,host"
|
||||||
|
# Options: env, host, os, process, serviceinstance, all, none
|
||||||
|
```
|
||||||
|
|
||||||
|
## Export to Backends
|
||||||
|
Example - Axiom:
|
||||||
|
```typescript
|
||||||
|
.use(opentelemetry({
|
||||||
|
spanProcessors: [
|
||||||
|
new BatchSpanProcessor(
|
||||||
|
new OTLPTraceExporter({
|
||||||
|
url: 'https://api.axiom.co/v1/traces',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${Bun.env.AXIOM_TOKEN}`,
|
||||||
|
'X-Axiom-Dataset': Bun.env.AXIOM_DATASET
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
## OpenTelemetry SDK
|
||||||
|
Use SDK normally - runs under Elysia's request span, auto-appears in trace.
|
||||||
|
|
||||||
|
## Record Utility
|
||||||
|
Equivalent to `startActiveSpan` - auto-closes + captures exceptions:
|
||||||
|
```typescript
|
||||||
|
import { record } from '@elysiajs/opentelemetry'
|
||||||
|
|
||||||
|
.get('', () => {
|
||||||
|
return record('database.query', () => {
|
||||||
|
return db.query('SELECT * FROM users')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Label for code shown in trace.
|
||||||
|
|
||||||
|
## Function Naming
|
||||||
|
Elysia reads function names as span names:
|
||||||
|
```typescript
|
||||||
|
// ⚠️ Anonymous span
|
||||||
|
.derive(async ({ cookie: { session } }) => {
|
||||||
|
return { user: await getProfile(session) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ Named span: "getProfile"
|
||||||
|
.derive(async function getProfile({ cookie: { session } }) {
|
||||||
|
return { user: await getProfile(session) }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## getCurrentSpan
|
||||||
|
Get current span outside handler (via AsyncLocalStorage):
|
||||||
|
```typescript
|
||||||
|
import { getCurrentSpan } from '@elysiajs/opentelemetry'
|
||||||
|
|
||||||
|
function utility() {
|
||||||
|
const span = getCurrentSpan()
|
||||||
|
span.setAttributes({ 'custom.attribute': 'value' })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## setAttributes
|
||||||
|
Sugar for `getCurrentSpan().setAttributes`:
|
||||||
|
```typescript
|
||||||
|
import { setAttributes } from '@elysiajs/opentelemetry'
|
||||||
|
|
||||||
|
function utility() {
|
||||||
|
setAttributes({ 'custom.attribute': 'value' })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Instrumentations (Advanced)
|
||||||
|
SDK must run before importing instrumented module.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
1. Separate file:
|
||||||
|
```typescript
|
||||||
|
// src/instrumentation.ts
|
||||||
|
import { opentelemetry } from '@elysiajs/opentelemetry'
|
||||||
|
import { PgInstrumentation } from '@opentelemetry/instrumentation-pg'
|
||||||
|
|
||||||
|
export const instrumentation = opentelemetry({
|
||||||
|
instrumentations: [new PgInstrumentation()]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Apply:
|
||||||
|
```typescript
|
||||||
|
// src/index.ts
|
||||||
|
import { instrumentation } from './instrumentation'
|
||||||
|
new Elysia().use(instrumentation).listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Preload:
|
||||||
|
```toml
|
||||||
|
# bunfig.toml
|
||||||
|
preload = ["./src/instrumentation.ts"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Deployment (Advanced)
|
||||||
|
OpenTelemetry monkey-patches `node_modules`. Exclude instrumented libs from bundling:
|
||||||
|
```bash
|
||||||
|
bun build --compile --external pg --outfile server src/index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Package.json:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dependencies": { "pg": "^8.15.6" },
|
||||||
|
"devDependencies": {
|
||||||
|
"@elysiajs/opentelemetry": "^1.2.0",
|
||||||
|
"@opentelemetry/instrumentation-pg": "^0.52.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Production install:
|
||||||
|
```bash
|
||||||
|
bun install --production
|
||||||
|
```
|
||||||
|
|
||||||
|
Keeps `node_modules` with instrumented libs at runtime.
|
||||||
71
.kilocode/skills/elysiajs/plugins/server-timing.md
Normal file
71
.kilocode/skills/elysiajs/plugins/server-timing.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Server Timing Plugin
|
||||||
|
This plugin adds support for auditing performance bottlenecks with Server Timing API
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/server-timing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript twoslash
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { serverTiming } from '@elysiajs/server-timing'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(serverTiming())
|
||||||
|
.get('/', () => 'hello')
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
Server Timing then will append header 'Server-Timing' with log duration, function name, and detail for each life-cycle function.
|
||||||
|
|
||||||
|
To inspect, open browser developer tools > Network > [Request made through Elysia server] > Timing.
|
||||||
|
|
||||||
|
Now you can effortlessly audit the performance bottleneck of your server.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
Below is a config which is accepted by the plugin
|
||||||
|
|
||||||
|
### enabled
|
||||||
|
@default `NODE_ENV !== 'production'`
|
||||||
|
|
||||||
|
Determine whether or not Server Timing should be enabled
|
||||||
|
|
||||||
|
### allow
|
||||||
|
@default `undefined`
|
||||||
|
|
||||||
|
A condition whether server timing should be log
|
||||||
|
|
||||||
|
### trace
|
||||||
|
@default `undefined`
|
||||||
|
|
||||||
|
Allow Server Timing to log specified life-cycle events:
|
||||||
|
|
||||||
|
Trace accepts objects of the following:
|
||||||
|
- request: capture duration from request
|
||||||
|
- parse: capture duration from parse
|
||||||
|
- transform: capture duration from transform
|
||||||
|
- beforeHandle: capture duration from beforeHandle
|
||||||
|
- handle: capture duration from the handle
|
||||||
|
- afterHandle: capture duration from afterHandle
|
||||||
|
- total: capture total duration from start to finish
|
||||||
|
|
||||||
|
## Pattern
|
||||||
|
Below you can find the common patterns to use the plugin.
|
||||||
|
|
||||||
|
## Allow Condition
|
||||||
|
You may disable Server Timing on specific routes via `allow` property
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { serverTiming } from '@elysiajs/server-timing'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(
|
||||||
|
serverTiming({
|
||||||
|
allow: ({ request }) => {
|
||||||
|
return new URL(request.url).pathname !== '/no-trace'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
```
|
||||||
84
.kilocode/skills/elysiajs/plugins/static.md
Normal file
84
.kilocode/skills/elysiajs/plugins/static.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Static Plugin
|
||||||
|
This plugin can serve static files/folders for Elysia Server
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/static
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
```typescript twoslash
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { staticPlugin } from '@elysiajs/static'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(staticPlugin())
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, the static plugin default folder is `public`, and registered with `/public` prefix.
|
||||||
|
|
||||||
|
Suppose your project structure is:
|
||||||
|
```
|
||||||
|
| - src
|
||||||
|
| - index.ts
|
||||||
|
| - public
|
||||||
|
| - takodachi.png
|
||||||
|
| - nested
|
||||||
|
| - takodachi.png
|
||||||
|
```
|
||||||
|
|
||||||
|
The available path will become:
|
||||||
|
- /public/takodachi.png
|
||||||
|
- /public/nested/takodachi.png
|
||||||
|
|
||||||
|
## Config
|
||||||
|
Below is a config which is accepted by the plugin
|
||||||
|
|
||||||
|
### assets
|
||||||
|
@default `"public"`
|
||||||
|
|
||||||
|
Path to the folder to expose as static
|
||||||
|
|
||||||
|
### prefix
|
||||||
|
@default `"/public"`
|
||||||
|
|
||||||
|
Path prefix to register public files
|
||||||
|
|
||||||
|
### ignorePatterns
|
||||||
|
@default `[]`
|
||||||
|
|
||||||
|
List of files to ignore from serving as static files
|
||||||
|
|
||||||
|
### staticLimit
|
||||||
|
@default `1024`
|
||||||
|
|
||||||
|
By default, the static plugin will register paths to the Router with a static name, if the limits are exceeded, paths will be lazily added to the Router to reduce memory usage.
|
||||||
|
Tradeoff memory with performance.
|
||||||
|
|
||||||
|
### alwaysStatic
|
||||||
|
@default `false`
|
||||||
|
|
||||||
|
If set to true, static files path will be registered to Router skipping the `staticLimits`.
|
||||||
|
|
||||||
|
### headers
|
||||||
|
@default `{}`
|
||||||
|
|
||||||
|
Set response headers of files
|
||||||
|
|
||||||
|
### indexHTML
|
||||||
|
@default `false`
|
||||||
|
|
||||||
|
If set to true, the `index.html` file from the static directory will be served for any request that is matching neither a route nor any existing static file.
|
||||||
|
|
||||||
|
## Pattern
|
||||||
|
Below you can find the common patterns to use the plugin.
|
||||||
|
|
||||||
|
## Single file
|
||||||
|
Suppose you want to return just a single file, you can use `file` instead of using the static plugin
|
||||||
|
```typescript
|
||||||
|
import { Elysia, file } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/file', file('public/takodachi.png'))
|
||||||
|
```
|
||||||
129
.kilocode/skills/elysiajs/references/bun-fullstack-dev-server.md
Normal file
129
.kilocode/skills/elysiajs/references/bun-fullstack-dev-server.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Fullstack Dev Server
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Bun 1.3 Fullstack Dev Server with HMR. React without bundler (no Vite/Webpack).
|
||||||
|
|
||||||
|
Example: [elysia-fullstack-example](https://github.com/saltyaom/elysia-fullstack-example)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
1. Install + use Elysia Static:
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { staticPlugin } from '@elysiajs/static'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(await staticPlugin()) // await required for HMR hooks
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create `public/index.html` + `public/index.tsx`:
|
||||||
|
```html
|
||||||
|
<!-- public/index.html -->
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Elysia React App</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// public/index.tsx
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
const increase = () => setCount((c) => c + 1)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h2>{count}</h2>
|
||||||
|
<button onClick={increase}>Increase</button>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = createRoot(document.getElementById('root')!)
|
||||||
|
root.render(<App />)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Enable JSX in `tsconfig.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Navigate to `http://localhost:3000/public`.
|
||||||
|
|
||||||
|
Frontend + backend in single project. No bundler. Works with HMR, Tailwind, Tanstack Query, Eden Treaty, path alias.
|
||||||
|
|
||||||
|
## Custom Prefix
|
||||||
|
```typescript
|
||||||
|
.use(await staticPlugin({ prefix: '/' }))
|
||||||
|
```
|
||||||
|
|
||||||
|
Serves at `/` instead of `/public`.
|
||||||
|
|
||||||
|
## Tailwind CSS
|
||||||
|
1. Install:
|
||||||
|
```bash
|
||||||
|
bun add tailwindcss@4
|
||||||
|
bun add -d bun-plugin-tailwind
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create `bunfig.toml`:
|
||||||
|
```toml
|
||||||
|
[serve.static]
|
||||||
|
plugins = ["bun-plugin-tailwind"]
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create `public/global.css`:
|
||||||
|
```css
|
||||||
|
@tailwind base;
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Add to HTML or TS:
|
||||||
|
```html
|
||||||
|
<link rel="stylesheet" href="tailwindcss">
|
||||||
|
```
|
||||||
|
Or:
|
||||||
|
```tsx
|
||||||
|
import './global.css'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Path Alias
|
||||||
|
1. Add to `tsconfig.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@public/*": ["public/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Use:
|
||||||
|
```tsx
|
||||||
|
import '@public/global.css'
|
||||||
|
```
|
||||||
|
|
||||||
|
Works out of box.
|
||||||
|
|
||||||
|
## Production Build
|
||||||
|
```bash
|
||||||
|
bun build --compile --target bun --outfile server src/index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates single executable `server`. Include `public` folder when running.
|
||||||
187
.kilocode/skills/elysiajs/references/cookie.md
Normal file
187
.kilocode/skills/elysiajs/references/cookie.md
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# Cookie
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Reactive mutable signal for cookie interaction. Auto-encodes/decodes objects.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
No get/set - direct value access:
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', ({ cookie: { name } }) => {
|
||||||
|
// Get
|
||||||
|
name.value
|
||||||
|
|
||||||
|
// Set
|
||||||
|
name.value = "New Value"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-encodes/decodes objects. Just works.
|
||||||
|
|
||||||
|
## Reactivity
|
||||||
|
Signal-like approach. Single source of truth. Auto-sets headers, syncs values.
|
||||||
|
|
||||||
|
Cookie jar = Proxy object. Extract value always `Cookie<unknown>`, never `undefined`. Access via `.value`.
|
||||||
|
|
||||||
|
Iterate over cookie jar → only existing cookies.
|
||||||
|
|
||||||
|
## Cookie Attributes
|
||||||
|
|
||||||
|
### Direct Property Assignment
|
||||||
|
```typescript
|
||||||
|
.get('/', ({ cookie: { name } }) => {
|
||||||
|
// Get
|
||||||
|
name.domain
|
||||||
|
|
||||||
|
// Set
|
||||||
|
name.domain = 'millennium.sh'
|
||||||
|
name.httpOnly = true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### set - Reset All Properties
|
||||||
|
```typescript
|
||||||
|
.get('/', ({ cookie: { name } }) => {
|
||||||
|
name.set({
|
||||||
|
domain: 'millennium.sh',
|
||||||
|
httpOnly: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Overwrites all properties.
|
||||||
|
|
||||||
|
### add - Update Specific Properties
|
||||||
|
Like `set` but only overwrites defined properties.
|
||||||
|
|
||||||
|
## Remove Cookie
|
||||||
|
```typescript
|
||||||
|
.get('/', ({ cookie, cookie: { name } }) => {
|
||||||
|
name.remove()
|
||||||
|
// or
|
||||||
|
delete cookie.name
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cookie Schema
|
||||||
|
Strict validation + type inference with `t.Cookie`:
|
||||||
|
```typescript
|
||||||
|
import { Elysia, t } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', ({ cookie: { name } }) => {
|
||||||
|
name.value = {
|
||||||
|
id: 617,
|
||||||
|
name: 'Summoning 101'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
cookie: t.Cookie({
|
||||||
|
name: t.Object({
|
||||||
|
id: t.Numeric(),
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nullable Cookie
|
||||||
|
```typescript
|
||||||
|
cookie: t.Cookie({
|
||||||
|
name: t.Optional(
|
||||||
|
t.Object({
|
||||||
|
id: t.Numeric(),
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cookie Signature
|
||||||
|
Cryptographic hash for verification. Prevents malicious modification.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new Elysia()
|
||||||
|
.get('/', ({ cookie: { profile } }) => {
|
||||||
|
profile.value = { id: 617, name: 'Summoning 101' }
|
||||||
|
}, {
|
||||||
|
cookie: t.Cookie({
|
||||||
|
profile: t.Object({
|
||||||
|
id: t.Numeric(),
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
secrets: 'Fischl von Luftschloss Narfidort',
|
||||||
|
sign: ['profile']
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Auto-signs/unsigns.
|
||||||
|
|
||||||
|
### Global Config
|
||||||
|
```typescript
|
||||||
|
new Elysia({
|
||||||
|
cookie: {
|
||||||
|
secrets: 'Fischl von Luftschloss Narfidort',
|
||||||
|
sign: ['profile']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cookie Rotation
|
||||||
|
Auto-handles secret rotation. Old signature verification + new signature signing.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new Elysia({
|
||||||
|
cookie: {
|
||||||
|
secrets: ['Vengeance will be mine', 'Fischl von Luftschloss Narfidort']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Array = key rotation (retire old, replace with new).
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
### secrets
|
||||||
|
Secret key for signing/unsigning. Array = key rotation.
|
||||||
|
|
||||||
|
### domain
|
||||||
|
Domain Set-Cookie attribute. Default: none (current domain only).
|
||||||
|
|
||||||
|
### encode
|
||||||
|
Function to encode value. Default: `encodeURIComponent`.
|
||||||
|
|
||||||
|
### expires
|
||||||
|
Date for Expires attribute. Default: none (non-persistent, deleted on browser exit).
|
||||||
|
|
||||||
|
If both `expires` and `maxAge` set, `maxAge` takes precedence (spec-compliant clients).
|
||||||
|
|
||||||
|
### httpOnly (false)
|
||||||
|
HttpOnly attribute. If true, JS can't access via `document.cookie`.
|
||||||
|
|
||||||
|
### maxAge (undefined)
|
||||||
|
Seconds for Max-Age attribute. Rounded down to integer.
|
||||||
|
|
||||||
|
If both `expires` and `maxAge` set, `maxAge` takes precedence (spec-compliant clients).
|
||||||
|
|
||||||
|
### path
|
||||||
|
Path attribute. Default: handler path.
|
||||||
|
|
||||||
|
### priority
|
||||||
|
Priority attribute: `low` | `medium` | `high`. Not fully standardized.
|
||||||
|
|
||||||
|
### sameSite
|
||||||
|
SameSite attribute:
|
||||||
|
- `true` = Strict
|
||||||
|
- `false` = not set
|
||||||
|
- `'lax'` = Lax
|
||||||
|
- `'none'` = None (explicit cross-site)
|
||||||
|
- `'strict'` = Strict
|
||||||
|
|
||||||
|
Not fully standardized.
|
||||||
|
|
||||||
|
### secure
|
||||||
|
Secure attribute. If true, only HTTPS. Clients won't send over HTTP.
|
||||||
413
.kilocode/skills/elysiajs/references/deployment.md
Normal file
413
.kilocode/skills/elysiajs/references/deployment.md
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
# Deployment
|
||||||
|
|
||||||
|
## Production Build
|
||||||
|
|
||||||
|
### Compile to Binary (Recommended)
|
||||||
|
```bash
|
||||||
|
bun build \
|
||||||
|
--compile \
|
||||||
|
--minify-whitespace \
|
||||||
|
--minify-syntax \
|
||||||
|
--target bun \
|
||||||
|
--outfile server \
|
||||||
|
src/index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- No runtime needed on deployment server
|
||||||
|
- Smaller memory footprint (2-3x reduction)
|
||||||
|
- Faster startup
|
||||||
|
- Single portable executable
|
||||||
|
|
||||||
|
**Run the binary:**
|
||||||
|
```bash
|
||||||
|
./server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile to JavaScript
|
||||||
|
```bash
|
||||||
|
bun build \
|
||||||
|
--minify-whitespace \
|
||||||
|
--minify-syntax \
|
||||||
|
--outfile ./dist/index.js \
|
||||||
|
src/index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run:**
|
||||||
|
```bash
|
||||||
|
NODE_ENV=production bun ./dist/index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
### Basic Dockerfile
|
||||||
|
```dockerfile
|
||||||
|
FROM oven/bun:1 AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Cache dependencies
|
||||||
|
COPY package.json bun.lock ./
|
||||||
|
RUN bun install
|
||||||
|
|
||||||
|
COPY ./src ./src
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
RUN bun build \
|
||||||
|
--compile \
|
||||||
|
--minify-whitespace \
|
||||||
|
--minify-syntax \
|
||||||
|
--outfile server \
|
||||||
|
src/index.ts
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/base
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=build /app/server server
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
CMD ["./server"]
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build and Run
|
||||||
|
```bash
|
||||||
|
docker build -t my-elysia-app .
|
||||||
|
docker run -p 3000:3000 my-elysia-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Environment Variables
|
||||||
|
```dockerfile
|
||||||
|
FROM gcr.io/distroless/base
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=build /app/server server
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV DATABASE_URL=""
|
||||||
|
ENV JWT_SECRET=""
|
||||||
|
|
||||||
|
CMD ["./server"]
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cluster Mode (Multiple CPU Cores)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/index.ts
|
||||||
|
import cluster from 'node:cluster'
|
||||||
|
import os from 'node:os'
|
||||||
|
import process from 'node:process'
|
||||||
|
|
||||||
|
if (cluster.isPrimary) {
|
||||||
|
for (let i = 0; i < os.availableParallelism(); i++) {
|
||||||
|
cluster.fork()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await import('./server')
|
||||||
|
console.log(`Worker ${process.pid} started`)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/server.ts
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.get('/', () => 'Hello World!')
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
### .env File
|
||||||
|
```env
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
DATABASE_URL=postgresql://user:password@localhost:5432/db
|
||||||
|
JWT_SECRET=your-secret-key
|
||||||
|
CORS_ORIGIN=https://example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Load in App
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/env', () => ({
|
||||||
|
env: process.env.NODE_ENV,
|
||||||
|
port: process.env.PORT
|
||||||
|
}))
|
||||||
|
.listen(parseInt(process.env.PORT || '3000'))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Platform-Specific Deployments
|
||||||
|
|
||||||
|
### Railway
|
||||||
|
```typescript
|
||||||
|
// Railway assigns random PORT via env variable
|
||||||
|
new Elysia()
|
||||||
|
.get('/', () => 'Hello Railway')
|
||||||
|
.listen(process.env.PORT ?? 3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vercel
|
||||||
|
```typescript
|
||||||
|
// src/index.ts
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
export default new Elysia()
|
||||||
|
.get('/', () => 'Hello Vercel')
|
||||||
|
|
||||||
|
export const GET = app.fetch
|
||||||
|
export const POST = app.fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
// vercel.json
|
||||||
|
{
|
||||||
|
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||||
|
"bunVersion": "1.x"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cloudflare Workers
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { CloudflareAdapter } from 'elysia/adapter/cloudflare-worker'
|
||||||
|
|
||||||
|
export default new Elysia({
|
||||||
|
adapter: CloudflareAdapter
|
||||||
|
})
|
||||||
|
.get('/', () => 'Hello Cloudflare!')
|
||||||
|
.compile()
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# wrangler.toml
|
||||||
|
name = "elysia-app"
|
||||||
|
main = "src/index.ts"
|
||||||
|
compatibility_date = "2025-06-01"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node.js Adapter
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { node } from '@elysiajs/node'
|
||||||
|
|
||||||
|
const app = new Elysia({ adapter: node() })
|
||||||
|
.get('/', () => 'Hello Node.js')
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Enable AoT Compilation
|
||||||
|
```typescript
|
||||||
|
new Elysia({
|
||||||
|
aot: true // Ahead-of-time compilation
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Native Static Response
|
||||||
|
```typescript
|
||||||
|
new Elysia({
|
||||||
|
nativeStaticResponse: true
|
||||||
|
})
|
||||||
|
.get('/version', 1) // Optimized for Bun.serve.static
|
||||||
|
```
|
||||||
|
|
||||||
|
### Precompile Routes
|
||||||
|
```typescript
|
||||||
|
new Elysia({
|
||||||
|
precompile: true // Compile all routes ahead of time
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Health Checks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new Elysia()
|
||||||
|
.get('/health', () => ({
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: Date.now()
|
||||||
|
}))
|
||||||
|
.get('/ready', ({ db }) => {
|
||||||
|
// Check database connection
|
||||||
|
const isDbReady = checkDbConnection()
|
||||||
|
|
||||||
|
if (!isDbReady) {
|
||||||
|
return status(503, { status: 'not ready' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: 'ready' }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Graceful Shutdown
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/', () => 'Hello')
|
||||||
|
.listen(3000)
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
console.log('SIGTERM received, shutting down gracefully')
|
||||||
|
app.stop()
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('SIGINT received, shutting down gracefully')
|
||||||
|
app.stop()
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
### OpenTelemetry
|
||||||
|
```typescript
|
||||||
|
import { opentelemetry } from '@elysiajs/opentelemetry'
|
||||||
|
|
||||||
|
new Elysia()
|
||||||
|
.use(opentelemetry({
|
||||||
|
serviceName: 'my-service',
|
||||||
|
endpoint: 'http://localhost:4318'
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Logging
|
||||||
|
```typescript
|
||||||
|
.onRequest(({ request }) => {
|
||||||
|
console.log(`[${new Date().toISOString()}] ${request.method} ${request.url}`)
|
||||||
|
})
|
||||||
|
.onAfterResponse(({ request, set }) => {
|
||||||
|
console.log(`[${new Date().toISOString()}] ${request.method} ${request.url} - ${set.status}`)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## SSL/TLS (HTTPS)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Elysia, file } from 'elysia'
|
||||||
|
|
||||||
|
new Elysia({
|
||||||
|
serve: {
|
||||||
|
tls: {
|
||||||
|
cert: file('cert.pem'),
|
||||||
|
key: file('key.pem')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get('/', () => 'Hello HTTPS')
|
||||||
|
.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Always compile to binary for production**
|
||||||
|
- Reduces memory usage
|
||||||
|
- Smaller deployment size
|
||||||
|
- No runtime needed
|
||||||
|
|
||||||
|
2. **Use environment variables**
|
||||||
|
- Never hardcode secrets
|
||||||
|
- Use different configs per environment
|
||||||
|
|
||||||
|
3. **Enable health checks**
|
||||||
|
- Essential for load balancers
|
||||||
|
- K8s/Docker orchestration
|
||||||
|
|
||||||
|
4. **Implement graceful shutdown**
|
||||||
|
- Handle SIGTERM/SIGINT
|
||||||
|
- Close connections properly
|
||||||
|
|
||||||
|
5. **Use cluster mode**
|
||||||
|
- Utilize all CPU cores
|
||||||
|
- Better performance under load
|
||||||
|
|
||||||
|
6. **Monitor your app**
|
||||||
|
- Use OpenTelemetry
|
||||||
|
- Log requests/responses
|
||||||
|
- Track errors
|
||||||
|
|
||||||
|
## Example Production Setup
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/server.ts
|
||||||
|
import { Elysia } from 'elysia'
|
||||||
|
import { cors } from '@elysiajs/cors'
|
||||||
|
import { opentelemetry } from '@elysiajs/opentelemetry'
|
||||||
|
|
||||||
|
export const app = new Elysia({
|
||||||
|
aot: true,
|
||||||
|
nativeStaticResponse: true
|
||||||
|
})
|
||||||
|
.use(cors({
|
||||||
|
origin: process.env.CORS_ORIGIN || 'http://localhost:3000'
|
||||||
|
}))
|
||||||
|
.use(opentelemetry({
|
||||||
|
serviceName: 'my-service'
|
||||||
|
}))
|
||||||
|
.get('/health', () => ({ status: 'ok' }))
|
||||||
|
.get('/', () => 'Hello Production')
|
||||||
|
.listen(parseInt(process.env.PORT || '3000'))
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
app.stop()
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/index.ts (cluster)
|
||||||
|
import cluster from 'node:cluster'
|
||||||
|
import os from 'node:os'
|
||||||
|
|
||||||
|
if (cluster.isPrimary) {
|
||||||
|
for (let i = 0; i < os.availableParallelism(); i++) {
|
||||||
|
cluster.fork()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await import('./server')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Dockerfile
|
||||||
|
FROM oven/bun:1 AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json bun.lock ./
|
||||||
|
RUN bun install
|
||||||
|
|
||||||
|
COPY ./src ./src
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
RUN bun build --compile --outfile server src/index.ts
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/base
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=build /app/server server
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
CMD ["./server"]
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
```
|
||||||
158
.kilocode/skills/elysiajs/references/eden.md
Normal file
158
.kilocode/skills/elysiajs/references/eden.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# Eden Treaty
|
||||||
|
e2e type safe RPC client for share type from backend to frontend.
|
||||||
|
|
||||||
|
## What It Is
|
||||||
|
Type-safe object representation for Elysia server. Auto-completion + error handling.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```bash
|
||||||
|
bun add @elysiajs/eden
|
||||||
|
bun add -d elysia
|
||||||
|
```
|
||||||
|
|
||||||
|
Export Elysia server type:
|
||||||
|
```typescript
|
||||||
|
const app = new Elysia()
|
||||||
|
.get('/', () => 'Hi Elysia')
|
||||||
|
.get('/id/:id', ({ params: { id } }) => id)
|
||||||
|
.post('/mirror', ({ body }) => body, {
|
||||||
|
body: t.Object({
|
||||||
|
id: t.Number(),
|
||||||
|
name: t.String()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.listen(3000)
|
||||||
|
|
||||||
|
export type App = typeof app
|
||||||
|
```
|
||||||
|
|
||||||
|
Consume on client side:
|
||||||
|
```typescript
|
||||||
|
import { treaty } from '@elysiajs/eden'
|
||||||
|
import type { App } from './server'
|
||||||
|
|
||||||
|
const client = treaty<App>('localhost:3000')
|
||||||
|
|
||||||
|
// response: Hi Elysia
|
||||||
|
const { data: index } = await client.get()
|
||||||
|
|
||||||
|
// response: 1895
|
||||||
|
const { data: id } = await client.id({ id: 1895 }).get()
|
||||||
|
|
||||||
|
// response: { id: 1895, name: 'Skadi' }
|
||||||
|
const { data: nendoroid } = await client.mirror.post({
|
||||||
|
id: 1895,
|
||||||
|
name: 'Skadi'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Errors & Fixes
|
||||||
|
- **Strict mode**: Enable in tsconfig
|
||||||
|
- **Version mismatch**: `npm why elysia` - must match server/client
|
||||||
|
- **TypeScript**: Min 5.0
|
||||||
|
- **Method chaining**: Required on server
|
||||||
|
- **Bun types**: `bun add -d @types/bun` if using Bun APIs
|
||||||
|
- **Path alias**: Must resolve same on frontend/backend
|
||||||
|
|
||||||
|
### Monorepo Path Alias
|
||||||
|
Must resolve to same file on frontend/backend
|
||||||
|
|
||||||
|
```json
|
||||||
|
// tsconfig.json at root
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@frontend/*": ["./apps/frontend/src/*"],
|
||||||
|
"@backend/*": ["./apps/backend/src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Syntax Mapping
|
||||||
|
| Path | Method | Treaty |
|
||||||
|
|----------------|--------|-------------------------------|
|
||||||
|
| / | GET | `.get()` |
|
||||||
|
| /hi | GET | `.hi.get()` |
|
||||||
|
| /deep/nested | POST | `.deep.nested.post()` |
|
||||||
|
| /item/:name | GET | `.item({ name: 'x' }).get()` |
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
### With body (POST/PUT/PATCH/DELETE):
|
||||||
|
```typescript
|
||||||
|
.user.post(
|
||||||
|
{ name: 'Elysia' }, // body
|
||||||
|
{ headers: {}, query: {}, fetch: {} } // optional
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### No body (GET/HEAD):
|
||||||
|
```typescript
|
||||||
|
.hello.get({ headers: {}, query: {}, fetch: {} })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Empty body with query/headers:
|
||||||
|
```typescript
|
||||||
|
.user.post(null, { query: { name: 'Ely' } })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fetch options:
|
||||||
|
```typescript
|
||||||
|
.hello.get({ fetch: { signal: controller.signal } })
|
||||||
|
```
|
||||||
|
|
||||||
|
### File upload:
|
||||||
|
```typescript
|
||||||
|
// Accepts: File | File[] | FileList | Blob
|
||||||
|
.image.post({
|
||||||
|
title: 'Title',
|
||||||
|
image: fileInput.files!
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response
|
||||||
|
```typescript
|
||||||
|
const { data, error, response, status, headers } = await api.user.post({ name: 'x' })
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
switch (error.status) {
|
||||||
|
case 400: throw error.value
|
||||||
|
default: throw error.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// data unwrapped after error handling
|
||||||
|
return data
|
||||||
|
```
|
||||||
|
|
||||||
|
status >= 300 → `data = null`, `error` has value
|
||||||
|
|
||||||
|
## Stream/SSE
|
||||||
|
Interpreted as `AsyncGenerator`:
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await treaty(app).ok.get()
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
for await (const chunk of data) console.log(chunk)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utility Types
|
||||||
|
```typescript
|
||||||
|
import { Treaty } from '@elysiajs/eden'
|
||||||
|
|
||||||
|
type UserData = Treaty.Data<typeof api.user.post>
|
||||||
|
type UserError = Treaty.Error<typeof api.user.post>
|
||||||
|
```
|
||||||
|
|
||||||
|
## WebSocket
|
||||||
|
```typescript
|
||||||
|
const chat = api.chat.subscribe()
|
||||||
|
|
||||||
|
chat.subscribe((message) => console.log('got', message))
|
||||||
|
chat.on('open', () => chat.send('hello'))
|
||||||
|
|
||||||
|
// Native access: chat.raw
|
||||||
|
```
|
||||||
|
|
||||||
|
`.subscribe()` accepts same params as `get`/`head`
|
||||||
198
.kilocode/skills/elysiajs/references/lifecycle.md
Normal file
198
.kilocode/skills/elysiajs/references/lifecycle.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# Lifecycle
|
||||||
|
|
||||||
|
Instead of a sequential process, Elysia's request handling is divided into multiple stages called lifecycle events.
|
||||||
|
|
||||||
|
It's designed to separate the process into distinct phases based on their responsibility without interfering with each others.
|
||||||
|
|
||||||
|
### List of events in order
|
||||||
|
|
||||||
|
1. **request** - early, global
|
||||||
|
2. **parse** - body parsing
|
||||||
|
3. **transform** / **derive** - mutate context pre validation
|
||||||
|
4. **beforeHandle** / **resolve** - auth/guard logic
|
||||||
|
5. **handler** - your business code
|
||||||
|
6. **afterHandle** - tweak response, set headers
|
||||||
|
7. **mapResponse** - turn anything into a proper `Response`
|
||||||
|
8. **onError** - centralized error handling
|
||||||
|
9. **onAfterResponse** - post response/cleanup tasks
|
||||||
|
|
||||||
|
## Request (`onRequest`)
|
||||||
|
|
||||||
|
Runs first for every incoming request.
|
||||||
|
|
||||||
|
- Ideal for **caching, rate limiting, CORS, adding global headers**.
|
||||||
|
- If the hook returns a value, the whole lifecycle stops and that value becomes the response.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().onRequest(({ ip, set }) => {
|
||||||
|
if (blocked(ip)) return (set.status = 429)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parse (`onParse`)
|
||||||
|
|
||||||
|
_Body parsing stage._
|
||||||
|
|
||||||
|
- Handles `text/plain`, `application/json`, `multipart/form-data`, `application/x www-form-urlencoded` by default.
|
||||||
|
- Use to add **custom parsers** or support extra `Content Type`s.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().onParse(({ request, contentType }) => {
|
||||||
|
if (contentType === 'application/custom') return request.text()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Transform (`onTransform`)
|
||||||
|
|
||||||
|
_Runs **just before validation**; can mutate the request context._
|
||||||
|
|
||||||
|
- Perfect for **type coercion**, trimming strings, or adding temporary fields that validation will use.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().onTransform(({ params }) => {
|
||||||
|
params.id = Number(params.id)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Derive
|
||||||
|
|
||||||
|
_Runs along with `onTransform` **but before validation**; adds per request values to the context._
|
||||||
|
|
||||||
|
- Useful for extracting info from headers, cookies, query, etc., that you want to reuse in handlers.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().derive(({ headers }) => ({
|
||||||
|
bearer: headers.authorization?.replace(/^Bearer /, '')
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Before Handle (`onBeforeHandle`)
|
||||||
|
|
||||||
|
_Executed after validation, right before the route handler._
|
||||||
|
|
||||||
|
- Great for **auth checks, permission gating, custom pre validation logic**.
|
||||||
|
- Returning a value skips the handler.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().get('/', () => 'hi', {
|
||||||
|
beforeHandle({ cookie, status }) {
|
||||||
|
if (!cookie.session) return status(401)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resolve
|
||||||
|
|
||||||
|
_Like `derive` but runs **after validation** along "Before Handle" (so you can rely on validated data)._
|
||||||
|
|
||||||
|
- Usually placed inside a `guard` because it isn't available as a local hook.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().guard(
|
||||||
|
{ headers: t.Object({ authorization: t.String() }) },
|
||||||
|
(app) =>
|
||||||
|
app
|
||||||
|
.resolve(({ headers }) => ({
|
||||||
|
bearer: headers.authorization.split(' ')[1]
|
||||||
|
}))
|
||||||
|
.get('/', ({ bearer }) => bearer)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## After Handle (`onAfterHandle`)
|
||||||
|
|
||||||
|
_Runs after the handler finishes._
|
||||||
|
|
||||||
|
- Can **modify response headers**, wrap the result in a `Response`, or transform the payload.
|
||||||
|
- Returning a value **replaces** the handler’s result, but the next `afterHandle` hooks still run.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().get('/', () => '<h1>Hello</h1>', {
|
||||||
|
afterHandle({ response, set }) {
|
||||||
|
if (isHtml(response)) {
|
||||||
|
set.headers['content-type'] = 'text/html; charset=utf-8'
|
||||||
|
return new Response(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Map Response (`mapResponse`)
|
||||||
|
|
||||||
|
_Runs right after all `afterHandle` hooks; maps **any** value to a Web standard `Response`._
|
||||||
|
|
||||||
|
- Ideal for **compression, custom content type mapping, streaming**.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().mapResponse(({ responseValue, set }) => {
|
||||||
|
const body =
|
||||||
|
typeof responseValue === 'object'
|
||||||
|
? JSON.stringify(responseValue)
|
||||||
|
: String(responseValue ?? '')
|
||||||
|
|
||||||
|
set.headers['content-encoding'] = 'gzip'
|
||||||
|
return new Response(Bun.gzipSync(new TextEncoder().encode(body)), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type':
|
||||||
|
typeof responseValue === 'object'
|
||||||
|
? 'application/json'
|
||||||
|
: 'text/plain'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## On Error (`onError`)
|
||||||
|
|
||||||
|
_Caught whenever an error bubbles up from any lifecycle stage._
|
||||||
|
|
||||||
|
- Use to **customize error messages**, **handle 404**, **log**, or **retry**.
|
||||||
|
- Must be registered **before** the routes it should protect.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().onError(({ code, status }) => {
|
||||||
|
if (code === 'NOT_FOUND') return status(404, 'â“ Not found')
|
||||||
|
return new Response('Oops', { status: 500 })
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## After Response (`onAfterResponse`)
|
||||||
|
|
||||||
|
_Runs **after** the response has been sent to the client._
|
||||||
|
|
||||||
|
- Perfect for **logging, metrics, cleanup**.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new Elysia().onAfterResponse(() =>
|
||||||
|
console.log('✅ response sent at', Date.now())
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hook Types
|
||||||
|
|
||||||
|
| Type | Scope | How to add |
|
||||||
|
| -------------------- | --------------------------------- | --------------------------------------------------------- |
|
||||||
|
| **Local Hook** | Single route | Inside route options (`afterHandle`, `beforeHandle`, …) |
|
||||||
|
| **Interceptor Hook** | Whole instance (and later routes) | `.onXxx(cb)` or `.use(plugin)` |
|
||||||
|
|
||||||
|
> **Remember:** Hooks only affect routes **defined after** they are registered, except `onRequest` which is global because it runs before route matching.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user