feat: add bun-fullstack agent and update skills
This commit is contained in:
475
.opencode/skills/tech-stack/elysiajs/SKILL.md
Normal file
475
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/examples/basic.ts
Normal file
9
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/examples/body-parser.ts
Normal file
33
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/examples/complex.ts
Normal file
112
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/examples/cookie.ts
Normal file
45
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/examples/error.ts
Normal file
38
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/examples/file.ts
Normal file
10
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/examples/guard.ts
Normal file
34
.opencode/skills/tech-stack/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)
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Elysia } from 'elysia'
|
||||
|
||||
const prettyJson = new Elysia()
|
||||
.mapResponse(({ response }) => {
|
||||
if (response instanceof Object)
|
||||
return new Response(JSON.stringify(response, null, 4))
|
||||
})
|
||||
.as('scoped')
|
||||
|
||||
new Elysia()
|
||||
.use(prettyJson)
|
||||
.get('/', () => ({
|
||||
hello: 'world'
|
||||
}))
|
||||
.listen(3000)
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Elysia } from 'elysia'
|
||||
|
||||
new Elysia()
|
||||
.get('/', () => 'Hi')
|
||||
.get('/redirect', ({ redirect }) => redirect('/'))
|
||||
.listen(3000)
|
||||
32
.opencode/skills/tech-stack/elysiajs/examples/rename.ts
Normal file
32
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/examples/schema.ts
Normal file
61
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/examples/state.ts
Normal file
6
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/examples/upload-file.ts
Normal file
20
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/examples/websocket.ts
Normal file
25
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/integrations/ai-sdk.md
Normal file
92
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/integrations/astro.md
Normal file
59
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/integrations/better-auth.md
Normal file
117
.opencode/skills/tech-stack/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.
|
||||
@@ -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
.opencode/skills/tech-stack/elysiajs/integrations/deno.md
Normal file
34
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/integrations/drizzle.md
Normal file
258
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/integrations/expo.md
Normal file
95
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/integrations/nextjs.md
Normal file
103
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/integrations/nodejs.md
Normal file
64
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/integrations/nuxt.md
Normal file
67
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/integrations/prisma.md
Normal file
93
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/integrations/react-email.md
Normal file
134
.opencode/skills/tech-stack/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).
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
# SvelteKit Integration
|
||||
|
||||
## What It Is
|
||||
Run Elysia on SvelteKit server routes.
|
||||
|
||||
## Setup
|
||||
1. Create `src/routes/[...slugs]/+server.ts`
|
||||
2. Define Elysia server
|
||||
3. Export fallback handler:
|
||||
```typescript
|
||||
// src/routes/[...slugs]/+server.ts
|
||||
import { Elysia, t } from 'elysia'
|
||||
|
||||
const app = new Elysia()
|
||||
.get('/', 'hello SvelteKit')
|
||||
.post('/', ({ body }) => body, {
|
||||
body: t.Object({ name: t.String() })
|
||||
})
|
||||
|
||||
interface WithRequest {
|
||||
request: Request
|
||||
}
|
||||
|
||||
export const fallback = ({ request }: WithRequest) => app.handle(request)
|
||||
```
|
||||
|
||||
Treat as normal SvelteKit server route.
|
||||
|
||||
## Prefix for Non-Root
|
||||
If placed in `src/routes/api/[...slugs]/+server.ts`, set prefix:
|
||||
```typescript
|
||||
// src/routes/api/[...slugs]/+server.ts
|
||||
import { Elysia, t } from 'elysia'
|
||||
|
||||
const app = new Elysia({ prefix: '/api' })
|
||||
.get('/', () => 'hi')
|
||||
.post('/', ({ body }) => body, {
|
||||
body: t.Object({ name: t.String() })
|
||||
})
|
||||
|
||||
type RequestHandler = (v: { request: Request }) => Response | Promise<Response>
|
||||
|
||||
export const fallback: RequestHandler = ({ request }) => app.handle(request)
|
||||
```
|
||||
|
||||
Ensures routing works in any location.
|
||||
|
||||
## pnpm
|
||||
Manual install:
|
||||
```bash
|
||||
pnpm add @sinclair/typebox openapi-types
|
||||
```
|
||||
@@ -0,0 +1,87 @@
|
||||
# Tanstack Start Integration
|
||||
|
||||
## What It Is
|
||||
Elysia runs inside Tanstack Start server routes.
|
||||
|
||||
## Setup
|
||||
1. Create `src/routes/api.$.ts`
|
||||
2. Define Elysia server
|
||||
3. Export handlers in `server.handlers`:
|
||||
```typescript
|
||||
// src/routes/api.$.ts
|
||||
import { Elysia } from 'elysia'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createIsomorphicFn } from '@tanstack/react-start'
|
||||
|
||||
const app = new Elysia({
|
||||
prefix: '/api'
|
||||
}).get('/', 'Hello Elysia!')
|
||||
|
||||
const handle = ({ request }: { request: Request }) => app.fetch(request)
|
||||
|
||||
export const Route = createFileRoute('/api/$')({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: handle,
|
||||
POST: handle
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Runs on `/api`. Add methods to `server.handlers` as needed.
|
||||
|
||||
## Eden (End-to-End Type Safety)
|
||||
Isomorphic pattern with `createIsomorphicFn`:
|
||||
```typescript
|
||||
// src/routes/api.$.ts
|
||||
export const getTreaty = createIsomorphicFn()
|
||||
.server(() => treaty(app).api)
|
||||
.client(() => treaty<typeof app>('localhost:3000').api)
|
||||
```
|
||||
|
||||
- Server: Direct call (no HTTP overhead)
|
||||
- Client: HTTP call
|
||||
|
||||
## Loader Data
|
||||
Fetch before render:
|
||||
```tsx
|
||||
// src/routes/index.tsx
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { getTreaty } from './api.$'
|
||||
|
||||
export const Route = createFileRoute('/a')({
|
||||
component: App,
|
||||
loader: () => getTreaty().get().then((res) => res.data)
|
||||
})
|
||||
|
||||
function App() {
|
||||
const data = Route.useLoaderData()
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
||||
Executed server-side during SSR. No HTTP overhead. Type-safe.
|
||||
|
||||
## React Query
|
||||
```tsx
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { getTreaty } from './api.$'
|
||||
|
||||
function App() {
|
||||
const { data: response } = useQuery({
|
||||
queryKey: ['get'],
|
||||
queryFn: () => getTreaty().get()
|
||||
})
|
||||
|
||||
return response?.data
|
||||
}
|
||||
```
|
||||
|
||||
Works with all React Query features.
|
||||
|
||||
## pnpm
|
||||
Manual install:
|
||||
```bash
|
||||
pnpm add @sinclair/typebox openapi-types
|
||||
```
|
||||
55
.opencode/skills/tech-stack/elysiajs/integrations/vercel.md
Normal file
55
.opencode/skills/tech-stack/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).
|
||||
7
.opencode/skills/tech-stack/elysiajs/metadata.json
Normal file
7
.opencode/skills/tech-stack/elysiajs/metadata.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": "1.0.1",
|
||||
"organization": "ElysiaJS",
|
||||
"date": "20 Jan 2026",
|
||||
"abstract": "Create backend with ElysiaJS, a type-safe, high-performance framework.",
|
||||
"references": ["https://elysiajs.com/llms.txt"]
|
||||
}
|
||||
380
.opencode/skills/tech-stack/elysiajs/patterns/mvc.md
Normal file
380
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/plugins/bearer.md
Normal file
30
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/plugins/cors.md
Normal file
141
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/plugins/cron.md
Normal file
265
.opencode/skills/tech-stack/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 * * *` |
|
||||
@@ -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
.opencode/skills/tech-stack/elysiajs/plugins/graphql-yoga.md
Normal file
87
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/plugins/html.md
Normal file
188
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/plugins/jwt.md
Normal file
197
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/plugins/openapi.md
Normal file
246
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/plugins/opentelemetry.md
Normal file
167
.opencode/skills/tech-stack/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.
|
||||
@@ -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
.opencode/skills/tech-stack/elysiajs/plugins/static.md
Normal file
84
.opencode/skills/tech-stack/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'))
|
||||
```
|
||||
@@ -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
.opencode/skills/tech-stack/elysiajs/references/cookie.md
Normal file
187
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/references/deployment.md
Normal file
413
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/references/eden.md
Normal file
158
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/references/lifecycle.md
Normal file
198
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/references/macro.md
Normal file
83
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/references/plugin.md
Normal file
207
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/references/route.md
Normal file
331
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/references/testing.md
Normal file
385
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/references/validation.md
Normal file
491
.opencode/skills/tech-stack/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
.opencode/skills/tech-stack/elysiajs/references/websocket.md
Normal file
250
.opencode/skills/tech-stack/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)
|
||||
}
|
||||
})
|
||||
```
|
||||
Reference in New Issue
Block a user