From 2d6d5e7bd67dd263547f518372f715e0404c2c7f Mon Sep 17 00:00:00 2001 From: DiangoGav Date: Sun, 8 Feb 2026 17:02:14 -0400 Subject: [PATCH 1/4] chore: reorganize skills to .agent/ and add elysiajs skill - Move existing skills from skills/ to .agent/skills/ - Add new ElysiaJS skill with examples, plugins, integrations - Update AGENTS.md with new paths Co-Authored-By: Warp --- .../skills}/ddd-implementation/SKILL.md | 0 .agent/skills/elysiajs | 1 + .../skills}/evolution-server/SKILL.md | 0 .../skills}/hexagonal-architecture/SKILL.md | 0 .agents/skills/elysiajs/SKILL.md | 475 +++++++++++++++++ .agents/skills/elysiajs/examples/basic.ts | 9 + .../skills/elysiajs/examples/body-parser.ts | 33 ++ .agents/skills/elysiajs/examples/complex.ts | 112 ++++ .agents/skills/elysiajs/examples/cookie.ts | 45 ++ .agents/skills/elysiajs/examples/error.ts | 38 ++ .agents/skills/elysiajs/examples/file.ts | 10 + .agents/skills/elysiajs/examples/guard.ts | 34 ++ .../skills/elysiajs/examples/map-response.ts | 15 + .agents/skills/elysiajs/examples/redirect.ts | 6 + .agents/skills/elysiajs/examples/rename.ts | 32 ++ .agents/skills/elysiajs/examples/schema.ts | 61 +++ .agents/skills/elysiajs/examples/state.ts | 6 + .../skills/elysiajs/examples/upload-file.ts | 20 + .agents/skills/elysiajs/examples/websocket.ts | 25 + .../skills/elysiajs/integrations/ai-sdk.md | 92 ++++ .agents/skills/elysiajs/integrations/astro.md | 59 +++ .../elysiajs/integrations/better-auth.md | 117 +++++ .../integrations/cloudflare-worker.md | 95 ++++ .agents/skills/elysiajs/integrations/deno.md | 34 ++ .../skills/elysiajs/integrations/drizzle.md | 258 +++++++++ .agents/skills/elysiajs/integrations/expo.md | 95 ++++ .../skills/elysiajs/integrations/nextjs.md | 103 ++++ .../skills/elysiajs/integrations/nodejs.md | 64 +++ .agents/skills/elysiajs/integrations/nuxt.md | 67 +++ .../skills/elysiajs/integrations/prisma.md | 93 ++++ .../elysiajs/integrations/react-email.md | 134 +++++ .../skills/elysiajs/integrations/sveltekit.md | 53 ++ .../elysiajs/integrations/tanstack-start.md | 87 ++++ .../skills/elysiajs/integrations/vercel.md | 55 ++ .agents/skills/elysiajs/patterns/mvc.md | 380 ++++++++++++++ .agents/skills/elysiajs/plugins/bearer.md | 30 ++ .agents/skills/elysiajs/plugins/cors.md | 141 +++++ .agents/skills/elysiajs/plugins/cron.md | 265 ++++++++++ .../skills/elysiajs/plugins/graphql-apollo.md | 90 ++++ .../skills/elysiajs/plugins/graphql-yoga.md | 87 ++++ .agents/skills/elysiajs/plugins/html.md | 188 +++++++ .agents/skills/elysiajs/plugins/jwt.md | 197 +++++++ .agents/skills/elysiajs/plugins/openapi.md | 246 +++++++++ .../skills/elysiajs/plugins/opentelemetry.md | 167 ++++++ .../skills/elysiajs/plugins/server-timing.md | 71 +++ .agents/skills/elysiajs/plugins/static.md | 84 +++ .../references/bun-fullstack-dev-server.md | 129 +++++ .agents/skills/elysiajs/references/cookie.md | 187 +++++++ .../skills/elysiajs/references/deployment.md | 413 +++++++++++++++ .agents/skills/elysiajs/references/eden.md | 158 ++++++ .../skills/elysiajs/references/lifecycle.md | 198 +++++++ .agents/skills/elysiajs/references/macro.md | 83 +++ .agents/skills/elysiajs/references/plugin.md | 207 ++++++++ .agents/skills/elysiajs/references/route.md | 331 ++++++++++++ .agents/skills/elysiajs/references/testing.md | 385 ++++++++++++++ .../skills/elysiajs/references/validation.md | 491 ++++++++++++++++++ .../skills/elysiajs/references/websocket.md | 250 +++++++++ AGENTS.md | 9 +- 58 files changed, 7111 insertions(+), 4 deletions(-) rename {skills => .agent/skills}/ddd-implementation/SKILL.md (100%) create mode 120000 .agent/skills/elysiajs rename {skills => .agent/skills}/evolution-server/SKILL.md (100%) rename {skills => .agent/skills}/hexagonal-architecture/SKILL.md (100%) create mode 100644 .agents/skills/elysiajs/SKILL.md create mode 100644 .agents/skills/elysiajs/examples/basic.ts create mode 100644 .agents/skills/elysiajs/examples/body-parser.ts create mode 100644 .agents/skills/elysiajs/examples/complex.ts create mode 100644 .agents/skills/elysiajs/examples/cookie.ts create mode 100644 .agents/skills/elysiajs/examples/error.ts create mode 100644 .agents/skills/elysiajs/examples/file.ts create mode 100644 .agents/skills/elysiajs/examples/guard.ts create mode 100644 .agents/skills/elysiajs/examples/map-response.ts create mode 100644 .agents/skills/elysiajs/examples/redirect.ts create mode 100644 .agents/skills/elysiajs/examples/rename.ts create mode 100644 .agents/skills/elysiajs/examples/schema.ts create mode 100644 .agents/skills/elysiajs/examples/state.ts create mode 100644 .agents/skills/elysiajs/examples/upload-file.ts create mode 100644 .agents/skills/elysiajs/examples/websocket.ts create mode 100644 .agents/skills/elysiajs/integrations/ai-sdk.md create mode 100644 .agents/skills/elysiajs/integrations/astro.md create mode 100644 .agents/skills/elysiajs/integrations/better-auth.md create mode 100644 .agents/skills/elysiajs/integrations/cloudflare-worker.md create mode 100644 .agents/skills/elysiajs/integrations/deno.md create mode 100644 .agents/skills/elysiajs/integrations/drizzle.md create mode 100644 .agents/skills/elysiajs/integrations/expo.md create mode 100644 .agents/skills/elysiajs/integrations/nextjs.md create mode 100644 .agents/skills/elysiajs/integrations/nodejs.md create mode 100644 .agents/skills/elysiajs/integrations/nuxt.md create mode 100644 .agents/skills/elysiajs/integrations/prisma.md create mode 100644 .agents/skills/elysiajs/integrations/react-email.md create mode 100644 .agents/skills/elysiajs/integrations/sveltekit.md create mode 100644 .agents/skills/elysiajs/integrations/tanstack-start.md create mode 100644 .agents/skills/elysiajs/integrations/vercel.md create mode 100644 .agents/skills/elysiajs/patterns/mvc.md create mode 100644 .agents/skills/elysiajs/plugins/bearer.md create mode 100644 .agents/skills/elysiajs/plugins/cors.md create mode 100644 .agents/skills/elysiajs/plugins/cron.md create mode 100644 .agents/skills/elysiajs/plugins/graphql-apollo.md create mode 100644 .agents/skills/elysiajs/plugins/graphql-yoga.md create mode 100644 .agents/skills/elysiajs/plugins/html.md create mode 100644 .agents/skills/elysiajs/plugins/jwt.md create mode 100644 .agents/skills/elysiajs/plugins/openapi.md create mode 100644 .agents/skills/elysiajs/plugins/opentelemetry.md create mode 100644 .agents/skills/elysiajs/plugins/server-timing.md create mode 100644 .agents/skills/elysiajs/plugins/static.md create mode 100644 .agents/skills/elysiajs/references/bun-fullstack-dev-server.md create mode 100644 .agents/skills/elysiajs/references/cookie.md create mode 100644 .agents/skills/elysiajs/references/deployment.md create mode 100644 .agents/skills/elysiajs/references/eden.md create mode 100644 .agents/skills/elysiajs/references/lifecycle.md create mode 100644 .agents/skills/elysiajs/references/macro.md create mode 100644 .agents/skills/elysiajs/references/plugin.md create mode 100644 .agents/skills/elysiajs/references/route.md create mode 100644 .agents/skills/elysiajs/references/testing.md create mode 100644 .agents/skills/elysiajs/references/validation.md create mode 100644 .agents/skills/elysiajs/references/websocket.md diff --git a/skills/ddd-implementation/SKILL.md b/.agent/skills/ddd-implementation/SKILL.md similarity index 100% rename from skills/ddd-implementation/SKILL.md rename to .agent/skills/ddd-implementation/SKILL.md diff --git a/.agent/skills/elysiajs b/.agent/skills/elysiajs new file mode 120000 index 0000000..7ee55cc --- /dev/null +++ b/.agent/skills/elysiajs @@ -0,0 +1 @@ +../../.agents/skills/elysiajs \ No newline at end of file diff --git a/skills/evolution-server/SKILL.md b/.agent/skills/evolution-server/SKILL.md similarity index 100% rename from skills/evolution-server/SKILL.md rename to .agent/skills/evolution-server/SKILL.md diff --git a/skills/hexagonal-architecture/SKILL.md b/.agent/skills/hexagonal-architecture/SKILL.md similarity index 100% rename from skills/hexagonal-architecture/SKILL.md rename to .agent/skills/hexagonal-architecture/SKILL.md diff --git a/.agents/skills/elysiajs/SKILL.md b/.agents/skills/elysiajs/SKILL.md new file mode 100644 index 0000000..d707a64 --- /dev/null +++ b/.agents/skills/elysiajs/SKILL.md @@ -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 diff --git a/.agents/skills/elysiajs/examples/basic.ts b/.agents/skills/elysiajs/examples/basic.ts new file mode 100644 index 0000000..61c8d14 --- /dev/null +++ b/.agents/skills/elysiajs/examples/basic.ts @@ -0,0 +1,9 @@ +import { Elysia, t } from 'elysia' + +new Elysia() + .get('/', 'Hello Elysia') + .post('/', ({ body: { name } }) => name, { + body: t.Object({ + name: t.String() + }) + }) diff --git a/.agents/skills/elysiajs/examples/body-parser.ts b/.agents/skills/elysiajs/examples/body-parser.ts new file mode 100644 index 0000000..533c7bf --- /dev/null +++ b/.agents/skills/elysiajs/examples/body-parser.ts @@ -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') diff --git a/.agents/skills/elysiajs/examples/complex.ts b/.agents/skills/elysiajs/examples/complex.ts new file mode 100644 index 0000000..436eda0 --- /dev/null +++ b/.agents/skills/elysiajs/examples/complex.ts @@ -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}`) + }) diff --git a/.agents/skills/elysiajs/examples/cookie.ts b/.agents/skills/elysiajs/examples/cookie.ts new file mode 100644 index 0000000..9a42720 --- /dev/null +++ b/.agents/skills/elysiajs/examples/cookie.ts @@ -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) diff --git a/.agents/skills/elysiajs/examples/error.ts b/.agents/skills/elysiajs/examples/error.ts new file mode 100644 index 0000000..2c2f126 --- /dev/null +++ b/.agents/skills/elysiajs/examples/error.ts @@ -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) diff --git a/.agents/skills/elysiajs/examples/file.ts b/.agents/skills/elysiajs/examples/file.ts new file mode 100644 index 0000000..504cad7 --- /dev/null +++ b/.agents/skills/elysiajs/examples/file.ts @@ -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) diff --git a/.agents/skills/elysiajs/examples/guard.ts b/.agents/skills/elysiajs/examples/guard.ts new file mode 100644 index 0000000..2fe158f --- /dev/null +++ b/.agents/skills/elysiajs/examples/guard.ts @@ -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) diff --git a/.agents/skills/elysiajs/examples/map-response.ts b/.agents/skills/elysiajs/examples/map-response.ts new file mode 100644 index 0000000..8cd4be4 --- /dev/null +++ b/.agents/skills/elysiajs/examples/map-response.ts @@ -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) diff --git a/.agents/skills/elysiajs/examples/redirect.ts b/.agents/skills/elysiajs/examples/redirect.ts new file mode 100644 index 0000000..28171b0 --- /dev/null +++ b/.agents/skills/elysiajs/examples/redirect.ts @@ -0,0 +1,6 @@ +import { Elysia } from 'elysia' + +new Elysia() + .get('/', () => 'Hi') + .get('/redirect', ({ redirect }) => redirect('/')) + .listen(3000) diff --git a/.agents/skills/elysiajs/examples/rename.ts b/.agents/skills/elysiajs/examples/rename.ts new file mode 100644 index 0000000..361f06f --- /dev/null +++ b/.agents/skills/elysiajs/examples/rename.ts @@ -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() + }) diff --git a/.agents/skills/elysiajs/examples/schema.ts b/.agents/skills/elysiajs/examples/schema.ts new file mode 100644 index 0000000..db79300 --- /dev/null +++ b/.agents/skills/elysiajs/examples/schema.ts @@ -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) diff --git a/.agents/skills/elysiajs/examples/state.ts b/.agents/skills/elysiajs/examples/state.ts new file mode 100644 index 0000000..8bcc993 --- /dev/null +++ b/.agents/skills/elysiajs/examples/state.ts @@ -0,0 +1,6 @@ +import { Elysia } from 'elysia' + +new Elysia() + .state('counter', 0) + .get('/', ({ store }) => store.counter++) + .listen(3000) diff --git a/.agents/skills/elysiajs/examples/upload-file.ts b/.agents/skills/elysiajs/examples/upload-file.ts new file mode 100644 index 0000000..4af5a19 --- /dev/null +++ b/.agents/skills/elysiajs/examples/upload-file.ts @@ -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) diff --git a/.agents/skills/elysiajs/examples/websocket.ts b/.agents/skills/elysiajs/examples/websocket.ts new file mode 100644 index 0000000..f97e47b --- /dev/null +++ b/.agents/skills/elysiajs/examples/websocket.ts @@ -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}`) + }) diff --git a/.agents/skills/elysiajs/integrations/ai-sdk.md b/.agents/skills/elysiajs/integrations/ai-sdk.md new file mode 100644 index 0000000..99f5409 --- /dev/null +++ b/.agents/skills/elysiajs/integrations/ai-sdk.md @@ -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. diff --git a/.agents/skills/elysiajs/integrations/astro.md b/.agents/skills/elysiajs/integrations/astro.md new file mode 100644 index 0000000..41cd451 --- /dev/null +++ b/.agents/skills/elysiajs/integrations/astro.md @@ -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 +``` diff --git a/.agents/skills/elysiajs/integrations/better-auth.md b/.agents/skills/elysiajs/integrations/better-auth.md new file mode 100644 index 0000000..0dfa3af --- /dev/null +++ b/.agents/skills/elysiajs/integrations/better-auth.md @@ -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 +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, + components: getSchema().then(({ components }) => components) as Promise +} 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. diff --git a/.agents/skills/elysiajs/integrations/cloudflare-worker.md b/.agents/skills/elysiajs/integrations/cloudflare-worker.md new file mode 100644 index 0000000..4245c1a --- /dev/null +++ b/.agents/skills/elysiajs/integrations/cloudflare-worker.md @@ -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 +``` diff --git a/.agents/skills/elysiajs/integrations/deno.md b/.agents/skills/elysiajs/integrations/deno.md new file mode 100644 index 0000000..28687d5 --- /dev/null +++ b/.agents/skills/elysiajs/integrations/deno.md @@ -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 +``` diff --git a/.agents/skills/elysiajs/integrations/drizzle.md b/.agents/skills/elysiajs/integrations/drizzle.md new file mode 100644 index 0000000..779db4e --- /dev/null +++ b/.agents/skills/elysiajs/integrations/drizzle.md @@ -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 + ? { + [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 => { + const newSchema: Record = {} + 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, + Mode extends 'select' | 'insert' | undefined, +>( + models: T, + mode?: Mode, +): { + [K in keyof T]: Spread +} => { + const newSchema: Record = {} + 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. diff --git a/.agents/skills/elysiajs/integrations/expo.md b/.agents/skills/elysiajs/integrations/expo.md new file mode 100644 index 0000000..fad1471 --- /dev/null +++ b/.agents/skills/elysiajs/integrations/expo.md @@ -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('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

