elysia

Ergonomic Framework for Humans

MIT License

Downloads
173.4K
Stars
8.4K
Committers
63

Bot releases are visible (Hide)

elysia - 1.1.13

Published by SaltyAom about 1 month ago

What's Changed

Feature:

  • #813 allow UnionEnum to get readonly array by @BleedingDev

Bug fix:

  • #830 Incorrect type for ws.publish
  • #827 returning a response is forcing application/json content-type
  • #821 handle "+" in query with validation
  • #820 params in hooks inside prefixed groups are incorrectly typed never
  • #819 setting cookie attribute before value cause cookie attribute to not be set
  • #810 wrong inference of response in afterResponse, includes status code

New Contributors

Full Changelog: https://github.com/elysiajs/elysia/compare/1.1.12...1.1.13

elysia - 1.1.12

Published by SaltyAom about 1 month ago

Feature:

  • setup provenance publish
  • #808 add UnionEnum type with JSON schema enum usage
  • #807 add closeActiveConnections to Elysia.stop()

Bug fix:

  • #808 ArrayString type cast as Object instead of Array
  • config.nativeStaticResponse is not defined
elysia - 1.1 - Grown-up's Paradise

Published by SaltyAom 3 months ago

elysia-11

Named after a song by Mili, "Grown-up's Paradise", and used as opening for commercial announcement of Arknights TV animation season 3.

As a day one Arknights player and long time Mili's fan, never once I would thought Mili would do a song for Arknights, you should check them out as they are the goat.

Elysia 1.1 focus on several improvement to Developer Experience as follows:

OpenTelemetry

Observability is one of an important aspect for production.

It allows us to understand how our server works on production, identifying problems and bottlenecks.

One of the most popular tools for observability is OpenTelemetry. However, we acknowledge that it's hard and take time to setup and instrument your server correctly.

It's hard to integrate OpenTelemetry to most existing framework and library.

Most revolve around hacky solution, monkey patching, prototype pollution, or manual instrumentation as the framework is not designed for observability from the start.

That's why we introduce first party support for OpenTelemetry on Elysia

To start using OpenTelemetry, install @elysiajs/opentelemetry and apply plugin to any instance.

import { Elysia } from 'elysia'
import { opentelemetry } from '@elysiajs/opentelemetry'

import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'

new Elysia()
	.use(
		opentelemetry({
			spanProcessors: [
				new BatchSpanProcessor(
					new OTLPTraceExporter()
				)
			]
		})
	)

jaeger showing collected trace automatically

Elysia OpenTelemetry is will collect span of any library compatible OpenTelemetry standard, and will apply parent and child span automatically.

In the code above, we apply Prisma to trace how long each query took.

By applying OpenTelemetry, Elysia will then:

  • collect telemetry data
  • Grouping relevant lifecycle together
  • Measure how long each function took
  • Instrument HTTP request and response
  • Collect error and exception

You can export telemetry data to Jaeger, Zipkin, New Relic, Axiom or any other OpenTelemetry compatible backend.

Here's an example of exporting telemetry to Axiom

const Bun = {
	env: {
		AXIOM_TOKEN: '',
		AXIOM_DATASET: ''
	}
}
// ---cut---
import { Elysia } from 'elysia'
import { opentelemetry } from '@elysiajs/opentelemetry'

import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'

new Elysia()
	.use(
		opentelemetry({
			spanProcessors: [
				new BatchSpanProcessor(
					new OTLPTraceExporter({
						url: 'https://api.axiom.co/v1/traces', // [!code ++]
						headers: { // [!code ++]
						    Authorization: `Bearer ${Bun.env.AXIOM_TOKEN}`, // [!code ++]
						    'X-Axiom-Dataset': Bun.env.AXIOM_DATASET // [!code ++]
						} // [!code ++]
					})
				)
			]
		})
	)

axiom showing collected trace from OpenTelemetry

Elysia OpenTelemetry is for applying OpenTelemetry to Elysia server only.

You can use OpenTelemetry SDK normally, and the span is run under Elysia's request span, it will be automatically appear in Elysia trace.

However, we also provide a getTracer, and record utility to collect span from any part of your application.

const db = {
	query(query: string) {
		return new Promise<unknown>((resolve) => {
			resolve('')
		})
	}
}
// ---cut---
import { Elysia } from 'elysia'
import { record } from '@elysiajs/opentelemetry'

export const plugin = new Elysia()
	.get('', () => {
		return record('database.query', () => {
			return db.query('SELECT * FROM users')
		})
	})

record is an equivalent to OpenTelemetry's startActiveSpan but it will handle auto-closing and capture exception automatically.

