localhost
GET
Elysia has a every important concepts that you need to understand to use.
This page covers most concepts that you should know before getting started.
Elysia lifecycle methods are encapsulated to its own instance only.
Which means if you create a new instance, it will not share the lifecycle methods with others.
import { Elysia } from 'elysia'
const profile = new Elysia()
.onBeforeHandle(({ cookie }) => {
throwIfNotSignIn(cookie)
})
.get('/profile', () => 'Hi there!')
const app = new Elysia()
.use(profile)
// ⚠️ This will NOT have sign in check
.patch('/rename', ({ body }) => updateProfile(body))
In this example, the isSignIn
check will only apply to profile
but not app
.
GET
Try changing the path in the URL bar to /rename and see the result
Elysia isolate lifecycle by default unless explicitly stated. This is similar to export in JavaScript, where you need to export the function to make it available outside the module.
To "export" the lifecycle to other instances, you must add specify the scope.
import { Elysia } from 'elysia'
const profile = new Elysia()
.onBeforeHandle(
{ as: 'global' },
({ cookie }) => {
throwIfNotSignIn(cookie)
}
)
.get('/profile', () => 'Hi there!')
const app = new Elysia()
.use(profile)
// This has sign in check
.patch('/rename', ({ body }) => updateProfile(body))
GET
Casting lifecycle to "global" will export lifecycle to every instance.
Learn more about this in scope.
Elysia code should ALWAYS use method chaining.
This is important to ensure type safety.
import { Elysia } from 'elysia'
new Elysia()
.state('build', 1)
// Store is strictly typed
.get('/', ({ store: { build } }) => build)
.listen(3000)
In the code above, state returns a new ElysiaInstance type, adding a typed build
property.
As Elysia type system is complex, every method in Elysia returns a new type reference.
Without using method chaining, Elysia doesn't save these new types, leading to no type inference.
import { Elysia } from 'elysia'
const app = new Elysia()
app.state('build', 1)
app.get('/', ({ store: { build } }) => build)Property 'build' does not exist on type '{}'.
app.listen(3000)
We recommend to always use method chaining to provide an accurate type inference.
Each plugin will be re-executed EVERY TIME when apply to another instance.
If the plugin is applied multiple time, it will cause an unnecessary duplication.
It's important that some methods, like lifecycle or routes, should only be called once.
To prevent this, Elysia can deduplicate lifecycle with an unique identifier.
import { Elysia } from 'elysia'
// `name` is an unique identifier
const ip = new Elysia({ name: 'ip' })
.derive(
{ as: 'global' },
({ server, request }) => ({
ip: server?.requestIP(request)
})
)
.get('/ip', ({ ip }) => ip)
const router1 = new Elysia()
.use(ip)
.get('/ip-1', ({ ip }) => ip)
const router2 = new Elysia()
.use(ip)
.get('/ip-2', ({ ip }) => ip)
const server = new Elysia()
.use(router1)
.use(router2)
Adding the name
property to the instance will make it a unique identifier prevent it from being called multiple times.
Learn more about this in plugin deduplication.
When you apply a plugin with to an instance, the instance will gain type safety.
But if you don't apply the plugin to another instance, it will not be able to infer the type.
import { Elysia } from 'elysia'
const child = new Elysia()
// ❌ 'a' is missing
.get('/', ({ a }) => a)Property 'a' does not exist on type '{ body: unknown; query: Record<string, string>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<unknown>>; server: Server | null; ... 6 more ...; status: <const Code extends number | keyof StatusMap, const T = Code extends 100 | ... 59 more ... | 511 ? { ...; }[Code] : Code>(co...'.
const main = new Elysia()
.decorate('a', 'a')
.use(child)
Elysia introduces the Service Locator pattern to counteract this.
By simply provide the plugin reference for Elysia to find the service to add type safety.
import { Elysia } from 'elysia'
const setup = new Elysia({ name: 'setup' })
.decorate('a', 'a')
// Without 'setup', type will be missing
const error = new Elysia()
.get('/', ({ a }) => a)Property 'a' does not exist on type '{ body: unknown; query: Record<string, string>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<unknown>>; server: Server | null; ... 6 more ...; status: <const Code extends number | keyof StatusMap, const T = Code extends 100 | ... 59 more ... | 511 ? { ...; }[Code] : Code>(co...'.
// With `setup`, type will be inferred
const child = new Elysia()
.use(setup)
.get('/', ({ a }) => a)
This is equivalent to TypeScript's type import, where you import the type without actually importing the code to run.
As mentioned in Elysia already handle deduplication, this will not have any performance penalty or lifecycle duplication.
The order of Elysia's life-cycle code is very important.
Because event will only apply to routes after it is registered.
If you put the onError before plugin, plugin will not inherit the onError event.
import { Elysia } from 'elysia'
new Elysia()
.onBeforeHandle(() => {
console.log('1')
})
.get('/', () => 'hi')
.onBeforeHandle(() => {
console.log('2')
})
.listen(3000)
Console should log the following:
1
Notice that it doesn't log 2, because the event is registered after the route so it is not applied to the route.
Learn more about this in order of code.
Elysia has a complex type system that allows you to infer types from the instance.
import { Elysia, t } from 'elysia'
const app = new Elysia()
.post('/', ({ body }) => body, {
body: t.Object({
name: t.String()
})
})
You should always use an inline function to provide an accurate type inference.
If you need to apply a separate function, eg. MVC's controller pattern, it's recommended to destructure properties from inline function to prevent unnecessary type inference as follows:
import { Elysia, t } from 'elysia'
abstract class Controller {
static greet({ name }: { name: string }) {
return 'hello ' + name
}
}
const app = new Elysia()
.post('/', ({ body }) => Controller.greet(body), {
body: t.Object({
name: t.String()
})
})
See Best practice: MVC Controller.
We can get a type definitions of every Elysia/TypeBox's type by accessing static
property as follows:
import { t } from 'elysia'
const MyType = t.Object({
hello: t.Literal('Elysia')
})
type MyType = typeof MyType.static
This allows Elysia to infer and provide type automatically, reducing the need to declare duplicate schema
A single Elysia/TypeBox schema can be used for:
This allows us to make a schema as a single source of truth.