Hello, {message}

+} +``` + +## 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 +``` diff --git a/.agents/skills/elysiajs/integrations/nextjs.md b/.agents/skills/elysiajs/integrations/nextjs.md new file mode 100644 index 0000000..ddbc849 --- /dev/null +++ b/.agents/skills/elysiajs/integrations/nextjs.md @@ -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('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

Hello, {message}

+} +``` + +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 +``` diff --git a/.agents/skills/elysiajs/integrations/nodejs.md b/.agents/skills/elysiajs/integrations/nodejs.md new file mode 100644 index 0000000..ce2edfa --- /dev/null +++ b/.agents/skills/elysiajs/integrations/nodejs.md @@ -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 +``` diff --git a/.agents/skills/elysiajs/integrations/nuxt.md b/.agents/skills/elysiajs/integrations/nuxt.md new file mode 100644 index 0000000..0b4d13d --- /dev/null +++ b/.agents/skills/elysiajs/integrations/nuxt.md @@ -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 + + +``` + +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 +``` diff --git a/.agents/skills/elysiajs/integrations/prisma.md b/.agents/skills/elysiajs/integrations/prisma.md new file mode 100644 index 0000000..f0684f1 --- /dev/null +++ b/.agents/skills/elysiajs/integrations/prisma.md @@ -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. diff --git a/.agents/skills/elysiajs/integrations/react-email.md b/.agents/skills/elysiajs/integrations/react-email.md new file mode 100644 index 0000000..1cb636f --- /dev/null +++ b/.agents/skills/elysiajs/integrations/react-email.md @@ -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 ( + +
+
+ + Verify your Email Address + + + Use the following code to verify your email address + + {otp} + + This code is valid for 10 minutes + + + Thank you for joining us + +
+
+
+ ) +} + +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() + + 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: // 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). diff --git a/.agents/skills/elysiajs/integrations/sveltekit.md b/.agents/skills/elysiajs/integrations/sveltekit.md new file mode 100644 index 0000000..4ad306a --- /dev/null +++ b/.agents/skills/elysiajs/integrations/sveltekit.md @@ -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 + +export const fallback: RequestHandler = ({ request }) => app.handle(request) +``` + +Ensures routing works in any location. + +## pnpm +Manual install: +```bash +pnpm add @sinclair/typebox openapi-types +``` diff --git a/.agents/skills/elysiajs/integrations/tanstack-start.md b/.agents/skills/elysiajs/integrations/tanstack-start.md new file mode 100644 index 0000000..2a1e642 --- /dev/null +++ b/.agents/skills/elysiajs/integrations/tanstack-start.md @@ -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('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 +``` diff --git a/.agents/skills/elysiajs/integrations/vercel.md b/.agents/skills/elysiajs/integrations/vercel.md new file mode 100644 index 0000000..555ec8e --- /dev/null +++ b/.agents/skills/elysiajs/integrations/vercel.md @@ -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). diff --git a/.agents/skills/elysiajs/patterns/mvc.md b/.agents/skills/elysiajs/patterns/mvc.md new file mode 100644 index 0000000..dc227c6 --- /dev/null +++ b/.agents/skills/elysiajs/patterns/mvc.md @@ -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. diff --git a/.agents/skills/elysiajs/plugins/bearer.md b/.agents/skills/elysiajs/plugins/bearer.md new file mode 100644 index 0000000..df529e5 --- /dev/null +++ b/.agents/skills/elysiajs/plugins/bearer.md @@ -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 diff --git a/.agents/skills/elysiajs/plugins/cors.md b/.agents/skills/elysiajs/plugins/cors.md new file mode 100644 index 0000000..2d8db2a --- /dev/null +++ b/.agents/skills/elysiajs/plugins/cors.md @@ -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** - 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` diff --git a/.agents/skills/elysiajs/plugins/cron.md b/.agents/skills/elysiajs/plugins/cron.md new file mode 100644 index 0000000..3905ad5 --- /dev/null +++ b/.agents/skills/elysiajs/plugins/cron.md @@ -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 * * *` | diff --git a/.agents/skills/elysiajs/plugins/graphql-apollo.md b/.agents/skills/elysiajs/plugins/graphql-apollo.md new file mode 100644 index 0000000..655f258 --- /dev/null +++ b/.agents/skills/elysiajs/plugins/graphql-apollo.md @@ -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. diff --git a/.agents/skills/elysiajs/plugins/graphql-yoga.md b/.agents/skills/elysiajs/plugins/graphql-yoga.md new file mode 100644 index 0000000..3203d02 --- /dev/null +++ b/.agents/skills/elysiajs/plugins/graphql-yoga.md @@ -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 diff --git a/.agents/skills/elysiajs/plugins/html.md b/.agents/skills/elysiajs/plugins/html.md new file mode 100644 index 0000000..777a536 --- /dev/null +++ b/.agents/skills/elysiajs/plugins/html.md @@ -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', + () => ` + + + Hello World + + +

