Skip to content
Our Sponsors

Handler

Handler - a function that accept an HTTP request, and return a response.

typescript
import { Elysia } from 'elysia'

new Elysia()
    // the function `() => 'hello world'` is a handler
    .get('/', () => 'hello world')
    .listen(3000)

A handler may be a literal value, and can be inlined.

typescript
import { Elysia, file } from 'elysia'

new Elysia()
    .get('/', 'Hello Elysia')
    .get('/video', file('kyuukurarin.mp4'))
    .listen(3000)

Using an inline value always returns the same value which is useful to optimize performance for static resources like files.

This allows Elysia to compile the response ahead of time to optimize performance.

TIP

Providing an inline value is not a cache.

Static resource values, headers and status can be mutated dynamically using lifecycle.

Context

Context contains request information which is unique for each request, and is not shared except for store (global mutable state).

typescript
import { 
Elysia
} from 'elysia'
new
Elysia
()
.
get
('/', (
context
) =>
context
.
path
)
// ^ This is a context

Context can only be retrieved in a route handler. It consists of:

Property

  • body - HTTP message, form or file upload.
  • query - Query String, include additional parameters for search query as JavaScript Object. (Query is extracted from a value after pathname starting from '?' question mark sign)
  • params - Elysia's path parameters parsed as JavaScript object
  • headers - HTTP Header, additional information about the request like User-Agent, Content-Type, Cache Hint.
  • cookie - A global mutable signal store for interacting with Cookie (including get/set)
  • store - A global mutable store for Elysia instance

Utility Function

  • redirect - A function to redirect a response
  • status - A function to return custom status code
  • set - Property to apply to Response:

Additional Property

status

A function to return a custom status code with type narrowing.

typescript
import { Elysia } from 'elysia'

new Elysia()
    .get('/', ({ status }) => status(418, "Kirifuji Nagisa"))
    .listen(3000)
localhost

GET

It's recommended use never-throw approach to return status instead of throw as it:

  • allows TypeScript to check if a return value is correctly type to response schema
  • autocompletion for type narrowing based on status code
  • type narrowing for error handling using End-to-end type safety (Eden)

Set

set is a mutable property that form a response accessible via Context.set.

ts
import { 
Elysia
} from 'elysia'
new
Elysia
()
.
get
('/', ({
set
,
status
}) => {
set
.
headers
= { 'X-Teapot': 'true' }
return
status
(418, 'I am a teapot')
}) .
listen
(3000)

set.headers

Allowing us to append or delete response headers represented as an Object.

typescript
import { 
Elysia
} from 'elysia'
new
Elysia
()
.
get
('/', ({
set
}) => {
set
.
headers
['x-powered-by'] = 'Elysia'
return 'a mimir' }) .
listen
(3000)

TIP

Elysia provide an auto-completion for lowercase for case-sensitivity consistency, eg. use set-cookie rather than Set-Cookie.

redirect Legacy

Redirect a request to another resource.

typescript
import { 
Elysia
} from 'elysia'
new
Elysia
()
.
get
('/', ({
redirect
}) => {
return
redirect
('https://youtu.be/whpVWVWBW4U?&t=8')
}) .
get
('/custom-status', ({
redirect
}) => {
// You can also set custom status to redirect return
redirect
('https://youtu.be/whpVWVWBW4U?&t=8', 302)
}) .
listen
(3000)

When using redirect, returned value is not required and will be ignored. As response will be from another resource.

set.status Legacy

Set a default status code if not provided.

It's recommended to use this in a plugin that only needs to return a specific status code while allowing the user to return a custom value. For example, HTTP 201/206 or 403/405, etc.

typescript
import { 
Elysia
} from 'elysia'
new
Elysia
()
.
onBeforeHandle
(({
set
}) => {
set
.
status
= 418
return 'Kirifuji Nagisa' }) .
get
('/', () => 'hi')
.
listen
(3000)

Unlike status function, set.status cannot infer the return value type, therefore it can't check if the return value is correctly type to response schema.

TIP

HTTP Status indicates the type of response. If the route handler is executed successfully without error, Elysia will return the status code 200.

You can also set a status code using the common name of the status code instead of using a number.

typescript
// @errors 2322
import { 
Elysia
} from 'elysia'
new
Elysia
()
.
get
('/', ({
set
}) => {
set
.
status
return 'Kirifuji Nagisa' }) .
listen
(3000)

Elysia provides a mutable signal for interacting with Cookie.

There's no get/set, you can extract the cookie name and retrieve or update its value directly.

typescript
import { 
Elysia
} from 'elysia'
new
Elysia
()
.
get
('/set', ({
cookie
: {
name
} }) => {
// Get
name
.
value
// Set
name
.
value
= "New Value"
})

