Elysia 0.6 - This Game | ElysiaJS

ID: 2206https://elysiajs.com/blog/elysia-06
Source

Elysia 0.6 – This Game

Author: saltyaom
Date: 6 Aug 2023

Crystal Knight Piece

Named after the opening of the legendary anime, “No Game No Life”, 「This Game」 composed by Konomi Suzuki.
This Game pushes the boundary of medium‑size projects to large‑scale apps with a re‑imagined plugin model, dynamic mode, better developer experience with declarative custom error, more metrics with onResponse, customizable loose and strict path mapping, TypeBox 0.30, and WinterCG framework interlop.


(We are still waiting for No Game No Life season 2)


New Plugin Model

Elysia 0.6 introduces a new syntax for plugin registration and an internally new plugin model.

Previously you could register a plugin by defining a callback function for an Elysia instance:

const plugin = (app: Elysia) => 
  app.get('/', () => 'hello')

With the new plugin, you can now turn an Elysia instance into a plugin:

const plugin = new Elysia()
  .get('/', () => 'hello')

This allows any Elysia instance—existing or new—to be used across an application, removing any additional callback and tab spacing.

This improved developer experience significantly when working with nested groups.

// < 0.6
const group = (app: Elysia) => 
  app
    .group('/v1', (app) => 
      app.get('/hello', () => 'Hello World')
    )
// >= 0.6
const group = new Elysia({ prefix: '/v1' })
  .get('/hello', () => 'Hello World')

We encourage you to use the new model of Elysia plugin instance, as we can take advantage of Plugin Checksum and new possible features in the future.

Note: This is not a deprecation of the callback function method; some cases still find the function model useful, such as:

  • Inline functions
  • Plugins that need information from the main instance (e.g., accessing an OpenAPI schema)

With this new plugin model, we hope that your codebase becomes even easier to maintain.


Plugin Checksum

By default, Elysia plugins use a function callback to register a plugin.
If you register a plugin for type declaration, it duplicates itself just to provide type support, leading to duplicate plugins in production.

Plugin Checksum de‑duplicates plugins registered for type declaration.
To opt‑in, use the new plugin model and provide a name property:

const plugin = new Elysia({
  name: 'plugin'
})

This allows Elysia to identify the plugin and deduplicate it based on its name.
Duplicated names will be registered only once while still providing type‑safety.

If your plugin requires configuration, supply a seed property to generate a checksum:

const plugin = (config) => new Elysia({
  name: 'plugin',
  seed: config
})

Name and seed are used to generate a checksum that de‑duplicates registration, improving performance.
This also fixes accidental inline lifecycle deduplication when Elysia is unsure whether a plugin is local or global.


Mount and WinterCG Compliance

WinterCG is a standard for web‑interoperable runtimes supported by Cloudflare, Deno, Vercel Edge Runtime, Netlify Function, and more.
Elysia is partially compliant, optimized for Bun but also supports other runtimes.

This enables any framework and code that is WinterCG compliant to run together in theory.
Hono demonstrates this with a .mount method that runs multiple frameworks in one codebase.

To use .mount, simply pass a fetch function:

const app = new Elysia()
  .get('/', () => 'Hello from Elysia')
  .mount('/hono', hono.fetch)

A fetch function accepts a Web‑Standard RequestLike and returns a Response.

// Web Standard Request-like object
// Web Standard Response
type fetch = (request: RequestLike) => Response

Supported runtimes include:

  • Bun
  • Deno
  • Vercel Edge Runtime
  • Cloudflare Worker
  • Netlify Edge Function
  • Remix Function Handler

You can nest frameworks that support .mount infinitely:

const elysia = new Elysia()
  .get('/Hello from Elysia inside Hono inside Elysia')

const hono = new Hono()
  .get('/', (c) => c.text('Hello from Hono!'))
  .mount('/elysia', elysia.fetch)

const main = new Elysia()
  .get('/', () => 'Hello from Elysia')
  .mount('/hono', hono.fetch)
  .listen(3000)

If the instance passed to mount is an Elysia instance, it will resolve to use automatically, providing type‑safety and support for Eden by default.

You can also reuse multiple existing Elysia projects in one server:

import A from 'project-a/elysia'
import B from 'project-b/elysia'
import C from 'project-c/elysia'

new Elysia()
  .mount(A)
  .mount(B)
  .mount(C)

Improved Start‑Up Time

Elysia generates an OpenAPI schema for every route automatically and stores it internally unless unused.
In this version, that compilation is deferred to @elysiajs/swagger, making start‑up time even faster.

