Validation
Elysia supports declarative schema validation.
Describe what your data should look like and Elysia will handle the rest.
Start with a parameter
Section titled “Start with a parameter”Without a schema, /id/:id captures a string.
/id/42gives the handler"42"/id/hellogives it"hello"
import { Elysia, t } from 'elysia'
new Elysia() .get('/id/:id', ({ params: { id } }) => id) .listen(3000)Add a schema
Section titled “Add a schema”By adding t.Number() to the params schema, Elysia will convert a numeric path value and validate it.
/id/42gives the handler the number42/id/hellofails validation with status422
import { Elysia, t } from 'elysia'
new Elysia() .get('/id/:id', { params: t.Object({ id: t.Number() }) }, ({ params: { id } }) => id) .listen(3000)Validate another field
Section titled “Validate another field”Add query to require a name query parameter too.
/id/42?name=Elysiareturns{ id: 42, name: 'Elysia' }./id/42fails validation becausenameis missing.
import { Elysia, t } from 'elysia'
new Elysia() .get('/id/:id', { params: t.Object({ id: t.Number() }), query: t.Object({ name: t.String() }) }, ({ params: { id }, query: { name } }) => ({ id, name })) .listen(3000)Check the response
Section titled “Check the response”Add response to enforce the shape of the data returned by the handler.
Elysia would also clean up any extra fields in the response, if the handler returned them.
import { Elysia, t } from 'elysia'
new Elysia() .get('/id/:id', { params: t.Object({ id: t.Number() }), query: t.Object({ name: t.String() }), response: t.Object({ id: t.Number(), name: t.String() }) }, ({ params: { id }, query: { name } }) => ({ id, name })) .listen(3000)TypeBox
Section titled “TypeBox”Elysia provides t, a named export from elysia, as a default schema builder. It’s a wrapper around TypeBox with extra features for API validation.
These schemas support runtime checks, TypeScript inference, and schema generation for OpenAPI. You don’t need to write a separate interface for the same data.
Standard Schema
Section titled “Standard Schema”You can also use a library that implements Standard Schema, including compatible versions of Zod, Valibot, ArkType, Effect Schema, Yup, and Joi and use them interchangeably.
import { Elysia } from 'elysia'import { z } from 'zod'import * as v from 'valibot'
new Elysia() .get('/id/:id', { params: z.object({ id: z.coerce.number() }), query: v.object({ name: v.literal('Lilith') }) }, ({ params: { id }, query: { name } }) => ({ id, name })) .listen(3000)/id/42?name=Lilith passes both schemas. Use your chosen library’s coercion API when you want to turn a URL string into another type.
Schema type
Section titled “Schema type”Choose the property that matches the data you want to check:
Body schema
Body checks the parsed HTTP request body, such as JSON or form fields.
Query schema
Query checks the query string after ? in the URL.
Params schema
Params checks values captured from the route’s path.
Headers schema
Headers checks incoming request headers.
Cookie schema
Cookie checks incoming cookie values.
Response schema
Response checks data returned by the handler.
Provide these properties in the route’s second argument, before its handler:
import { Elysia, t } from 'elysia'
new Elysia() .get('/id/:id', { query: t.Object({ name: t.String() }), params: t.Object({ id: t.Number() }) }, () => 'Hello World!') .listen(3000)| URL | Query schema | Params schema |
|---|---|---|
/id/a | Fails | Fails |
/id/1?name=Elysia | Passes | Passes |
/id/1?alias=Elysia | Fails | Passes |
/id/a?name=Elysia | Passes | Fails |
/id/a?alias=Elysia | Fails | Fails |
Both schemas must pass for the handler to run. The table describes each check independently; the error response may report the first failing part.
An HTTP Message body. It can be JSON, form fields, or a file.
import { Elysia, t } from 'elysia'
new Elysia() .post('/body', { body: t.Object({ name: t.String() }) }, ({ body }) => body) .listen(3000)HTTP 200
{
"name": "Elysia"
}| JSON body | Validation |
|---|---|
{ "name": "Elysia" } | Passes |
{ "name": 1 } | Fails |
{ "alias": "Elysia" } | Fails |
| No body | Fails |
Use t.File() for a single file and t.Files() for multiple files.
A type check needs a file type detector. Install file-type and register it once with setFileTypeDetector; without a detector, every upload checked against type fails validation.
import { Elysia, t, setFileTypeDetector } from 'elysia'import { fileTypeFromBlob } from 'file-type'
setFileTypeDetector(fileTypeFromBlob)
new Elysia() .post('/body', { body: t.Object({ file: t.File({ type: 'image/*' }), multipleFiles: t.Files() }) }, ({ body }) => ({ file: body.file.name, count: body.multipleFiles.length })) .listen(3000)File (Standard Schema)
Section titled “File (Standard Schema)”Standard Schema libraries don’t automatically get Elysia’s file checks. Add a refinement using the asynchronous fileType() helper and explicitly select multipart parsing:
import { Elysia, fileType, setFileTypeDetector } from 'elysia'import { fileTypeFromBlob } from 'file-type'import { z } from 'zod'
setFileTypeDetector(fileTypeFromBlob)
new Elysia() .post('/body', { parse: 'formdata', body: z.object({ file: z.file().refine( (file) => fileType(file, 'image/jpeg'), 'Expected a JPEG file' ) }) }, ({ body }) => ({ name: body.file.name }))The registered detector supplies the byte check here too. A validator that only reads file.type doesn’t establish what the uploaded file actually contains.
Query parameters follow ? in a URL. Describe them with an object schema:
import { Elysia, t } from 'elysia'
new Elysia() .get('/query', { query: t.Object({ name: t.String() }) }, ({ query }) => query) .listen(3000)HTTP 200
{
"name": "Elysia"
}| Request | Validation |
|---|---|
/query?name=Elysia | Passes |
/query?name=1 | Passes; name is a string |
/query?alias=Elysia | Fails |
/query?name=ElysiaJS&alias=Elysia | Passes |
/query | Fails |
Coercion
Section titled “Coercion”Elysia converts applicable TypeBox schemas on query automatically. For example, t.Number() accepts a numeric string and gives the handler a number:
import { Elysia, t } from 'elysia'
new Elysia() .get('/query', { query: t.Object({ id: t.Number() }) }, ({ query: { id } }) => id) .listen(3000)HTTP 200 1
| Request | Result |
|---|---|
/query?id=1 | Number 1 |
/query?id=salt | Validation error, status 422 |
See Elysia’s type behavior for other automatic conversions.
Declare an array schema to validate each value and support comma-separated input:
import { Elysia, t } from 'elysia'
new Elysia() .get('/query', { query: t.Object({ name: t.Array(t.String()), squad: t.String() }) }, ({ query }) => query) .listen(3000)Both requests below produce { name: ['rapi', 'anis', 'neon'], squad: 'counter' } with this schema.
nuqs format
Section titled “nuqs format”Use commas to separate values, as in the nuqs array format:
/query?name=rapi,anis,neon&squad=counterThe array schema tells Elysia to split name. Without one, a comma-separated value remains a string.
HTML form format
Section titled “HTML form format”Repeat a key to send several values, as an HTML form does for inputs with the same name:
/query?name=rapi&name=anis&name=neon&squad=counterDeclare the array schema to retain and validate every repeated value. Without it, an ordinary query property is a single string; a repeated key keeps its last value.
Params
Section titled “Params”Path parameters capture values from a route such as /id/:id. Use an object schema to validate the captured fields:
import { Elysia, t } from 'elysia'
new Elysia() .get('/id/:id', { params: t.Object({ id: t.Number() }) }, ({ params }) => params)HTTP 200
{
"id": 1
}| Request | Result |
|---|---|
/id/1 | { "id": 1 } |
/id/a | Validation error, status 422 |
Headers
Section titled “Headers”Use headers to require particular incoming headers:
import { Elysia, t } from 'elysia'
new Elysia() .get('/headers', { headers: t.Object({ authorization: t.String() }) }, ({ headers }) => headers.authorization)HTTP 200 Bearer 12345
Header schemas allow additional properties by default. Your client can still send headers such as accept; the fields you’ve declared must satisfy their schemas.
If you want to reject any extra headers, set additionalProperties: false:
import { Elysia, t } from 'elysia'
new Elysia() .get('/headers', { headers: t.Object( { authorization: t.String() }, { additionalProperties: false } ) }, ({ headers }) => headers.authorization)Response
Section titled “Response”Use response to check the data returned by your handler:
import { Elysia, t } from 'elysia'
new Elysia() .get('/response', { response: t.Object({ name: t.String() }) }, () => ({ name: 'Jane Doe (ZZZ)' }))TypeScript checks the handler’s return type, and Elysia validates the response at runtime.
Response per status
Section titled “Response per status”Map status codes to schemas when different outcomes have different response shapes:
import { Elysia, t } from 'elysia'
new Elysia() .get('/response', { response: { 200: t.Object({ name: t.String() }), 400: t.Object({ error: t.String() }) } }, ({ query, status }) => { if (query.fail === 'true') return status(400, { error: 'Something went wrong' })
return { name: 'Jane Doe (ZZZ)' } })HTTP 200
{
"name": "Jane Doe"
}/response returns the 200 shape. /response?fail=true returns the 400 shape. Use status() to preserve the relationship between the status code and its body type.
Validation errors
Section titled “Validation errors”Customize a schema’s validation message, handle failures with an error hook, or inspect the error list.
For named schemas shared across routes and OpenAPI documentation, see reference models.
TypeScript
Section titled “TypeScript”Use UnwrapSchema to get a schema’s inferred TypeScript type:
import { t, type UnwrapSchema } from 'elysia'
const MyType = t.Object({ hello: t.Literal('Elysia')})
type MyType = UnwrapSchema<typeof MyType>A schema can supply runtime validation, coercion, TypeScript types, and OpenAPI metadata. Keep the definition in one place, and let each use follow it.
Use .guard() to apply a schema to several routes. It affects routes registered after it:
import { Elysia, t } from 'elysia'
new Elysia() .get('/none', () => 'hi') .guard({ query: t.Object({ name: t.String() }) }) .get('/query', ({ query: { name } }) => name) .listen(3000)HTTP 200 hi
| Request | Result |
|---|---|
/none | hi |
/none?name=a | hi |
/query | Validation error, status 422 |
/query?name=a | a |
For the same schema property, a later guard takes precedence over an earlier one. A route’s own schema takes precedence over an inherited guard schema.
Guard schema type
Section titled “Guard schema type”Guards support two ways to handle overlapping schemas.
Override (default)
Section titled “Override (default)”The nearer schema replaces the inherited schema for that property. For example, a route’s body replaces a guard’s body; it doesn’t merge their fields.
import { Elysia, t } from 'elysia'
new Elysia() .guard({ body: t.Object({ name: t.String() }) }) .post('/age', { body: t.Object({ age: t.Number() }) }, ({ body }) => body)HTTP 200
{
"age": 18
}{ "age": 18 } satisfies /age without a name field.
Set schema: 'merge' when both the guard and route schemas must be satisfied:
import { Elysia, t } from 'elysia'
new Elysia() .guard({ schema: 'merge', response: t.Object({ title: t.String() }) }) .get('/article', { response: t.Object({ title: t.String(), author: t.String() }) }, () => ({ title: 'Hello Elysia', author: 'Nagisa' }))The response must satisfy the guard and the route schema. Merge accumulates their constraints instead of replacing the inherited schema.