See Patterns: Cookie for more information.

Redirect

Redirect a request to another resource.

typescript
import { 
Elysia
} from 'elysia'
new
Elysia
()
.
get
('/', ({
redirect
}) => {
return
redirect
('https://youtu.be/whpVWVWBW4U?&t=8')
}) .
get
('/custom-status', ({
redirect
}) => {
// You can also set custom status to redirect return
redirect
('https://youtu.be/whpVWVWBW4U?&t=8', 302)
}) .
listen
(3000)

When using redirect, returned value is not required and will be ignored. As response will be from another resource.

Formdata

We may return a FormData by using returning form utility directly from the handler.

typescript
import { Elysia, form, file } from 'elysia'

new Elysia()
	.get('/', () => form({
		name: 'Tea Party',
		images: [file('nagi.web'), file('mika.webp')]
	}))
	.listen(3000)

This pattern is useful if even need to return a file or multipart form data.

Return a file

Or alternatively, you can return a single file by returning file directly without form.

typescript
import { Elysia, file } from 'elysia'

new Elysia()
	.get('/', file('nagi.web'))
	.listen(3000)

Stream

To return a response streaming out of the box by using a generator function with yield keyword.

typescript
import { Elysia } from 'elysia'

const app = new Elysia()
	.get('/ok', function* () {
		yield 1
		yield 2
		yield 3
	})

This this example, we may stream a response by using yield keyword.

Server Sent Events (SSE)

Elysia supports Server Sent Events by providing a sse utility function.

typescript
import { 
Elysia
,
sse
} from 'elysia'
new
Elysia
()
.
get
('/sse', function* () {
yield
sse
('hello world')
yield
sse
({
event
: 'message',
data
: {
message
: 'This is a message',
timestamp
: new
Date
().
toISOString
()
}, }) })

When a value is wrapped in sse, Elysia will automatically set the response headers to text/event-stream and format the data as an SSE event.

Headers in Server-Sent Event

Headers can only be set before the first chunk is yielded.

typescript
import { 
Elysia
} from 'elysia'
const
app
= new
Elysia
()
.
get
('/ok', function* ({
set
}) {
// This will set headers
set
.
headers
['x-name'] = 'Elysia'
yield 1 yield 2 // This will do nothing
set
.
headers
['x-id'] = '1'
yield 3 })

Once the first chunk is yielded, Elysia will send the headers to the client, therefore mutating headers after the first chunk is yielded will do nothing.

Conditional Stream

If the response is returned without yield, Elysia will automatically convert stream to normal response instead.

typescript
import { Elysia } from 'elysia'

const app = new Elysia()
	.get('/ok', function* () {
		if (Math.random() > 0.5) return 'ok'

		yield 1
		yield 2
		yield 3
	})

This allows us to conditionally stream a response or return a normal response if necessary.

Automatic cancellation

Before response streaming is completed, if the user cancels the request, Elysia will automatically stop the generator function.

Eden

Eden will interpret a stream response as AsyncGenerator allowing us to use for await loop to consume the stream.

typescript
import { 
Elysia
} from 'elysia'
import {
treaty
} from '@elysiajs/eden'
const
app
= new
Elysia
()
.
get
('/ok', function* () {
yield 1 yield 2 yield 3 }) const {
data
,
error
} = await
treaty
(
app
).
ok
.
get
()
if (
error
) throw
error
for await (const
chunk
of
data
)
console
.
log
(
chunk
)

Request

Elysia is built on top of Web Standard Request which is shared between multiple runtime like Node, Bun, Deno, Cloudflare Worker, Vercel Edge Function, and more.

typescript
import { Elysia } from 'elysia'

new Elysia()
	.get('/user-agent', ({ request }) => {
		return request.headers.get('user-agent')
	})
	.listen(3000)

Allowing you to access low-level request information if necessary.

Server Bun only

Server instance is a Bun server instance, allowing us to access server information like port number or request IP.

Server will only be available when HTTP server is running with listen.

typescript
import { Elysia } from 'elysia'

new Elysia()
	.get('/port', ({ server }) => {
		return server?.port
	})
	.listen(3000)

Request IP Bun only

We can get request IP by using server.requestIP method

typescript
import { Elysia } from 'elysia'

new Elysia()
	.get('/ip', ({ server, request }) => {
		return server?.requestIP(request)
	})
	.listen(3000)

Extending context Advance concept

As Elysia only provides essential information, we can customize Context for our specific need for instance:

  • extracting user ID as variable
  • inject a common pattern repository
  • add a database connection