Hello World

+ + ` + ) + .get('/jsx', () => ( + + + Hello World + + +

Hello World

+ + + )) + .listen(3000) +``` + +This plugin will automatically add `Content-Type: text/html; charset=utf8` header to the response, add ``, 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('/', () => ( + + + Hello World + + +

Hello World

+ + + )) + .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 }) => ( + + + Hello World + + +

{body}

+ + + ), + { + 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 `` to a response starting with ``, if not found. + +Use `full` to also automatically add doctypes on responses returned without this plugin + +```ts +// without the plugin +app.get('/', () => '') + +// With the plugin +app.get('/', ({ 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. diff --git a/.agents/skills/elysiajs/plugins/jwt.md b/.agents/skills/elysiajs/plugins/jwt.md new file mode 100644 index 0000000..b5767bf --- /dev/null +++ b/.agents/skills/elysiajs/plugins/jwt.md @@ -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 +``` + +`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` 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. diff --git a/.agents/skills/elysiajs/plugins/openapi.md b/.agents/skills/elysiajs/plugins/openapi.md new file mode 100644 index 0000000..c69150d --- /dev/null +++ b/.agents/skills/elysiajs/plugins/openapi.md @@ -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 = { [K in keyof T]: T[K] } & {} + +function getUser(): Prettify { } +``` + +## 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 diff --git a/.agents/skills/elysiajs/plugins/opentelemetry.md b/.agents/skills/elysiajs/plugins/opentelemetry.md new file mode 100644 index 0000000..0ca95c3 --- /dev/null +++ b/.agents/skills/elysiajs/plugins/opentelemetry.md @@ -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. diff --git a/.agents/skills/elysiajs/plugins/server-timing.md b/.agents/skills/elysiajs/plugins/server-timing.md new file mode 100644 index 0000000..0021424 --- /dev/null +++ b/.agents/skills/elysiajs/plugins/server-timing.md @@ -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' + } + }) + ) +``` diff --git a/.agents/skills/elysiajs/plugins/static.md b/.agents/skills/elysiajs/plugins/static.md new file mode 100644 index 0000000..82fa1da --- /dev/null +++ b/.agents/skills/elysiajs/plugins/static.md @@ -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')) +``` diff --git a/.agents/skills/elysiajs/references/bun-fullstack-dev-server.md b/.agents/skills/elysiajs/references/bun-fullstack-dev-server.md new file mode 100644 index 0000000..70d721b --- /dev/null +++ b/.agents/skills/elysiajs/references/bun-fullstack-dev-server.md @@ -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 + + + + + + Elysia React App + + + +
+ + + +``` + +```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 ( +
+

{count}

+ +
+ ) +} + +const root = createRoot(document.getElementById('root')!) +root.render() +``` + +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 + +``` +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. diff --git a/.agents/skills/elysiajs/references/cookie.md b/.agents/skills/elysiajs/references/cookie.md new file mode 100644 index 0000000..9e1aa1c --- /dev/null +++ b/.agents/skills/elysiajs/references/cookie.md @@ -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`, 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. diff --git a/.agents/skills/elysiajs/references/deployment.md b/.agents/skills/elysiajs/references/deployment.md new file mode 100644 index 0000000..3c4cca8 --- /dev/null +++ b/.agents/skills/elysiajs/references/deployment.md @@ -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 +``` diff --git a/.agents/skills/elysiajs/references/eden.md b/.agents/skills/elysiajs/references/eden.md new file mode 100644 index 0000000..7d9165d --- /dev/null +++ b/.agents/skills/elysiajs/references/eden.md @@ -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('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 +type UserError = Treaty.Error +``` + +## 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` diff --git a/.agents/skills/elysiajs/references/lifecycle.md b/.agents/skills/elysiajs/references/lifecycle.md new file mode 100644 index 0000000..645584e --- /dev/null +++ b/.agents/skills/elysiajs/references/lifecycle.md @@ -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('/', () => '

Hello

', { + 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. diff --git a/.agents/skills/elysiajs/references/macro.md b/.agents/skills/elysiajs/references/macro.md new file mode 100644 index 0000000..f89ee75 --- /dev/null +++ b/.agents/skills/elysiajs/references/macro.md @@ -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) diff --git a/.agents/skills/elysiajs/references/plugin.md b/.agents/skills/elysiajs/references/plugin.md new file mode 100644 index 0000000..cd10e64 --- /dev/null +++ b/.agents/skills/elysiajs/references/plugin.md @@ -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) diff --git a/.agents/skills/elysiajs/references/route.md b/.agents/skills/elysiajs/references/route.md new file mode 100644 index 0000000..c767283 --- /dev/null +++ b/.agents/skills/elysiajs/references/route.md @@ -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 diff --git a/.agents/skills/elysiajs/references/testing.md b/.agents/skills/elysiajs/references/testing.md new file mode 100644 index 0000000..ffcdff3 --- /dev/null +++ b/.agents/skills/elysiajs/references/testing.md @@ -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) + }) +}) +``` diff --git a/.agents/skills/elysiajs/references/validation.md b/.agents/skills/elysiajs/references/validation.md new file mode 100644 index 0000000..ba723e0 --- /dev/null +++ b/.agents/skills/elysiajs/references/validation.md @@ -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 diff --git a/.agents/skills/elysiajs/references/websocket.md b/.agents/skills/elysiajs/references/websocket.md new file mode 100644 index 0000000..b2c86a8 --- /dev/null +++ b/.agents/skills/elysiajs/references/websocket.md @@ -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() + +.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>() + +.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('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) + } + }) +``` diff --git a/AGENTS.md b/AGENTS.md index 79046da..e8eccdd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ - Start here for project norms and development patterns. - This project is the backend API (`evolution-api`) for the Evolution Yu-Gi-Oh! platform. -- Consult specific skills in the `skills/` directory for detailed implementation guides. +- Consult specific skills in the `.agent/skills/` directory for detailed implementation guides. ## Available Skills @@ -13,13 +13,14 @@ Use these skills for detailed patterns on-demand: ### Generic Skills | Skill | Description | URL | |-------|-------------|-----| -| `ddd-implementation` | Patterns for Entities, Value Objects, Aggregates, and Domain Services in TypeScript | [SKILL.md](skills/ddd-implementation/SKILL.md) | -| `hexagonal-architecture` | Guide for Ports and Adapters, layer responsibilities, and dependency rules | [SKILL.md](skills/hexagonal-architecture/SKILL.md) | +| `ddd-implementation` | Patterns for Entities, Value Objects, Aggregates, and Domain Services in TypeScript | [SKILL.md](.agent/skills/ddd-implementation/SKILL.md) | +| `hexagonal-architecture` | Guide for Ports and Adapters, layer responsibilities, and dependency rules | [SKILL.md](.agent/skills/hexagonal-architecture/SKILL.md) | +| `elysiajs` | ElysiaJS framework patterns, plugins, and integrations | [SKILL.md](.agent/skills/elysiajs/SKILL.md) | ### Project-Specific Skills | Skill | Description | URL | |-------|-------------|-----| -| `evolution-server` | Reference to EDOpro-server-ts for stats calculation and game logic | [SKILL.md](skills/evolution-server/SKILL.md) | +| `evolution-server` | Reference to EDOpro-server-ts for stats calculation and game logic | [SKILL.md](.agent/skills/evolution-server/SKILL.md) | ### Auto-invoke Skills From 7e444ac1a7b70cbb0855daa5b8c192cce22f3235 Mon Sep 17 00:00:00 2001 From: DiangoGav Date: Sun, 8 Feb 2026 17:26:41 -0400 Subject: [PATCH 2/4] chore: remove .agents folder --- .agents/skills/elysiajs/SKILL.md | 475 ----------------- .agents/skills/elysiajs/examples/basic.ts | 9 - .../skills/elysiajs/examples/body-parser.ts | 33 -- .agents/skills/elysiajs/examples/complex.ts | 112 ---- .agents/skills/elysiajs/examples/cookie.ts | 45 -- .agents/skills/elysiajs/examples/error.ts | 38 -- .agents/skills/elysiajs/examples/file.ts | 10 - .agents/skills/elysiajs/examples/guard.ts | 34 -- .../skills/elysiajs/examples/map-response.ts | 15 - .agents/skills/elysiajs/examples/redirect.ts | 6 - .agents/skills/elysiajs/examples/rename.ts | 32 -- .agents/skills/elysiajs/examples/schema.ts | 61 --- .agents/skills/elysiajs/examples/state.ts | 6 - .../skills/elysiajs/examples/upload-file.ts | 20 - .agents/skills/elysiajs/examples/websocket.ts | 25 - .../skills/elysiajs/integrations/ai-sdk.md | 92 ---- .agents/skills/elysiajs/integrations/astro.md | 59 --- .../elysiajs/integrations/better-auth.md | 117 ----- .../integrations/cloudflare-worker.md | 95 ---- .agents/skills/elysiajs/integrations/deno.md | 34 -- .../skills/elysiajs/integrations/drizzle.md | 258 --------- .agents/skills/elysiajs/integrations/expo.md | 95 ---- .../skills/elysiajs/integrations/nextjs.md | 103 ---- .../skills/elysiajs/integrations/nodejs.md | 64 --- .agents/skills/elysiajs/integrations/nuxt.md | 67 --- .../skills/elysiajs/integrations/prisma.md | 93 ---- .../elysiajs/integrations/react-email.md | 134 ----- .../skills/elysiajs/integrations/sveltekit.md | 53 -- .../elysiajs/integrations/tanstack-start.md | 87 ---- .../skills/elysiajs/integrations/vercel.md | 55 -- .agents/skills/elysiajs/patterns/mvc.md | 380 -------------- .agents/skills/elysiajs/plugins/bearer.md | 30 -- .agents/skills/elysiajs/plugins/cors.md | 141 ----- .agents/skills/elysiajs/plugins/cron.md | 265 ---------- .../skills/elysiajs/plugins/graphql-apollo.md | 90 ---- .../skills/elysiajs/plugins/graphql-yoga.md | 87 ---- .agents/skills/elysiajs/plugins/html.md | 188 ------- .agents/skills/elysiajs/plugins/jwt.md | 197 ------- .agents/skills/elysiajs/plugins/openapi.md | 246 --------- .../skills/elysiajs/plugins/opentelemetry.md | 167 ------ .../skills/elysiajs/plugins/server-timing.md | 71 --- .agents/skills/elysiajs/plugins/static.md | 84 --- .../references/bun-fullstack-dev-server.md | 129 ----- .agents/skills/elysiajs/references/cookie.md | 187 ------- .../skills/elysiajs/references/deployment.md | 413 --------------- .agents/skills/elysiajs/references/eden.md | 158 ------ .../skills/elysiajs/references/lifecycle.md | 198 ------- .agents/skills/elysiajs/references/macro.md | 83 --- .agents/skills/elysiajs/references/plugin.md | 207 -------- .agents/skills/elysiajs/references/route.md | 331 ------------ .agents/skills/elysiajs/references/testing.md | 385 -------------- .../skills/elysiajs/references/validation.md | 491 ------------------ .../skills/elysiajs/references/websocket.md | 250 --------- 53 files changed, 7105 deletions(-) delete mode 100644 .agents/skills/elysiajs/SKILL.md delete mode 100644 .agents/skills/elysiajs/examples/basic.ts delete mode 100644 .agents/skills/elysiajs/examples/body-parser.ts delete mode 100644 .agents/skills/elysiajs/examples/complex.ts delete mode 100644 .agents/skills/elysiajs/examples/cookie.ts delete mode 100644 .agents/skills/elysiajs/examples/error.ts delete mode 100644 .agents/skills/elysiajs/examples/file.ts delete mode 100644 .agents/skills/elysiajs/examples/guard.ts delete mode 100644 .agents/skills/elysiajs/examples/map-response.ts delete mode 100644 .agents/skills/elysiajs/examples/redirect.ts delete mode 100644 .agents/skills/elysiajs/examples/rename.ts delete mode 100644 .agents/skills/elysiajs/examples/schema.ts delete mode 100644 .agents/skills/elysiajs/examples/state.ts delete mode 100644 .agents/skills/elysiajs/examples/upload-file.ts delete mode 100644 .agents/skills/elysiajs/examples/websocket.ts delete mode 100644 .agents/skills/elysiajs/integrations/ai-sdk.md delete mode 100644 .agents/skills/elysiajs/integrations/astro.md delete mode 100644 .agents/skills/elysiajs/integrations/better-auth.md delete mode 100644 .agents/skills/elysiajs/integrations/cloudflare-worker.md delete mode 100644 .agents/skills/elysiajs/integrations/deno.md delete mode 100644 .agents/skills/elysiajs/integrations/drizzle.md delete mode 100644 .agents/skills/elysiajs/integrations/expo.md delete mode 100644 .agents/skills/elysiajs/integrations/nextjs.md delete mode 100644 .agents/skills/elysiajs/integrations/nodejs.md delete mode 100644 .agents/skills/elysiajs/integrations/nuxt.md delete mode 100644 .agents/skills/elysiajs/integrations/prisma.md delete mode 100644 .agents/skills/elysiajs/integrations/react-email.md delete mode 100644 .agents/skills/elysiajs/integrations/sveltekit.md delete mode 100644 .agents/skills/elysiajs/integrations/tanstack-start.md delete mode 100644 .agents/skills/elysiajs/integrations/vercel.md delete mode 100644 .agents/skills/elysiajs/patterns/mvc.md delete mode 100644 .agents/skills/elysiajs/plugins/bearer.md delete mode 100644 .agents/skills/elysiajs/plugins/cors.md delete mode 100644 .agents/skills/elysiajs/plugins/cron.md delete mode 100644 .agents/skills/elysiajs/plugins/graphql-apollo.md delete mode 100644 .agents/skills/elysiajs/plugins/graphql-yoga.md delete mode 100644 .agents/skills/elysiajs/plugins/html.md delete mode 100644 .agents/skills/elysiajs/plugins/jwt.md delete mode 100644 .agents/skills/elysiajs/plugins/openapi.md delete mode 100644 .agents/skills/elysiajs/plugins/opentelemetry.md delete mode 100644 .agents/skills/elysiajs/plugins/server-timing.md delete mode 100644 .agents/skills/elysiajs/plugins/static.md delete mode 100644 .agents/skills/elysiajs/references/bun-fullstack-dev-server.md delete mode 100644 .agents/skills/elysiajs/references/cookie.md delete mode 100644 .agents/skills/elysiajs/references/deployment.md delete mode 100644 .agents/skills/elysiajs/references/eden.md delete mode 100644 .agents/skills/elysiajs/references/lifecycle.md delete mode 100644 .agents/skills/elysiajs/references/macro.md delete mode 100644 .agents/skills/elysiajs/references/plugin.md delete mode 100644 .agents/skills/elysiajs/references/route.md delete mode 100644 .agents/skills/elysiajs/references/testing.md delete mode 100644 .agents/skills/elysiajs/references/validation.md delete mode 100644 .agents/skills/elysiajs/references/websocket.md diff --git a/.agents/skills/elysiajs/SKILL.md b/.agents/skills/elysiajs/SKILL.md deleted file mode 100644 index d707a64..0000000 --- a/.agents/skills/elysiajs/SKILL.md +++ /dev/null @@ -1,475 +0,0 @@ ---- -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 diff --git a/.agents/skills/elysiajs/examples/basic.ts b/.agents/skills/elysiajs/examples/basic.ts deleted file mode 100644 index 61c8d14..0000000 --- a/.agents/skills/elysiajs/examples/basic.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Elysia, t } from 'elysia' - -new Elysia() - .get('/', 'Hello Elysia') - .post('/', ({ body: { name } }) => name, { - body: t.Object({ - name: t.String() - }) - }) diff --git a/.agents/skills/elysiajs/examples/body-parser.ts b/.agents/skills/elysiajs/examples/body-parser.ts deleted file mode 100644 index 533c7bf..0000000 --- a/.agents/skills/elysiajs/examples/body-parser.ts +++ /dev/null @@ -1,33 +0,0 @@ -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') diff --git a/.agents/skills/elysiajs/examples/complex.ts b/.agents/skills/elysiajs/examples/complex.ts deleted file mode 100644 index 436eda0..0000000 --- a/.agents/skills/elysiajs/examples/complex.ts +++ /dev/null @@ -1,112 +0,0 @@ -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}`) - }) diff --git a/.agents/skills/elysiajs/examples/cookie.ts b/.agents/skills/elysiajs/examples/cookie.ts deleted file mode 100644 index 9a42720..0000000 --- a/.agents/skills/elysiajs/examples/cookie.ts +++ /dev/null @@ -1,45 +0,0 @@ -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) diff --git a/.agents/skills/elysiajs/examples/error.ts b/.agents/skills/elysiajs/examples/error.ts deleted file mode 100644 index 2c2f126..0000000 --- a/.agents/skills/elysiajs/examples/error.ts +++ /dev/null @@ -1,38 +0,0 @@ -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) diff --git a/.agents/skills/elysiajs/examples/file.ts b/.agents/skills/elysiajs/examples/file.ts deleted file mode 100644 index 504cad7..0000000 --- a/.agents/skills/elysiajs/examples/file.ts +++ /dev/null @@ -1,10 +0,0 @@ -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) diff --git a/.agents/skills/elysiajs/examples/guard.ts b/.agents/skills/elysiajs/examples/guard.ts deleted file mode 100644 index 2fe158f..0000000 --- a/.agents/skills/elysiajs/examples/guard.ts +++ /dev/null @@ -1,34 +0,0 @@ -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) diff --git a/.agents/skills/elysiajs/examples/map-response.ts b/.agents/skills/elysiajs/examples/map-response.ts deleted file mode 100644 index 8cd4be4..0000000 --- a/.agents/skills/elysiajs/examples/map-response.ts +++ /dev/null @@ -1,15 +0,0 @@ -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) diff --git a/.agents/skills/elysiajs/examples/redirect.ts b/.agents/skills/elysiajs/examples/redirect.ts deleted file mode 100644 index 28171b0..0000000 --- a/.agents/skills/elysiajs/examples/redirect.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Elysia } from 'elysia' - -new Elysia() - .get('/', () => 'Hi') - .get('/redirect', ({ redirect }) => redirect('/')) - .listen(3000) diff --git a/.agents/skills/elysiajs/examples/rename.ts b/.agents/skills/elysiajs/examples/rename.ts deleted file mode 100644 index 361f06f..0000000 --- a/.agents/skills/elysiajs/examples/rename.ts +++ /dev/null @@ -1,32 +0,0 @@ -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() - }) diff --git a/.agents/skills/elysiajs/examples/schema.ts b/.agents/skills/elysiajs/examples/schema.ts deleted file mode 100644 index db79300..0000000 --- a/.agents/skills/elysiajs/examples/schema.ts +++ /dev/null @@ -1,61 +0,0 @@ -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) diff --git a/.agents/skills/elysiajs/examples/state.ts b/.agents/skills/elysiajs/examples/state.ts deleted file mode 100644 index 8bcc993..0000000 --- a/.agents/skills/elysiajs/examples/state.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Elysia } from 'elysia' - -new Elysia() - .state('counter', 0) - .get('/', ({ store }) => store.counter++) - .listen(3000) diff --git a/.agents/skills/elysiajs/examples/upload-file.ts b/.agents/skills/elysiajs/examples/upload-file.ts deleted file mode 100644 index 4af5a19..0000000 --- a/.agents/skills/elysiajs/examples/upload-file.ts +++ /dev/null @@ -1,20 +0,0 @@ -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) diff --git a/.agents/skills/elysiajs/examples/websocket.ts b/.agents/skills/elysiajs/examples/websocket.ts deleted file mode 100644 index f97e47b..0000000 --- a/.agents/skills/elysiajs/examples/websocket.ts +++ /dev/null @@ -1,25 +0,0 @@ -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}`) - }) diff --git a/.agents/skills/elysiajs/integrations/ai-sdk.md b/.agents/skills/elysiajs/integrations/ai-sdk.md deleted file mode 100644 index 99f5409..0000000 --- a/.agents/skills/elysiajs/integrations/ai-sdk.md +++ /dev/null @@ -1,92 +0,0 @@ -# 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. diff --git a/.agents/skills/elysiajs/integrations/astro.md b/.agents/skills/elysiajs/integrations/astro.md deleted file mode 100644 index 41cd451..0000000 --- a/.agents/skills/elysiajs/integrations/astro.md +++ /dev/null @@ -1,59 +0,0 @@ -# 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 -``` diff --git a/.agents/skills/elysiajs/integrations/better-auth.md b/.agents/skills/elysiajs/integrations/better-auth.md deleted file mode 100644 index 0dfa3af..0000000 --- a/.agents/skills/elysiajs/integrations/better-auth.md +++ /dev/null @@ -1,117 +0,0 @@ -# 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 -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, - components: getSchema().then(({ components }) => components) as Promise -} 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. diff --git a/.agents/skills/elysiajs/integrations/cloudflare-worker.md b/.agents/skills/elysiajs/integrations/cloudflare-worker.md deleted file mode 100644 index 4245c1a..0000000 --- a/.agents/skills/elysiajs/integrations/cloudflare-worker.md +++ /dev/null @@ -1,95 +0,0 @@ - -# 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 -``` diff --git a/.agents/skills/elysiajs/integrations/deno.md b/.agents/skills/elysiajs/integrations/deno.md deleted file mode 100644 index 28687d5..0000000 --- a/.agents/skills/elysiajs/integrations/deno.md +++ /dev/null @@ -1,34 +0,0 @@ -# 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 -``` diff --git a/.agents/skills/elysiajs/integrations/drizzle.md b/.agents/skills/elysiajs/integrations/drizzle.md deleted file mode 100644 index 779db4e..0000000 --- a/.agents/skills/elysiajs/integrations/drizzle.md +++ /dev/null @@ -1,258 +0,0 @@ -# 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 - ? { - [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 => { - const newSchema: Record = {} - 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, - Mode extends 'select' | 'insert' | undefined, ->( - models: T, - mode?: Mode, -): { - [K in keyof T]: Spread -} => { - const newSchema: Record = {} - 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. diff --git a/.agents/skills/elysiajs/integrations/expo.md b/.agents/skills/elysiajs/integrations/expo.md deleted file mode 100644 index fad1471..0000000 --- a/.agents/skills/elysiajs/integrations/expo.md +++ /dev/null @@ -1,95 +0,0 @@ -# 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('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

