Skip to content

Context

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:

  • path - Pathname of the request
  • 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.
  • request - Web Standard Request
  • redirect - A function to redirect a response
  • store - A global mutable store for Elysia instance
  • cookie - A global mutable signal store for interacting with Cookie (including get/set)
  • set - Property to apply to Response:
    • status - HTTP status, defaults to 200 if not set.
    • headers - Response headers
    • redirect - Response as a path to redirect to
  • error - A function to return custom status code

Extending context

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 can extend Elysia's context by using the following APIs to customize the Context:

  • state - Create a global mutable state into Context.store
  • decorate - Add additional function or property assigned to Context
  • derive / resolve - Additional property based on existing property, uniquely assigned to each request.

TIP

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

Store

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.

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

Once state is called, value will be added to store property, and can be used in 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 state value before assign.

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

Decorate

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.

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' })

Derive

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).

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.

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.

Pattern

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

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
)

Reference and value

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