localhost
GET
Handler is a function that responds to the request for each route.
Accepting request information and returning a response to the client.
Altenatively, handler is also known as a Controller in other frameworks.
import { Elysia } from 'elysia'
new Elysia()
// the function `() => 'hello world'` is a handler
.get('/', () => 'hello world')
.listen(3000)
Handler maybe a literal value, and can be inlined.
import { Elysia } from 'elysia'
new Elysia()
.get('/', 'Hello Elysia')
.get('/video', Bun.file('kyuukurarin.mp4'))
.listen(3000)
Using an inline value always returns the same value which is useful to optimize performance for static resource like file.
This allows Elysia to compile the response ahead of time to optimize performance.
TIP
Providing an inline value is not a cache.
Static Resource value, headers and status can be mutate dynamically using lifecycle.
Context contains a request information which unique for each request, and is not shared except for store
(global mutable state).
import { Elysia } from 'elysia'
new Elysia()
.get('/', (context) => context.path)
// ^ This is a context
Context can be only retrieve in a route handler, consists of:
set is a mutable property that form a response accessible via Context.set
.
import { Elysia } from 'elysia'
new Elysia()
.get('/', ({ set, error }) => {
set.headers = { 'X-Teapot': 'true' }
return error(418, 'I am a teapot')
})
.listen(3000)
We can return a custom status code by using either:
import { Elysia } from 'elysia'
new Elysia()
.get('/error', ({ error }) => error(418, 'I am a teapot'))
.get('/set.status', ({ set }) => {
set.status = 418
return 'I am a teapot'
})
.listen(3000)
A dedicated error
function for returning status code with response.
import { Elysia } from 'elysia'
new Elysia()
.get('/', ({ error }) => error(418, "Kirifuji Nagisa"))
.listen(3000)
GET
It's recommend to use error
inside main handler as it has better inference:
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.
import { Elysia } from 'elysia'
new Elysia()
.onBeforeHandle(({ set }) => {
set.status = 418
return 'Kirifuji Nagisa'
})
.get('/', () => 'hi')
.listen(3000)
Unlike error
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.
// @errors 2322
import { Elysia } from 'elysia'
new Elysia()
.get('/', ({ set }) => {
set.status
return 'Kirifuji Nagisa'
})
.listen(3000)
Allowing us to append or delete a response headers represent as Object.
import { Elysia } from 'elysia'
new Elysia()
.get('/', ({ set }) => {
set.headers['x-powered-by'] = 'Elysia'
return 'a mimir'
})
.listen(3000)
WARNING
The names of headers should be lowercase to force case-sensitivity consistency for HTTP headers and auto-completion, eg. use set-cookie
rather than Set-Cookie
.
Redirect a request to another resource.
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.
Server instance is accessible via Context.server
to interact with the server.
Server could be nullable as it could be running in a different environment (test).
If server is running (allocating) using Bun, server
will be available (not null).
import { Elysia } from 'elysia'
new Elysia()
.get('/port', ({ server }) => {
return server?.port
})
.listen(3000)
We can get request IP by using server.ip
method
import { Elysia } from 'elysia'
new Elysia()
.get('/ip', ({ server, request }) => {
return server?.ip(request)
})
.listen(3000)
Elysia is built on top of Web Standard Request/Response.
To comply with the Web Standard, a value returned from route handler will be mapped into a Response by Elysia.
Letting you focus on business logic rather than boilerplate code.
import { Elysia } from 'elysia'
new Elysia()
// Equivalent to "new Response('hi')"
.get('/', () => 'hi')
.listen(3000)
If you prefer an explicit Response class, Elysia also handles that automatically.
import { Elysia } from 'elysia'
new Elysia()
.get('/', () => new Response('hi'))
.listen(3000)
TIP
Using a primitive value or Response
has near identical performance (+- 0.1%), so pick the one you prefer, regardless of performance.
We may return a FormData
by using returning form
utility directly from the handler.
import { Elysia, form } from 'elysia'
new Elysia()
.get('/', () => form({
name: 'Tea Party',
images: [Bun.file('nagi.web'), Bun.file('mika.webp')]
}))
.listen(3000)
This pattern is useful if even need to return a file or multipart form data.
Or alternatively, you can return a single file by returning Bun.file
directly without form
.
import { Elysia } from 'elysia'
new Elysia()
.get('/', () => Bun.file('nagi.web'))
.listen(3000)
As Elysia is built on top of Web Standard Request, we can programmatically test it using Elysia.handle
.
import { Elysia } from 'elysia'
const app = new Elysia()
.get('/', () => 'hello')
.post('/hi', () => 'hi')
.listen(3000)
app.handle(new Request('http://localhost/')).then(console.log)
Elysia.handle is a function to process an actual request sent to the server.
TIP
Unlike unit test's mock, you can expect it to behave like an actual request sent to the server.
But also useful for simulating or creating unit tests.
To return a response streaming out of the box by using a generator function with yield
keyword.
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.
Elysia will defers returning response headers until the first chunk is yielded.
This allows us to set headers before the response is streamed.
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 and the first chunk in the same response.
Setting headers after the first chunk is yielded will do nothing.
If the response is returned without yield, Elysia will automatically convert stream to normal response instead.
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.
While streaming a response, it's common that request may be cancelled before the response is fully streamed.
Elysia will automatically stop the generator function when the request is cancelled.
Eden will will interpret a stream response as AsyncGenerator
allowing us to use for await
loop to consume the stream.
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)
As Elysia only provides essential information, we can customize Context for our specific need for instance:
We may extend Elysia's context by using the following APIs to customize the Context:
You should only extend context when:
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 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.
import { Elysia } from 'elysia'
new Elysia()
.state('version', 1)
.get('/a', ({ store: { version } }) => version)
.get('/b', ({ store }) => store)
.get('/c', () => 'still ok')
.listen(3000)
GET
wrapper
value or class that mutate an internal state, use decorate instead.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)
GET
TIP
Beware that we cannot use state value before assign.
Elysia registers state values into the store automatically without explicit type or additional TypeScript generic needed.
decorate assigns an additional property to Context directly at call time.
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'
})
Retrieve values from existing properties in Context and assign an new properties.
Derive assigns when request happens at transform lifecycle allowing us to "derive" create a new property from existing property.
import { Elysia } from 'elysia'
new Elysia()
.derive(({ headers }) => {
const auth = headers['authorization']
return {
bearer: auth?.startsWith('Bearer ') ? auth.slice(7) : null
}
})
.get('/', ({ bearer }) => bearer)
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.
Same as derive, resolve allow us to assign a new property to context.
Resolve is called at beforeHandle lifecycle or after validation, allowing us to derive request properties safely.
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)
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.
import { Elysia } from 'elysia'
new Elysia()
.derive(({ headers, error }) => {
const auth = headers['authorization']
if(!auth) return error(400)
return {
bearer: auth?.startsWith('Bearer ') ? auth.slice(7) : null
}
})
.get('/', ({ bearer }) => bearer)
state, decorate offers a similar APIs pattern for assigning property to Context as the following:
Where derive can be only used with remap because it depends on existing value.
We can use state, and decorate to assign a value using a key-value pattern.
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.
Assigning multiple properties is better contained in an object for a single assignment.
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 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.
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; }'.
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.
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.
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)
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:
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)
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:
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
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.
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)
GET
Macro allows us to define a custom field to the hook.
Elysia.macro allows us to compose custom heavy logic into a simple configuration available in hook, and guard with full type safety.
import { Elysia } from 'elysia'
const plugin = new Elysia({ name: 'plugin' })
.macro(({ onBeforeHandle }) => ({
hi(word: string) {
onBeforeHandle(() => {
console.log(word)
})
}
}))
const app = new Elysia()
.use(plugin)
.get('/', () => 'hi', {
hi: 'Elysia'
})
Accessing the path should log "Elysia" as the results.
macro should return an object, each key is reflected to the hook, and the provided value inside the hook will be sent back as the first parameter.
In previous example, we create hi accepting a string.
We then assigned hi to "Elysia", the value was then sent back to the hi function, and then the function added a new event to beforeHandle stack.
Which is an equivalent of pushing function to beforeHandle as the following:
import { Elysia } from 'elysia'
const app = new Elysia()
.get('/', () => 'hi', {
beforeHandle() {
console.log('Elysia')
}
})
macro shine when a logic is more complex than accepting a new function, for example creating an authorization layer for each route.
import { Elysia } from 'elysia'
import { auth } from './auth'
const app = new Elysia()
.use(auth)
.get('/', () => 'hi', {
isAuth: true,
role: 'admin'
})
The field can accept anything ranging from string to function, allowing us to create a custom life cycle event.
macro will be executed in order from top-to-bottom according to definition in hook, ensure that the stack should be handle in correct order.
Elysia.macro parameters to interact with the life cycle event as the following:
Parameters start with on is a function to appends function into a life cycle stack.
While events is an actual stack that stores an order of the life-cycle event. You may mutate the stack directly or using the helper function provided by Elysia.
The life cycle function of an extension API accepts additional options to ensure control over life cycle events.
import { Elysia } from 'elysia'
const plugin = new Elysia({ name: 'plugin' })
.macro(({ onBeforeHandle }) => {
return {
hi(word: string) {
onBeforeHandle(
{ insert: 'before' },
() => {
console.log(word)
}
)
}
}
})
Options may accept the following parameter:
Elysia automatically type context base on various of factors like store, decorators, schema.
It's recommended to leave Elysia to type context instead of manually define one.
However, Elysia also offers some utility type to help you define a handler type.
Infer context is a utility type to help you define a context type based on Elysia instance.
import { Elysia, type InferContext } from 'elysia'
const setup = new Elysia()
.state('a', 'a')
.decorate('b', 'b')
type Context = InferContext<typeof setup>
const handler = ({ store }: Context) => store.a
Infer handler is a utility type to help you define a handler type based on Elysia instance, path, and schema.
import { Elysia, type InferHandler } from 'elysia'
const setup = new Elysia()
.state('a', 'a')
.decorate('b', 'b')
type Handler = InferHandler<
// Elysia instance to based on
typeof setup,
// path
'/path',
// schema
{
body: string
response: {
200: string
}
}
>
const handler: Handler = ({ body }) => body
const app = new Elysia()
.get('/', handler)
Unlike InferContext
, InferHandler
requires a path and schema to define a handler type and can safely ensure type safety of a return type.