Hello, {message}

-} -``` - -## 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 -``` diff --git a/.agents/skills/elysiajs/integrations/nextjs.md b/.agents/skills/elysiajs/integrations/nextjs.md deleted file mode 100644 index ddbc849..0000000 --- a/.agents/skills/elysiajs/integrations/nextjs.md +++ /dev/null @@ -1,103 +0,0 @@ - -# 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('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

Hello, {message}

-} -``` - -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 -``` diff --git a/.agents/skills/elysiajs/integrations/nodejs.md b/.agents/skills/elysiajs/integrations/nodejs.md deleted file mode 100644 index ce2edfa..0000000 --- a/.agents/skills/elysiajs/integrations/nodejs.md +++ /dev/null @@ -1,64 +0,0 @@ -# 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 -``` diff --git a/.agents/skills/elysiajs/integrations/nuxt.md b/.agents/skills/elysiajs/integrations/nuxt.md deleted file mode 100644 index 0b4d13d..0000000 --- a/.agents/skills/elysiajs/integrations/nuxt.md +++ /dev/null @@ -1,67 +0,0 @@ -# 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 - - -``` - -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 -``` diff --git a/.agents/skills/elysiajs/integrations/prisma.md b/.agents/skills/elysiajs/integrations/prisma.md deleted file mode 100644 index f0684f1..0000000 --- a/.agents/skills/elysiajs/integrations/prisma.md +++ /dev/null @@ -1,93 +0,0 @@ - -# 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. diff --git a/.agents/skills/elysiajs/integrations/react-email.md b/.agents/skills/elysiajs/integrations/react-email.md deleted file mode 100644 index 1cb636f..0000000 --- a/.agents/skills/elysiajs/integrations/react-email.md +++ /dev/null @@ -1,134 +0,0 @@ -# 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 ( - -
-
- - Verify your Email Address - - - Use the following code to verify your email address - - {otp} - - This code is valid for 10 minutes - - - Thank you for joining us - -
-
-
- ) -} - -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() - - 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: // 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). diff --git a/.agents/skills/elysiajs/integrations/sveltekit.md b/.agents/skills/elysiajs/integrations/sveltekit.md deleted file mode 100644 index 4ad306a..0000000 --- a/.agents/skills/elysiajs/integrations/sveltekit.md +++ /dev/null @@ -1,53 +0,0 @@ - -# 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 - -export const fallback: RequestHandler = ({ request }) => app.handle(request) -``` - -Ensures routing works in any location. - -## pnpm -Manual install: -```bash -pnpm add @sinclair/typebox openapi-types -``` diff --git a/.agents/skills/elysiajs/integrations/tanstack-start.md b/.agents/skills/elysiajs/integrations/tanstack-start.md deleted file mode 100644 index 2a1e642..0000000 --- a/.agents/skills/elysiajs/integrations/tanstack-start.md +++ /dev/null @@ -1,87 +0,0 @@ -# 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('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 -``` diff --git a/.agents/skills/elysiajs/integrations/vercel.md b/.agents/skills/elysiajs/integrations/vercel.md deleted file mode 100644 index 555ec8e..0000000 --- a/.agents/skills/elysiajs/integrations/vercel.md +++ /dev/null @@ -1,55 +0,0 @@ -# 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). diff --git a/.agents/skills/elysiajs/patterns/mvc.md b/.agents/skills/elysiajs/patterns/mvc.md deleted file mode 100644 index dc227c6..0000000 --- a/.agents/skills/elysiajs/patterns/mvc.md +++ /dev/null @@ -1,380 +0,0 @@ -# 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. diff --git a/.agents/skills/elysiajs/plugins/bearer.md b/.agents/skills/elysiajs/plugins/bearer.md deleted file mode 100644 index df529e5..0000000 --- a/.agents/skills/elysiajs/plugins/bearer.md +++ /dev/null @@ -1,30 +0,0 @@ -# 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 diff --git a/.agents/skills/elysiajs/plugins/cors.md b/.agents/skills/elysiajs/plugins/cors.md deleted file mode 100644 index 2d8db2a..0000000 --- a/.agents/skills/elysiajs/plugins/cors.md +++ /dev/null @@ -1,141 +0,0 @@ -# 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** - 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` diff --git a/.agents/skills/elysiajs/plugins/cron.md b/.agents/skills/elysiajs/plugins/cron.md deleted file mode 100644 index 3905ad5..0000000 --- a/.agents/skills/elysiajs/plugins/cron.md +++ /dev/null @@ -1,265 +0,0 @@ -# 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 * * *` | diff --git a/.agents/skills/elysiajs/plugins/graphql-apollo.md b/.agents/skills/elysiajs/plugins/graphql-apollo.md deleted file mode 100644 index 655f258..0000000 --- a/.agents/skills/elysiajs/plugins/graphql-apollo.md +++ /dev/null @@ -1,90 +0,0 @@ -# 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. diff --git a/.agents/skills/elysiajs/plugins/graphql-yoga.md b/.agents/skills/elysiajs/plugins/graphql-yoga.md deleted file mode 100644 index 3203d02..0000000 --- a/.agents/skills/elysiajs/plugins/graphql-yoga.md +++ /dev/null @@ -1,87 +0,0 @@ -# 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 diff --git a/.agents/skills/elysiajs/plugins/html.md b/.agents/skills/elysiajs/plugins/html.md deleted file mode 100644 index 777a536..0000000 --- a/.agents/skills/elysiajs/plugins/html.md +++ /dev/null @@ -1,188 +0,0 @@ -# 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', - () => ` - - - Hello World - - -

