Elysia Versions Save

Ergonomic Framework for Humans

1.0

2 months ago

lament-of-the-fallent

Elysia 1.0 is the first stable release after development for 1.8 years.

Since started, we have always waiting for a framework that focuses on developer experience, velocity, and how to make writing code for humans, not a machine.

We battle-test Elysia in various situations, simulate medium and large-scale projects, shipping code to clients and this is the first version that we felt confident enough to ship.

Elysia 1.0 introduces significant improvements and contains 1 necessary breaking change.


It's a tradition that Elysia's release note have a version named after a song or media.

This important version is named after "Lament of the Fallen".

Animated short from "Honkai Impact 3rd" from my favorite arc, and my favorite character, "Raiden Mei" featuring her theme song, "Honkai World Diva".

It's a very good game, and you should check it out.

ー SaltyAom

Also known as Raiden Mei from Gun Girl Z, Honkai Impact 3rd, Honkai Star Rail. And her "variation", Raiden Shogun from Genshin Impact, and possibly Acheron from Honkai Star Rail (since she's likely a bad-end herrscher form mentioned in Star Rail 2.1).

::: tip Remember, ElysiaJS is an open source library maintain by volunteers, and isn't associate with Mihoyo nor Hoyoverse. But we are a huge fan of Honkai series, alright? :::

Sucrose

Elysia is optimized to have an excellent performance proven in various benchmarks, one of the main factors is thanks to Bun, and our custom JIT static code analysis.

If you are not aware, Elysia has some sort of "compiler" embedded that reads your code and produces an optimized way to handle functions.

The process is fast and happens on the fly without a need for a build step. However, it's challenging to maintain as it's written mostly in many complex RegEx, and can be slow at times if recursion happens.

That's why we rewrote our static analysis part to separate the code injection phase using a hybrid approach between partial AST-based and pattern-matching name "Sucrose".

Instead of using full AST-based which is more accurate, we choose to implement only a subset of rules that is needed to improve performance as it needs to be fast on runtime.

Sucrose is good at inferring the recursive property of the handler function accurately with low memory usage, resulting in up to 37% faster inference time and significantly reduced memory usage.

Sucrose is shipped to replace RegEx-based to partial AST, and pattern matching starting from Elysia 1.0.

Improved Startup time

Thanks to Sucrose, and separation from the dynamic injection phase, we can defer the analysis time JIT instead of AOT.

In other words, the "compile" phase can be lazily evaluated.

Offloading the evaluation phase from AOT to JIT when a route is matched for the first time and caching the result to compile on demand instead of all routes before server start.

In a runtime performance, a single compilation is usually fast and takes no longer than 0.01-0.03 ms (millisecond not second).

In a medium-sized application and stress test, we measure up to between ~6.5-14x faster start-up time.

Remove ~40 routes/instance limit

Previously you could only stack up to ~40 routes / 1 Elysia instance since Elysia 0.1.

This is the limitation of TypeScript that each queue that has a limited memory and if exceeded, TypeScript will think that "Type instantiation is excessively deep and possibly infinite".

const main = new Elysia()
    .get('/1', () => '1')
    .get('/2', () => '2')
    .get('/3', () => '3')
    // repeat for 40 times
    .get('/42', () => '42')
    // Type instantiation is excessively deep and possibly infinite

As a workaround, we need to separate an instance into a controller to overcome the limit and remerge the type to offload the queue like this.

const controller1 = new Elysia()
    .get('/42', () => '42')
    .get('/43', () => '43')

const main = new Elysia()
    .get('/1', () => '1')
    .get('/2', () => '2')
    // repeat for 40 times
    .use(controller1)

However, starting from Elysia 1.0, we have overcome the limit after a year after optimizing for type-performance, specifically Tail Call Optimization, and variances.

This means theoretically, we can stack an unlimited amount of routes and methods until TypeScript breaks.

(spoiler: we have done that and it's around 558 routes/instance before TypeScript CLI and language server because of JavaScript memory limit per stack/queue)

const main = new Elysia()
    .get('/1', () => '1')
    .get('/2', () => '2')
    .get('/3', () => '42')
    // repeat for n times
    .get('/550', () => '550')

So we increase the limit of ~40 routes to JavaScript memory limit instead, so try not to stack more than ~558 routes/instance, and separate into a plugin if necessary.

TypeScript breaks on 558 routes

The blocker that made us feel like Elysia is not ready for production has been finally resolved.

Type Inference improvement

Thanks to the effort we put into optimization, we measure up to ~82% in most Elysia servers.

Thanks to the removed limitation of stack, and improved type performance, we can expect almost instant type check and auto-completion even after 500 routes stacks.

https://github.com/elysiajs/elysia/assets/35027979/2048c7fe-72b8-4274-8d48-b6eac3372215

Up to 13x faster for Eden Treaty, type inference performance by precomputing the type instead offload type remap to Eden.

Overall, Elysia, and Eden Treaty performing together would be up to ~3.9x faster.

Here's a comparison between the Elysia + Eden Treaty on 0.8 and 1.0 for 450 routes.

Type performance comparison between Elysia Eden 0.8 and 1.0, the graph shows that Elysia 0.8 took ~1500ms while Elysia 1.0 took ~400ms

Stress test with 450 routes for Elysia with Eden Treaty, result as follows:

  • Elysia 0.8 took ~1500ms
  • Elysia 1.0 took ~400ms

And thanks to the removal of stack limitation, and remapping process, it's now possible to stack up to over 1,000 routes for a single Eden Treaty instance.

Treaty 2

We ask you for feedback on Eden Treaty what you like and what could have been improved. and you have given us some flaws in Treaty design and several proposals to improvement.

That's why today, we introduce Eden Treaty 2, an overhaul to a more ergonomic design.

As much as we dislike breaking change, Treaty 2 is a successor to Treaty 1.

What's new in Treaty 2:

  • More ergonomic syntax
  • End-to-end type safety for Unit Test
  • Interceptor
  • No "$" prefix and property

Our favorite one is end-to-end type safety for Unit tests.

So instead of starting a mock server and sending a fetch request, we can use Eden Treaty 2 to write unit tests with auto-completion and type safety instead.

// test/index.test.ts
import { describe, expect, it } from 'bun:test'
import { Elysia } from 'elysia'
import { treaty } from '@elysiajs/eden'

const app = new Elysia().get('/hello', () => 'hi')
const api = treaty(app)

describe('Elysia', () => {
    it('return a response', async () => {
        const { data } = await api.hello.get()

        expect(data).toBe('hi')
    })
})

The difference between the two is that Treaty 2 is a successor to Treaty 1.

We don't intend to introduce any breaking change to Treaty 1 nor force you to update to Treaty 2.

You can choose to continue using Treaty 1 for your current project without updating to Treaty 2, and we maintain it in a maintenance mode.

  • You can import treaty to use Treaty 2.
  • And import edenTreaty for Treaty 1.

The documentation for the new Treaty can be found in Treaty overview, and for Treaty 1 in Treaty legacy

Hook type (breaking change)

We hate breaking changes, and this is the first time we do it in large-scale.

We put a lot of effort into API design to reduce changes made to Elysia, but this is necessary to fix a flawed design.

Previously when we added a hook with "on" like onTransform, or onBeforeHandle, it would become a global hook.

This is great for creating something like a plugin but is not ideal for a local instance like a controller.

const plugin = new Elysia()
    .onBeforeHandle(() => {
        console.log('Hi')
    })
    // log Hi
    .get('/hi', () => 'in plugin')

const app = new Elysia()
    .use(plugin)
    // will also log hi
    .get('/no-hi-please', () => 'oh no')

However, we found several problems arise from this behavior.

  • We found that many developers have a lot of nested guards even on the new instance. Guard is almost used as a way to start a new instance to avoid side effects.
  • global by default may cause unpredictable (side-effect) behavior if not careful, especially in a team with inexperienced developers.
  • We asked many developers both familiar and not familiar with Elysia, and found that most expected hook to be local at first.
  • Following the previous point, we found that making hook global by default can easily cause accidental bugs (side-effect) if not reviewed carefully and hard to debug and observe.

To fix this, we introduce a hook type to specify how the hook should be inherited by introducing a "hook-type".

Hook types can be classified as follows:

  • local (default) - apply to only current instance and descendant only
  • scoped - apply to only 1 ascendant, current instance, and descendants
  • global (old behavior) - apply to all instances that apply the plugin (all ascendants, current, and descendants)

To specify the hook's type, simply add a { as: hookType } to the hook.

const plugin = new Elysia()
    .onBeforeHandle(() => { // [!code --]
    .onBeforeHandle({ as: 'global' }, () => { // [!code ++]
        console.log('hi')
    })
    .get('/child', () => 'log hi')

const main = new Elysia()
    .use(plugin)
    .get('/parent', () => 'log hi')

This API is designed to fix the guard nesting problem for Elysia, where developers are afraid to introduce a hook on root instances because of fear of side effects.

For example, to create an authentication check for an entire instance, we need to wrap a route in a guard.

const plugin = new Elysia()
    .guard((app) =>
        app
            .onBeforeHandle(checkAuthSomehow)
            .get('/profile', () => 'log hi')
    )

However, with the introduction of hook type, we can remove the nesting guard boilerplate.

const plugin = new Elysia()
    .guard((app) => // [!code --]
        app // [!code --]
            .onBeforeHandle(checkAuthSomehow)
            .get('/profile', () => 'log hi')
    ) // [!code --]

Hook type will specify how the hook should be inherited, let's create a plugin to illustrate how hook type works.

// ? Value based on table value provided below
const type = 'local'

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

const current = new Elysia()
    .onBeforeHandle({ as: type }, () => {
        console.log('hi')
    })
    .use(child)
    .get('/current', () => 'hello')

const parent = new Elysia()
    .use(current)
    .get('/parent', () => 'hello')

const main = new Elysia()
    .use(parent)
    .get('/main', () => 'hello')

By changing the type value, the result should be as follows:

type child current parent main
'local'
'scope'
'global'

Migrating from Elysia 0.8, if you want make a hook global, you have to specify that hook is global.

// From Elysia 0.8
new Elysia()
    .onBeforeHandle(() => "A")
    .derive(() => {})

// Into Elysia 1.0
new Elysia()
    .onBeforeHandle({ as: 'global' }, () => "A")
    .derive({ as: 'global' }, () => {})

As much as we hate breaking change and migration, we think this is an important fix that will happen sooner or later to fix problems.

Most of the server might not need to apply migration yourself but heavily depends on plugin authors, or should migration required, it usually take no longer than 5-15 minutes.

For a complete migration note, see Elysia#513.

For the documentation of hook type, see Lifecycle#hook-type

Inline error

Starting from Elysia 0.8, we can use the error function to return a response with a status code for Eden inference.

However, this has some flaws.

If you specify a response schema for a route, Elysia will be unable to provide an accurate auto-completion for the status code.

For example, narrowing down an available status code.

Using import error in Elysia

Inline error can be destructured from handler as follows:

import { Elysia } from 'elysai'

new Elysia()
    .get('/hello', ({ error }) => {
        if(Math.random() > 0.5) return error(418, 'Nagisa')

        return 'Azusa'
    }, {
        response: t.Object({
            200: t.Literal('Azusa'),
            418: t.Literal('Nagisa')
        })
    })

Inline error can produce a fine-grained type from a schema, providing type narrowing, auto-completion, and type checking to the accuracy of value, underlining red squiggly at a value instead of an entire function.

Using inline error function from Elysia with an auto-completion that shows narrowed down status code

We recommended using inline error instead of import error for more accurate type safety.

What does it mean for v1, and what's next

Reaching stable release means we believe that Elysia is stable enough and ready to be used in production.

Maintaining backward compatibility is now one of our goals, putting effort into not introducing breaking changes to Elysia except for security.

Our goal is to make backend development feel easy, fun, and intuitive while making sure that the product built with Elysia will have a solid foundation.

After this, we will be focusing on refining our ecosystem and plugins. Introducing an ergonomic way to handle redundant and mundane tasks, starting some internal plugin rewrite, authentication, synchronize behavior between JIT and non-JIT mode, and universal runtime support.

Bun works excellently in both runtime, package manager and all the toolings they offers, and we believe that Bun is going to be a future of JavaScript.

We believe that by opening Elysia to more runtime and offers interesting Bun specific feature (or at-least easy to config, eg. Bun Loaders API) will eventually gets people to try Bun more than Elysia choosing to support only Bun.

Elysia core itself partially WinterCG compatible, but not all the official plugin works with WinterCG, there are some with Bun specific features, and we want to fix that.

We don't have a specific date or version for universal runtime supports yet as we will gradually adopting and test until we make sure that it would works without unexpected behavior.

You can looks forward for the following runtime to support:

  • Node
  • Deno
  • Cloudflare Worker

We also want to support the following:

  • Vercel Edge Function
  • Netlify Function
  • AWS Lambda / LLRT

More over, we also support, and test Elysia on the following frameworks that support Server Side Rendering or Edge Function:

  • Nextjs
  • Expo
  • Astro
  • SvelteKit

In the meantime, there's an Elysia Polyfills maintained by Bogeychan, one of an active contributor to Elysia.

Additionally, we have rewrote Eden documentation to explain more in depth details about Eden and we think you should check it out.

We also improve several pages, and remove redundant part of the documentation, You can check the affected pages on Elysia 1.0 documentation PR.

And finally, if you have problems with migration and additional questions related to Elysia, feels free to ask one in Elysia's Discord server.

Notable Improvement

Improvement:

  • fine-grained reactive cookie
  • using single source of truth for cookie
  • macro support for websocket
  • add mapResolve
  • add { as: 'global' | 'scoped' | 'local' } to lifecycle event
  • add ephemeral type
  • inline error to handler
  • inline error has auto-completion and type checking based on status code
  • handler now check return type of error based on status code
  • utility Elysia._types for types inference
  • #495 Provide user friendly error for failed parse
  • handler now infers return type for error status for Treaty
  • t.Date now allow stringified date
  • improves type test case
  • add test case for all life-cycle
  • resolve, mapResolve, derive, mapDerive use ephemeral type to scope down accurately
  • inference query dynamic variable

Breaking Change:

  • #513 lifecycle is now local first

Change:

  • group private API property
  • move Elysia.routes to Elysia.router.history
  • detect possible json before return
  • unknown response now return as-is instead of JSON.stringify()
  • change Elysia validation error to JSON instead of string

Bug fix:

  • #466 Async Derive leaks request context to other requests if aot: true
  • #505 Empty ObjectString missing validation inside query schema
  • #503 Beta: undefined class when using decorate and derive
  • onStop callback called twice when calling .stop
  • mapDerive now resolve to Singleton['derive'] instead of Singleton['store']
  • ValidationError doesn't return content-type as application/json
  • validate error(status, value) validate per status
  • derive/resolve always scoped to Global
  • duplicated onError call if not handled
  • #516 server timing breaks beforeHandle guards
  • cookie.remove() doesn't set correct cookie path

Afterword

::: tip The following contains personal feeling, possibly venting, ranting, possibly cringe and unprofessionalism that shouldn't be written in software release note. You may choose to not continue reading as we have stated all the necessary content for the release. :::

2 years ago, I have a tragic memory.

It's easily one of the most painful memory I have, working days and nights to keeps up with unfair tasks that take advantage from loose contract we had with some software house.

It took more than 6 months, and I have to work since I woke up until I sleep (15 hours) on repeat, without doing anything else not even 5 minutes break for a day, no time for relax, nothing beside coding for almost 2 months, not even a single break day, not even weekdays that I knocked out and almost have to work in hospital bed.

I was souless, no purpose in life at all, my only wish is to make it a dream.

At the time, there are so many breaking changes, uncountable new features introduced from loop hole of loose requirement and contract.

Keeping track of it is almost impossible, and we even got scammed not even getting the pay we deserved because of "not satisfied", and we couldn't do anything with it.

It took me a month to recover from a fear of writing code, being unprofessional I couldn't even do my job properly in trauma and consults my manager that I suffered burn out.

That's why we hate breaking change so much, and want to design Elysia to handle changes easily with TypeScript soundness even if it's not good but it's all we have.

I don't want anyone to ever experienced something like that.

We designed a framework to encounter all the flaws that we had from that contract.

The technical flaws I saw in there doesn't have any JavaScript based solution that could satisfies me, yet so I experiment with one.

I could just move on as I could avoid loose contract like this in the future, and make money and not spending most of my free time creating a framework but I didn't.

There's a my favorite part, a quote in the animated short where Mei is against Kiana of the idea that she would sacrifice herself for the world, and Mei replies:

> Yet you shoulder everything alone, at the cost of your life.

> Maybe this is for the greater good...

> But how can I pretend this is the right thing?

> I only know that deep down...

> the world means nothing to me...

> without you

It's depiction of a duality between the person who would sacrifice themself for the world, and the person who would sacrifice themself to save who they love.

If we saw a problem and move on, how can we know that the person who came after us will not stumble upon the same problem we had, someone need to do something.

That someone would sacrifice themself to save the others but then who would save the sacrified one?

The name "Lament of the Fallen" describe that, and why we create Elysia.

*Despite everything about it being my favorite, and I might relate myself personally a bit too much.


Despite being build from the bad memory, and tragic event. It's a privilege to see that Elysia grew into something with so much love. And to see what you built are loved, and well received by others.

Elysia is a work of Open Source developer, and not backed by any company.

We have to do something for living, and build Elysia in free time.

At one point I chose not to not looking for a job straight away just to work on Elysia for several months.

We would love to spent our time to improve Elysia continously, and you could help us with GitHub sponsors to reduce the work we need to support ourself, and have more free time to work on Elysia.

We are just makers that wants to create something to solve problems we have.


We have been creating and experimented a lot with Elysia, shipping real code to clients, and use Elysia in real projects to power tools behind our local community, CreatorsGarten (local tech community, not organization).

It took a lot of time, preparation, and courage to make sure that Elysia is ready for production. Of course, there will be bugs, but we are willing to listen, and fix it.

It's a start of a something new.

And it's possible because of you.

ー SaltyAom

All the incandescent stars of heaven will die at the end of days,

Your gentle soul given to damnation.

"Crimson moon shines upon a town that is smeared in blood"

Cried the diva given into lament.

All those sweeet little dreams buried deep in memories until the very end.


If rescuing you is a sin, I’ll gladly become a sinner.

0.8

2 months ago

Gate

Named after the ending song of Steins;Gate Zero, "Gate of Steiner".

Gate of Steiner isn't focused on new exciting APIs and features but on API stability and a solid foundation to make sure that the API will be stable once Elysia 1.0 is released.

However, we do bring improvement and new features including:

Macro API

Macro allows us to define a custom field to hook and guard by exposing full control of the life cycle event stack.

Allowing us to compose custom logic into a simple configuration with full type safety.

Suppose we have an authentication plugin to restrict access based on role, we can define a custom role field.

import { Elysia } from 'elysia'
import { auth } from '@services/auth'

const app = new Elysia()
    .use(auth)
    .get('/', ({ user }) => user.profile, {
        role: 'admin'
    })

Macro has full access to the life cycle stack, allowing us to add, modify, or delete existing events directly for each route.

const plugin = new Elysia({ name: 'plugin' }).macro(({ beforeHandle }) => {
    return {
        role(type: 'admin' | 'user') {
            beforeHandle(
                { insert: 'before' }, 
                async ({ cookie: { session } }) => {
                  const user = await validateSession(session.value)
                  await validateRole('admin', user)
}
            )
        }
    }
})

We hope that with this macro API, plugin maintainers will be able to customize Elysia to their heart's content opening a new way to interact better with Elysia, and Elysia users will be able to enjoy even more ergonomic API Elysia could provide.

The documentation of Macro API is now available in pattern section.

The next generation of customizability is now only a reach away from your keyboard and imagination.

New Life Cycle

Elysia introduced a new life cycle to fix an existing problem and highly requested API including Resolve and MapResponse: resolve: a safe version of derive. Execute in the same queue as beforeHandle mapResponse: Execute just after afterResponse for providing transform function from primitive value to Web Standard Response

Resolve

A "safe" version of derive.

Designed to append new value to context after validation process storing in the same stack as beforeHandle.

Resolve syntax is identical to derive, below is an example of retrieving a bearer header from Authorization plugin.

import { Elysia } from 'elysia'

new Elysia()
    .guard(
        {
            headers: t.Object({
                authorization: t.TemplateLiteral('Bearer ${string}')
            })
        },
        (app) =>
            app
                .resolve(({ headers: { authorization } }) => {
                    return {
                        bearer: authorization.split(' ')[1]
                    }
                })
                .get('/', ({ bearer }) => bearer)
    )
    .listen(3000)

MapResponse

Executed just after "afterHandle", designed to provide custom response mapping from primitive value into a Web Standard Response.

Below is an example of using mapResponse to provide Response compression.

import { Elysia, mapResponse } from 'elysia'
import { gzipSync } from 'bun'

new Elysia()
    .mapResponse(({ response }) => {
        return new Response(
            gzipSync(
                typeof response === 'object'
                    ? JSON.stringify(response)
                    : response.toString()
            )
        )
    })
    .listen(3000)

Why not use afterHandle but introduce a new API?

Because afterHandle is designed to read and modify a primitive value. Storing plugins like HTML, and Compression which depends on creating Web Standard Response.

This means that plugins registered after this type of plugin will be unable to read a value or modify the value making the plugin behavior incorrect.

This is why we introduce a new life-cycle run after afterHandle dedicated to providing a custom response mapping instead of mixing the response mapping and primitive value mutation in the same queue.

Error Function

We can set the status code by using either set.status or returning a new Response.

import { Elysia } from 'elysia'

new Elysia()
    .get('/', ({ set }) => {
        set.status = 418

        return "I'm a teapot"
    })
    .listen(3000)

This aligns with our goal, to just the literal value to the client instead of worrying about how the server should behave.

However, this is proven to have a challenging integration with Eden. Since we return a literal value, we can't infer the status code from the response making Eden unable to differentiate the response from the status code.

This results in Eden not being able to use its full potential, especially in error handling as it cannot infer type without declaring explicit response type for each status.

Along with many requests from our users wanting to have a more explicit way to return the status code directly with the value, not wanting to rely on set.status, and new Response for verbosity or returning a response from utility function declared outside handler function.

This is why we introduce an error function to return a status code alongside with value back to the client.

import { Elysia, error } from 'elysia' // [!code ++]

new Elysia()
    .get('/', () => error(418, "I'm a teapot")) // [!code ++]
    .listen(3000)

Which is an equivalent to:

import { Elysia } from 'elysia'

new Elysia()
    .get('/', ({ set }) => {
        set.status = 418

        return "I'm a teapot"
    })
    .listen(3000)

The difference is that using an error function, Elysia will automatically differentiate from the status code into a dedicated response type, helping Eden to infer a response based on status correctly.

This means that by using error, we don't have to include the explicit response schema to make Eden infers type correctly for each status code.

import { Elysia, error, t } from 'elysia'

new Elysia()
    .get('/', ({ set }) => {
        set.status = 418
        return "I'm a teapot"
    }, { // [!code --]
        response: { // [!code --]
            418: t.String() // [!code --]
        } // [!code --]
    }) // [!code --]
    .listen(3000)

We recommended using error function to return a response with the status code for the correct type inference, however, we do not intend to remove the usage of set.status from Elysia to keep existing servers working.

Static Content

Static Content refers to a response that almost always returns the same value regardless of the incoming request.

This type of resource on the server is usually something like a public File, video or hardcode value that is rarely changed unless the server is updated.

By far, most content in Elysia is static content. But we also found that many cases like serving a static file or serving an HTML page using a template engine are usually static content.

This is why Elysia introduced a new API to optimize static content by determining the Response Ahead of Time.

new Elysia()
    .get('/', () => Bun.file('video/kyuukurarin.mp4')) // [!code --]
    .get('/', Bun.file('video/kyuukurarin.mp4')) // [!code ++]
    .listen(3000)

Notice that the handler now isn't a function but is an inline value instead.

This will improve the performance by around 20-25% by compiling the response ahead of time.

Default Property

Elysia 0.8 updates to TypeBox to 0.32 which introduces many new features including dedicated RegEx, Deref but most importantly the most requested feature in Elysia, default field support.

Now defining a default field in Type Builder, Elysia will provide a default value if the value is not provided, supporting schema types from type to body.

import { Elysia } from 'elysia'

new Elysia()
    .get('/', ({ query: { name } }) => name, {
        query: t.Object({
            name: t.String({
                default: 'Elysia'
            }) 
        })
    })
    .listen(3000)

This allows us to provide a default value from schema directly, especially useful when using reference schema.

Default Header

We can set headers using set.headers, which Elysia always creates a default empty object for every request.

Previously we could use onRequest to append desired values into set.headers, but this will always have some overhead because a function is called.

Stacking functions to mutate an object can be a little slower than having the desired value set in the first hand if the value is always the same for every request like CORS or cache header.

This is why we now support setting default headers out of the box instead of creating an empty object for every new request.

new Elysia()
    .headers({
        'X-Powered-By': 'Elysia'
    })

Elysia CORS plugin also has an update to use this new API to improve this performance.

Performance and notable improvement

As usual, we found a way to optimize Elysia even more to make sure you have the best performance out of the box.

Removable of bind

We found that .bind is slowing down the path lookup by around ~5%, with the removal of bind from our codebase we can speed up that process a little bit.

Static Query Analysis

Elysia Static Code Analysis is now able to infer a query if the query name is referenced in the code.

This usually results in a speed-up of 15-20% by default.

Video Stream

Elysia now adds content-range header to File and Blob by default to fix problems with large files like videos that require to be sent by chunk.

To fix this, Elysia now adds content-range header to by default.

Elysia will not send the content-range if the status code is set to 206, 304, 412, 416, or if the headers explicitly provide the content-range.

It's recommended to use ETag plugin to handle the correct status code to avoid content-range collision from the cache.

This is an initial support for content-range header, we have created a discussion to implement more accurate behavior based on RPC-7233 in the future. Feels free to join the discussion to propose a new behavior for Elysia with content-range and etag generation at Discussion 371.

Runtime Memory improvement

Elysia now reuses the return value of the life cycle event instead of declaring a new dedicated value.

This reduces the memory usage of Elysia by a little bit better for peak concurrent requests a little better.

Plugins

Most official plugins now take advantage of newer Elysia.headers, Static Content, MapResponse ,and revised code to comply with static code analysis even more to improve the overall performance.

Plugins that are improved by this are the following: Static, HTML, and CORS.

Validation Error

Elysia now returns validation error as JSON instead of text.

Showing current errors and all errors and expected values instead, to help you identify an error easier.

Example:

{
  "type": "query",
  "at": "password",
  "message": "Required property",
  "expected": {
    "email": "[email protected]",
    "password": ""
  },
  "found": {
    "email": "[email protected]"
  },
  "errors": [
    {
      "type": 45,
      "schema": {
        "type": "string"
      },
      "path": "/password",
      "message": "Required property"
    },
    {
      "type": 54,
      "schema": {
        "type": "string"
      },
      "path": "/password",
      "message": "Expected string"
    }
  ]
}

expect, and errors fields are removed by default on the production environment to prevent an attacker from identifying a model for further attack.

Notable Improvement

Improvement

  • lazy query reference
  • add content-range header to Blob by default
  • update TypeBox to 0.32
  • override lifecycle response of be and af

Breaking Change

  • afterHandle no longer early return

Change

  • change validation response to JSON
  • differentiate derive from decorator['request'] as decorator['derive']
  • derive now don't show infer type in onRequest

Bug fix

  • remove headers, path from PreContext
  • remove derive from PreContext
  • Elysia type doesn't output custom error

Afterward

It has been a great journey since the first release.

Elysia evolved from a generic REST API framework to an ergonomic framework to support End-to-end type safety, OpenAPI documentation generation, we we would like to keep introduce more exciting features in the future.


We are happy to have you, and the developers to build amazing stuff with Elysia and your overwhelming continuous support for Elysia encourages us to keep going.

It's exciting to see Elysia grow more as a community:

  • Scalar's Elysia theme for new documentation instead of Swagger UI.
  • pkgx supports Elysia out of the box.
  • Community submitted Elysia to TechEmpower ranking at 32 out of all frameworks in composite score, even surpassing Go's Gin and Echo.

We are now trying to provide more support for each runtime, plugin, and integration to return the kindness you have given us, starting with the rewrite of the documentation with more detailed and beginner-friendliness, Integration with Nextjs, Astro and more to come in the future.

And since the release of 0.7, we have seen fewer issues compared to the previous releases.

Now we are preparing for the first stable release of Elysia, Elysia 1.0 aiming to release in Q1 2024 to repay your kindness. Elysia will now enter soft API lockdown mode, to prevent an API change and make sure that there will be no or less breaking change once the stable release arrives.

So you can expect your Elysia app to work starting from 0.7 with no or minimal change to support the stable release of Elysia.

We again thank your continuous support for Elysia, and we hope to see you again on the stable release day.

Keep fighting for all that is beautiful in this world.

Until then, El Psy Congroo.

A drop in the darkness 小さな命

Unique and precious forever

Bittersweet memories 夢幻の刹那

Make this moment last, last forever

We drift through the heavens 果てない想い

Filled with the love from up above

He guides my travels せまる刻限

Shed a tear and leap to a new world

0.7

7 months ago

Landscape of wild and mountain in the night full of star

Name after our never giving up spirit, our beloved Virtual YouTuber, Suicopath Hoshimachi Suisei, and her brilliance voice: 「Stellar Stellar」from her first album:「Still Still Stellar」

For once being forgotten, she really is a star that truly shine in the dark.

Stellar Stellar brings many exciting new update to help Elysia solid the foundation, and handle complexity with ease, featuring:

  • Entirely rewrite type, up to 13x faster type inference.
  • "Trace" for declarative telemetry and better performance audit.
  • Reactive Cookie model and cookie valiation to simplify cookie handling.
  • TypeBox 0.31 with a custom decoder support.
  • Rewritten Web Socket for even better support.
  • Definitions remapping, and declarative affix for preventing name collision.
  • Text-based status

Rewritten Type

Core feature of Elysia about developer experience.

Type is one of the most important aspect of Elysia, as it allows us to do many amazing thing like unified type, syncing your business logic, typing, documentation and frontend.

We want you to have an outstanding experience with Elysia, focusing on your business logic part, and let's Elysia handle the rest whether it's type-inference with unified type, and Eden connector for syncing type with backend.

To achieve that, we put our effort on creating a unified type system for to synchronize all of the type, but as the feature grow, we found that our type inference might not be fast enough from our lack of TypeScript experience we have year ago.

With our experience we made along the way of handling complex type system, various optimization and many project like Mobius. We challenge our self to speed up our type system once again, making this a second type rewrite for Elysia.

We delete and rewrite every Elysia type from ground up to make Elysia type to be magnitude faster.

Here's a comparison between 0.6 and 0.7 on a simple Elysia.get code:

0.6

0.7

With our new found experience, and newer TypeScript feature like const generic, we are able to simplify a lot of our code, reducing our codebase over a thousand line in type.

Allowing us to refine our type system to be even faster, and even more stable.

Comparison between Elysia 0.6 and 0.7 on complex project with our 300 routes, and 3,500 lines of type declaration

Using Perfetto and TypeScript CLI to generate trace on a large-scale and complex app, we measure up to 13x inference speed.

And if you might wonder if we might break type inference with 0.6 or not, we do have a unit test in type-level to make sure most of the case, there's no breaking change for type.

We hope this improvement will help you with even faster type inference like faster auto-completion, and load time from your IDE to be near instant to help your development to be even more faster and more fluent than ever before.

Trace

Performance is another one of important aspect for Elysia.

We don't want to be fast for benchmarking purpose, we want you to have a real fast server in real-world scenario, not just benchmarking.

There are many factor that can slow down your app, and it's hard to identifying one, that's why we introduce "Trace".

Trace allow us to take tap into a life-cycle event and identifying performance bottleneck for our app.

Example of usage of Trace

This example code allow us tap into all beforeHandle event, and extract the execution time one-by-one before setting the Server-Timing API to inspect the performance bottleneck.

And this is not limited to only beforeHandle, and event can be trace even the handler itself. The naming convention is name after life-cycle event you are already familiar with.

This API allows us to effortlessly auditing performance bottleneck of your Elysia server and integrate with the report tools of your choice.

By default, Trace use AoT compilation and Dynamic Code injection to conditionally report and even that you actually use automatically, which means there's no performance impact at all.

We merged our cookie plugin into Elysia core.

Same as Trace, Reactive Cookie use AoT compilation and Dynamic Code injection to conditionally inject the cookie usage code, leading to no performance impact if you don't use one.

Reactive Cookie take a more modern approach like signal to handle cookie with an ergonomic API.

Example usage of Reactive COokie

There's no getCookie, setCookie, everything is just a cookie object.

When you want to use cookie, you just extract the name get/set its value like:

app.get('/', ({ cookie: { name } }) => {
    // Get
    name.value

    // Set
    name.value = "New Value"
})

Then cookie will be automatically sync the value with headers, and the cookie jar, making the cookie object a single source of truth for handling cookie.

The Cookie Jar is reactive, which means that if you don't set the new value for the cookie, the Set-Cookie header will not be send to keep the same cookie value and reduce performance bottleneck.

With the merge of cookie into the core of Elysia, we introduce a new Cookie Schema for validating cookie value.

This is useful when you have to strictly validate cookie session or want to have a strict type or type inference for handling cookie.

app.get('/', ({ cookie: { name } }) => {
    // Set
    name.value = {
        id: 617,
        name: 'Summoning 101'
    }
}, {
    cookie: t.Cookie({
        value: t.Object({
            id: t.Numeric(),
            name: t.String()
        })
    })
})

Elysia encode and decode cookie value for you automatically, so if you want to store JSON in a cookie like decoded JWT value, or just want to make sure if the value is a numeric string, you can do that effortlessly.

And lastly, with an introduction of Cookie Schema, and t.Cookie type. We are able to create a unified type for handling sign/verify cookie signature automatically.

Cookie signature is a cryptographic hash appended to a cookie's value, generated using a secret key and the content of the cookie to enhance security by adding a signature to the cookie.

This make sure that the cookie value is not modified by malicious actor, helps in verifying the authenticity and integrity of the cookie data.

To handle cookie signature in Elysia, it's a simple as providing a secert and sign property:

new Elysia({
    cookie: {
        secret: 'Fischl von Luftschloss Narfidort'
    }
})
    .get('/', ({ cookie: { profile } }) => {
        profile.value = {
            id: 617,
            name: 'Summoning 101'
        }
    }, {
        cookie: t.Cookie({
            profile: t.Object({
                id: t.Numeric(),
                name: t.String()
            })
        }, {
            sign: ['profile']
        })
    })

By provide a cookie secret, and sign property to indicate which cookie should have a signature verification.

Elysia then sign and unsign cookie value automatically, eliminate the need of sign / unsign function manually.

Elysia handle Cookie's secret rotation automatically, so if you have to migrate to a new cookie secret, you can just append the secret, and Elysia will use the first value to sign a new cookie, while trying to unsign cookie with the rest of the secret if match.

new Elysia({
    cookie: {
        secret: ['Vengeance will be mine', 'Fischl von Luftschloss Narfidort']
    }
})

The Reactive Cookie API is declarative and straigth forward, and there's some magical thing about the ergonomic it provide, and we really looking forward for you to try it.

TypeBox 0.31

With the release of 0.7, we are updating to TypeBox 0.31 to brings even more feature to Elysia.

This brings new exciting feature like support for TypeBox's Decode in Elysia natively.

Previously, a custom type like Numeric require a dyanmic code injection to convert numeric string to number, but with the use of TypeBox's decode, we are allow to define a custom function to encode and decode the value of a type automatically.

Allowing us to simplify type to:

Numeric: (property?: NumericOptions<number>) =>
    Type.Transform(Type.Union([Type.String(), Type.Number(property)]))
        .Decode((value) => {
            const number = +value
            if (isNaN(number)) return value

            return number
        })
        .Encode((value) => value) as any as TNumber,

Instead of relying on an extensive check and code injection, it's simplified by a Decode function in TypeBox.

We have rewrite all type that require Dynamic Code Injection to use Transform for easier code maintainance.

Not only limited to that, with t.Transform you can now also define a custom type to with a custom function to Encode and Decode manually, allowing you to write a more expressive code than ever before.

We can't wait to see what you will brings with the introduction of t.Transform.

New Type

With an introduction Transform, we have add a new type like t.ObjectString to automatically decode a value of Object in request.

This is useful when you have to use multipart/formdata for handling file uploading but doesn't support object. You can now just use t.ObjectString() to tells Elysia that the field is a stringified JSON, so Elysia can decode it automatically.

new Elysia({
    cookie: {
        secret: 'Fischl von Luftschloss Narfidort'
    }
})
    .post('/', ({ body: { data: { name } } }) => name, {
        body: t.Object({
            image: t.File(),
            data: t.ObjectString({
                name: t.String()
            })
        })
    })

We hope that this will simplify the need for JSON with multipart.

Rewritten Web Socket

Aside from entirely rewritten type, we also entirely rewritten Web Socket as well.

Previously, we found that Web Socket has 3 major problem:

  1. Schema is not strictly validated
  2. Slow type inference
  3. The need for .use(ws()) in every plugin

With this new update, solve all of problem above and while improving the performance of Web Socket.

  1. Now, Elysia's Web Socket is strictly validated, and type is synced automatically.
  2. We remove the need for .use(ws()) for using WebSocket in every plugin.

And we bring a performance improvement to already fast Web Socket.

Previously, Elysia Web Socket needs to handle routing for every incoming request to unified the data and context, but with the new model. Web Socket now can infers the data for its route without relying on router.

Bringing the performance to near Bun native Web Socket performance.

Thanks to Bogeychan for providing the test case for Elysia Web Socket, helping us to rewrite Web Socket with confidence.

Definitions Remap

Proposed on #83 by Bogeychan

To summarize, Elysia allows us to decorate and request and store with any value we desire, however some plugin might a duplicate name with the value we have, and sometime plugin has a name collision but we can't rename the property at all.

Remapping

As the name suggest, this allow us to remap existing state, decorate, model, derive to anything we like to prevent name collision, or just wanting to rename a property.

By providing a function as a first parameters, the callback will accept current value, allowing us to remap the value to anything we like.

new Elysia()
    .state({
        a: "a",
        b: "b"
    })
    // Exclude b state
    .state(({ b, ...rest }) => rest)

This is useful when you have to deal with a plugin that has some duplicate name, allowing you to remap the name of the plugin:

new Elysia()
    .use(
        plugin
            .decorate(({ logger, ...rest }) => ({
                pluginLogger: logger,
                ...rest
            }))
    )

Remap function can be use with state, decorate, model, derive to helps you define a correct property name and preventing name collision.

Affix

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

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, you can also remap all property of the plugin:

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

We hope that remapping and affix will provide a powerful API for you to handle multiple complex plugin with ease.

True Encapsulation Scope

With the introduction of Elysia 0.7, Elysia can now truly encapsulate an instance by treating a scoped instance as another instance.

The new scope model can even prevent event like onRequest to be resolve on a main instance which is not possible.

const plugin = new Elysia({ scoped: true, prefix: '/hello' })
    .onRequest(() => {
        console.log('In Scoped')
    })
    .get('/', () => 'hello')

const app = new Elysia()
    .use(plugin)
    // 'In Scoped' will not log
    .get('/', () => 'Hello World')

Further more, scoped is now truly scoped down both in runtime, and type level which is not possible without the type rewrite mentioned before.

This is exciting from maintainer side because previously, it's almost impossible to truly encapsulate the scope the an instance, but using mount and WinterCG compilance, we are finally able to truly encapsulate the instance of the plugin while providing a soft link with main instance property like state, decorate.

Text based status

There are over 64 standard HTTP status codes to remember, and to admit it, sometime we forget them and search it on MDN.

This is why we ship 64 HTTP Status codes in text-based form with autocompletion for you.

Example usage of text-based status

Text will then resolved to status code automatically as expected.

As you type, there should be auto-completion for text popup automatically for your IDE, whether it's NeoVim or VSCode, as it's a built-in TypeScript fefature.

Text-base status code showing auto-completion

This is a small ergonomic feature to helps you develop your server without switching between IDE and MDN to search for a correct status code.

Notable Improvement

Improvement:

  • onRequest can now be async
  • add Context to onError
  • lifecycle hook now accept array function
  • static Code Analysis now support rest parameter
  • breakdown dynamic router into single pipeline instead of inlining to static router to reduce memory usage
  • set t.File and t.Files to File instead of Blob
  • skip class instance merging
  • handle UnknownContextPassToFunction
  • #157 WebSocket - added unit tests and fixed example & api by @bogeychan
  • #179 add github action to run bun test by @arthurfiorette

Breaking Change:

  • remove ws plugin, migrate to core
  • rename addError to error

Change:

  • using single findDynamicRoute instead of inlining to static map
  • remove mergician
  • remove array routes due to problem with TypeScript
  • rewrite Type.ElysiaMeta to use TypeBox.Transform

Bug fix:

  • strictly validate response by default
  • t.Numeric not working on headers / query / params
  • t.Optional(t.Object({ [name]: t.Numeric })) causing error
  • add null check before converting Numeric
  • inherits store to instance plugin
  • handle class overlapping
  • #187 InternalServerError message fixed to "INTERNAL_SERVER_ERROR" instead of "NOT_FOUND" by @bogeychan
  • #167 mapEarlyResponse with aot on after handle

Afterward

Since the latest release, we have gained over 2,000 stars on GitHub!

Taking a look back, we have progressed more than we have ever imagined back then.

Pushing the boundary of TypeScript, and developer experience even to the point that we are doing something we feels truly profound.

With every release, we are gradually one step closer to brings the future we drawn long time ago.

A future where we can freely create anything we want with an astonishing developer experience.

We feels truly thanksful to be loved by you and lovely community of TypeScript and Bun.

It's exciting to see Elysia is bring to live with amazing developer like:

And much more developers that choose Elysia for their next project.

Our goal is simple, to brings an eternal paradise where you can persue your dream and everyone can live happily.

Thanks you and your love and overwhelming support for Elysia, we hope we can paint the future to persue our dream a reality one day.

May all the beauty be blessed

Stretch out that hand as if to reach someone

I'm just like you, nothing special

That's right, I'll sing the song of the night

Stellar Stellar

In the middle of the world, the universe

The music won't ever, ever stop tonight

That's right, I'd always longed to be

Not Cinderella, forever waiting

But the prince that came to for her

Cause I'm a star, that's why

Stellar Stellar

0.6

7 months ago

Chess piece

Named after the opening of the legendary anime, "No Game No Life", 「This Game」composed by Konomi Suzuki.

This Game push the boundary of medium-size project to large-scale app with re-imagined plugin model, dynamic mode, pushing developer experience with declarative custom error, collecting more metric 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)

Read more on Elysia release note.

0.5

1 year ago

radiant

Named after Arknights' original music, 「Radiant」composed by Monster Sirent Records.

Radiant push the boundary of performance with more stability improvement especially types, and dynamic routes.

Static Code Analysis

With Elysia 0.4 introducing Ahead of Time compliation, allowing Elysia to optimize function calls, and eliminate many overhead we previously had.

Today we are expanding Ahead of Time compliation to be even faster wtih Static Code Analysis, to be the fastest Bun web framework.

Static Code Analysis allow Elysia to read your function, handlers, life-cycle and schema, then try to adjust fetch handler compile the handler ahead of time, and eliminating any unused code and optimize where possible.

For example, if you're using schema with body type of Object, Elysia expect that this route is JSON first, and will parse the body as JSON instead of relying on dynamic checking with Content-Type header:

app.post('/sign-in', ({ body }) => signIn(body), {
    schema: {
        body: t.Object({
            username: t.String(),
            password: t.String()
        })
    }
})

This allows us to improve performance of body parsing by almost 2.5x.

With Static Code Analysis, instead of relying on betting if you will use expensive properties or not.

Elysia can read your code and detect what you will be using, and adjust itself ahead of time to fits your need.

This means that if you're not using expensive property like query, or body, Elysia will skip the parsing entirely to improve the performance.

// Body is not used, skip body parsing
app.post('/id/:id', ({ params: { id } }) => id, {
    schema: {
        body: t.Object({
            username: t.String(),
            password: t.String()
        })
    }
})

With Static Code Analysis, and Ahead of Time compilation, you can rest assure that Elysia is very good at reading your code and adjust itself to maximize the performance automatically.

Static Code Analysis allows us to improve Elysia performance beyond we have imagined, here's a notable mention:

  • overall improvement by ~15%
  • static router fast ~33%
  • empty query parsing ~50%
  • strict type body parsing faster by ~100%
  • empty body parsing faster by ~150%

With this improvement, we are able to surpass Stricjs in term of performance, compared using Elysia 0.5.0-beta.0 and Stricjs 2.0.4

We intent to explain this in more detail with our research paper to explain this topic and how we improve the performance with Static Code Analysis to be published in the future.

New Router, "Memoirist"

Since 0.2, we have been building our own Router, "Raikiri".

Raikiri served it purposed, it's build on the ground up to be fast with our custom Radix Tree implementation.

But as we progress, we found that Raikiri doesn't perform well complex recoliation with of Radix Tree, which cause developers to report many bugs especially with dynamic route which usually solved by re-ordering routees.

We understand, and patched many area in Raikiri's Radix Tree reconcilation algorithm, however our algorithm is complex, and getting more expensive as we patch the router until it doesn't fits our purpose anymore.

That's why we introduce a new router, "Memoirist".

Memoirist is a stable Raix Tree router to fastly handle dynamic path based on Medley Router's algorithm, while on the static side take advantage of Ahead of Time compilation.

With this release, we will be migrating from Raikiri to Memoirist for stability improvement and even faster path mapping than Raikiri.

We hope that any problems you have encountered with Raikiri will be solved with Memoirist and we encourage you to give it a try.

TypeBox 0.28

TypeBox is a core library that powered Elysia's strict type system known as Elysia.t.

In this update, we update TypeBox from 0.26 to 0.28 to make even more fine-grained Type System near strictly typed language.

We update Typebox to improve Elysia typing system to match new TypeBox feature with newer version of TypeScript like Constant Generic

new Elysia()
    .decorate('version', 'Elysia Radiant')
    .model(
        'name',
        Type.TemplateLiteral([
            Type.Literal('Elysia '),
            Type.Union([
                Type.Literal('The Blessing'),
                Type.Literal('Radiant')
            ])
        ])
    )
    // Strictly check for template literal
    .get('/', ({ version }) => version)

This allows us to strictly check for template literal, or a pattern of string/number to validate for your on both runtime and development process all at once.

Ahead of Time & Type System

And with Ahead of Time compilation, Elysia can adjust itself to optimize and match schema defined type to reduce overhead.

That's why we introduced a new Type, URLEncoded.

As we previously mentioned before, Elysia now can take an advantage of schema and optimize itself Ahead of Time, body parsing is one of more expensive area in Elysia, that's why we introduce a dedicated type for parsing body like URLEncoded.

By default, Elysia will parse body based on body's schema type as the following:

  • t.URLEncoded -> application/x-www-form-urlencoded
  • t.Object -> application/json
  • t.File -> multipart/form-data
  • the rest -> text/plain

However, you can explictly tells Elysia to parse body with the specific method using type as the following:

app.post('/', ({ body }) => body, {
    type: 'json'
})

type may be one of the following:

type ContentType = |
    // Shorthand for 'text/plain'
    | 'text'
    // Shorthand for 'application/json'
    | 'json'
    // Shorthand for 'multipart/form-data'
    | 'formdata'
    // Shorthand for 'application/x-www-form-urlencoded'\
    | 'urlencoded'
    | 'text/plain'
    | 'application/json'
    | 'multipart/form-data'
    | 'application/x-www-form-urlencoded'

You can find more detail at explicity body page in concept.

Numeric Type

We found that one of the redundant task our developers found using Elysia is to parse numeric string.

That's we introduce a new Numeric Type.

Previously on Elysia 0.4, to parse numeric string, we need to use transform to manually parse the string ourself.

app.get('/id/:id', ({ params: { id } }) => id, {
    schema: {
        params: t.Object({
            id: t.Number()
        })
    },
    transform({ params }) {
        const id = +params.id

        if(!Number.isNan(id))
            params.id = id
    }
})

We found that this step is redundant, and full of boiler-plate, we want to tap into this problem and solve it in a declarative way.

Thanks to Static Code Analysis, Numeric type allow you to defined a numeric string and parse it to number automatically.

Once validated, a numeric type will be parsed as number automatically both on runtime and type level to fits our need.

app.get('/id/:id', ({ params: { id } }) => id, {
    params: t.Object({
        id: t.Numeric()
    })
})

You can use numeric type on any property that support schema typing, including:

  • params
  • query
  • headers
  • body
  • response

We hope that you will find this new Numeric type useful in your server.

You can find more detail at numeric type page in concept.

With TypeBox 0.28, we are making Elysia type system we more complete, and we excited to see how it play out on your end.

Inline Schema

You might have notice already that our example are not using a schema.type to create a type anymore, because we are making a breaking change to move schema and inline it to hook statement instead.

// ? From
app.get('/id/:id', ({ params: { id } }) => id, {
    schema: {
        params: t.Object({
            id: t.Number()
        })
    },
})

// ? To
app.get('/id/:id', ({ params: { id } }) => id, {
    params: t.Object({
        id: t.Number()
    })
})

We think a lot when making a breaking change especially to one of the most important part of Elysia.

Based on a lot of tinkering and real-world usage, we try to suggest this new change for our community with a vote, and found that around 60% of Elysia developer are happy with migrating to the inline schema.

But we also listen the the rest of our community, and try to get around with the argument against this decision:

Clear separation

With the old syntax, you have to explicitly tells Elysia that the part you are creating are a schema using Elysia.t.

Creating a clear separation between life-cycle and schema are more clear and has a better readability.

But from our intense test, we found that most people don't have any problem struggling reading a new syntax, separating life-cycle hook from schema type, we found that it still has clear separation with t.Type and function, and a different syntax highlight when reviewing the code, although not as good as clear as explicit schema, but people can get used to the new syntax very quickly especially if they are familiar the Elysia.

Auto completion

One of the other area that people are concerned about are reading auto-completion.

Merging schema and life-cycle hook caused the auto-completion to have around 10 properties for auto-complete to suggest, and based on many proven general User Experience research, it can be frastating for user to that many options to choose from, and can cause a steeper learning curve.

However, we found that the schema property name of Elysia is quite predictable to get over this problem once developer are used to Elysia type.

For example, if you want to access a headers, you can acceess headers in Context, and to type a headers, you can type a header in a hook, both shared a same name for predictability.

With this, Elysia might have a little more learning curve, however it's a trade-off that we are willing to take for better developer experience.

"headers" fields

Previously, you can get headers field by accessing request.headers.get, and you might wonder why we don't ship headers by default.

app.post('/headers', ({ request: { headers } }) => {
    return headers.get('content-type')
})

Because parsing a headers has it own overhead, and we found that many developers doesn't access headers often, so we decided to leave headers un-implemented.

But that has changed with Static Code Analysis, Elysia can read your code if you intend to use a header or, and then dynamically parse headers based on your code.

Static Code Analysis allows us to more new new features without any overhead.

app.post('/headers', ({ headers }) => headers['content-type'])

Parsed headers will be available as plain object with a lower-case key of the header name.

State, Decorate, Model rework

One of the main feature of Elysia is able to customize Elysia to your need.

We revisits state, decorate, and setModel, and we saw that api is not consistant, and can be improved.

We found that many have been using state, and decorate repeatly for when setting multiple value, and want to set them all at once as same as setModel, and we want to unified API specification of setModel to be used the same way as state and decorate to be more predictable.

So we renamed setModel to model, and add support for setting single and multiple value for state, decorate, and model with function overloading.

import { Elysia, t } from 'elysia'

const app = new Elysia()
	// ? set model using label
	.model('string', t.String())
	.model({
		number: t.Number()
	})
	.state('visitor', 1)
	// ? set model using object
	.state({
		multiple: 'value',
		are: 'now supported!'
	})
	.decorate('visitor', 1)
	// ? set model using object
	.decorate({
		name: 'world',
		number: 2
	})

And as we raised minimum support of TypeScript to 5.0 to improve strictly typed with Constant Generic.

state, decorate, and model now support literal type, and template string to strictly validate type both runtime and type-level.

	// ? state, decorate, now support literal
app.get('/', ({ body }) => number, {
		body: t.Literal(1),
		response: t.Literal(2)
	})

Group and Guard

We found that many developers often use group with guard, we found that nesting them can be later redundant and maybe boilerplate full.

Starting with Elysia 0.5, we add a guard scope for .group as an optional second parameter.

// ✅ previously, you need to nest guard inside a group
app.group('/v1', (app) =>
    app.guard(
        {
            body: t.Literal()
        },
        (app) => app.get('/student', () => 'Rikuhachima Aru')
    )
)

// ✅ new, compatible with old syntax
app.group(
    '/v1', {
        body: t.Literal('Rikuhachima Aru')
    }, 
    app => app.get('/student', () => 'Rikuhachima Aru')
)

// ✅ compatible with function overload
app.group('/v1', app => app.get('/student', () => 'Rikuhachima Aru'))

We hope that you will find all these new revisited API more useful and fits more to your use-case.

Type Stability

Elysia Type System is complex.

We can declare variable on type-level, reference type by name, apply multiple Elysia instance, and even have support for clousure-like at type level, which is really complex to make you have the best developer experience especially with Eden.

But sometime type isn't working as intent when we update Elysia version, because we have to manually check it before every release, and can caused human error.

With Elysia 0.5, we add unit-test for testing at type-level to prevent possible bugs in the future, these tests will run before every release and if error happens will prevent us for publishing the package, forcing us to fix the type problem.

Which means that you can now rely on us to check for type integrity for every release, and confident that there will be less bug in regard of type reference.


Notable Improvement:

  • Add CommonJS support for running Elysia with Node adapter
  • Remove manual fragment mapping to speed up path extraction
  • Inline validator in composeHandler to improve performance
  • Use one time context assignment
  • Add support for lazy context injection via Static Code Analysis
  • Ensure response non nullability
  • Add unioned body validator check
  • Set default object handler to inherits
  • Using constructor.name mapping instead of instanceof to improve speed
  • Add dedicated error constructor to improve performance
  • Conditional literal fn for checking onRequest iteration
  • improve WebSocket type

Breaking Change:

  • Rename innerHandle to fetch
    • to migrate: rename .innerHandle to fetch
  • Rename .setModel to .model
    • to migrate: rename setModel to model
  • Remove hook.schema to hook
    • to migrate: remove schema and curly brace schema.type:
    // from
    app.post('/', ({ body }) => body, {
        schema: {
            body: t.Object({
                username: t.String()
            })
        }
    })
    
    // to
    app.post('/', ({ body }) => body, {
        body: t.Object({
            username: t.String()
        })
    })
    
  • remove mapPathnameRegex (internal)

Afterward

Pushing performance boundary of JavaScript with Bun is what we really excited!

Even with the new features every release, Elysia keeps getting faster, with an improved reliabilty and stability, we hope that Elysia will become one of the choice for the next generation TypeScript framework.

We're glad to see many talent open-source developers bring Elysia to life with their outstanding work like Bogeychan's Elysia Node and Deno adapter, Rayriffy's Elysia rate limit, and we hope to see yours in the future too!

Thanks for your continuous support for Elysia, and we hope to see you on the next release.

I won't let the people down, gonna raise them high

We're getting louder everyday, yeah, we're amplified

Stunning with the light

You're gonna want to be on my side

Yeah, you know it's full speed ahead

0.4

1 year ago

moonlit night concert

Named after the opening music of "The Liar Princess and the Blind Prince" trailer, 「月夜の音楽会」(Moonlit Night Concert) composed and sang by Akiko Shikata.

This version doesn't introduce an exciting new feature, later but a foundation for more solid ground, and internal improvement for the future of Elysia.

Ahead of Time Complie

By default, Elysia has to deal with conditional checking in various situations, for example, checking if the life-cycle of the route existed before performing, or unwrapping validation schema if provided.

This introduces a minimal overhead to Elysia and overall because even if the route doesn't have a life-cycle event attached to it, it needs to be runtime checked.

Since every function is checked on compile time, it's not possible to have a conditional async, for example, a simple route that returns a file should be synced, but since it's compile-time checking, some routes might be async thus making the same simple route async too.

An async function introduces an additional tick to the function, making it a bit slower. But since Elysia is a foundation for web servers, we want to optimize every part to make sure that you don't run into performance problems.

We fix this small overhead by introducing Ahead Time Compilation.

As the name suggests, instead of checking dynamic life-cycle and validation on the runtime, Elysia checks life-cycle, validation, and the possibility of an async function and generates a compact function, removing an unnecessary part like an un-used life-cycle and validation.

Making conditional async function possible, since instead of using a centralized function for handling, we compose a new function especially created for each route instead. Elysia then checks all life-cycle functions and handlers to see if there's an async, and if not, the function will be synced to reduce additional overhead.

TypeBox 0.26

TypeBox is a library that powered Elysia's validation and type provider to create a type-level single source of truth, re-exported as Elysia.t.

In this update, we update TypeBox from 0.25.4 to 0.26.

This brings a lot of improvement and new features, for example, a Not type and Convert for coercion value which we might support in some next version of Elysia.

But the one benefit for Elysia would be, Error.First() which allows us to get the first error of type instead of using Iterator, this reduces overhead in creating a new Error to send back to the client.

There are some changes to TypeBox and Elysia.t that normally wouldn't have much effect on your end, but you can see what's a new feature in TypeBox release here.

Validate response per status

Previously, Elysia's response validate multiple status responses using union type.

This might have unexpected results for highly dynamic apps with a strict response for status. For example if you have a route like:

app.post('/strict-status', process, {
    schema: {
        response: {
            200: t.String(),
            400: t.Number()
        }
    }
})

It's expected that if 200 response is not a string, then it should throw a validation error, but in reality, it wouldn't throw an error because response validation is using union. This might leave an unexpected value to the end user and a type error for Eden Treaty.

With this release, a response is validated per status instead of union, which means that it will strictly validate based on response status instead of unioned type.

Separation of Elysia Fn

Elysia Fn is a great addition to Elysia, with Eden, it breaks the boundary between client and server allowing you to use any server-side function in your client, fully type-safe and even with primitive types like Error, Set, and Map.

But with the primitive type support, Elysia Fn depends on "superjson" which is around 38% of Elysia's dependency size.

In this release, to use Elysia Fn, you're required to explicitly install @elysiajs/fn to use Elysia Fn. This approach is like installing an additional feature same as cargo add --feature.

This makes Elysia lighter for servers that don't use Elysia Fn, Following our philosophy, To ensure that you have what you actually need

However, if you forgot to install Elysia Fn and accidentally use Elysia Fn, there will be a type warning reminding you to install Elysia Fn before usage, and a runtime error telling the same thing.

For migration, besides a breaking change of installing @elysiajs/fn explicitly, there's no migration need.

Conditional Route

This release introduces .if method for registering a conditional route or plugin.

This allows you to declaratively for a specific conditional, for example excluding Swagger documentation from the production environment.

const isProduction = process.env.NODE_ENV === 'production'

const app = new Elysia().if(!isProduction, (app) =>
    app.use(swagger())
)

Eden Treaty will be able to recognize the route as if it's a normal route/plugin.

Custom Validation Error

Big thanks to amirrezamahyari on #31 which allows Elysia to access TypeBox's error property, for a better programmatically error response.

new Elysia()
    .onError(({ code, error, set }) => {
        if (code === 'NOT_FOUND') {
            set.status = 404

            return 'Not Found :('
        }

        if (code === 'VALIDATION') {
            set.status = 400

            return {
                fields: error.all()
            }
        }
    })
    .post('/sign-in', () => 'hi', {
        schema: {
            body: t.Object({
                username: t.String(),
                password: t.String()
            })
        }
    })
    .listen(8080)

Now you can create a validation error for your API not limited to only the first error.


Notable Improvement:

  • Update TypeBox to 0.26.8
  • Inline a declaration for response type
  • Refactor some type for faster response
  • Use Typebox Error().First() instead of iteration
  • Add innerHandle for returning an actual response (for benchmark)

Breaking Change:

  • Separate .fn to @elysiajs/fn

Afterward

This release might not be a big release with a new exciting feature, but this improve a solid foundation, and Proof of Concept for planned I have for Elysia in the future, and making Elysia even faster and more versatile than it was.

I'm really excited for what will be unfold in the future.

Thank you for your continuous support for Elysia~

the moonlit night concert, our secret

let’s start again without letting go of this hand

the moonlit night concert, our bonds

I want to tell you, “you are not a liar”

the memories in my heart is like flower that keeps blooming

no matter what you look like, you are my songstress

be by my side tonight

0.3

1 year ago

Looking for Edge of Ground

Named after Camellia's song「大地の閾を探して [Looking for Edge of Ground]」ft. Hatsune Miku, is the last track of my most favorite's Camellia album,「U.U.F.O」. This song has a high impact on me personally, so I'm not taking the name lightly.

This is the most challenging update, bringing the biggest release of Elysia yet, with rethinking and redesigning of Elysia architecture to be highly scalable while making less breaking change as possible.

I'm pleased to announce the release candidate of Elysia 0.3 with exciting new features coming right up.

Elysia Fn

Introducing Elysia Fn, run any backend function on the frontend with full auto-completion and full type support.

https://user-images.githubusercontent.com/35027979/223431969-7b3feac6-a885-4e3f-a3a3-77cac1d3309a.mp4

For rapid development, Elysia Fn allows you to "expose" backend code to call from the frontend with full type-safety, autocompletion, original code comment, and "click-to-definition", allowing you to speed up the development.

You can use Elysia Fn with Eden for full-type safety via Eden Fn.

Permission

You can limit allow or deny scopes of the function, check for authorization header and other headers' fields, validate parameters, or limit keys access programmatically.

Keys checking supports type-safety and auto-completion of all possible functions, so you're not missing out on some function or accidentally typing down the wrong name. Narrowed Key

And narrowing the scope of property programmatically also narrow down the type of parameters, or in other words, full type-safety. Narrowed Params

Technical detail

In technical detail, Elysia Fn uses JavaScript's Proxy to capture object property, and parameters to create batched requests to the server to handle and returns the value across the network. Elysia Fn extends superjson, allowing native type in JavaScript like Error, Map, Set, and undefined to parse across JSON data.

Elysia Fn supports multiple use-cases, for example accessing Prisma on the client-side Nextjs app. Theoretically, it's possible to use Redis, Sequelize, RabbitMQ, and more. As Elysia is running on Bun, Elysia Fn can run over 1.2 million operation/second concurrently (tested on M1 Max).

Learn more about Elysia Fn at Eden Fn.

Type Rework

Over 6.5-9x faster for type checking, and uncountable type's LoC reduction.

Elysia 0.3, over 80% of Elysia, and Eden types have been rewritten to focus on performance, type-inference, and fast auto-completion.

Testing for over 350 routes with complex types, Elysia uses only 0.22 seconds to generate a type declaration to use with Eden.

As the Elysia route now compile directly to literal object instead of Typebox reference, Elysia type declaration is much smaller than it used to be on 0.2 and is easier to be consumed by Eden. And by much smaller, it means 50-99% smaller.

Not only Elysia integration with TypeScript is significantly faster, but Elysia is better at understanding TypeScript and your code better.

For example, with 0.3, Elysia will be less strict with plugin registration, allowing you to register the plugin without full type-completion of Elysia Instance. Inlining use function now infers the parent type, and the nested guard can reference types of models from the parent more accurately.

Type Declaration is now also can be built, and exported.

With the rewrite of type, Elysia understands TypeScript far better than it used to, type-completion will be significantly faster than it was, and we encourage you to try it out to see how fast it is. For more detail, see this thread on Twitter

File Upload

Thanks to Bun 0.5.7, Form Data is implemented and enabled by default in Elysia 0.3 with multipart/formdata.

To define type completion and validation for uploading a file, Elysia.t now extends TypeBox with File and Files for file validation.

The validation includes checking for file type with auto-completion of standard file size, the minimum and maximum size of the file, and the total of files per field.

Elysia 0.3 also features schema.contentType to explicitly validate incoming request type to strictly check headers before validating the data.

OpenAPI Schema 3.0.x

With Elysia 0.3, Elysia now uses OpenAPI schema 3.0.x by default for better stating API definitions, and better support for multiple types based on content-type.

schema.details are now updated to OpenAPI 3.0.x, and Elysia also updates the Swagger plugin to match the OpenAPI 3.0.x to take advantage of new features in OpenAPI 3 and Swagger, especially with file uploading.

Eden Rework

To support more demand for Elysia, supporting Elysia Fn, Rest all together, Eden has been reworked to scale with the new architecture.

Eden now exports 3 types of function.

  • Eden Treaty eden/treaty: Original Eden syntax you know and love
  • Eden Fn eden/fn: Access to Eden Fn
  • Eden Fetch eden/fetch: Fetch-like syntax, for highly complex Elysia type (> 1,000 route / Elysia instance)

With the rework of type definitions and support for Elysia Eden, Eden is now much faster and better at inference type from the server.

Auto-completion and faster and use fewer resources than it used to, in fact, Eden's type declaration has been almost 100% reworked to reduce the size and inference time, making it support over 350 routes of auto-completion in a blink of an eye (~0.26 seconds).

To make Elysia Eden, fully type-safe, with Elysia's better understanding of TypeScript, Eden can now narrow down the type based on response status, allowing you to capture the type correctly in any matter of condition. Narrowed error.webp

Notable Improvement:

  • Add string format: 'email', 'uuid', 'date', 'date-time'
  • Update @sinclair/typebox to 0.25.24
  • Update Raikiri to 0.2.0-beta.0 (ei)
  • Add file upload test thanks to #21 (@amirrezamahyari)
  • Pre compile lowercase method for Eden
  • Reduce complex instruction for most Elysia types
  • Compile ElysiaRoute type to literal
  • Optimize type compliation, type inference and auto-completion
  • Improve type compilation speed
  • Improve TypeScript inference between plugin registration
  • Optimize TypeScript inference size
  • Context creation optimization
  • Use Raikiri router by default
  • Remove unused function
  • Refactor registerSchemaPath to support OpenAPI 3.0.3
  • Add error inference for Eden
  • Mark @sinclair/typebox as optional peerDenpendencies

Fix:

  • Raikiri 0.2 thrown error on not found
  • Union response with t.File is not working
  • Definitions isn't defined on Swagger
  • details are missing on group plugin
  • group plugin, isn't unable to compile schema
  • group is not exportable because EXPOSED is a private property
  • Multiple cookies doesn't set content-type to application/json
  • EXPOSED is not export when using fn.permission
  • Missing merged return type for .ws
  • Missing nanoid
  • context side-effects
  • t.Files in swagger is referring to single file
  • Eden response type is unknown
  • Unable to type setModel inference definition via Eden
  • Handle error thrown in non permission function
  • Exported variable has or is using name 'SCHEMA' from external module
  • Exported variable has or is using name 'DEFS' from external module
  • Possible errors for building Elysia app with declaration: true in tsconfig.json

Breaking Change:

  • Rename inject to derive
  • Depreacate ElysiaRoute, changed to inline
  • Remove derive
  • Update from OpenAPI 2.x to OpenAPI 3.0.3
  • Move context.store[SYMBOL] to meta[SYMBOL]

Afterward

With the introduction of Elysia Fn, I'm personally excited to see how it will be adopted in frontend development, removing the line between frontend and backend. And Type Rework of Elysia, making type-checking and auto-completion much faster.

I'm excited to see how you will use Elysia to create the wonderful things you are going to build.

We have Discord server dedicated to Elysia. Feel free to say hi or just chill and hang out.

Thank you for supporting Elysia.

Under a celestial map that never have ends

On a cliff that never have name

I just holwed

Hoping the neverending reverberation will reach you

And I believe someday, I will stand on edge of the ground

(Until the day I can be back to you to tell it)

0.3-rc

1 year ago

edge of ground

Named after Camellia's song「大地の閾を探して [Looking for Edge of Ground]」ft. Hatsune Miku, is the last track of my most favorite's Camellia album,「U.U.F.O」. This song has a high impact on me personally, so I'm not taking the name lightly.

This is the most challenging update, bringing the biggest release of Elysia yet, with rethinking and redesigning of Elysia architecture to be highly scalable while making less breaking change as possible.

And I'm pleased to announce the release candidate of Elysia 0.3 with exciting new features coming right up.

Elysia Fn

Introducing Elysia Fn, run any backend function on the frontend with full auto-completion and full type support.

https://user-images.githubusercontent.com/35027979/223431969-7b3feac6-a885-4e3f-a3a3-77cac1d3309a.mp4

For rapid development, Elysia Fn allows you to "expose" backend code to call from the frontend with full type-safety, autocompletion, original code comment, and "click-to-definition", allowing you to speed up the development.

You can use Elysia Fn with Eden for full-type safety via Eden Fn.

Permission

You can limit allow or deny scopes of the function, check for authorization header and other headers' fields, validate parameters, or limit keys access programmatically.

Keys checking supports type-safety and auto-completion of all possible functions, so you're not missing out on some function or accidentally typing down the wrong name. FpvAKdSaEAAkyyk

And narrowing the scope of property programmatically also narrow down the type of parameters, or in other words, full type-safety. FpvAY9yaMAAzqyL

Technical detail

In technical detail, Elysia Fn uses JavaScript's Proxy to capture object property, and parameters to create batched requests to the server to handle and returns the value across the network. Elysia Fn extends superjson, allowing native type in JavaScript like Error, Map, Set, and undefined to parse across JSON data.

Elysia Fn supports multiple use-cases, for example accessing Prisma on the client-side Nextjs app. Theoretically, it's possible to use Redis, Sequelize, RabbitMQ, and more. As Elysia is running on Bun, Elysia Fn can run over 1.2 million operation/second concurrently (tested on M1 Max).

Type Rework

Over 6.5-9x faster for type checking, and uncountable type's LoC reduction.

Elysia 0.3, over 80% of Elysia, and Eden types have been rewritten to focus on performance, type-inference, and fast auto-completion.

Testing for over 350 routes with complex types, Elysia uses only 0.22 seconds to generate a type declaration to use with Eden.

As the Elysia route now compile directly to literal object instead of Typebox reference, Elysia type declaration is much smaller than it used to be on 0.2 and is easier to be consumed by Eden. And by much smaller, it means 50-99% smaller.

Not only Elysia integration with TypeScript is significantly faster, but Elysia is better at understanding TypeScript and your code better.

For example, with 0.3, Elysia will be less strict with plugin registration, allowing you to register the plugin without full type-completion of Elysia Instance. Inlining use function now infers the parent type, and the nested guard can reference types of models from the parent more accurately.

Type Declaration is now also can be built, and exported.

With the rewrite of type, Elysia understands TypeScript far better than it used to, type-completion will be significantly faster than it was, and we encourage you to try it out to see how fast it is. For more detail, see this thread on Twitter

File Upload

Thanks to Bun 0.5.7, Form Data is implemented and enabled by default in Elysia 0.3 with multipart/formdata.

To define type completion and validation for uploading a file, Elysia.t now extends TypeBox with File and Files for file validation.

The validation includes checking for file type with auto-completion of standard file size, the minimum and maximum size of the file, and the total of files per field.

Elysia 0.3 also features schema.contentType to explicitly validate incoming request type to strictly check headers before validating the data.

OpenAPI Schema 3.0.x

With Elysia 0.3, Elysia now uses OpenAPI schema 3.0.x by default for better stating API definitions, and better support for multiple types based on content-type.

schema.details are now updated to OpenAPI 3.0.x, and Elysia also updates the Swagger plugin to match the OpenAPI 3.0.x to take advantage of new features in OpenAPI 3 and Swagger, especially with file uploading.

Eden Rework

To support more demand for Elysia, supporting Elysia Fn, Rest all together, Eden has been reworked to scale with the new architecture.

Eden now exports 3 types of function. Eden Treaty eden/treaty: Original Eden syntax you know and love, Eden Fn eden/fn: Access to Eden Fn Eden Fetch eden/fetch: Fetch-like syntax, for highly complex Elysia type (> 1,000 route / Elysia instance)

With the rework of type definitions and support for Elysia Eden, Eden is now much faster and better at inference type from the server.

Auto-completion and faster and use fewer resources than it used to, in fact, Eden's type declaration has been almost 100% reworked to reduce the size and inference time, making it support over 350 routes of auto-completion in a blink of an eye (~0.26 seconds).

To make Elysia Eden, fully type-safe, with Elysia's better understanding of TypeScript, Eden can now narrow down the type based on response status, allowing you to capture the type correctly in any matter of condition. narrowed-error

Notable Improvement:

  • Improve TypeScript inference between plugin registration
  • Optimize TypeScript inference size
  • Context creation optimization
  • Use Raikiri router by default
  • Remove unused function
  • Refactor registerSchemaPath to support OpenAPI 3.0.3
  • Add error inference for Eden
  • Mark @sinclair/typebox as optional peerDenpendencies

Fix:

  • Exported variable has or is using name 'SCHEMA' from an external module
  • Exported variable has or is using name 'DEFS' from an external module
  • Possible errors for building the Elysia app with declaration: true in tsconfig.json

Breaking Change:

  • Remove derive
  • Update from OpenAPI 2.x to OpenAPI 3.0.3
  • Moved context.store[SYMBOL] to meta[SYMBOL] (internal)

Afterword

With the introduction of Elysia Fn, I'm personally excited to see how it will be adopted in frontend development, removing the line between frontend and backend. And Type Rework of Elysia, making type-checking and auto-completion much faster.

I'm excited to see how you will use Elysia to create the wonderful things you are going to build.

We have Discord server dedicated to Elysia. Feel free to say hi or just chill and hang out.

Thank you for supporting Elysia.

Under a celestial map that never have ends On a cliff that never have name I just holwed Hoping the neverending reverberation will reach you And I believe someday, I will stand on edge of the ground (Until the day I can be back to you to tell it)

0.2.0

1 year ago

The Blessing

Blessing」brings more improvement, mainly on TypeScript performance, type-inference, and better auto-completion and some new features to reduce boilerplate.

Named after YOASOBI's song「祝福」, an opening for Witch from "Mobile Suit Gundam: The Witch from Mercury".

Defers / Lazy Loading Module

With Elysia 0.2 now add support for the lazy loading module and async plugin.

This made it possible to defer plugin registration and incrementally apply after the Elysia server is started to achieve the fastest possible start-up time in Serverless/Edge environments.

To create defers module, simply mark the plugin as async:

const plugin = async (app: Elysia) => {
    const stuff = await doSomeHeavyWork()

    return app.get('/heavy', stuff)
}

app.use(plugin)

Lazy Loading

Some modules might be heavy and importing before starting the server might not be a good idea.

We can tell Elysia to skip the module then register the module later, and register the module when finish loading by using import statement in use:

app.use(import('./some-heavy-module'))

This will register the module after the import is finished making the module lazy-load.

Defers Plugin and lazy loading module will have all type-inference available right out of the box.

Reference Model

Now Elysia can memorize schema and reference the schema directly in Schema fields, without creating an import file via Elysia.setModel

This list of schema available, brings auto-completion, complete type-inference, and validation as you expected from inline schema.

To use a reference model, first, register the model with setModel, then write a model name to reference a model in schema

const app = new Elysia()
    .setModel({
        sign: t.Object({
            username: t.String(),
            password: t.String()
        })
    })
    .post('/sign', ({ body }) => body, {
        schema: {
            body: 'sign',
            response: 'sign'
        }
    })

This will bring auto-completion of known models. Screenshot 2566-01-23 at 13 24 28

And type reference stopping you from accidentally returning invalid type. Screenshot 2566-01-23 at 13 26 00

Using @elysiajs/swagger will also create a separate Model section for listing available models. Screenshot 2566-01-23 at 13 23 41

Reference also handles validation as you expected.

In short, it's as same as inline schema but now you only need to type the name of the schema to handle validation and typing instead of a long list of imports.

OpenAPI Detail field

Introducing new field schema.detail for customizing detail for the route following the standard of OpenAPI Schema V2 with auto-completion.

Screenshot 2566-01-23 at 13 54 11

This allows you to write better documentation and fully editable Swagger as you want: Screenshot 2566-01-23 at 13 23 41

Union Type

The previous version of Elysia sometime has a problem with distinct Union types, as Elysia tries to catch the response to create a full type reference for Eden.

Results in invalidation of possible types,

Union Response

Made possible by Union Type, now returning multiple response status for schema now available using schema.response[statusCode]

app
    .post(
        '/json/:id',
        ({ body, params: { id } }) => ({
            ...body,
            id
        }),
        {
            schema: {
                body: 'sign',
                response: {
                    200: t.Object({
                        username: t.String(),
                        password: t.String(),
                        id: t.String()
                    }),
                    400: t.Object({
                        error: t.String()
                    })
                }
            }
        }
    )

Elysia will try to validate all schema in response allowing one of the types to be returned.

Return types are also supported report in Swagger's response.

Faster Type Inference

As Elysia 0.1 explore the possibility of using type inference for improving better Developer Experience, we found that sometimes it takes a long time to update type inference because of heavy type inference and in-efficient custom generic.

With Elysia 0.2 now optimized for faster type-inference, preventing duplication of heavy type unwrap, results in better performance for updating type and inference.

Ecosystem

With Elysia 0.2 enabling async plugin and deferred module many new plugins that isn't possible before became reality.

Like:

  • Elysia Static plugin with the non-blocking capability
  • Eden with union-type inference for multiple responses
  • New Elysia Apollo Plugin for Elysia

Notable Improvement:

  • onRequest and onParse now can access PreContext
  • Support application/x-www-form-urlencoded by default
  • body parser now parse content-type with extra attribute eg. application/json;charset=utf-8
  • Decode URI parameter path parameter
  • Eden now reports an error if Elysia is not installed
  • Skip declaration of existing model and decorators

Breaking Changes:

  • onParse now accepts (context: PreContext, contentType: string) instead of (request: Request, contentType: string)
    • To migrate, add .request to context to access Request

Afterward

Thank you for supporting Elysia and being interested in this project.

This release brings better DX and hopefully all you need to write great software with Bun.

Now we have Discord server where you can ask any questions about Elysia or just hang out and chill around is also welcome.

With the wonderful tools, we are happy to see what wonderful software you will build.

Not to be part of those images someone paints Not advancing in that show chosen by someone else You and I, alive to write our story Will never let you be lone and be gone from your side

0.2

1 year ago

0.2.0 RC - 23 Jan 2022

Blessing」brings more improvement, mainly on TypeScript performance, type-inference, and better auto-completion and some new features to reduce boilerplate.

Named after YOASOBI's song「祝福」, an opening for Witch from "Mobile Suit Gundam: The Witch from Mercury".

Defers / Lazy Loading Module

With Elysia 0.2 now add support for the lazy loading module and async plugin.

This made it possible to defer plugin registration and incrementally apply after the Elysia server is started to achieve the fastest possible start-up time in Serverless/Edge environments.

To create defers module, simply mark the plugin as async:

const plugin = async (app: Elysia) => {
    const stuff = await doSomeHeavyWork()

    return app.get('/heavy', stuff)
}

app.use(plugin)

Lazy Loading

Some modules might be heavy and importing before starting the server might not be a good idea.

We can tell Elysia to skip the module then register the module later, and register the module when finish loading by using import statement in use:

app.use(import('./some-heavy-module'))

This will register the module after the import is finished making the module lazy-load.

Defers Plugin and lazy loading module will have all type-inference available right out of the box.

Reference Model

Now Elysia can memorize schema and reference the schema directly in Schema fields, without creating an import file via Elysia.setModel

This list of schema available, brings auto-completion, complete type-inference, and validation as you expected from inline schema.

To use a reference model, first, register the model with setModel, then write a model name to reference a model in schema

const app = new Elysia()
    .setModel({
        sign: t.Object({
            username: t.String(),
            password: t.String()
        })
    })
    .post('/sign', ({ body }) => body, {
        schema: {
            body: 'sign',
            response: 'sign'
        }
    })

This will bring auto-completion of known models. Screenshot 2566-01-23 at 13 24 28

And type reference stopping you from accidentally returning invalid type. Screenshot 2566-01-23 at 13 26 00

Using @elysiajs/swagger will also create a separate Model section for listing available models. Screenshot 2566-01-23 at 13 23 41

Reference also handles validation as you expected.

In short, it's as same as inline schema but now you only need to type the name of the schema to handle validation and typing instead of a long list of imports.

OpenAPI Detail field

Introducing new field schema.detail for customizing detail for the route following the standard of OpenAPI Schema V2 with auto-completion.

Screenshot 2566-01-23 at 13 54 11

This allows you to write better documentation and fully editable Swagger as you want: Screenshot 2566-01-23 at 13 23 41

Union Type

The previous version of Elysia sometime has a problem with distinct Union types, as Elysia tries to catch the response to create a full type reference for Eden.

Results in invalidation of possible types,

Union Response

Made possible by Union Type, now returning multiple response status for schema now available using schema.response[statusCode]

app
    .post(
        '/json/:id',
        ({ body, params: { id } }) => ({
            ...body,
            id
        }),
        {
            schema: {
                body: 'sign',
                response: {
                    200: t.Object({
                        username: t.String(),
                        password: t.String(),
                        id: t.String()
                    }),
                    400: t.Object({
                        error: t.String()
                    })
                }
            }
        }
    )

Elysia will try to validate all schema in response allowing one of the types to be returned.

Return types are also supported report in Swagger's response.

Faster Type Inference

As Elysia 0.1 explore the possibility of using type inference for improving better Developer Experience, we found that sometimes it takes a long time to update type inference because of heavy type inference and in-efficient custom generic.

With Elysia 0.2 now optimized for faster type-inference, preventing duplication of heavy type unwrap, results in better performance for updating type and inference.

Notable Improvement:

  • onRequest and onParse now can access PreContext
  • Support application/x-www-form-urlencoded by default
  • body parser now parse content-type with extra attribute eg. application/json;charset=utf-8
  • Decode URI parameter path parameter

Breaking Changes:

  • onParse now accepts (context: PreContext, contentType: string) instead of (request: Request, contentType: string)
    • To migrate, add .request to context to access Request

Afterward

Thank you for supporting Elysia and being interested in this project.

This release brings better DX and hopefully all you need to write great software with Bun.

Now we have Discord server where you can ask any questions about Elysia or just hang out and chill around is also welcome.

With the wonderful tools, we are happy to see what wonderful software you will build.

Not to be part of those images someone paints Not advancing in that show chosen by someone else You and I, alive to write our story Will never let you be lone and be gone from your side