We may extend Elysia's context by using the following APIs to customize the Context:

  • state - a global mutable state
  • decorate - additional property assigned to Context
  • derive / resolve - create a new value from existing property

When to extend context

You should only extend context when:

  • A property is a global mutable state, and shared across multiple routes using state
  • A property is associated with a request or response using decorate
  • A property is derived from an existing property using derive / resolve

Otherwise, we recommend defining a value or function separately than extending the context.

TIP

It's recommended to assign properties related to request and response, or frequently used functions to Context for separation of concerns.

State

State is a global mutable object or state shared across the Elysia app.

Once state is called, value will be added to store property once at call time, and can be used in handler.

typescript
import { 
Elysia
} from 'elysia'
new
Elysia
()
.
state
('version', 1)
.
get
('/a', ({
store
: {
version
} }) =>
version
)
.
get
('/b', ({
store
}) =>
store
)
.
get
('/c', () => 'still ok')
.
listen
(3000)
localhost

GET

When to use

  • When you need to share a primitive mutable value across multiple routes
  • If you want to use a non-primitive or a wrapper value or class that mutate an internal state, use decorate instead.

Key takeaway

  • store is a representation of a single-source-of-truth global mutable object for the entire Elysia app.
  • state is a function to assign an initial value to store, which could be mutated later.
  • Make sure to assign a value before using it in a handler.
typescript
import { 
Elysia
} from 'elysia'
new
Elysia
()
// ❌ TypeError: counter doesn't exist in store .
get
('/error', ({
store
}) =>
store
.counter)
Property 'counter' does not exist on type '{}'.
.
state
('counter', 0)
// ✅ Because we assigned a counter before, we can now access it .
get
('/', ({
store
}) =>
store
.
counter
)
localhost

GET

TIP

Beware that we cannot use a state value before assign.

Elysia registers state values into the store automatically without explicit type or additional TypeScript generic needed.

Reference and value Gotcha

To mutate the state, it's recommended to use reference to mutate rather than using an actual value.

When accessing the property from JavaScript, if we define a primitive value from an object property as a new value, the reference is lost, the value is treated as new separate value instead.

For example:

typescript
const store = {
    counter: 0
}

store.counter++
console.log(store.counter) // ✅ 1

We can use store.counter to access and mutate the property.

However, if we define a counter as a new value

typescript
const store = {
    counter: 0
}

let counter = store.counter

counter++
console.log(store.counter) // ❌ 0
console.log(counter) // ✅ 1

Once a primitive value is redefined as a new variable, the reference "link" will be missing, causing unexpected behavior.

This can apply to store, as it's a global mutable object instead.

typescript
import { Elysia } from 'elysia'

new Elysia()
    .state('counter', 0)
    // ✅ Using reference, value is shared
    .get('/', ({ store }) => store.counter++)
    // ❌ Creating a new variable on primitive value, the link is lost
    .get('/error', ({ store: { counter } }) => counter)
localhost

GET

Decorate

decorate assigns an additional property to Context directly at call time.

typescript
import { 
Elysia
} from 'elysia'
class
Logger
{
log
(
value
: string) {
console
.
log
(
value
)
} } new
Elysia
()
.
decorate
('logger', new
Logger
())
// ✅ defined from the previous line .
get
('/', ({
logger
}) => {
logger
.
log
('hi')
return 'hi' })

When to use

  • A constant or readonly value object to Context
  • Non primitive value or class that may contain internal mutable state
  • Additional functions, singleton, or immutable property to all handlers.

Key takeaway

  • Unlike state, decorated value SHOULD NOT be mutated although it's possible
  • Make sure to assign a value before using it in a handler.

Derive

⚠️ Derive doesn't handle type integrity, you might want to use resolve instead.

Retrieve values from existing properties in Context and assign new properties.

Derive assigns when request happens at transform lifecycle allowing us to "derive" (create new properties from existing properties).

typescript
import { 
Elysia
} from 'elysia'
new
Elysia
()
.
derive
(({
headers
}) => {
const
auth
=
headers
['authorization']
return {
bearer
:
auth
?.
startsWith
('Bearer ') ?
auth
.
slice
(7) : null
} }) .
get
('/', ({
bearer
}) =>
bearer
)
localhost

GET

Because derive is assigned once a new request starts, derive can access request properties like headers, query, body where store, and decorate can't.

When to use

  • Create a new property from existing properties in Context without validation or type checking
  • When you need to access request properties like headers, query, body without validation

Key takeaway

  • Unlike state and decorate instead of assign at call time, derive is assigned once a new request starts.
  • derive is called at transform, or before validation happens, Elysia cannot safely confirm the type of request property resulting in as unknown. If you want to assign a new value from typed request properties, you may want to use resolve instead.