Hello World

- - ` - ) - .get('/jsx', () => ( - - - Hello World - - -

Hello World

- - - )) - .listen(3000) -``` - -This plugin will automatically add `Content-Type: text/html; charset=utf8` header to the response, add ``, 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('/', () => ( - - - Hello World - - -

Hello World

- - - )) - .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 }) => ( - - - Hello World - - -

{body}

- - - ), - { - 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 `` to a response starting with ``, if not found. - -Use `full` to also automatically add doctypes on responses returned without this plugin - -```ts -// without the plugin -app.get('/', () => '') - -// With the plugin -app.get('/', ({ 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. diff --git a/.agents/skills/elysiajs/plugins/jwt.md b/.agents/skills/elysiajs/plugins/jwt.md deleted file mode 100644 index b5767bf..0000000 --- a/.agents/skills/elysiajs/plugins/jwt.md +++ /dev/null @@ -1,197 +0,0 @@ -# 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 -``` - -`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` 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. diff --git a/.agents/skills/elysiajs/plugins/openapi.md b/.agents/skills/elysiajs/plugins/openapi.md deleted file mode 100644 index c69150d..0000000 --- a/.agents/skills/elysiajs/plugins/openapi.md +++ /dev/null @@ -1,246 +0,0 @@ -# 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 = { [K in keyof T]: T[K] } & {} - -function getUser(): Prettify { } -``` - -## 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 diff --git a/.agents/skills/elysiajs/plugins/opentelemetry.md b/.agents/skills/elysiajs/plugins/opentelemetry.md deleted file mode 100644 index 0ca95c3..0000000 --- a/.agents/skills/elysiajs/plugins/opentelemetry.md +++ /dev/null @@ -1,167 +0,0 @@ -# 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. diff --git a/.agents/skills/elysiajs/plugins/server-timing.md b/.agents/skills/elysiajs/plugins/server-timing.md deleted file mode 100644 index 0021424..0000000 --- a/.agents/skills/elysiajs/plugins/server-timing.md +++ /dev/null @@ -1,71 +0,0 @@ -# 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' - } - }) - ) -``` diff --git a/.agents/skills/elysiajs/plugins/static.md b/.agents/skills/elysiajs/plugins/static.md deleted file mode 100644 index 82fa1da..0000000 --- a/.agents/skills/elysiajs/plugins/static.md +++ /dev/null @@ -1,84 +0,0 @@ -# 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')) -``` diff --git a/.agents/skills/elysiajs/references/bun-fullstack-dev-server.md b/.agents/skills/elysiajs/references/bun-fullstack-dev-server.md deleted file mode 100644 index 70d721b..0000000 --- a/.agents/skills/elysiajs/references/bun-fullstack-dev-server.md +++ /dev/null @@ -1,129 +0,0 @@ -# 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 - - - - - - Elysia React App - - - -
- - - -``` - -```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 ( -
-

{count}

- -
- ) -} - -const root = createRoot(document.getElementById('root')!) -root.render() -``` - -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 - -``` -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. diff --git a/.agents/skills/elysiajs/references/cookie.md b/.agents/skills/elysiajs/references/cookie.md deleted file mode 100644 index 9e1aa1c..0000000 --- a/.agents/skills/elysiajs/references/cookie.md +++ /dev/null @@ -1,187 +0,0 @@ -# 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`, 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. diff --git a/.agents/skills/elysiajs/references/deployment.md b/.agents/skills/elysiajs/references/deployment.md deleted file mode 100644 index 3c4cca8..0000000 --- a/.agents/skills/elysiajs/references/deployment.md +++ /dev/null @@ -1,413 +0,0 @@ -# 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 -``` diff --git a/.agents/skills/elysiajs/references/eden.md b/.agents/skills/elysiajs/references/eden.md deleted file mode 100644 index 7d9165d..0000000 --- a/.agents/skills/elysiajs/references/eden.md +++ /dev/null @@ -1,158 +0,0 @@ -# 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('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 -type UserError = Treaty.Error -``` - -## 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` diff --git a/.agents/skills/elysiajs/references/lifecycle.md b/.agents/skills/elysiajs/references/lifecycle.md deleted file mode 100644 index 645584e..0000000 --- a/.agents/skills/elysiajs/references/lifecycle.md +++ /dev/null @@ -1,198 +0,0 @@ -# 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('/', () => '