You may think of record as a label for your code that will be shown in trace.

Prepare your codebase for observability

Elysia OpenTelemetry will group lifecycle and read the function name of each hook as the name of the span.

It's a good time to name your function.

If your hook handler is an arrow function, you may refactor it to named function to understand the trace better otherwise, your trace span will be named as anonymous.

const bad = new Elysia()
	// ⚠️ span name will be anonymous
	.derive(async ({ cookie: { session } }) => {
		return {
			user: await getProfile(session)
		}
	})

const good = new Elysia()
	// ✅ span name will be getProfile
	.derive(async function getProfile({ cookie: { session } }) {
		return {
			user: await getProfile(session)
		}
	})

Trace v2

Elysia OpenTelemetry is built on Trace v2, replacing Trace v1.

Trace v2 allows us to trace any part of our server with 100% synchronous behavior, instead of relying on parallel event listener bridge (goodbye dead lock)

It's entirely rewritten to not only be faster, but also reliable, and accurate down to microsecond by relying on Elysia's ahead of time compilation and code injection.

Trace v2 use a callback listener instead of Promise to ensure that callback is finished before moving on to the next lifecycle event.

Here's an example usage of Trace v2:

import { Elysia } from 'elysia'

new Elysia()
	.trace(({ onBeforeHandle, set }) => {
		// Listen to before handle event
		onBeforeHandle(({ onEvent }) => {
			// Listen to all child event in order
			onEvent(({ onStop, name }) => {
				// Execute something after a child event is finished
				onStop(({ elapsed }) => {
					console.log(name, 'took', elapsed, 'ms')

					// callback is executed synchronously before next event
					set.headers['x-trace'] = 'true'
				})
			})
		})
	})

You may also use async inside trace, Elysia will block and event before proceeding to the next event until the callback is finished.

Trace v2 is a breaking change to Trace v1, please check out trace api documentation for more information.

Normalization

Elysia 1.1 now normalize data before it's being processed.

To ensure that data is consistent and safe, Elysia will try to coerce data into an exact data shape defined in schema, removing additional fields, and normalizing data into a consistent format.

For example if you have a schema like this:

// @errors: 2353
import { Elysia, t } from 'elysia'
import { treaty } from '@elysiajs/eden'

const app = new Elysia()
	.post('/', ({ body }) => body, {
		body: t.Object({
			name: t.String(),
			point: t.Number()
		}),
		response: t.Object({
			name: t.String()
		})
	})

const { data } = await treaty(app).index.post({
	name: 'SaltyAom',
	point: 9001,
	// ⚠️ additional field
	title: 'maintainer'
})

// 'point' is removed as defined in response
console.log(data) // { name: 'SaltyAom' }

This code does 2 thing:

  • Remove title from body before it's being used on the server
  • Remove point from response before it's being sent to the client

This is useful to prevent data inconsistency, and ensure that data is always in the correct format, and not leaking any sensitive information.

Data type coercion

Previously Elysia is using an exact data type without coercion unless explicitly specified to.

For example, to parse a query parameter as a number, you need to explicitly cast it as t.Numeric instead of t.Number.

import { Elysia, t } from 'elysia'

const app = new Elysia()
	.get('/', ({ query }) => query, {
		query: t.Object({
			page: t.Numeric()
		})
	})

However, in Elysia 1.1, we introduce data type coercion, which will automatically coerce data into the correct data type if possible.

Allowing us to simply set t.Number instead of t.Numeric to parse a query parameter as a number.

import { Elysia, t } from 'elysia'

const app = new Elysia()
	.get('/', ({ query }) => query, {
		query: t.Object({
			// ✅ page will be coerced into a number automatically
			page: t.Number()
		})
	})

This also apply to t.Boolean, t.Object, and t.Array.

This is done by swapping schema with possible coercion counterpart during compilation phase ahead of time, and has the same as using t.Numeric or other coercion counterpart.

Guard as

Previously, guard will only apply to the current instance only.

import { Elysia } from 'elysia'

const plugin = new Elysia()
	.guard({
		beforeHandle() {
			console.log('called')
		}
	})
	.get('/plugin', () => 'ok')

const main = new Elysia()
	.use(plugin)
	.get('/', () => 'ok')

Using this code, onBeforeHandle will only be called when accessing /plugin but not /.

In Elysia 1.1, we add as property to guard allowing us to apply guard as scoped or global as same as adding event listener.

import { Elysia } from 'elysia'

const plugin1 = new Elysia()
	.guard({
		as: 'scoped', // [!code ++]
		beforeHandle() {
			console.log('called')
		}
	})
	.get('/plugin', () => 'ok')

