localhost
GET
Elysia provides a minimal Context by default, allowing us to extend Context for our specific need using state, decorate, derive, and resolve.
Elysia allows us to extend Context for various use cases like:
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 a state value before assign.
Elysia registers state values into the store automatically without explicit type or additional TypeScript generic needed.
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
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 new properties.
Derive assigns when request happens at transform lifecycle allowing us to "derive" (create new properties from existing properties).
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.
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.
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, status }) => {
const auth = headers['authorization']
if(!auth) return status(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)