With various micro‑optimizations and the new plugin model, start‑up time is up to 35 % faster.


Dynamic Mode

Elysia introduces Static Code Analysis and Ahead‑of‑Time (AoT) compilation to push performance boundaries.
Static analysis reads code and produces an optimized version. However, on runtimes like Cloudflare Workers that lack function composition, AoT isn’t possible, so a dynamic mode (JIT compilation) is available.

To enable dynamic mode, set aot to false:

new Elysia({
  aot: false
})

Dynamic mode is enabled by default on Cloudflare Worker.

Note: Enabling Dynamic Mode will disable some features such as dynamic injected code like t.Numeric which parses strings to numbers automatically.

AoT offers startup time trade‑off for extra performance, while dynamic mode allows up to 6× faster start‑up.
Elysia can register 10,000 routes in 78 ms (~0.0079 ms per route).
You can decide which mode best suits your project.


Declarative Custom Error

This update adds support for handling custom error types with type narrowing and auto‑completion for error codes.

class CustomError extends Error {
  constructor(public message: string) {
    super(message)
  }
}

new Elysia()
  .addError({
    MyError: CustomError
  })
  .onError(({ code, error }) => {
    switch (code) {
      // With auto‑completion
      case 'MyError':
        // With type narrowing
        // Error is typed as CustomError
        return error
    }
  })

This allows fully type‑safe declarative error handling. Elysia’s type system remains simple while providing robust type safety.


TypeBox 0.30

TypeBox powers Elysia’s strict type system (Elysia.t).
In this update, TypeBox is upgraded from 0.28 to 0.30, adding features such as Iterator type, reducing package size, and TypeScript code generation.
It also supports utility types like:

  • t.Awaited
  • t.Uppercase
  • t.Capitlized

Strict Path

Elysia handles paths strictly by default, requiring duplicate definitions for optional trailing slashes.

new Elysia()
  .group('/v1', (app) =>
    // Handle /v1
    app.get('', handle)
    // Handle /v1/
    .get('/', handle)
  )

Requests for /v1 and /v1/ must both be defined.

With this update, loose path matching is enabled by default. To opt‑in:

new Elysia()
  .group('/v1', (app) =>
    // Handle /v1 and /v1/
    app.get('/', handle)
  )

To disable loose path mapping, set strictPath to true:

new Elysia({
  strictPath: false
})

onResponse

A new lifecycle hook onResponse handles all response cases (success, error, routing error, etc.).
It complements existing hooks like onAfterHandle which only fires on successful responses.

“However, the onAfterHandle function only fires on successful responses. For instance, if the route is not found, or the body is invalid, or an error is thrown, it is not fired. How can I listen to both successful and non‑successful requests? This is why I suggested onResponse.”

Notable Improvements

  • Added an error field to the Elysia type system for custom error messages
  • Supports Cloudflare worker with Dynamic Mode (and ENV)
  • afterHandle now automatically maps the value
  • Improved performance using Bun build by 5‑10 %
  • Deduplicated inline lifecycle when using plugin registration
  • Support for setting prefix
  • Recursive path typing
  • Slightly improved type‑checking speed
  • Fixed recursive schema collision causing infinite types

Changes

  • registerSchemaPath moved to @elysiajs/swagger
  • Added qi (queryIndex) to context internally

Breaking Changes

  • Removed Elysia Symbol (internal)
  • Refactored getSchemaValidator, getResponseSchemaValidator to named parameters
  • Moved registerSchemaPath to @elysiajs/swagger

Afterward

We’ve just passed a one‑year milestone, and are excited about how Elysia and Bun have improved over the year!
Pushing the performance boundaries of JavaScript with Bun and developer experience with Elysia, we’re thrilled to keep growing with our community.

Some of the projects that bring Elysia to life include:

We also introduced Mobius, an open‑source TypeScript library to parse GraphQL to TypeScript types using template literal types, achieving end‑to‑end type safety without code generation.

Thank you for your overwhelming continuous support for Elysia. We look forward to pushing the boundaries together in the next release.


As this whole new world cheers my name
I will never leave it to fate
and when I see a chance, I will pave the way
I calls checkmate
This is the time to breakthrough
So I will rewrite the story and finally change all the rule
We are maverick
We won't give in, until we win this game
Though I don't know what tomorrow holds
I'll make a bet any play my cards to win this game
Unlike the rest, I'll do my best, and I won't ever lose
To give up this chance would be a deadly since, so let's bet it all
I put all my fate in used let the game begin