Resolve

Similar as derive but ensure type integrity.

Resolve allow us to assign a new property to context.

Resolve is called at beforeHandle lifecycle or after validation, allowing us to resolve request properties safely.

typescript
import { 
Elysia
,
t
} from 'elysia'
new
Elysia
()
.
guard
({
headers
:
t
.
Object
({
bearer
:
t
.
String
({
pattern
: '^Bearer .+$'
}) }) }) .
resolve
(({
headers
}) => {
return {
bearer
:
headers
.
bearer
.
slice
(7)
} }) .
get
('/', ({
bearer
}) =>
bearer
)

When to use

  • Create a new property from existing properties in Context with type integrity (type checked)
  • When you need to access request properties like headers, query, body with validation

Key takeaway

  • resolve is called at beforeHandle, or after validation happens. Elysia can safely confirm the type of request property resulting in as typed.

Error from resolve/derive

As resolve and derive is based on transform and beforeHandle lifecycle, we can return an error from resolve and derive. If error is returned from derive, Elysia will return early exit and return the error as response.

typescript
import { 
Elysia
} from 'elysia'
new
Elysia
()
.
derive
(({
headers
,
status
}) => {
const
auth
=
headers
['authorization']
if(!
auth
) return
status
(400)
return {
bearer
:
auth
?.
startsWith
('Bearer ') ?
auth
.
slice
(7) : null
} }) .
get
('/', ({
bearer
}) =>
bearer
)

Pattern Advance Concept

state, decorate offers a similar APIs pattern for assigning property to Context as the following:

  • key-value
  • object
  • remap

Where derive can be only used with remap because it depends on existing value.

key-value

We can use state, and decorate to assign a value using a key-value pattern.

typescript
import { Elysia } from 'elysia'

class Logger {
    log(value: string) {
        console.log(value)
    }
}

new Elysia()
    .state('counter', 0)
    .decorate('logger', new Logger())

This pattern is great for readability for setting a single property.

Object

Assigning multiple properties is better contained in an object for a single assignment.

typescript
import { Elysia } from 'elysia'

new Elysia()
    .decorate({
        logger: new Logger(),
        trace: new Trace(),
        telemetry: new Telemetry()
    })

The object offers a less repetitive API for setting multiple values.

Remap

Remap is a function reassignment.

Allowing us to create a new value from existing value like renaming or removing a property.

By providing a function, and returning an entirely new object to reassign the value.

typescript
import { 
Elysia
} from 'elysia'
new
Elysia
()
.
state
('counter', 0)
.
state
('version', 1)
.
state
(({
version
, ...
store
}) => ({
...
store
,
elysiaVersion
: 1
})) // ✅ Create from state remap .
get
('/elysia-version', ({
store
}) =>
store
.
elysiaVersion
)
// ❌ Excluded from state remap .
get
('/version', ({
store
}) =>
store
.version)
Property 'version' does not exist on type '{ elysiaVersion: number; counter: number; }'.
localhost

GET

It's a good idea to use state remap to create a new initial value from the existing value.

However, it's important to note that Elysia doesn't offer reactivity from this approach, as remap only assigns an initial value.

TIP

Using remap, Elysia will treat a returned object as a new property, removing any property that is missing from the object.

Affix Advance Concept

To provide a smoother experience, some plugins might have a lot of property value which can be overwhelming to remap one-by-one.

The Affix function which consists of prefix and suffix, allowing us to remap all property of an instance.

ts
import { 
Elysia
} from 'elysia'
const
setup
= new
Elysia
({
name
: 'setup' })
.
decorate
({
argon
: 'a',
boron
: 'b',
carbon
: 'c'
}) const
app
= new
Elysia
()
.
use
(
setup
)
.
prefix
('decorator', 'setup')
.
get
('/', ({
setupCarbon
, ...
rest
}) =>
setupCarbon
)
localhost

GET

Allowing us to bulk remap a property of the plugin effortlessly, preventing the name collision of the plugin.

By default, affix will handle both runtime, type-level code automatically, remapping the property to camelCase as naming convention.

In some condition, we can also remap all property of the plugin:

ts
import { 
Elysia
} from 'elysia'
const
setup
= new
Elysia
({
name
: 'setup' })
.
decorate
({
argon
: 'a',
boron
: 'b',
carbon
: 'c'
}) const
app
= new
Elysia
()
.
use
(
setup
)
.
prefix
('all', 'setup')
.
get
('/', ({
setupCarbon
, ...
rest
}) =>
setupCarbon
)