476 lines
13 KiB
Markdown
476 lines
13 KiB
Markdown
---
|
|
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
|