Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/elysiajs/documentation/llms.txt

Use this file to discover all available pages before exploring further.

Elysia validates incoming data and outgoing responses against declarative schemas. A single schema acts as the single source of truth for runtime validation, TypeScript types, data coercion, and OpenAPI documentation — with no duplication.
import { Elysia, t } from 'elysia'

new Elysia()
    .get('/id/:id', ({ params: { id } }) => id, {
        params: t.Object({
            id: t.Number()
        })
    })
    .listen(3000)

Schema builders

Elysia.t (TypeBox)

t is Elysia’s built-in schema builder, based on TypeBox. It generates JSON Schema-compatible definitions that work at runtime, at compile time, and in OpenAPI output.
import { t } from 'elysia'

const MyType = t.Object({
    hello: t.Literal('Elysia')
})

type MyType = typeof MyType.static
// { hello: 'Elysia' }
A single TypeBox schema covers:
  • Runtime validation
  • Data coercion
  • TypeScript types
  • OpenAPI schema generation

Standard Schema support

Elysia supports Standard Schema, so you can use any compatible library alongside or instead of t. Supported libraries include Zod, Valibot, ArkType, Effect Schema, Yup, Joi, and more.
import { Elysia } from 'elysia'
import { z } from 'zod'
import * as v from 'valibot'

new Elysia()
    .get('/id/:id', ({ params: { id }, query: { name } }) => id, {
        params: z.object({
            id: z.coerce.number()
        }),
        query: v.object({
            name: v.literal('Lilith')
        })
    })
    .listen(3000)
You can mix validators from different libraries in the same handler without any issues.

Schema types

Pass a schema object as the third argument to any route handler to validate the corresponding part of the request or response.
import { Elysia, t } from 'elysia'

new Elysia()
    .get('/id/:id', () => 'Hello World!', {
        query: t.Object({
            name: t.String()
        }),
        params: t.Object({
            id: t.Number()
        })
    })
    .listen(3000)

body

Incoming HTTP message body

query

URL query string parameters

params

Path parameters

headers

Request headers

cookie

Request cookies

response

Handler response shape

body

Validates the HTTP message body. Applicable to POST, PUT, and PATCH requests.
import { Elysia, t } from 'elysia'

new Elysia()
    .post('/body', ({ body }) => body, {
        body: t.Object({
            name: t.String()
        })
    })
    .listen(3000)
BodyResult
{ name: 'Elysia' }
{ name: 1 }
{ alias: 'Elysia' }
undefined
Elysia disables body parsing for GET and HEAD requests per HTTP/1.1 RFC 2616.

File uploads

Use t.File for single file uploads and t.Files for multiple. Elysia automatically sets the content-type to multipart/form-data.
import { Elysia, t } from 'elysia'

new Elysia()
    .post('/upload', ({ body }) => body, {
        body: t.Object({
            file: t.File({ format: 'image/*' }),
            multipleFiles: t.Files()
        })
    })
    .listen(3000)
When using Standard Schema for file uploads, use the exported fileType utility to validate file contents by magic number — most validators only check the MIME type string, which can be spoofed.
import { Elysia, fileType } from 'elysia'
import { z } from 'zod'

new Elysia()
    .post('/upload', ({ body }) => body, {
        body: z.object({
            file: z.file().refine((f) => fileType(f, 'image/jpeg'))
        })
    })

query

Validates URL query parameters. All query values arrive as strings; Elysia coerces them to the declared type.
import { Elysia, t } from 'elysia'

new Elysia()
    .get('/search', ({ query }) => query, {
        query: t.Object({
            name: t.String()
        })
    })
    .listen(3000)
To accept an array of values, declare the property as t.Array:
import { Elysia, t } from 'elysia'

new Elysia()
    .get('/', ({ query }) => query, {
        query: t.Object({
            name: t.Array(t.String())
        })
    })
    .listen(3000)
Elysia supports two array formats:
  • nuqs format — comma-delimited: ?name=rapi,anis,neon
  • HTML form format — repeated key: ?name=rapi&name=anis&name=neon

params

Validates path parameters. You usually only need this when you require a specific type (e.g. numeric ID) or a template literal pattern.
import { Elysia, t } from 'elysia'

