localhost
GET
Context is a request information passed to a route handler.
Context is unique for each request, and is not shared except for store
which is a global mutable state.
Elysia context consists of:
As Elysia only provides essential information, we can customize Context for our specific need for instance:
We can extend Elysia's context by using the following APIs to customize 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.
If we are familiar with frontend libraries like React, Vue, or Svelte, there's a concept of Global State Management, which is also partially implemented in Elysia via state and store.
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.
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
Once state is called, value will be added to store property, and can be used in handler.
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 without prefix.
The difference is that the value should be read-only and not reassigned later.
This is an ideal way to assign additional functions, singleton, or immutable property to all handlers.
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'
})
Like decorate
, we can assign an additional property to Context directly.
Instead of assign before server started, derive assigns when request happens.
Allowing us to "derive" (create a new property based on 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.
Unlike state, and decorate. Properties that are assigned by derive are unique and not shared with another request.
TIP
Derive is similar to resolve but store in a different queue.
derive is stored in transform queue while resolve stored in beforeHandle queue.
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