// Same as
const plugin2 = new Elysia()
	.onBeforeHandle({ as: 'scoped' }, () => {
		console.log('called')
	})
	.get('/plugin', () => 'ok')

This will ensure that onBeforeHandle will be called on parent as well, and follow scoping mechanism.

Adding as to guard is useful, because it allow us to apply multiple hooks respecting scoping mechanism all at once.

However, it also allows us to apply schema to ensure type safety for all the routes at once.

// @errors: 2304 2345
import { Elysia, t } from 'elysia'

const plugin = new Elysia()
	.guard({
		as: 'scoped',
		response: t.String()
	})
	.get('/ok', () => 'ok')
	.get('/not-ok', () => 1)

const instance = new Elysia()
	.use(plugin)
	.get('/no-ok-parent', () => 2)

const parent = new Elysia()
	.use(instance)
	// This is fine because response is defined as scoped
	.get('/ok', () => 3)

Bulk cast

Continue from code above, sometimes we want to reapply plugin to parent instance as well but as it's limited by scoped mechanism, it's limited to 1 parent only.

To apply to the parent instance, we need to "lift the scope up to the parent instance.

We can achieve this by casting it `as('plugin').

// @errors: 2304 2345
import { Elysia, t } from 'elysia'

const plugin = new Elysia()
	.guard({
		as: 'scoped',
		response: t.String()
	})
	.get('/ok', () => 'ok')
	.get('/not-ok', () => 1)

const instance = new Elysia()
	.use(plugin)
	.as('plugin') // [!code ++]
	.get('/no-ok-parent', () => 2)

const parent = new Elysia()
	.use(instance)
	// This now error because `scoped` is lifted up to parent
	.get('/ok', () => 3)

The as cast will lift all an instance's scope up.

How it work is that, it read all hooks and schema scope, and lift it up to the parent instance.

Which means if you have local scope, and want to apply it to the parent instance, you can use as('plugin') to lift it up.

// @errors: 2304 2345
import { Elysia, t } from 'elysia'

const plugin = new Elysia()
	.guard({
		response: t.String()
	})
	.onBeforeHandle(() => { console.log('called') })
	.get('/ok', () => 'ok')
	.get('/not-ok', () => 1)
	.as('plugin') // [!code ++]

const instance = new Elysia()
	.use(plugin)
	.get('/no-ok-parent', () => 2)
	.as('plugin') // [!code ++]

const parent = new Elysia()
	.use(instance)
	// This now error because `scoped` is lifted up to parent
	.get('/ok', () => 3)

This will cast guard's response and onBeforeHandle as scoped thus lifting it up to the parent instance.

as accept two possible arguments:

  • plugin cast event to scoped
  • global cast event to global
// @errors: 2304 2345
import { Elysia, t } from 'elysia'

const plugin = new Elysia()
	.guard({
		response: t.String()
	})
	.onBeforeHandle(() => { console.log('called') })
	.get('/ok', () => 'ok')
	.get('/not-ok', () => 1)
	.as('global') // [!code ++]

const instance = new Elysia()
	.use(plugin)
	.get('/no-ok-parent', () => 2)

const parent = new Elysia()
	.use(instance)
	// This now error because `scoped` is lifted up to parent
	.get('/ok', () => 3)

This allow us to cast multiple hook scope all at once without adding as to each hook or applying it to guard, or lifting and existing plugin scope up.

import { Elysia, t } from 'elysia'

// On 1.0
const from = new Elysia()
	// Not possible to apply guard to parent on 1.0
	.guard({
		response: t.String()
	})
	.onBeforeHandle({ as: 'scoped' }, () => { console.log('called') })
	.onAfterHandle({ as: 'scoped' }, () => { console.log('called') })
	.onParse({ as: 'scoped' }, () => { console.log('called') })

// On 1.1
const to = new Elysia()
	.guard({
		response: t.String()
	})
	.onBeforeHandle(() => { console.log('called') })
	.onAfterHandle(() => { console.log('called') })
	.onParse(() => { console.log('called') })
	.as('plugin')

Response reconcilation

In Elysia 1.0, Elysia will prefers either one of the schema from the scope, and will not merge them together.

However, on Elysia 1.1, Elysia will try to reconcile response schema from all scope from each status code and merge them together.

// @errors: 2304 2345
import { Elysia, t } from 'elysia'

const plugin = new Elysia()
	.guard({
		as: 'global',
		response: {
			200: t.Literal('ok'),
			418: t.Literal('Teapot')
		}
	})
	.get('/ok', ({ error }) => error(418, 'Teapot'))

const instance = new Elysia()
	.use(plugin)
	.guard({
		response: {
			418: t.String()
		}
	})
	// This is fine because local response override
	.get('/ok', ({ error }) => error(418, 'ok'))

const parent = new Elysia()
	.use(instance)
	// Error because global response
	.get('/not-ok', ({ error }) => error(418, 'ok'))

We can see that:

  • on instance: the response schema from the global scope is merged with the local scope, allowing us to override the global response schema in this instance
  • on parent: the response schema from the global scope is used, local scoped from instance is not applied because of scoping mechanism

This is handled in both type-level and runtime, providing us with a better type-integrity.

Optional Path Parameter

Elysia now support optional path parameter by adding ? to the end of path parameter.

import { Elysia } from 'elysia'

new Elysia()
	.get('/ok/:id?', ({ params: { id } }) => id)
	.get('/ok/:id/:name?', ({ params: { id, name } }) => name)

In the example above, if we access:
/ok/1 will return 1
/ok will return undefined

By default, accessing the optional path parameter will return undefined if it's not provided.

You can provide a default value by either using JavaScript default value or schema default value.

import { Elysia, t } from 'elysia'

new Elysia()
	.get('/ok/:id?', ({ params: { id } }) => id, {
		params: t.Object({
			id: t.Number({
				default: 1
			})
		})
	})

In this example, if we access:
/ok/2 will return 1
/ok will return 1

Generator response stream

Previously, you can stream a response by using @elysiajs/stream package.

However, there's a limitation:

  • Doesn't provide inference type safety for Eden
  • Not as straightforward way to stream response

Now, Elysia support response streaming out of the box by using a generator function.

import { Elysia } from 'elysia'

const app = new Elysia()
	.get('/ok', function* () {
		yield 1
		yield 2
		yield 3
	})

This this example, we can stream a response by using yield keyword.

Using generator function, we can now infers return type from the generator function and provide it to Eden directly.

Eden will now infer the response type from the generator function as AsyncGenerator

import { Elysia } from 'elysia'
import { treaty } from '@elysiajs/eden'

const app = new Elysia()
	.get('/ok', function* () {
		yield 1
		yield 2
		yield 3
	})

const { data, error } = await treaty(app).ok.get()
if (error) throw error

for await (const chunk of data)
	console.log(chunk)

While streaming a response, it's common that request may be cancelled before the response is fully streamed, In that case, Elysia will automatically stop the generator function when the request is cancelled.

We recommended migrating from @elysiajs/stream to generator function for streaming response, as it's more straightforward and provide better type inference.

As the stream plugin will be in maintainance mode and will be deprecated in the future.

Breaking Change

  • Parse value as string for all validators unless explicitly specified.
    • See 50a5d9244bf279.
    • Remove objects auto-parsing in query unless explicitly specified via query
    • Except query string as defined in RFC 3986, TLDR; query string could be either string or array of string.
  • Rename onResponse to onAfterResponse
  • [Internal] Remove $passthrough in favor of toResponse
  • [Internal] UnwrapRoute type now always resolve with status code

Notable Change:

  • Add auto-complete for set.headers
  • Remove prototype poluation from hook
  • remove static analysis for query name
  • remove query replace '+' in favor removing static query analysis
  • Add server property
  • mapResponse is now called in error event
  • reconcilation decorator in type level
  • onError supports array function
  • Parse query object with and without schema
  • Deprecated ObjectString for parsing array
  • Sucrose: improve isContextPassToFunction, and extractMainParameter stability
  • Add replaceSchemaType
  • Add route to context
  • Optimize recursive MacroToProperty type
  • Parse query array and object
  • Optimize code path for composeGeneralHandler
  • Add debug report on compiler panic
  • Using Cookie<unknown> instead of Cookie<any> if schema is not defined
  • Reduce memory usage of route registration ~36% on large codebase
    • Reduce compilation code path
    • Remove trace inference
    • Reduce router compilation code path
    • removing route handler compilation cache (st${index}, stc${index})
  • Add undefined union to cookie in case if cookie is not present
  • Optimize response status resolve type inference

Bug fix:

  • Normalize headers accidentally use query validator check instead
  • onError missing trace symbol
  • Headers validator compilation is not cached
  • Deduplicate macro propagation
  • Websocket in nested group now work
  • Error response is not check unless successful status code is provided

Afterword

Hi, SaltyAom here again and thanks you for supporting Elysia for the past 2 years.

It has been a lovely journey, and to see so many overwhelming support for Elysia make me feels so happy so much that I don't know how to express it.

I'm still very happy to work on Elysia and looking forward for a long journey with you and Elysia.

However, working alone on Elysia is not easy, that's why I need your help to support Elysia by reporting a bug, creating a PR (we are opensource after all), or share anything you like about Elysia or even just say hi.

Past 2 years, I know that Elysia is not perfect, and sometime I might not have all the time to respond to issues but I'm trying my best to make it better and have a vision of what it could be.

That's why in the future, we will have more maintainers to help maintain Elysia plugins, currently Bogeychan and Fecony are doing great on helping maintain community server.


As you may or may not know, orginally ElysiaJS is named as "KingWorld" before chaning name to "Elysia".

Same as Elysia naming convention, both are inspired by anime/game/vtuber subculture.

KingWorld is name after the song KINGWORLD by Shirakami Fubuki and Sasakure.uk, both are my favorite vtuber and music producer.

That's why logo is designed in the style of Arctic fox after Fubuki.

While Elysia is obviously name after Elysia, my favorite character from game Honkai Impact 3rd which I also name my cat after her as well.

Also I have a little gift, as you may know I'm also a cosplayer in my spare time, and I have a cosplay of Honkai 3rd Elysia as well.

Elysia maintainer

So uh, Elysia maintaining Elysia I guess?

I'm planning to do a photoshoot of Elysia cosplay and share it with you in the future, as I like her so much, I want to make it perfect.

That being said, I'm looking forward to see you on the next release, and thank you for supporting Elysia.

We were so easily satisfied and happy

Even if I break your favorite teddy bear

A "sorry" could fix everything

When did it change? When did we forget?

Why is it now so hard to forgive?

Do we advance, never stopping our steps

Because we are scared to look back on what we did?

Truth is, I know as long as we live

Our ideals dye rivers scarlet

Answer me, my sinking ship

Where's our tomorrow?

Where does our future go?

Does our hope have to be sown upon somebody's sorrow?

ขอให้โลกใจดีกับเธอบ้างนะ

elysia - 1.0 - Lament of the Fallen Latest Release

Published by SaltyAom 7 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.

elysia - 0.8 - Gate of Steiners

Published by SaltyAom 7 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.

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

elysia - 0.7 - Stellar Stellar

Published by SaltyAom about 1 year 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.

Reactive Cookie

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.

Cookie Schema

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.

Cookie Signature

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

elysia - 0.6 - This Game

Published by SaltyAom about 1 year 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.

elysia - 0.5 - Radiant

Published by SaltyAom over 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

elysia - 0.4 - 月夜の音楽会 (Moonlit Night Concert)

Published by SaltyAom over 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

elysia - 0.3 ー「大地の閾を探して [Looking for Edge of Ground] 」

Published by SaltyAom over 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)

elysia - 0.3 ー「大地の閾を探して [Looking for Edge of Ground] 」(RC)

Published by SaltyAom over 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)

elysia - 0.2 ー「The Blessing」

Published by SaltyAom over 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.

And type reference stopping you from accidentally returning invalid type.

Using @elysiajs/swagger will also create a separate Model section for listing available models.

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.

This allows you to write better documentation and fully editable Swagger as you want:

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

elysia - 0.2 - Blessing (RC)

Published by SaltyAom over 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.

And type reference stopping you from accidentally returning invalid type.

Using @elysiajs/swagger will also create a separate Model section for listing available models.

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.

This allows you to write better documentation and fully editable Swagger as you want:

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

elysia - 0.1 ー「Reburn」

Published by SaltyAom almost 2 years ago

0.1.0 - 24 Dec 2022

Reburn」is the first stable beta release for Elysia.

Happy Christmas, wishing you happiness tonight as we release the first stable release of Elysia.

The API is now stabilized, and Elysia will focus on growing its ecosystem and plugins for common patterns.

Eden

Introducing Eden, a fully type-safe client for Elysia server like tRPC.

A 600 bytes client for Elysia server, no code generation need, creating a fully type-safe, and auto-complete for both client and server.

See Eden in action on Twitter

https://user-images.githubusercontent.com/35027979/209444959-c4840a82-9abb-46cc-8447-433dde85ed58.mp4

The fastest

With a lot of effort put into micro-optimization and re-architecture, Elysia is the fastest Bun web framework benchmarked on 24 December 2022, outperforming 2/3 category put into the test.

See benchmark results at Bun http benchmark

Improved Documentation

Elysia now has improved documentation at elysiajs.com.

Now with a proper landing page, searchable content, and revised content put into.

Afterward

Merry Christmas, and happy new year.

As 0.1 release, we recommended giving Elysia a try and building stuff with it.

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

Fly away, let me fly away
Never hide in dark
Head on, start a riot
Fly away, defying fate in my way
Crush it
Make it!
Feel
My
Heart!