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
  • 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('/', ({ store: { version } }) => version)
import { Elysia } from 'elysia'

new Elysia()
    .state('version', 1)
    .get('/', ({ store: { version } }) => version)

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)
    .state('counter', 0)
    // ✅ Because we assigned a counter before, we can now access it
    .get('/', ({ store }) => store.counter)
import { Elysia } from 'elysia'

new Elysia()
    // ❌ TypeError: counter doesn't exist in store
    .get('/error', ({ store }) => store.counter)
    .state('counter', 0)
    // ✅ Because we assigned a counter before, we can now access it
    .get('/', ({ store }) => store.counter)

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'

new Elysia()
    .decorate('logger', new Logger())
    // ✅ defined from the previous line
    .get('/', ({ logger }) => {
        logger.log('hi')

        return 'hi'
    })
import { Elysia } from 'elysia'

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)
import { Elysia } from 'elysia'

new Elysia()
    .derive(({ headers }) => {
        const auth = headers['authorization']

        return {
            bearer: auth?.startsWith('Bearer ') ? auth.slice(7) : null
        }
    })
    .get('/', ({ bearer }) => bearer)

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'

new Elysia()
    .state('counter', 0)
    .decorate('logger', new Logger())
import { Elysia } from 'elysia'

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()
})
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('/', ({ store }) => store.elysiaVersion)
    // ❌ Excluded from state remap
    .get('/', ({ store }) => store.version)
import { Elysia } from 'elysia'

new Elysia()
    .state('counter', 0)
    .state('version', 1)
    .state(({ version, ...store }) => ({
        ...store,
        elysiaVersion: 1
    }))
    // ✅ Create from state remap
    .get('/', ({ store }) => store.elysiaVersion)
    // ❌ Excluded from state remap
    .get('/', ({ store }) => store.version)

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
const setup = new Elysia({ name: 'setup' })
    .decorate({
        argon: 'a',
        boron: 'b',
        carbon: 'c'
    })

const app = new Elysia()
    .use(
        setup
            .prefix('decorator', 'setup')
    )
    .get('/', ({ setupCarbon }) => setupCarbon)
const setup = new Elysia({ name: 'setup' })
    .decorate({
        argon: 'a',
        boron: 'b',
        carbon: 'c'
    })

const app = new Elysia()
    .use(
        setup
            .prefix('decorator', 'setup')
    )
    .get('/', ({ setupCarbon }) => setupCarbon)

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
const app = new Elysia()
    .use(setup.prefix('all', 'setup'))
    .get('/', ({ setupCarbon }) => setupCarbon)
const app = new Elysia()
    .use(setup.prefix('all', 'setup'))
    .get('/', ({ setupCarbon }) => 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
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
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)
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)