Elysia 1.0 is the first stable release after development for 1.8 years.
Since we started, we have always been waiting for a framework that focuses on developer experience, velocity, and how to write code for humans, not a machine.
We battle-test Elysia in various situations, simulate medium and large-scale projects, and ship 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.
- Sucrose - Rewritten pattern matching static analysis instead of RegEx
- Improved startup time up to 14x
- Remove ~40 routes/instance TypeScript limitation
- Faster type inference up to ~3.8x
- Treaty 2
- Hook type (breaking changes)
- Inline error for strict error check
It’s a tradition that Elysia’s release notes have a version named after a song or media.
This important version is named after “Lament of the Fallen”.
It is an 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).Sucrose
Section titled “Sucrose”Elysia is optimized to have excellent performance proven in various benchmarks. This is thanks in large part 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 analysis and pattern-matching named “Sucrose”.
Instead of using full AST-based analysis, which is more accurate, we choose to implement only a subset of rules that are needed to improve performance as it needs to be fast at 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 analysis with partial AST and pattern matching starting from Elysia 1.0.
Improved Startup time
Section titled “Improved Startup time”Thanks to Sucrose, and separation from the dynamic injection phase, we can defer the analysis to JIT instead of AOT.
In other words, the “compile” phase can be lazily evaluated.
We offload the evaluation phase from AOT to JIT when a route is matched for the first time and cache the result to compile on demand instead of compiling all routes before the server starts.
At runtime, a single compilation is usually fast and takes no longer than 0.01-0.03 ms (milliseconds, not seconds).
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
Section titled “Remove ~40 routes/instance limit”Previously you could only stack up to ~40 routes / 1 Elysia instance since Elysia 0.1.
This is a limitation of TypeScript. Each queue has limited memory and, if that limit is 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 infiniteAs 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 of optimizing for type-performance, specifically Tail Call Optimization, and variances.
This means theoretically, we can stack an unlimited number of routes and methods until TypeScript breaks.
(spoiler: we have done that and it’s around 558 routes/instance before the TypeScript CLI and language server break because of the 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.

The blocker that made us feel like Elysia is not ready for production has been finally resolved.
Type Inference improvement
Section titled “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 checking and auto-completion even after stacking 500 routes.
Type inference performance is up to 13x faster for Eden Treaty by precomputing the type instead of offloading type remapping 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.

A stress test with 450 routes for Elysia with Eden Treaty gives the following results:
- 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
Section titled “Treaty 2”We asked you for feedback on Eden Treaty, what you like and what could have been improved, and you have pointed out some flaws in Treaty’s design and given us several proposals for improvement.
That’s why today, we introduce Eden Treaty 2, an overhaul to a more ergonomic design.
As much as we dislike breaking changes, 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.
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
treatyto use Treaty 2. - And import
edenTreatyfor 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)
Section titled “Hook type (breaking change)”We hate breaking changes, and this is the first time we do it on a 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 hooks to be local at first.
- Following the previous point, we found that making hooks global by default can easily cause accidental bugs (side-effects) if not reviewed carefully, which are 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 only to the current instance and descendants
- 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(() => { .onBeforeHandle({ as: 'global' }, () => { 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) => app .onBeforeHandle(checkAuthSomehow) .get('/profile', () => 'log hi') )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 belowconst 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 to make a hook global, you have to specify that hook is global.
// From Elysia 0.8new Elysia() .onBeforeHandle(() => "A") .derive(() => {})
// Into Elysia 1.0new Elysia() .onBeforeHandle({ as: 'global' }, () => "A") .derive({ as: 'global' }, () => {})As much as we hate breaking changes and migration, we think this is an important fix that will happen sooner or later to fix problems.
Most servers might not need you to apply migration yourself, but this heavily depends on plugin authors. If migration is required, it usually takes 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
Section titled “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.

Inline error can be destructured from the handler as follows:
import { Elysia } from 'elysia'
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, placing a red squiggly underline at a value instead of an entire function.

We recommend using inline error instead of import error for more accurate type safety.
What does it mean for v1, and what’s next
Section titled “What does it mean for v1, and what’s next”Reaching a 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 rewrites, authentication, synchronizing behavior between JIT and non-JIT mode, and universal runtime support.
Bun works excellently as a runtime, package manager, and in all the tooling it offers, and we believe that Bun is going to be a future of JavaScript.
We believe that opening Elysia to more runtimes and offering interesting Bun specific features (or at least ones that are easy to configure, e.g., Bun Loaders API) will eventually get people to try Bun more than Elysia choosing to support only Bun.
Bun was right, the best way to migrate people from Node is to have compatibility layer and offers better DX, and performance on Bun
— SaltyAom (@saltyAom), March 14, 2024
Elysia core itself is partially WinterCG compatible, but not all the official plugins work 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 support yet as we will gradually adopt and test until we make sure that it will work without unexpected behavior.
You can look forward to support for the following runtimes:
- Node
- Deno
- Cloudflare Worker
We also want to support the following:
- Vercel Edge Function
- Netlify Function
- AWS Lambda / LLRT
Moreover, 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 the active contributors to Elysia.
Additionally, we have rewritten 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 parts 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, feel free to ask in Elysia’s Discord server.
Notable Improvement
Section titled “Notable Improvement”Improvement:
Section titled “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
errorto handler - inline
errorhas auto-completion and type checking based on status code - handler now checks the return type of
errorbased on status code - utility
Elysia._typesfor type inference - #495 Provide user friendly error for failed parse
- handler now infers return type for error status for Treaty
t.Datenow allows stringified dates- improves type test case
- add test case for all life-cycle
- resolve, mapResolve, derive, mapDerive use ephemeral type to scope down accurately
- infer query dynamic variables
Breaking Change:
Section titled “Breaking Change:”- #513 lifecycle is now local first
Change:
Section titled “Change:”- group private API property
- move
Elysia.routestoElysia.router.history - detect possible json before return
- unknown response now returns as-is instead of JSON.stringify()
- change Elysia validation error to JSON instead of string
Bug fix:
Section titled “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 resolves to
Singleton['derive']instead ofSingleton['store'] ValidationErrordoesn’t returncontent-typeasapplication/json- validate
error(status, value)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
Section titled “Afterword”2 years ago, I had a tragic experience.
It’s easily one of the most painful memories I have, working days and nights to keep up with unfair tasks that took advantage of a loose contract we had with some software house.
It took more than 6 months, and I had to work from when I woke up until I slept (15 hours) on repeat, without doing anything else, not even a 5-minute break for a day, no time to relax, nothing besides coding for almost 2 months, not even a single day off. I worked even on the days when I was knocked out and almost had to work in a hospital bed.
I was soulless, with no purpose in life at all. My only wish was to make it a dream.
At the time, there were so many breaking changes, uncountable new features introduced through loopholes in loose requirements and contracts.
Keeping track of it was almost impossible, and we even got scammed, not even getting the pay we deserved because they were “not satisfied”, and we couldn’t do anything about it.
It took me a month to recover from a fear of writing code. Being unprofessional, I couldn’t even do my job properly due to trauma and told my manager that I suffered burnout.
That’s why we hate breaking changes 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 experience 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 didn’t have any JavaScript based solution that could satisfy me yet, so I experimented with one.
I could just move on as I could avoid loose contracts like this in the future, and make money and not spend most of my free time creating a framework but I didn’t.
There’s my favorite part, a quote in the animated short where Mei is against Kiana’s 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 a 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 see a problem and move on, how can we know that the person who comes after us will not stumble upon the same problem we had? Someone needs to do something.
That someone would sacrifice themself to save the others but then who would save the sacrificed one?
The name “Lament of the Fallen” describes that, and why we create Elysia.
*Despite everything about it being my favorite, and I might relate myself personally a bit too much.Although Elysia was built from a bad memory and a tragic event, it’s a privilege to see that Elysia grew into something with so much love. And to see that what you built is loved and well received by others.
Elysia is the work of Open Source developers, and not backed by any company.
We have to do something for a living, and build Elysia in our free time.
At one point I chose not to look for a job straight away just to work on Elysia for several months.
We would love to spend our time improving Elysia continuously, and you could help us with GitHub sponsors to reduce the work we need to support ourselves, and have more free time to work on Elysia.
We are just makers that want to create something to solve problems we have.
We have been creating and experimenting a lot with Elysia, shipping real code to clients, and using Elysia in real projects to power tools behind our local community, CreatorsGarten (local tech community, not an 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 them.
It’s the start of 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.

