Skip to content

Validation

Elysia supports declarative schema validation.

Describe what your data should look like and Elysia will handle the rest.

Without a schema, /id/:id captures a string.

  • /id/42 gives the handler "42"
  • /id/hello gives it "hello"
import { Elysia, t } from 'elysia'
new Elysia()
.get('/id/:id', ({ params: { id } }) => id)
.listen(3000)

By adding t.Number() to the params schema, Elysia will convert a numeric path value and validate it.

  • /id/42 gives the handler the number 42
  • /id/hello fails validation with status 422
import { Elysia, t } from 'elysia'
new Elysia()
.get('/id/:id', {
params: t.Object({
id: t.Number()
})
}, ({ params: { id } }) => id)
.listen(3000)

Add query to require a name query parameter too.

  • /id/42?name=Elysia returns { id: 42, name: 'Elysia' }.
  • /id/42 fails validation because name is 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)

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)

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.

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.

Choose the property that matches the data you want to check:

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)
URLQuery schemaParams schema
/id/aFailsFails
/id/1?name=ElysiaPassesPasses
/id/1?alias=ElysiaFailsPasses
/id/a?name=ElysiaPassesFails
/id/a?alias=ElysiaFailsFails

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 bodyValidation
{ "name": "Elysia" }Passes
{ "name": 1 }Fails
{ "alias": "Elysia" }Fails
No bodyFails

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)

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"
}
RequestValidation
/query?name=ElysiaPasses
/query?name=1Passes; name is a string
/query?alias=ElysiaFails
/query?name=ElysiaJS&alias=ElysiaPasses
/queryFails

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
RequestResult
/query?id=1Number 1
/query?id=saltValidation 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.

Use commas to separate values, as in the nuqs array format:

/query?name=rapi,anis,neon&squad=counter

The array schema tells Elysia to split name. Without one, a comma-separated value remains a string.

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=counter

Declare 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.

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
}
RequestResult
/id/1{ "id": 1 }
/id/aValidation error, status 422

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)

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.

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.

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.

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
RequestResult
/nonehi
/none?name=ahi
/queryValidation error, status 422
/query?name=aa

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.

Guards support two ways to handle overlapping schemas.

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.

Type to search the documentation.

    Up arrowDown arrow to navigateEnter to selectEscape to close