10 KiB
Validation Schema - SKILLS.md
What It Is
Runtime validation + type inference + OpenAPI schema from single source. TypeBox-based with Standard Schema support.
Basic Usage
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:
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
body: t.Object({ name: t.String() })
GET/HEAD: body-parser disabled by default (RFC2616).
File Upload
body: t.Object({
file: t.File({ format: 'image/*' }),
multipleFiles: t.Files()
})
// Auto-assumes multipart/form-data
File (Standard Schema)
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
query: t.Object({ name: t.String() })
// /?name=Elysia
Auto-coerces to specified type.
Arrays
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
params: t.Object({ id: t.Number() })
// /id/1
Auto-inferred as string if schema not provided.
Headers
headers: t.Object({ authorization: t.String() })
additionalProperties: true by default. Always lowercase keys.
Cookie
cookie: t.Cookie({
name: t.String()
}, {
secure: true,
httpOnly: true
})
Or use t.Object. additionalProperties: true by default.
Response
response: t.Object({ name: t.String() })
Per Status
response: {
200: t.Object({ name: t.String() }),
400: t.Object({ error: t.String() })
}
Error Handling
Inline Error Property
body: t.Object({
x: t.Number({ error: 'x must be number' })
})
Or function:
x: t.Number({
error({ errors, type, validation, value }) {
return 'Expected x to be number'
}
})
onError Hook
.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:
.model({
sign: t.Object({
username: t.String(),
password: t.String()
})
})
.post('/sign-in', ({ body }) => body, {
body: 'sign',
response: 'sign'
})
Extract to plugin:
// 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:
.model({
'auth.admin': t.Object({...}),
'auth.user': t.Object({...})
})
Or use prefix / suffix to rename models in current instance
.model({ sign: t.Object({...}) })
.prefix('model', 'auth')
.post('/', () => '', {
body: 'auth.User'
})
Models with prefix will be capitalized.
TypeScript Types
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
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
- override (default) Latest schema overrides collided schema.
.guard({ query: t.Object({ name: t.String() }) })
.guard({ query: t.Object({ id: t.Number() }) })
// Only id required, name overridden
- standalone Both schemas run independently. Both validated.
.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)
// 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)
t.Union([t.String(), t.Number()])
// type: string | number
// values: "Hello" or 123
Optional (Field Optional)
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)
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)
t.UnionEnum(['rapi', 'anis', 1, true, false])
File (Single File Upload)
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)
t.Files() // extends File + array
Cookie (Cookie Jar)
t.Cookie({
name: t.String()
}, {
secrets: 'secret-key' // or ['key1', 'key2'] for rotation
})
Nullable (Allow null)
t.Nullable(t.String())
// type: string | null
MaybeEmpty (Allow null + undefined)
t.MaybeEmpty(t.String())
// type: string | null | undefined
Form (FormData Validation)
t.Form({
someValue: t.File()
})
// Syntax sugar for t.Object with FormData support
UInt8Array (Buffer → Uint8Array)
t.UInt8Array()
// For binary file uploads with arrayBuffer parser
ArrayBuffer (Buffer → ArrayBuffer)
t.ArrayBuffer()
// For binary file uploads with arrayBuffer parser
ObjectString (String → Object)
t.ObjectString()
// Accepts: '{"x":1}' → parses to { x: 1 }
// Use in: query string, headers, FormData
BooleanString (String → Boolean)
t.BooleanString()
// Accepts: 'true'/'false' → parses to boolean
// Use in: query string, headers, FormData
Numeric (String/Number → Number)
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):
.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):
.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:
.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
import { Elysia, t } from 'elysia'
new Elysia()
.post('/', ({ body }) => `Hello ${body}`, {
body: t.String() // validates body is string
})
.listen(3000)
Validation flow:
- Request arrives
- Schema validates against HTTP body/params/query/headers
- If valid → handler executes
- 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.Optionalfor 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