Files
agent/.agent/skills/tech-stack/elysiajs/references/validation.md

492 lines
10 KiB
Markdown

# 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