Hello

', { - 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. diff --git a/.agents/skills/elysiajs/references/macro.md b/.agents/skills/elysiajs/references/macro.md deleted file mode 100644 index f89ee75..0000000 --- a/.agents/skills/elysiajs/references/macro.md +++ /dev/null @@ -1,83 +0,0 @@ -# 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) diff --git a/.agents/skills/elysiajs/references/plugin.md b/.agents/skills/elysiajs/references/plugin.md deleted file mode 100644 index cd10e64..0000000 --- a/.agents/skills/elysiajs/references/plugin.md +++ /dev/null @@ -1,207 +0,0 @@ -# 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) diff --git a/.agents/skills/elysiajs/references/route.md b/.agents/skills/elysiajs/references/route.md deleted file mode 100644 index c767283..0000000 --- a/.agents/skills/elysiajs/references/route.md +++ /dev/null @@ -1,331 +0,0 @@ -# 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 diff --git a/.agents/skills/elysiajs/references/testing.md b/.agents/skills/elysiajs/references/testing.md deleted file mode 100644 index ffcdff3..0000000 --- a/.agents/skills/elysiajs/references/testing.md +++ /dev/null @@ -1,385 +0,0 @@ -# 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) - }) -}) -``` diff --git a/.agents/skills/elysiajs/references/validation.md b/.agents/skills/elysiajs/references/validation.md deleted file mode 100644 index ba723e0..0000000 --- a/.agents/skills/elysiajs/references/validation.md +++ /dev/null @@ -1,491 +0,0 @@ -# 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 diff --git a/.agents/skills/elysiajs/references/websocket.md b/.agents/skills/elysiajs/references/websocket.md deleted file mode 100644 index b2c86a8..0000000 --- a/.agents/skills/elysiajs/references/websocket.md +++ /dev/null @@ -1,250 +0,0 @@ -# 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() - -.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>() - -.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('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) - } - }) -``` From 4e4d76e753e0f6b0665294f2e8250eb4234c233d Mon Sep 17 00:00:00 2001 From: DiangoGav Date: Sun, 8 Feb 2026 18:35:25 -0400 Subject: [PATCH 3/4] chore: update evolution-types submodule --- src/evolution-types | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/evolution-types b/src/evolution-types index 6e7aa7f..bcfd727 160000 --- a/src/evolution-types +++ b/src/evolution-types @@ -1 +1 @@ -Subproject commit 6e7aa7f98cd0a303a97e1f66434bdc79a13c4890 +Subproject commit bcfd727f64ef18f2208f360504d709c0caa1bf06 From b00301417ea7aea9befd0d32df88155eab0845a3 Mon Sep 17 00:00:00 2001 From: DiangoGav Date: Sun, 8 Feb 2026 18:45:10 -0400 Subject: [PATCH 4/4] chore: added elysiajs skill --- .agent/skills/elysiajs | 1 - .agent/skills/elysiajs/SKILL.md | 475 +++++++++++++++++ .agent/skills/elysiajs/examples/basic.ts | 9 + .../skills/elysiajs/examples/body-parser.ts | 34 ++ .agent/skills/elysiajs/examples/complex.ts | 114 ++++ .agent/skills/elysiajs/examples/cookie.ts | 46 ++ .agent/skills/elysiajs/examples/error.ts | 38 ++ .agent/skills/elysiajs/examples/file.ts | 10 + .agent/skills/elysiajs/examples/guard.ts | 35 ++ .../skills/elysiajs/examples/map-response.ts | 15 + .agent/skills/elysiajs/examples/redirect.ts | 6 + .agent/skills/elysiajs/examples/rename.ts | 33 ++ .agent/skills/elysiajs/examples/schema.ts | 62 +++ .agent/skills/elysiajs/examples/state.ts | 6 + .../skills/elysiajs/examples/upload-file.ts | 21 + .agent/skills/elysiajs/examples/websocket.ts | 25 + .agent/skills/elysiajs/integrations/ai-sdk.md | 92 ++++ .agent/skills/elysiajs/integrations/astro.md | 59 +++ .../elysiajs/integrations/better-auth.md | 117 +++++ .../integrations/cloudflare-worker.md | 95 ++++ .agent/skills/elysiajs/integrations/deno.md | 34 ++ .../skills/elysiajs/integrations/drizzle.md | 258 +++++++++ .agent/skills/elysiajs/integrations/expo.md | 95 ++++ .agent/skills/elysiajs/integrations/nextjs.md | 103 ++++ .agent/skills/elysiajs/integrations/nodejs.md | 64 +++ .agent/skills/elysiajs/integrations/nuxt.md | 67 +++ .agent/skills/elysiajs/integrations/prisma.md | 93 ++++ .../elysiajs/integrations/react-email.md | 134 +++++ .../skills/elysiajs/integrations/sveltekit.md | 53 ++ .../elysiajs/integrations/tanstack-start.md | 87 ++++ .agent/skills/elysiajs/integrations/vercel.md | 55 ++ .agent/skills/elysiajs/patterns/mvc.md | 380 ++++++++++++++ .agent/skills/elysiajs/plugins/bearer.md | 30 ++ .agent/skills/elysiajs/plugins/cors.md | 141 +++++ .agent/skills/elysiajs/plugins/cron.md | 265 ++++++++++ .../skills/elysiajs/plugins/graphql-apollo.md | 90 ++++ .../skills/elysiajs/plugins/graphql-yoga.md | 87 ++++ .agent/skills/elysiajs/plugins/html.md | 188 +++++++ .agent/skills/elysiajs/plugins/jwt.md | 197 +++++++ .agent/skills/elysiajs/plugins/openapi.md | 246 +++++++++ .../skills/elysiajs/plugins/opentelemetry.md | 167 ++++++ .../skills/elysiajs/plugins/server-timing.md | 71 +++ .agent/skills/elysiajs/plugins/static.md | 84 +++ .../references/bun-fullstack-dev-server.md | 129 +++++ .agent/skills/elysiajs/references/cookie.md | 187 +++++++ .../skills/elysiajs/references/deployment.md | 413 +++++++++++++++ .agent/skills/elysiajs/references/eden.md | 158 ++++++ .../skills/elysiajs/references/lifecycle.md | 198 +++++++ .agent/skills/elysiajs/references/macro.md | 83 +++ .agent/skills/elysiajs/references/plugin.md | 207 ++++++++ .agent/skills/elysiajs/references/route.md | 331 ++++++++++++ .agent/skills/elysiajs/references/testing.md | 385 ++++++++++++++ .../skills/elysiajs/references/validation.md | 491 ++++++++++++++++++ .../skills/elysiajs/references/websocket.md | 250 +++++++++ 54 files changed, 7113 insertions(+), 1 deletion(-) delete mode 120000 .agent/skills/elysiajs create mode 100644 .agent/skills/elysiajs/SKILL.md create mode 100644 .agent/skills/elysiajs/examples/basic.ts create mode 100644 .agent/skills/elysiajs/examples/body-parser.ts create mode 100644 .agent/skills/elysiajs/examples/complex.ts create mode 100644 .agent/skills/elysiajs/examples/cookie.ts create mode 100644 .agent/skills/elysiajs/examples/error.ts create mode 100644 .agent/skills/elysiajs/examples/file.ts create mode 100644 .agent/skills/elysiajs/examples/guard.ts create mode 100644 .agent/skills/elysiajs/examples/map-response.ts create mode 100644 .agent/skills/elysiajs/examples/redirect.ts create mode 100644 .agent/skills/elysiajs/examples/rename.ts create mode 100644 .agent/skills/elysiajs/examples/schema.ts create mode 100644 .agent/skills/elysiajs/examples/state.ts create mode 100644 .agent/skills/elysiajs/examples/upload-file.ts create mode 100644 .agent/skills/elysiajs/examples/websocket.ts create mode 100644 .agent/skills/elysiajs/integrations/ai-sdk.md create mode 100644 .agent/skills/elysiajs/integrations/astro.md create mode 100644 .agent/skills/elysiajs/integrations/better-auth.md create mode 100644 .agent/skills/elysiajs/integrations/cloudflare-worker.md create mode 100644 .agent/skills/elysiajs/integrations/deno.md create mode 100644 .agent/skills/elysiajs/integrations/drizzle.md create mode 100644 .agent/skills/elysiajs/integrations/expo.md create mode 100644 .agent/skills/elysiajs/integrations/nextjs.md create mode 100644 .agent/skills/elysiajs/integrations/nodejs.md create mode 100644 .agent/skills/elysiajs/integrations/nuxt.md create mode 100644 .agent/skills/elysiajs/integrations/prisma.md create mode 100644 .agent/skills/elysiajs/integrations/react-email.md create mode 100644 .agent/skills/elysiajs/integrations/sveltekit.md create mode 100644 .agent/skills/elysiajs/integrations/tanstack-start.md create mode 100644 .agent/skills/elysiajs/integrations/vercel.md create mode 100644 .agent/skills/elysiajs/patterns/mvc.md create mode 100644 .agent/skills/elysiajs/plugins/bearer.md create mode 100644 .agent/skills/elysiajs/plugins/cors.md create mode 100644 .agent/skills/elysiajs/plugins/cron.md create mode 100644 .agent/skills/elysiajs/plugins/graphql-apollo.md create mode 100644 .agent/skills/elysiajs/plugins/graphql-yoga.md create mode 100644 .agent/skills/elysiajs/plugins/html.md create mode 100644 .agent/skills/elysiajs/plugins/jwt.md create mode 100644 .agent/skills/elysiajs/plugins/openapi.md create mode 100644 .agent/skills/elysiajs/plugins/opentelemetry.md create mode 100644 .agent/skills/elysiajs/plugins/server-timing.md create mode 100644 .agent/skills/elysiajs/plugins/static.md create mode 100644 .agent/skills/elysiajs/references/bun-fullstack-dev-server.md create mode 100644 .agent/skills/elysiajs/references/cookie.md create mode 100644 .agent/skills/elysiajs/references/deployment.md create mode 100644 .agent/skills/elysiajs/references/eden.md create mode 100644 .agent/skills/elysiajs/references/lifecycle.md create mode 100644 .agent/skills/elysiajs/references/macro.md create mode 100644 .agent/skills/elysiajs/references/plugin.md create mode 100644 .agent/skills/elysiajs/references/route.md create mode 100644 .agent/skills/elysiajs/references/testing.md create mode 100644 .agent/skills/elysiajs/references/validation.md create mode 100644 .agent/skills/elysiajs/references/websocket.md diff --git a/.agent/skills/elysiajs b/.agent/skills/elysiajs deleted file mode 120000 index 7ee55cc..0000000 --- a/.agent/skills/elysiajs +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/elysiajs \ No newline at end of file diff --git a/.agent/skills/elysiajs/SKILL.md b/.agent/skills/elysiajs/SKILL.md new file mode 100644 index 0000000..d707a64 --- /dev/null +++ b/.agent/skills/elysiajs/SKILL.md @@ -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 diff --git a/.agent/skills/elysiajs/examples/basic.ts b/.agent/skills/elysiajs/examples/basic.ts new file mode 100644 index 0000000..61c8d14 --- /dev/null +++ b/.agent/skills/elysiajs/examples/basic.ts @@ -0,0 +1,9 @@ +import { Elysia, t } from 'elysia' + +new Elysia() + .get('/', 'Hello Elysia') + .post('/', ({ body: { name } }) => name, { + body: t.Object({ + name: t.String() + }) + }) diff --git a/.agent/skills/elysiajs/examples/body-parser.ts b/.agent/skills/elysiajs/examples/body-parser.ts new file mode 100644 index 0000000..32e4053 --- /dev/null +++ b/.agent/skills/elysiajs/examples/body-parser.ts @@ -0,0 +1,34 @@ +import { Elysia, t } from 'elysia' + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +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') diff --git a/.agent/skills/elysiajs/examples/complex.ts b/.agent/skills/elysiajs/examples/complex.ts new file mode 100644 index 0000000..1f86ede --- /dev/null +++ b/.agent/skills/elysiajs/examples/complex.ts @@ -0,0 +1,114 @@ +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')) + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +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() + }) + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .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}`) + }) diff --git a/.agent/skills/elysiajs/examples/cookie.ts b/.agent/skills/elysiajs/examples/cookie.ts new file mode 100644 index 0000000..504bb54 --- /dev/null +++ b/.agent/skills/elysiajs/examples/cookie.ts @@ -0,0 +1,46 @@ +import { Elysia, t } from 'elysia' + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +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) diff --git a/.agent/skills/elysiajs/examples/error.ts b/.agent/skills/elysiajs/examples/error.ts new file mode 100644 index 0000000..2c2f126 --- /dev/null +++ b/.agent/skills/elysiajs/examples/error.ts @@ -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) diff --git a/.agent/skills/elysiajs/examples/file.ts b/.agent/skills/elysiajs/examples/file.ts new file mode 100644 index 0000000..504cad7 --- /dev/null +++ b/.agent/skills/elysiajs/examples/file.ts @@ -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) diff --git a/.agent/skills/elysiajs/examples/guard.ts b/.agent/skills/elysiajs/examples/guard.ts new file mode 100644 index 0000000..48fe58a --- /dev/null +++ b/.agent/skills/elysiajs/examples/guard.ts @@ -0,0 +1,35 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +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) diff --git a/.agent/skills/elysiajs/examples/map-response.ts b/.agent/skills/elysiajs/examples/map-response.ts new file mode 100644 index 0000000..8cd4be4 --- /dev/null +++ b/.agent/skills/elysiajs/examples/map-response.ts @@ -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) diff --git a/.agent/skills/elysiajs/examples/redirect.ts b/.agent/skills/elysiajs/examples/redirect.ts new file mode 100644 index 0000000..28171b0 --- /dev/null +++ b/.agent/skills/elysiajs/examples/redirect.ts @@ -0,0 +1,6 @@ +import { Elysia } from 'elysia' + +new Elysia() + .get('/', () => 'Hi') + .get('/redirect', ({ redirect }) => redirect('/')) + .listen(3000) diff --git a/.agent/skills/elysiajs/examples/rename.ts b/.agent/skills/elysiajs/examples/rename.ts new file mode 100644 index 0000000..41c0a8a --- /dev/null +++ b/.agent/skills/elysiajs/examples/rename.ts @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +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() + }) diff --git a/.agent/skills/elysiajs/examples/schema.ts b/.agent/skills/elysiajs/examples/schema.ts new file mode 100644 index 0000000..3297924 --- /dev/null +++ b/.agent/skills/elysiajs/examples/schema.ts @@ -0,0 +1,62 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +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) diff --git a/.agent/skills/elysiajs/examples/state.ts b/.agent/skills/elysiajs/examples/state.ts new file mode 100644 index 0000000..8bcc993 --- /dev/null +++ b/.agent/skills/elysiajs/examples/state.ts @@ -0,0 +1,6 @@ +import { Elysia } from 'elysia' + +new Elysia() + .state('counter', 0) + .get('/', ({ store }) => store.counter++) + .listen(3000) diff --git a/.agent/skills/elysiajs/examples/upload-file.ts b/.agent/skills/elysiajs/examples/upload-file.ts new file mode 100644 index 0000000..15d5fe4 --- /dev/null +++ b/.agent/skills/elysiajs/examples/upload-file.ts @@ -0,0 +1,21 @@ +import { Elysia, t } from 'elysia' + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +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) diff --git a/.agent/skills/elysiajs/examples/websocket.ts b/.agent/skills/elysiajs/examples/websocket.ts new file mode 100644 index 0000000..f97e47b --- /dev/null +++ b/.agent/skills/elysiajs/examples/websocket.ts @@ -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}`) + }) diff --git a/.agent/skills/elysiajs/integrations/ai-sdk.md b/.agent/skills/elysiajs/integrations/ai-sdk.md new file mode 100644 index 0000000..99f5409 --- /dev/null +++ b/.agent/skills/elysiajs/integrations/ai-sdk.md @@ -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. diff --git a/.agent/skills/elysiajs/integrations/astro.md b/.agent/skills/elysiajs/integrations/astro.md new file mode 100644 index 0000000..41cd451 --- /dev/null +++ b/.agent/skills/elysiajs/integrations/astro.md @@ -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 +``` diff --git a/.agent/skills/elysiajs/integrations/better-auth.md b/.agent/skills/elysiajs/integrations/better-auth.md new file mode 100644 index 0000000..0dfa3af --- /dev/null +++ b/.agent/skills/elysiajs/integrations/better-auth.md @@ -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 +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, + components: getSchema().then(({ components }) => components) as Promise +} 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. diff --git a/.agent/skills/elysiajs/integrations/cloudflare-worker.md b/.agent/skills/elysiajs/integrations/cloudflare-worker.md new file mode 100644 index 0000000..4245c1a --- /dev/null +++ b/.agent/skills/elysiajs/integrations/cloudflare-worker.md @@ -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 +``` diff --git a/.agent/skills/elysiajs/integrations/deno.md b/.agent/skills/elysiajs/integrations/deno.md new file mode 100644 index 0000000..28687d5 --- /dev/null +++ b/.agent/skills/elysiajs/integrations/deno.md @@ -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 +``` diff --git a/.agent/skills/elysiajs/integrations/drizzle.md b/.agent/skills/elysiajs/integrations/drizzle.md new file mode 100644 index 0000000..779db4e --- /dev/null +++ b/.agent/skills/elysiajs/integrations/drizzle.md @@ -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 + ? { + [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 => { + const newSchema: Record = {} + 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, + Mode extends 'select' | 'insert' | undefined, +>( + models: T, + mode?: Mode, +): { + [K in keyof T]: Spread +} => { + const newSchema: Record = {} + 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. diff --git a/.agent/skills/elysiajs/integrations/expo.md b/.agent/skills/elysiajs/integrations/expo.md new file mode 100644 index 0000000..fad1471 --- /dev/null +++ b/.agent/skills/elysiajs/integrations/expo.md @@ -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('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

Hello, {message}

+} +``` + +## 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 +``` diff --git a/.agent/skills/elysiajs/integrations/nextjs.md b/.agent/skills/elysiajs/integrations/nextjs.md new file mode 100644 index 0000000..ddbc849 --- /dev/null +++ b/.agent/skills/elysiajs/integrations/nextjs.md @@ -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('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

Hello, {message}

+} +``` + +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 +``` diff --git a/.agent/skills/elysiajs/integrations/nodejs.md b/.agent/skills/elysiajs/integrations/nodejs.md new file mode 100644 index 0000000..ce2edfa --- /dev/null +++ b/.agent/skills/elysiajs/integrations/nodejs.md @@ -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 +``` diff --git a/.agent/skills/elysiajs/integrations/nuxt.md b/.agent/skills/elysiajs/integrations/nuxt.md new file mode 100644 index 0000000..0b4d13d --- /dev/null +++ b/.agent/skills/elysiajs/integrations/nuxt.md @@ -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 + + +``` + +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 +``` diff --git a/.agent/skills/elysiajs/integrations/prisma.md b/.agent/skills/elysiajs/integrations/prisma.md new file mode 100644 index 0000000..f0684f1 --- /dev/null +++ b/.agent/skills/elysiajs/integrations/prisma.md @@ -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. diff --git a/.agent/skills/elysiajs/integrations/react-email.md b/.agent/skills/elysiajs/integrations/react-email.md new file mode 100644 index 0000000..1cb636f --- /dev/null +++ b/.agent/skills/elysiajs/integrations/react-email.md @@ -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 ( + +
+
+ + Verify your Email Address + + + Use the following code to verify your email address + + {otp} + + This code is valid for 10 minutes + + + Thank you for joining us + +
+
+
+ ) +} + +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() + + 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: // 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). diff --git a/.agent/skills/elysiajs/integrations/sveltekit.md b/.agent/skills/elysiajs/integrations/sveltekit.md new file mode 100644 index 0000000..4ad306a --- /dev/null +++ b/.agent/skills/elysiajs/integrations/sveltekit.md @@ -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 + +export const fallback: RequestHandler = ({ request }) => app.handle(request) +``` + +Ensures routing works in any location. + +## pnpm +Manual install: +```bash +pnpm add @sinclair/typebox openapi-types +``` diff --git a/.agent/skills/elysiajs/integrations/tanstack-start.md b/.agent/skills/elysiajs/integrations/tanstack-start.md new file mode 100644 index 0000000..2a1e642 --- /dev/null +++ b/.agent/skills/elysiajs/integrations/tanstack-start.md @@ -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('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 +``` diff --git a/.agent/skills/elysiajs/integrations/vercel.md b/.agent/skills/elysiajs/integrations/vercel.md new file mode 100644 index 0000000..555ec8e --- /dev/null +++ b/.agent/skills/elysiajs/integrations/vercel.md @@ -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). diff --git a/.agent/skills/elysiajs/patterns/mvc.md b/.agent/skills/elysiajs/patterns/mvc.md new file mode 100644 index 0000000..dc227c6 --- /dev/null +++ b/.agent/skills/elysiajs/patterns/mvc.md @@ -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. diff --git a/.agent/skills/elysiajs/plugins/bearer.md b/.agent/skills/elysiajs/plugins/bearer.md new file mode 100644 index 0000000..df529e5 --- /dev/null +++ b/.agent/skills/elysiajs/plugins/bearer.md @@ -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 diff --git a/.agent/skills/elysiajs/plugins/cors.md b/.agent/skills/elysiajs/plugins/cors.md new file mode 100644 index 0000000..2d8db2a --- /dev/null +++ b/.agent/skills/elysiajs/plugins/cors.md @@ -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** - 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` diff --git a/.agent/skills/elysiajs/plugins/cron.md b/.agent/skills/elysiajs/plugins/cron.md new file mode 100644 index 0000000..3905ad5 --- /dev/null +++ b/.agent/skills/elysiajs/plugins/cron.md @@ -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 * * *` | diff --git a/.agent/skills/elysiajs/plugins/graphql-apollo.md b/.agent/skills/elysiajs/plugins/graphql-apollo.md new file mode 100644 index 0000000..655f258 --- /dev/null +++ b/.agent/skills/elysiajs/plugins/graphql-apollo.md @@ -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. diff --git a/.agent/skills/elysiajs/plugins/graphql-yoga.md b/.agent/skills/elysiajs/plugins/graphql-yoga.md new file mode 100644 index 0000000..3203d02 --- /dev/null +++ b/.agent/skills/elysiajs/plugins/graphql-yoga.md @@ -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 diff --git a/.agent/skills/elysiajs/plugins/html.md b/.agent/skills/elysiajs/plugins/html.md new file mode 100644 index 0000000..777a536 --- /dev/null +++ b/.agent/skills/elysiajs/plugins/html.md @@ -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', + () => ` + + + Hello World + + +

Hello World

+ + ` + ) + .get('/jsx', () => ( + + + Hello World + + +

Hello World

+ + + )) + .listen(3000) +``` + +This plugin will automatically add `Content-Type: text/html; charset=utf8` header to the response, add ``, 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('/', () => ( + + + Hello World + + +

Hello World

+ + + )) + .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 }) => ( + + + Hello World + + +

{body}

+ + + ), + { + 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 `` to a response starting with ``, if not found. + +Use `full` to also automatically add doctypes on responses returned without this plugin + +```ts +// without the plugin +app.get('/', () => '') + +// With the plugin +app.get('/', ({ 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. diff --git a/.agent/skills/elysiajs/plugins/jwt.md b/.agent/skills/elysiajs/plugins/jwt.md new file mode 100644 index 0000000..b5767bf --- /dev/null +++ b/.agent/skills/elysiajs/plugins/jwt.md @@ -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 +``` + +`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` 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. diff --git a/.agent/skills/elysiajs/plugins/openapi.md b/.agent/skills/elysiajs/plugins/openapi.md new file mode 100644 index 0000000..c69150d --- /dev/null +++ b/.agent/skills/elysiajs/plugins/openapi.md @@ -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 = { [K in keyof T]: T[K] } & {} + +function getUser(): Prettify { } +``` + +## 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 diff --git a/.agent/skills/elysiajs/plugins/opentelemetry.md b/.agent/skills/elysiajs/plugins/opentelemetry.md new file mode 100644 index 0000000..0ca95c3 --- /dev/null +++ b/.agent/skills/elysiajs/plugins/opentelemetry.md @@ -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. diff --git a/.agent/skills/elysiajs/plugins/server-timing.md b/.agent/skills/elysiajs/plugins/server-timing.md new file mode 100644 index 0000000..0021424 --- /dev/null +++ b/.agent/skills/elysiajs/plugins/server-timing.md @@ -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' + } + }) + ) +``` diff --git a/.agent/skills/elysiajs/plugins/static.md b/.agent/skills/elysiajs/plugins/static.md new file mode 100644 index 0000000..82fa1da --- /dev/null +++ b/.agent/skills/elysiajs/plugins/static.md @@ -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')) +``` diff --git a/.agent/skills/elysiajs/references/bun-fullstack-dev-server.md b/.agent/skills/elysiajs/references/bun-fullstack-dev-server.md new file mode 100644 index 0000000..70d721b --- /dev/null +++ b/.agent/skills/elysiajs/references/bun-fullstack-dev-server.md @@ -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 + + + + + + Elysia React App + + + +
+ + + +``` + +```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 ( +
+

{count}

+ +
+ ) +} + +const root = createRoot(document.getElementById('root')!) +root.render() +``` + +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 + +``` +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. diff --git a/.agent/skills/elysiajs/references/cookie.md b/.agent/skills/elysiajs/references/cookie.md new file mode 100644 index 0000000..9e1aa1c --- /dev/null +++ b/.agent/skills/elysiajs/references/cookie.md @@ -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`, 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. diff --git a/.agent/skills/elysiajs/references/deployment.md b/.agent/skills/elysiajs/references/deployment.md new file mode 100644 index 0000000..3c4cca8 --- /dev/null +++ b/.agent/skills/elysiajs/references/deployment.md @@ -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 +``` diff --git a/.agent/skills/elysiajs/references/eden.md b/.agent/skills/elysiajs/references/eden.md new file mode 100644 index 0000000..7d9165d --- /dev/null +++ b/.agent/skills/elysiajs/references/eden.md @@ -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('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 +type UserError = Treaty.Error +``` + +## 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` diff --git a/.agent/skills/elysiajs/references/lifecycle.md b/.agent/skills/elysiajs/references/lifecycle.md new file mode 100644 index 0000000..645584e --- /dev/null +++ b/.agent/skills/elysiajs/references/lifecycle.md @@ -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('/', () => '

Hello

', { + 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. diff --git a/.agent/skills/elysiajs/references/macro.md b/.agent/skills/elysiajs/references/macro.md new file mode 100644 index 0000000..f89ee75 --- /dev/null +++ b/.agent/skills/elysiajs/references/macro.md @@ -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) diff --git a/.agent/skills/elysiajs/references/plugin.md b/.agent/skills/elysiajs/references/plugin.md new file mode 100644 index 0000000..cd10e64 --- /dev/null +++ b/.agent/skills/elysiajs/references/plugin.md @@ -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) diff --git a/.agent/skills/elysiajs/references/route.md b/.agent/skills/elysiajs/references/route.md new file mode 100644 index 0000000..c767283 --- /dev/null +++ b/.agent/skills/elysiajs/references/route.md @@ -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 diff --git a/.agent/skills/elysiajs/references/testing.md b/.agent/skills/elysiajs/references/testing.md new file mode 100644 index 0000000..ffcdff3 --- /dev/null +++ b/.agent/skills/elysiajs/references/testing.md @@ -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) + }) +}) +``` diff --git a/.agent/skills/elysiajs/references/validation.md b/.agent/skills/elysiajs/references/validation.md new file mode 100644 index 0000000..ba723e0 --- /dev/null +++ b/.agent/skills/elysiajs/references/validation.md @@ -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 diff --git a/.agent/skills/elysiajs/references/websocket.md b/.agent/skills/elysiajs/references/websocket.md new file mode 100644 index 0000000..b2c86a8 --- /dev/null +++ b/.agent/skills/elysiajs/references/websocket.md @@ -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() + +.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>() + +.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('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) + } + }) +```