Skip to content

At a glance

Elysia is an ergonomic web framework for building backend servers. It’s designed for simplicity and type safety, with a familiar API, extensive TypeScript support, and optimizations for Bun.

Let’s start with a small server.

import { Elysia } from 'elysia'
new Elysia()
.get('/', 'Hello Elysia')
.get('/user/:id', ({ params: { id } }) => id)
.post('/form', ({ body }) => body)
.listen(3000)
Hello Elysia

Elysia uses Bun and optimizations such as static code analysis to generate code for handling requests efficiently.

The following results come from the Bun HTTP Framework Benchmark. Average is the arithmetic mean of requests per second. Startup is in milliseconds. Memory usage shows server RSS before and after the load tests in MB. See the benchmark conditions.

FrameworkRuntimeRequest/secStartupMemory usage
elysiabun214,518.6716.0 ms36.3 / 48.7 MB
h3bun172,978.6737.3 ms33.6 / 50.0 MB
honobun169,752.097.4 ms33.5 / 44.3 MB
effectbun123,353.12232.5 ms48.8 / 73.6 MB
fastifybun122,655.53876.8 ms74.9 / 193.8 MB
fastifynode103,292.99869.7 ms95.7 / 205.0 MB
h3node98,758.28845.5 ms85.3 / 190.1 MB
hononode93,107.47848.9 ms97.0 / 220.6 MB
elysianode92,489.33858.6 ms93.0 / 181.3 MB
effectnode88,635.6853.5 ms93.0 / 177.6 MB
expressbun63,217.94542.5 ms64.9 / 152.4 MB
koanode44,591.62551.7 ms96.1 / 192.2 MB
expressnode42,293.95344.2 ms87.2 / 208.8 MB
nestnode31,939.89116.5 ms118.7 / 224.3 MB

Elysia has also been compared with Go and Rust frameworks in TechEmpower round 22. Performance depends on the workload, runtime, hardware, and framework version. Measure your own workload before choosing a framework.

Elysia is designed to help you write less TypeScript. It infers types from your routes so you can focus on business logic instead of repeating declarations.

import { Elysia } from 'elysia'
new Elysia()
.get('/user/:id', ({ params: { id } }) => id)
.get('/user/abc', () => 'abc')
.listen(3000)
123

The :id segment becomes params.id both at runtime and in the handler’s types, without a manual declaration. Its type is string.

TypeScript is recommended, but it isn’t required to use Elysia. Type inference helps while you write code; use a schema when you also need runtime validation.

Import t from Elysia to define a schema that validates values at runtime and provides their TypeScript types. That gives your input a single source of truth.

Let’s change the path parameter from a string to a number:

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

The schema comes before the handler in Elysia 2. A request to /user/123 gives the handler the number 123; /user/abc fails validation. Hover over id in your editor to see the inferred number type.

The same schema describes the runtime check and the handler’s TypeScript input.

Elysia supports Standard Schema, so you can use compatible versions of your preferred validation library.

The ecosystem includes Zod, Valibot, ArkType, Effect Schema, Yup, Joi integrations, and other implementations. Check that the library or adapter you use implements the Standard Schema interface.

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)
.listen(3000)

Here Zod converts the path parameter to a number, while Valibot requires ?name=Lilith. Elysia infers the validated types from both schemas.

Elysia works with standards such as OpenAPI, WinterTC, and Standard Schema so you can connect it to tools you already use.

Add the OpenAPI plugin to generate API documentation from your routes and schemas:

import { Elysia, t } from 'elysia'
import { openapi } from '@elysia/openapi'
new Elysia()
.use(openapi())
.get('/user/:id', {
params: t.Object({
id: t.Number()
})
}, ({ params: { id } }) => id)
.listen(3000)

The plugin provides a documentation page you can share with your team. Use a release compatible with Elysia 2; see the plugin reference for installation and configuration.

Elysia also supports generating OpenAPI schemas from TypeScript types with fromTypes():

import { Elysia, t } from 'elysia'
import { openapi, fromTypes } from '@elysia/openapi'
export const app = new Elysia()
.use(openapi({
references: fromTypes()
}))
.get('/user/:id', {
params: t.Object({
id: t.Number()
})
}, ({ params: { id } }) => id)
.listen(3000)

This brings a FastAPI-like approach to TypeScript: use your code’s types as input to documentation generation instead of duplicating them in manual annotations. See OpenAPI from types for its setup and supported types.

Type safety doesn’t stop at the server. Elysia’s client library, Eden, lets your frontend use the server’s types, in a workflow similar to tRPC.

Export the type of your server from server.ts, then import it in client.ts:

import { Elysia, t } from 'elysia'
import { openapi, fromTypes } from '@elysia/openapi'
export const app = new Elysia()
.use(openapi({
references: fromTypes()
}))
.get('/user/:id', {
params: t.Object({
id: t.Number()
})
}, ({ params: { id } }) => id)
.listen(3000)
export type App = typeof app

Eden uses the existing Elysia types without a separate client code-generation step. The client imports only the server’s type, keeping server implementation code out of its bundle.

Elysia is here to help you build a confident backend, and everything you connect to it.

Successful responses are only part of an API. Elysia can infer all possible response types, including errors, so your client can handle them safely.

server.ts
import { Elysia, t } from 'elysia'
const plugin = new Elysia()
.macro({
auth: {
cookie: t.Cookie({
session: t.String()
}),
beforeHandle({ cookie: { session }, status }) {
if (session.value !== 'valid')
return status(401)
}
}
})
const app = new Elysia()
.use(plugin)
.get('/user/:id', {
auth: true,
params: t.Object({
id: t.Number()
})
}, ({ params: { id }, status }) => {
if (Math.random() > 0.1)
return status(420)
return id
})
.listen(3000)
export type App = typeof app

Here, Elysia can return a number for a successful request but also infer 401 or 420 error in type-level so client can handle all three cases with type safety.

client.ts
import { treaty } from '@elysia/eden'
import type { App } from './server'
const app = treaty<App>('localhost:3000')
const { data, error } = await app.user({ id: 617 }).get()
if (error)
console.log(error.status, error.value)
else
console.log(data)

The client can handle the typed success and error results. See Eden for the client API and macros for reusable route behavior.

Elysia is optimized for Bun’s native features, but it isn’t limited to Bun. Its WinterTC-compatible Request/Response interface makes other runtime integrations possible.

You can use Elysia with:

Each integration describes its adapter and deployment setup. The integrations section has more supported environments.

We want Elysia to feel friendly and welcoming, with a bright, playful design and our hand-drawn anime mascot, Elysia chan.

Technology can be approachable and bring a little joy to people’s lives. We still take the framework seriously: reliability and performance matter when you’re trusting it with your next project.

Community members have shared production-use stories involving X, Bank for Agriculture and Agricultural Co-operatives, Cluely, CS.Money, and Abacate Pay.

At one point, more than 10,000 open-source projects used Elysia. See GitHub dependents for the changing project list. Development began in 2022 and has continued with contributions from the community.

Here are a few places to join in:


1. Average throughput, startup time, and server memory usage. See the benchmark conditions and results at commit f654bcd.

2. The cross-language comparison refers to TechEmpower benchmark round 22.

Type to search the documentation.

    Up arrowDown arrow to navigateEnter to selectEscape to close