new Elysia()
    .get('/id/:id', ({ params }) => params, {
        params: t.Object({
            id: t.Number()
        })
    })
When no params schema is provided, Elysia infers all params as string.

headers

Validates request headers. By default, additionalProperties is true so extra headers are allowed.
import { Elysia, t } from 'elysia'

new Elysia()
    .get('/protected', ({ headers }) => headers, {
        headers: t.Object({
            authorization: t.String()
        })
    })
Elysia parses headers as lowercase keys only. Always use lowercase field names in your header schema (e.g. authorization, not Authorization).
Validates request cookies. Use t.Cookie for cookie-specific options like secure and httpOnly.
import { Elysia, t } from 'elysia'

new Elysia()
    .get('/me', ({ cookie }) => cookie.name.value, {
        cookie: t.Cookie({
            name: t.String()
        }, {
            secure: true,
            httpOnly: true
        })
    })

response

Validates the value returned by the handler. Use a plain schema to cover all status codes, or a status-keyed object for per-status validation.
import { Elysia, t } from 'elysia'

new Elysia()
    .get('/response', () => ({ name: 'Jane Doe' }), {
        response: t.Object({
            name: t.String()
        })
    })

Guard

Apply a schema to multiple handlers at once using .guard(). Any route registered after the guard inherits its constraints.
import { Elysia, t } from 'elysia'

new Elysia()
    .get('/none', () => 'hi')       // no validation

    .guard({
        query: t.Object({
            name: t.String()
        })
    })
    .get('/query', ({ query }) => query) // requires ?name=...
    .listen(3000)
Guards support two schema composition modes, set with the schema property:
ModeBehaviour
override (default)Local schema replaces the guard schema when they conflict
standaloneBoth schemas are validated independently
import { Elysia, t } from 'elysia'

new Elysia()
    .guard({
        schema: 'standalone',
        response: t.Object({ title: t.String() })
    })

Custom error messages

Inline error property

Pass an error property to any t.* type to override the default error message.
import { Elysia, t } from 'elysia'

new Elysia()
    .post('/', () => 'Hello World!', {
        body: t.Object({
            x: t.Number({ error: 'x must be a number' })
        })
    })
    .listen(3000)
The error value can also be a function for dynamic messages:
import { Elysia, t } from 'elysia'

new Elysia()
    .post('/', () => 'Hello World!', {
        body: t.Object({
            x: t.Number({
                error() {
                    return 'Expected x to be a number'
                }
            })
        })
    })
    .listen(3000)

onError handler

Handle all validation failures globally by listening for the VALIDATION error code.
import { Elysia, t } from 'elysia'

new Elysia()
    .onError(({ code, error }) => {
        if (code === 'VALIDATION')
            return error.message
    })
    .listen(3000)
ValidationError exposes error.all — an array of every validation failure — so you can inspect or log individual field errors.
import { Elysia, t } from 'elysia'

new Elysia()
    .post('/', ({ body }) => body, {
        body: t.Object({
            name: t.String(),
            age: t.Number()
        }),
        error({ code, error }) {
            if (code === 'VALIDATION') {
                const name = error.all.find(
                    (x) => x.summary && x.path === '/name'
                )
                if (name) console.log(name)
            }
        }
    })
    .listen(3000)

Reference models

Register a named model with .model() and reference it by name in any route. This avoids repeating the same schema in multiple places and keeps OpenAPI output consistent.
import { Elysia, t } from 'elysia'

const app = new Elysia()
    .model({
        sign: t.Object({
            username: t.String(),
            password: t.String()
        })
    })
    .post('/sign-in', ({ body }) => body, {
        body: 'sign',
        response: 'sign'
    })
Models compose naturally with plugins, letting you co-locate related schemas and import them as a unit.
// auth.model.ts
import { Elysia, t } from 'elysia'

export const authModel = new Elysia()
    .model({
        sign: t.Object({
            username: t.String(),
            password: t.String()
        })
    })
// index.ts
import { Elysia } from 'elysia'
import { authModel } from './auth.model'

const app = new Elysia()
    .use(authModel)
    .post('/sign-in', ({ body }) => body, {
        body: 'sign',
        response: 'sign'
    })

Build docs developers (and LLMs) love