--- url: /blog/with-prisma.md --- Prisma is a renowned TypeScript ORM for its developer experience. With type-safe and intuitive API that allows us to interact with databases using a fluent and natural syntax. Writing a database query is as simple as writing a shape of data with TypeScript auto-completion, then Prisma takes care of the rest by generating efficient SQL queries and handling database connections in the background. One of the standout features of Prisma is its seamless integration with popular databases like: - PostgreSQL - MySQL - SQLite - SQL Server - MongoDB - CockroachDB So we have the flexibility to choose the database that best suits our project's needs, without compromising on the power and performance that Prisma brings to the table. This means you can focus on what really matters: building your application logic. Prisma is one of an inspiration for Elysia, its declarative API, fluent developer experience is an absolute joy to work with. Now we can bring the long-awaited imagination to life with [the release of Bun 0.6.7](https://bun.sh/blog/bun-v0.6.7), Bun now support Prisma out of the box. ## Elysia Elysia is one of the answer that would come to mind when you asked what framework should I use with Bun. Although, you can use Express with Bun, but with Elysia is built specifically for Bun. Elysia can outperforms Express by nearly ~19x faster supercharged with declarative API for creating a unified type system and end-to-end type safety Elysia also known for having a fluent Developer Experience especially as Elysia is designed to be used with Prisma since its early day. With Elysia's strict-type validation, we can integrate Elysia and Prisma with ease using declarative API. In other word, Elysia will ensure that runtime type and TypeScript's type will be always in sync, making it behave like Type Strict Language where you can completely trust the type system and looks ahead for any type error, and easier debugging errors relating to type. ## Setting up All we need to get start is to run `bun create` to setup an Elysia server ```bash bun create elysia elysia-prisma ``` Where `elysia-prisma` is our project name (folder destination), feels free to change the name to anything you like. Now in our folder, and let's install Prisma CLI as dev dependency. ```ts bun add -d prisma ``` Then we can setup prisma project with `prisma init` ```ts bunx prisma init ``` `bunx` is a bun command equivalent to `npx`, which allows us to execute package bin. Once setup, we can see that Prisma will update `.env` file and generate a folder named **prisma** with **schema.prisma** as a file inside. **schema.prisma** is an database model defined with Prisma's schema language. Let's update our **schema.prisma** file like this for a demonstration: ```ts generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) username String @unique password String } ``` Telling Prisma that we want to create a table name **User** with column as: | Column | Type | Constraint | | --- | --- | --- | | id | Number | Primary Key with auto increment | | username | String | Unique | | password | String | - | Prisma will then read the schema, and DATABASE_URL from an `.env` file, so before syncing our database we need to define the `DATABASE_URL` first. Since we don't have any database running, we can setup one using docker: ```bash docker run -p 5432:5432 -e POSTGRES_PASSWORD=12345678 -d postgres ``` Now go into `.env` file at the root of our project then edit: ``` DATABASE_URL="postgresql://postgres:12345678@localhost:5432/db?schema=public" ``` Then we can run `prisma migrate` to sync our database with Prisma schema: ```bash bunx prisma migrate dev --name init ``` Prisma then generate a strongly-typed Prisma Client code based on our schema. This means we get autocomplete and type checking in our code editor, catching potential errors at compile time rather than runtime. ## Into the code In our **src/index.ts**, let's update our Elysia server to create a simple user sign-up endpoint. ```ts import { Elysia } from 'elysia' import { PrismaClient } from '@prisma/client' // [!code ++] const db = new PrismaClient() // [!code ++] const app = new Elysia() .post( // [!code ++] '/sign-up', // [!code ++] async ({ body }) => db.user.create({ // [!code ++] data: body // [!code ++] }) // [!code ++] ) // [!code ++] .listen(3000) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` We have just created a simple endpoint to insert new user into our database using Elysia and Prisma. ::: tip **It's important** that when returning Prisma function, you should always marked a callback function as async. As Prisma function doesn't return native Promise, Elysia can not dynamically handle the custom promise type, but with Static Code Analysis, by marking callback function as async, Elysia will try to await the return type of a function thus allowing us to map Prisma result. ::: Now the problem is that the body could be anything, not limited to our expected defined type. We can improve that by using Elysia's type system. ```ts import { Elysia, t } from 'elysia' // [!code ++] import { PrismaClient } from '@prisma/client' const db = new PrismaClient() const app = new Elysia() .post( '/sign-up', async ({ body }) => db.user.create({ data: body }), { // [!code ++] body: t.Object({ // [!code ++] username: t.String(), // [!code ++] password: t.String({ // [!code ++] minLength: 8 // [!code ++] }) // [!code ++] }) // [!code ++] } // [!code ++] ) .listen(3000) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` This tells Elysia to validate the body of an incoming request to match the shape, and update TypeScript's type of the `body` inside the callback to match the exact same type: ```ts // 'body' is now typed as the following: { username: string password: string } ``` This means that if you the shape doesn't interlop with database table, it would warn you immediately. Which is effective when you need to edit a table or perform a migration, Elysia can log the error immediately line by line because of a type conflict before reaching the production. ## Error Handling Since our `username` field is unique, sometime Prisma can throw an error there could be an accidental duplication of `username` when trying to sign up like this: ```ts Invalid `prisma.user.create()` invocation: Unique constraint failed on the fields: (`username`) ``` Default Elysia's error handler can handle the case automatically but we can improve that by specifying a custom error using Elysia's local `onError` hook: ```ts import { Elysia, t } from 'elysia' import { PrismaClient } from '@prisma/client' const db = new PrismaClient() const app = new Elysia() .post( '/', async ({ body }) => db.user.create({ data: body }), { error({ code }) { // [!code ++] switch (code) { // [!code ++] // Prisma P2002: "Unique constraint failed on the {constraint}" // [!code ++] case 'P2002': // [!code ++] return { // [!code ++] error: 'Username must be unique' // [!code ++] } // [!code ++] } // [!code ++] }, // [!code ++] body: t.Object({ username: t.String(), password: t.String({ minLength: 8 }) }) } ) .listen(3000) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` Using `error` hook, any error thown inside a callback will be populate to `error` hook, allowing us to define a custom error handler. According to [Prisma documentation](https://www.prisma.io/docs/reference/api-reference/error-reference#p2002), error code 'P2002' means that by performing the query, it will failed a unique constraint. Since this table only a single `username` field that is unique, we can imply that the error is caused because username is not unique, so we return a custom erorr message of: ```ts { error: 'Username must be unique' } ``` This will return a JSON equivalent of our custom error message when a unique constraints failed. Allowing us to seemlessly define any custom error from Prisma error. ## Bonus: Reference Schema When our server grow complex and type becoming more redundant and become a boilerplate, inlining an Elysia type can be improved by using **Reference Schema**. To put it simply, we can named our schema and reference the type by using the name. ```ts import { Elysia, t } from 'elysia' import { PrismaClient } from '@prisma/client' const db = new PrismaClient() const app = new Elysia() .model({ // [!code ++] 'user.sign': t.Object({ // [!code ++] username: t.String(), // [!code ++] password: t.String({ // [!code ++] minLength: 8 // [!code ++] }) // [!code ++] }) // [!code ++] }) // [!code ++] .post( '/', async ({ body }) => db.user.create({ data: body }), { error({ code }) { switch (code) { // Prisma P2002: "Unique constraint failed on the {constraint}" case 'P2002': return { error: 'Username must be unique' } } }, body: 'user.sign', // [!code ++] body: t.Object({ // [!code --] username: t.String(), // [!code --] password: t.String({ // [!code --] minLength: 8 // [!code --] }) // [!code --] }) // [!code --] } ) .listen(3000) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` This works as same as using an inline but instead you defined it once and refers to the schema by name to remove redundant validation codes. TypeScript and validation code will works as expected. ## Bonus: Documentation As a bonus, Elysia type system is also OpenAPI Schema 3.0 compliance, which means that it can generate documentation with tools that support OpenAPI Schema like Swagger. We can use Elysia Swagger plugin to generate an API documentation in a single line. ```bash bun add @elysiajs/swagger ``` And then just add the plugin: ```ts import { Elysia, t } from 'elysia' import { PrismaClient } from '@prisma/client' import { swagger } from '@elysiajs/swagger' // [!code ++] const db = new PrismaClient() const app = new Elysia() .use(swagger()) // [!code ++] .post( '/', async ({ body }) => db.user.create({ data: body, select: { // [!code ++] id: true, // [!code ++] username: true // [!code ++] } // [!code ++] }), { error({ code }) { switch (code) { // Prisma P2002: "Unique constraint failed on the {constraint}" case 'P2002': return { error: 'Username must be unique' } } }, body: t.Object({ username: t.String(), password: t.String({ minLength: 8 }) }), response: t.Object({ // [!code ++] id: t.Number(), // [!code ++] username: t.String() // [!code ++] }) // [!code ++] } ) .listen(3000) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` And that's all it takes to create a well-defined documentation for our API. Swagger documentation generated by Elysia And thanks to defining a strict type for the documentation, we found that we accidentally return `password` field from our API which is not a good idea to return a private information. Thanks to Elysia's type system, we define that response shouldn't contains `password` which automatically warn us that our Prisma query are returning a password allows us to fix that a head of time. And if anything more, we don't have to worry that we might forget a specification of OpenAPI Schema 3.0, as we have auto-completion and type safety too. We can define our route detail with `detail` that also follows OpenAPI Schema 3.0, so we can properly create documentation effortlessly. ## What's next With the support of Prisma with Bun and Elysia, we are entering a new era of a new level of developer experience. For Prisma we can accelerate our interaction with database, Elysia accelerate our creation of backend web server in term of both developer experience and performance. > It's an absolute joy to work with. Elysia is on a journey to create a new standard for a better developer experience with Bun for high performance TypeScript server that can match the performance of Go and Rust. If you're looking for a place to start learning about out Bun, consider take a look for what Elysia can offer especially with an [end-to-end type safety](/eden/overview) like tRPC but built on REST standard without any code generation. If you're interested in Elysia, feel free to check out our [Discord server](https://discord.gg/eaFJ2KDJck) or see [Elysia on GitHub](https://github.com/elysiajs/elysia) --- --- url: /plugins/graphql-apollo.md --- # GraphQL Apollo Plugin Plugin for [elysia](https://github.com/elysiajs/elysia) for using GraphQL Apollo. Install with: ```bash bun add graphql @elysiajs/apollo @apollo/server ``` Then use it: ```typescript twoslash import { Elysia } from 'elysia' import { apollo, gql } from '@elysiajs/apollo' const app = new Elysia() .use( apollo({ typeDefs: gql` type Book { title: String author: String } type Query { books: [Book] } `, resolvers: { Query: { books: () => { return [ { title: 'Elysia', author: 'saltyAom' } ] } } } }) ) .listen(3000) ``` Accessing `/graphql` should show Apollo GraphQL playground work with. ## Context Because Elysia is based on Web Standard Request and Response which is different from Node's `HttpRequest` and `HttpResponse` that Express uses, results in `req, res` being undefined in context. Because of this, Elysia replaces both with `context` like route parameters. ```typescript const app = new Elysia() .use( apollo({ typeDefs, resolvers, context: async ({ request }) => { const authorization = request.headers.get('Authorization') return { authorization } } }) ) .listen(3000) ``` ## Config This plugin extends Apollo's [ServerRegistration](https://www.apollographql.com/docs/apollo-server/api/apollo-server/#options) (which is `ApolloServer`'s' constructor parameter). Below are the extended parameters for configuring Apollo Server with Elysia. ### path @default `"/graphql"` Path to expose Apollo Server. ### enablePlayground @default `process.env.ENV !== 'production'` Determine whether should Apollo should provide Apollo Playground. --- --- url: /at-glance.md --- # At glance Elysia is an ergonomic web framework for building backend servers with Bun. Designed with simplicity and type-safety in mind, Elysia has a familiar API with extensive support for TypeScript, optimized for Bun. Here's a simple hello world in Elysia. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/', 'Hello Elysia') .get('/user/:id', ({ params: { id }}) => id) .post('/form', ({ body }) => body) .listen(3000) ``` Navigate to [localhost:3000](http://localhost:3000/) and it should show 'Hello Elysia' as a result. ::: tip Hover over the code snippet to see the type definition. In the mock browser, click on the path highlighted in blue to change paths and preview the response. Elysia can run in the browser, and the results you see are actually run using Elysia. ::: ## Performance Building on Bun and extensive optimization like Static Code Analysis allows Elysia to generate optimized code on the fly. Elysia can outperform most of the web frameworks available today[1], and even match the performance of Golang and Rust frameworks[2]. | Framework | Runtime | Average | Plain Text | Dynamic Parameters | JSON Body | | ------------- | ------- | ----------- | ---------- | ------------------ | ---------- | | bun | bun | 262,660.433 | 326,375.76 | 237,083.18 | 224,522.36 | | elysia | bun | 255,574.717 | 313,073.64 | 241,891.57 | 211,758.94 | | hyper-express | node | 234,395.837 | 311,775.43 | 249,675 | 141,737.08 | | hono | bun | 203,937.883 | 239,229.82 | 201,663.43 | 170,920.4 | | h3 | node | 96,515.027 | 114,971.87 | 87,935.94 | 86,637.27 | | oak | deno | 46,569.853 | 55,174.24 | 48,260.36 | 36,274.96 | | fastify | bun | 65,897.043 | 92,856.71 | 81,604.66 | 23,229.76 | | fastify | node | 60,322.413 | 71,150.57 | 62,060.26 | 47,756.41 | | koa | node | 39,594.14 | 46,219.64 | 40,961.72 | 31,601.06 | | express | bun | 29,715.537 | 39,455.46 | 34,700.85 | 14,990.3 | | express | node | 15,913.153 | 17,736.92 | 17,128.7 | 12,873.84 | ## TypeScript Elysia is designed to help you write less TypeScript. Elysia's Type System is fine-tuned to infer your code into types automatically, without needing to write explicit TypeScript, while providing type-safety at both runtime and compile time to provide you with the most ergonomic developer experience. Take a look at this example: ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/user/:id', ({ params: { id } }) => id) // ^? .listen(3000) ```
The above code creates a path parameter "id". The value that replaces `:id` will be passed to `params.id` both at runtime and in types without manual type declaration. Elysia's goal is to help you write less TypeScript and focus more on business logic. Let the complex types be handled by the framework. TypeScript is not needed to use Elysia, but it's recommended to use Elysia with TypeScript. ## Type Integrity To take a step further, Elysia provides **Elysia.t**, a schema builder to validate types and values at both runtime and compile-time to create a single source of truth for your data-type. Let's modify the previous code to accept only a numeric value instead of a string. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/user/:id', ({ params: { id } }) => id, { // ^? params: t.Object({ id: t.Numeric() }) }) .listen(3000) ``` This code ensures that our path parameter **id** will always be a numeric string and then transforms it into a number automatically at both runtime and compile-time (type-level). ::: tip Hover over "id" in the above code snippet to see a type definition. ::: With Elysia's schema builder, we can ensure type safety like a strongly-typed language with a single source of truth. ## Standard Elysia adopts many standards by default, like OpenAPI, and WinterCG compliance, allowing you to integrate with most of the industry standard tools or at least easily integrate with tools you are familiar with. For instance, because Elysia adopts OpenAPI by default, generating documentation with Swagger is as easy as adding a one-liner: ```typescript twoslash import { Elysia, t } from 'elysia' import { swagger } from '@elysiajs/swagger' new Elysia() .use(swagger()) .get('/user/:id', ({ params: { id } }) => id, { params: t.Object({ id: t.Number() }) }) .listen(3000) ``` With the Swagger plugin, you can seamlessly generate a Swagger page without additional code or specific config and share it with your team effortlessly. ## End-to-end Type Safety With Elysia, type safety is not limited to server-side only. With Elysia, you can synchronize your types with your frontend team automatically like tRPC, with Elysia's client library, "Eden". ```typescript twoslash import { Elysia, t } from 'elysia' import { swagger } from '@elysiajs/swagger' const app = new Elysia() .use(swagger()) .get('/user/:id', ({ params: { id } }) => id, { params: t.Object({ id: t.Number() }) }) .listen(3000) export type App = typeof app ``` And on your client-side: ```typescript twoslash // @filename: server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/user/:id', ({ params: { id } }) => id, { params: t.Object({ id: t.Number() }) }) .listen(3000) export type App = typeof app // @filename: client.ts // ---cut--- // client.ts import { treaty } from '@elysiajs/eden' import type { App } from './server' const app = treaty('localhost:3000') // Get data from /user/617 const { data } = await app.user({ id: 617 }).get() // ^? console.log(data) ``` With Eden, you can use the existing Elysia types to query an Elysia server **without code generation** and synchronize types for both frontend and backend automatically. Elysia is not only about helping you create a confident backend but for all that is beautiful in this world. ## Platform Agnostic Elysia was designed for Bun, but is **not limited to Bun**. Being [WinterCG compliant](https://wintercg.org/) allows you to deploy Elysia servers on Cloudflare Workers, Vercel Edge Functions, and most other runtimes that support Web Standard Requests. ## Our Community If you have questions or get stuck regarding Elysia, feel free to ask our community on GitHub Discussions, Discord, and Twitter. Official ElysiaJS discord community server Track update and status of Elysia Source code and development --- 1. Measured in requests/second. The benchmark for parsing query, path parameter and set response header on Debian 11, Intel i7-13700K tested on Bun 0.7.2 on 6 Aug 2023. See the benchmark condition [here](https://github.com/SaltyAom/bun-http-framework-benchmark/tree/c7e26fe3f1bfee7ffbd721dbade10ad72a0a14ab#results). 2. Based on [TechEmpower Benchmark round 22](https://www.techempower.com/benchmarks/#section=data-r22&hw=ph&test=composite). --- --- url: /plugins/bearer.md --- # Bearer Plugin Plugin for [elysia](https://github.com/elysiajs/elysia) for retrieving the Bearer token. Install with: ```bash bun add @elysiajs/bearer ``` Then use it: ```typescript twoslash import { Elysia } from 'elysia' import { bearer } from '@elysiajs/bearer' const app = new Elysia() .use(bearer()) .get('/sign', ({ bearer }) => bearer, { beforeHandle({ bearer, set, error }) { if (!bearer) { set.headers[ 'WWW-Authenticate' ] = `Bearer realm='sign', error="invalid_request"` return error(400, 'Unauthorized') } } }) .listen(3000) ``` This plugin is for retrieving a Bearer token specified in [RFC6750](https://www.rfc-editor.org/rfc/rfc6750#section-2). This plugin DOES NOT handle authentication validation for your server. Instead, the plugin leaves the decision to developers to apply logic for handling validation check themselves. --- --- url: /essential/best-practice.md --- # Best Practice Elysia is a pattern-agnostic framework, leaving the decision of which coding patterns to use up to you and your team. However, there are several concern from trying to adapt an MVC pattern [(Model-View-Controller)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) with Elysia, and found it's hard to decouple and handle types. This page is a guide to on how to follows Elysia structure best practice combined with MVC pattern but can be adapted to any coding pattern you like. ## Method Chaining Elysia code should always use **method chaining**. As Elysia type system is complex, every methods in Elysia returns a new type reference. **This is important** to ensure type integrity and inference. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .state('build', 1) // Store is strictly typed // [!code ++] .get('/', ({ store: { build } }) => build) .listen(3000) ``` In the code above **state** returns a new **ElysiaInstance** type, adding a `build` type. ### ❌ Don't: Use Elysia without method chaining Without using method chaining, Elysia doesn't save these new types, leading to no type inference. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' const app = new Elysia() app.state('build', 1) app.get('/', ({ store: { build } }) => build) app.listen(3000) ``` We recommend to **always use method chaining** to provide an accurate type inference. ## Controller > 1 Elysia instance = 1 controller Elysia does a lot to ensure type integrity, if you pass an entire `Context` type to a controller, these might be the problems: 1. Elysia type is complex and heavily depends on plugin and multiple level of chaining. 2. Hard to type, Elysia type could change at anytime, especially with decorators, and store 3. Type casting may lead to a loss of type integrity or an inability to ensure consistency between types and runtime code. 4. This makes it more challenging for [Sucrose](/blog/elysia-10#sucrose) *(Elysia's "kind of" compiler)* to statically analyze your code ### ❌ Don't: Create a separate controller Don't create a separate controller, use Elysia itself as a controller instead: ```typescript import { Elysia, t, type Context } from 'elysia' abstract class Controller { static root(context: Context) { return Service.doStuff(context.stuff) } } // ❌ Don't new Elysia() .get('/', Controller.hi) ``` By passing an entire `Controller.method` to Elysia is an equivalent of having 2 controllers passing data back and forth. It's against the design of framework and MVC pattern itself. ### ✅ Do: Use Elysia as a controller Instead treat an Elysia instance as a controller itself instead. ```typescript import { Elysia } from 'elysia' import { Service } from './service' new Elysia() .get('/', ({ stuff }) => { Service.doStuff(stuff) }) ``` ### Testing You can test your controller using `handle` to directly call a function (and it's lifecycle) ```typescript import { Elysia } from 'elysia' import { Service } from './service' import { describe, it, should } from 'bun:test' const app = new Elysia() .get('/', ({ stuff }) => { Service.doStuff(stuff) return 'ok' }) describe('Controller', () => { it('should work', async () => { const response = await app .handle(new Request('http://localhost/')) .then((x) => x.text()) expect(response).toBe('ok') }) }) ``` You may find more information about testing in [Unit Test](/patterns/unit-test.html). ## Service Service is a set of utility/helper functions decoupled as a business logic to use in a module/controller, in our case, an Elysia instance. Any technical logic that can be decoupled from controller may live inside a **Service**. There're 2 types of service in Elysia: 1. Non-request dependent service 2. Request dependent service ### ✅ Do: Non-request dependent service This kind of service doesn't need to access any property from the request or `Context`, and can be initiated as a static class same as usual MVC service pattern. ```typescript import { Elysia, t } from 'elysia' abstract class Service { static fibo(number: number): number { if(number < 2) return number return Service.fibo(number - 1) + Service.fibo(number - 2) } } new Elysia() .get('/fibo', ({ body }) => { return Service.fibo(body) }, { body: t.Numeric() }) ``` If your service doesn't need to store a property, you may use `abstract class` and `static` instead to avoid allocating class instance. ### Request Dependent Service This kind of service may require some property from the request, and should be **initiated as an Elysia instance**. ### ❌ Don't: Pass entire `Context` to a service **Context is a highly dynamic type** that can be inferred from Elysia instance. Do not pass an entire `Context` to a service, instead use object destructuring to extract what you need and pass it to the service. ```typescript import type { Context } from 'elysia' class AuthService { constructor() {} // ❌ Don't do this isSignIn({ cookie: { session } }: Context) { if (session.value) return error(401) } } ``` As Elysia type is complex, and heavily depends on plugin and multiple level of chaining, it can be challenging to manually type as it's highly dynamic. ### ✅ Do: Use Elysia instance as a service We recommended to use Elysia instance as a service to ensure type integrity and inference: ```typescript import { Elysia } from 'elysia' // ✅ Do const AuthService = new Elysia({ name: 'Service.Auth' }) .derive({ as: 'scoped' }, ({ cookie: { session } }) => ({ // This is equivalent to dependency injection Auth: { user: session.value } })) .macro(({ onBeforeHandle }) => ({ // This is declaring a service method isSignIn(value: boolean) { onBeforeHandle(({ Auth, error }) => { if (!Auth?.user || !Auth.user) return error(401) }) } })) const UserController = new Elysia() .use(AuthService) .get('/profile', ({ Auth: { user } }) => user, { isSignIn: true }) ``` ::: tip Elysia handle [plugin deduplication](/essential/plugin.html#plugin-deduplication) by default so you don't have to worry about performance, as it's going to be Singleton if you specified a **"name"** property. ::: ### ⚠️ Infers Context from Elysia instance In case of **absolute necessity**, you may infer the `Context` type from the Elysia instance itself: ```typescript import { Elysia, type InferContext } from 'elysia' const setup = new Elysia() .state('a', 'a') .decorate('b', 'b') class AuthService { constructor() {} // ✅ Do isSignIn({ cookie: { session } }: InferContext) { if (session.value) return error(401) } } ``` However we recommend to avoid this if possible, and use [Elysia as a service](#✅-do-use-elysia-as-a-controller) instead. You may find more about [InferContext](/essential/handler#infercontext) in [Essential: Handler](/essential/handler). ## Model Model or [DTO (Data Transfer Object)](https://en.wikipedia.org/wiki/Data_transfer_object) is handle by [Elysia.t (Validation)](/essential/validation.html#elysia-type). Elysia has a validation system built-in which can infers type from your code and validate it at runtime. ### ❌ Don't: Declare a class instance as a model Do not declare a class instance as a model: ```typescript // ❌ Don't class CustomBody { username: string password: string constructor(username: string, password: string) { this.username = username this.password = password } } // ❌ Don't interface ICustomBody { username: string password: string } ``` ### ✅ Do: Use Elysia's validation system Instead of declaring a class or interface, use Elysia's validation system to define a model: ```typescript twoslash // ✅ Do import { Elysia, t } from 'elysia' const customBody = t.Object({ username: t.String(), password: t.String() }) // Optional if you want to get the type of the model // Usually if we didn't use the type, as it's already inferred by Elysia type CustomBody = typeof customBody.static // ^? export { customBody } ``` We can get type of model by using `typeof` with `.static` property from the model. Then you can use the `CustomBody` type to infer the type of the request body. ```typescript twoslash import { Elysia, t } from 'elysia' const customBody = t.Object({ username: t.String(), password: t.String() }) // ---cut--- // ✅ Do new Elysia() .post('/login', ({ body }) => { // ^? return body }, { body: customBody }) ``` ### ❌ Don't: Declare type separate from the model Do not declare a type separate from the model, instead use `typeof` with `.static` property to get the type of the model. ```typescript // ❌ Don't import { Elysia, t } from 'elysia' const customBody = t.Object({ username: t.String(), password: t.String() }) type CustomBody = { username: string password: string } // ✅ Do const customBody = t.Object({ username: t.String(), password: t.String() }) type CustomBody = typeof customBody.static ``` ### Group You can group multiple models into a single object to make it more organized. ```typescript import { Elysia, t } from 'elysia' export const AuthModel = { sign: t.Object({ username: t.String(), password: t.String() }) } const models = AuthModel.models ``` ### Model Injection Though this is optional, if you are strictly following MVC pattern, you may want to inject like a service into a controller. We recommended using [Elysia reference model](/essential/validation#reference-model) Using Elysia's model reference ```typescript twoslash import { Elysia, t } from 'elysia' const customBody = t.Object({ username: t.String(), password: t.String() }) const AuthModel = new Elysia() .model({ 'auth.sign': customBody }) const models = AuthModel.models const UserController = new Elysia({ prefix: '/auth' }) .use(AuthModel) .post('/sign-in', async ({ body, cookie: { session } }) => { // ^? return true }, { body: 'auth.sign' }) ``` This approach provide several benefits: 1. Allow us to name a model and provide auto-completion. 2. Modify schema for later usage, or perform a [remap](/essential/handler.html#remap). 3. Show up as "models" in OpenAPI compliance client, eg. Swagger. 4. Improve TypeScript inference speed as model type will be cached during registration. ## Reuse a plugin It's ok to reuse plugins multiple time to provide type inference. Elysia handle plugin deduplication automatically by default, and the performance is negligible. To create a unique plugin, you may provide a **name** or optional **seed** to an Elysia instance. ```typescript import { Elysia } from 'elysia' const plugin = new Elysia({ name: 'my-plugin' }) .decorate("type", "plugin") const app = new Elysia() .use(plugin) .use(plugin) .use(plugin) .use(plugin) .listen(3000) ``` This allows Elysia to improve performance by reusing the registered plugins instead of processing the plugin over and over again. --- --- url: /integrations/better-auth.md --- # Better Auth Better Auth is framework-agnostic authentication (and authorization) framework for TypeScript. It provides a comprehensive set of features out of the box and includes a plugin ecosystem that simplifies adding advanced functionalities. Better Auth has a cli tool to generate auth schema and migrate our database as well. It currently has 3 database adapters: - [Prisma](https://www.prisma.io/) - [Drizzle](https://orm.drizzle.team/) - [Mongoose](https://mongoosejs.com/) ## Better Auth CLI Better Auth has a cli tool to generate auth schema with the following core tables in our database: `user`, `session`, `account`, and `verification`. More information about the core schema can be found in [Better Auth Core Schema](https://www.better-auth.com/docs/concepts/database#core-schema). To read more on configuring your database, please refer to [Better Auth Database](https://www.better-auth.com/docs/concepts/database). To read more on how to use the cli, please refer to [Better Auth CLI](https://www.better-auth.com/docs/concepts/cli). ## Installation To install Better Auth, run the following command: ```bash bun add better-auth ``` Make sure to set your environment variables for better auth secret `BETTER_AUTH_SECRET=` and other enviroment variables such as Github and Google client id and secret. In your project inside the `src` folder, create a `libs/auth` or `utils/auth` folder, and create a `auth.ts` file inside it and copy the following code: ## Better Auth Instance ```ts import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import db from "../../database"; import { account, session, user, verification } from "../../database/schema"; export const auth = betterAuth({ database: drizzleAdapter(db, { // We're using Drizzle as our database provider: "pg", /* * Map your schema into a better-auth schema */ schema: { user, session, verification, account, }, }), emailAndPassword: { enabled: true // If you want to use email and password auth }, socialProviders: { /* * We're using Google and Github as our social provider, * make sure you have set your environment variables */ github: { clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }, google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }, }, }); ``` Now just run to generate an auth schema with the necessary tables. ``` bash bunx @better-auth/cli generate --config ./src/libs/auth/auth.ts ``` Additionally you can use the `--output` option to specify the output directory for the generated files. We can then use the drizzle migrate command to migrate our database `drizzle-kit migrate`. ## Better Auth View We need to setup a view to handle contexts for better auth. Create a file inside `src/utils/auth-view.ts` or `src/libs/auth/auth-view.ts` and copy the following code: ```ts import { Context } from "elysia"; import { auth } from "./auth"; const betterAuthView = (context: Context) => { const BETTER_AUTH_ACCEPT_METHODS = ["POST", "GET"] if(BETTER_AUTH_ACCEPT_METHODS.includes(context.request.method)) { console.log(context.request) return auth.handler(context.request); } else { context.error(405) } } export default betterAuthView; ``` ## Better Auth Middleware We can setup a simple middleware to handle better auth. Create a file inside `src/middlewares/auth-middleware.ts` and copy the following code: ```ts import { Session, User } from "better-auth/types"; import { auth } from "../../utils/auth/auth"; import { Context } from "elysia"; export const userMiddleware = async (c: Context) => { const session = await auth.api.getSession({ headers: c.request.headers }); if (!session) { c.set.status = 401; return { success: 'error', message: "Unauthorized Access: Token is missing" }; } return { user: session.user, session: session.session } } export const userInfo = (user: User | null, session: Session | null) => { return { user: user, session: session } } ``` ## Attaching Better Auth Into Our Elysia App Inside our index.ts file, we can attach the auth view so that it listens to our auth routes and add the following code: ```ts const app = new Elysia() .use(cors()).use(swagger()).all("/api/auth/*", betterAuthView); app.listen(process.env.BACKEND_PORT || 8000); console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ); ``` Our Auth Should now be working as expected! We can then just access our auth routes from our frontend as such: ```ts import { createAuthClient } from "better-auth/client" export const authClient = createAuthClient({ baseURL: process.env.BETTER_AUTH_URL! }) export const signinGoogle = async () => { const data = await authClient.signIn.social({ provider: "google", }); return data; }; ``` For a detailed client side guide do check out [Better Auth Frontend](https://www.better-auth.com/docs/concepts/client) --- --- url: /integrations/cheat-sheet.md --- # Cheat Sheet Here are a quick overview for a common Elysia patterns ## Hello World A simple hello world ```typescript import { Elysia } from 'elysia' new Elysia() .get('/', () => 'Hello World') .listen(3000) ``` ## Custom HTTP Method Define route using custom HTTP methods/verbs See [Route](/essential/route.html#custom-method) ```typescript import { Elysia } from 'elysia' new Elysia() .get('/hi', () => 'Hi') .post('/hi', () => 'From Post') .put('/hi', () => 'From Put') .route('M-SEARCH', '/hi', () => 'Custom Method') .listen(3000) ``` ## Path Parameter Using dynamic path parameter See [Path](/essential/route.html#path-type) ```typescript import { Elysia } from 'elysia' new Elysia() .get('/id/:id', ({ params: { id } }) => id) .get('/rest/*', () => 'Rest') .listen(3000) ``` ## Return JSON Elysia converts response to JSON automatically See [Handler](/essential/handler.html) ```typescript import { Elysia } from 'elysia' new Elysia() .get('/json', () => { return { hello: 'Elysia' } }) .listen(3000) ``` ## Return a file A file can be return in as formdata response The response must be a 1-level deep object ```typescript import { Elysia, file } from 'elysia' new Elysia() .get('/json', () => { return { hello: 'Elysia', image: file('public/cat.jpg') } }) .listen(3000) ``` ## Header and status Set a custom header and a status code See [Handler](/essential/handler.html) ```typescript import { Elysia } from 'elysia' new Elysia() .get('/', ({ set, error }) => { set.headers['x-powered-by'] = 'Elysia' return error(418, "I'm a teapot") }) .listen(3000) ``` ## Group Define a prefix once for sub routes See [Group](/essential/route.html#group) ```typescript import { Elysia } from 'elysia' new Elysia() .get("/", () => "Hi") .group("/auth", app => { return app .get("/", () => "Hi") .post("/sign-in", ({ body }) => body) .put("/sign-up", ({ body }) => body) }) .listen(3000) ``` ## Schema Enforce a data type of a route See [Validation](/essential/validation) ```typescript import { Elysia, t } from 'elysia' new Elysia() .post('/mirror', ({ body: { username } }) => username, { body: t.Object({ username: t.String(), password: t.String() }) }) .listen(3000) ``` ## File upload See [Validation#file](/essential/validation#file) ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .post('/body', ({ body }) => body, { // ^? body: t.Object({ file: t.File({ format: 'image/*' }), multipleFiles: t.Files() }) }) .listen(3000) ``` ## Lifecycle Hook Intercept an Elysia event in order See [Lifecycle](/essential/life-cycle.html) ```typescript import { Elysia, t } from 'elysia' new Elysia() .onRequest(() => { console.log('On request') }) .on('beforeHandle', () => { console.log('Before handle') }) .post('/mirror', ({ body }) => body, { body: t.Object({ username: t.String(), password: t.String() }), afterHandle: () => { console.log("After handle") } }) .listen(3000) ``` ## Guard Enforce a data type of sub routes See [Scope](/essential/plugin.html#scope) ```typescript twoslash // @errors: 2345 import { Elysia, t } from 'elysia' new Elysia() .guard({ response: t.String() }, (app) => app .get('/', () => 'Hi') // Invalid: will throws error, and TypeScript will report error .get('/invalid', () => 1) ) .listen(3000) ``` ## Custom context Add custom variable to route context See [Context](/essential/handler.html#context) ```typescript import { Elysia } from 'elysia' new Elysia() .state('version', 1) .decorate('getDate', () => Date.now()) .get('/version', ({ getDate, store: { version } }) => `${version} ${getDate()}`) .listen(3000) ``` ## Redirect Redirect a response See [Handler](/essential/handler.html#redirect) ```typescript import { Elysia } from 'elysia' new Elysia() .get('/', () => 'hi') .get('/redirect', ({ redirect }) => { return redirect('/') }) .listen(3000) ``` ## Plugin Create a separate instance See [Plugin](/essential/plugin) ```typescript import { Elysia } from 'elysia' const plugin = new Elysia() .state('plugin-version', 1) .get('/hi', () => 'hi') new Elysia() .use(plugin) .get('/version', ({ store }) => store['plugin-version']) .listen(3000) ``` ## Web Socket Create a realtime connection using Web Socket See [Web Socket](/patterns/websocket) ```typescript import { Elysia } from 'elysia' new Elysia() .ws('/ping', { message(ws, message) { ws.send('hello ' + message) } }) .listen(3000) ``` ## OpenAPI documentation Create interactive documentation using Scalar (or optionally Swagger) See [swagger](/plugins/swagger.html) ```typescript import { Elysia } from 'elysia' import { swagger } from '@elysiajs/swagger' const app = new Elysia() .use(swagger()) .listen(3000) console.log(`View documentation at "${app.server!.url}swagger" in your browser`); ``` ## Unit Test Write a unit test of your Elysia app See [Unit Test](/patterns/unit-test) ```typescript // test/index.test.ts import { describe, expect, it } from 'bun:test' import { Elysia } from 'elysia' describe('Elysia', () => { it('return a response', async () => { const app = new Elysia().get('/', () => 'hi') const response = await app .handle(new Request('http://localhost/')) .then((res) => res.text()) expect(response).toBe('hi') }) }) ``` ## Custom body parser Create custom logic for parsing body See [Parse](/essential/life-cycle.html#parse) ```typescript import { Elysia } from 'elysia' new Elysia() .onParse(({ request, contentType }) => { if (contentType === 'application/custom-type') return request.text() }) ``` ## GraphQL Create a custom GraphQL server using GraphQL Yoga or Apollo See [GraphQL Yoga](/plugins/graphql-yoga) ```typescript import { Elysia } from 'elysia' import { yoga } from '@elysiajs/graphql-yoga' const app = new Elysia() .use( yoga({ typeDefs: /* GraphQL */` type Query { hi: String } `, resolvers: { Query: { hi: () => 'Hello from Elysia' } } }) ) .listen(3000) ``` --- --- url: /patterns/configuration.md --- # Config Elysia comes with a configurable behavior, allowing us to customize various aspects of its functionality. We can define a configuration by using a constructor. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia({ prefix: '/v1', normalize: true }) ``` ## adapter ###### Since 1.1.11 Runtime adapter for using Elysia in different environments. Default to appropriate adapter based on the environment. ```ts import { Elysia, t } from 'elysia' import { BunAdapter } from 'elysia/adapter/bun' new Elysia({ adapter: BunAdapter }) ``` ## aot ###### Since 0.4.0 Ahead of Time compilation. Elysia has a built-in JIT _"compiler"_ that can [optimize performance](/blog/elysia-04.html#ahead-of-time-complie). ```ts twoslash import { Elysia } from 'elysia' new Elysia({ aot: true }) ``` ## detail Define an OpenAPI schema for all routes of an instance. This schema will be used to generate OpenAPI documentation for all routes of an instance. ```ts twoslash import { Elysia } from 'elysia' new Elysia({ detail: { hide: true, tags: ['elysia'] } }) ``` Disable Ahead of Time compilation #### Options - @default `false` - `true` - Precompile every route before starting the server - `false` - Disable JIT entirely. Faster startup time without cost of performance ## name Define a name of an instance which is used for debugging and [Plugin Deduplication](/essential/plugin.html#plugin-deduplication) ```ts twoslash import { Elysia } from 'elysia' new Elysia({ name: 'service.thing' }) ``` ## nativeStaticResponse ###### Since 1.1.11 Use an optimized function for handling inline value for each respective runtime. ```ts twoslash import { Elysia } from 'elysia' new Elysia({ nativeStaticResponse: true }) ``` #### Example If enabled on Bun, Elysia will insert inline value into `Bun.serve.static` improving performance for static value. ```ts import { Elysia } from 'elysia' // This new Elysia({ nativeStaticResponse: true }).get('/version', 1) // is an equivalent to Bun.serve({ static: { '/version': new Response(1) } }) ``` ## normalize ###### Since 1.1.0 Whether Elysia should coerce field into a specified schema. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia({ normalize: true }) ``` When unknown properties that is not specified in schema is found on either input and output, how should Elysia handle the field? Options - @default `true` - `true`: Elysia will coerce fields into a specified schema. - `false`: Elysia will raise an error if a request or response contains fields that are not explicitly allowed in the schema of the respective handler. ## precompile ###### Since 1.0.0 Whether should Elysia should [precompile all routes](/blog/elysia-10.html#improved-startup-time) a head of time before starting the server. ```ts twoslash import { Elysia } from 'elysia' new Elysia({ precompile: true }) ``` Options - @default `false` - `true`: Run JIT on all routes before starting the server - `false`: Dynamically compile routes on demand It's recommended to leave it as `false`. ## prefix Define a prefix for all routes of an instance ```ts twoslash import { Elysia, t } from 'elysia' new Elysia({ prefix: '/v1' }) ``` When prefix is defined, all routes will be prefixed with the given value. #### Example ```ts twoslash import { Elysia, t } from 'elysia' new Elysia({ prefix: '/v1' }).get('/name', 'elysia') // Path is /v1/name ``` ## seed Define a value which will be used to generate checksum of an instance, used for [Plugin Deduplication](/essential/plugin.html#plugin-deduplication) ```ts twoslash import { Elysia } from 'elysia' new Elysia({ seed: { value: 'service.thing' } }) ``` The value could be any type not limited to string, number, or object. ## strictPath Whether should Elysia handle path strictly. According to [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.3), a path should be strictly equal to the path defined in the route. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia({ strictPath: true }) ``` #### Options - @default `false` - `true` - Follows [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.3) for path matching strictly - `false` - Tolerate suffix '/' or vice-versa. #### Example ```ts twoslash import { Elysia, t } from 'elysia' // Path can be either /name or /name/ new Elysia({ strictPath: false }).get('/name', 'elysia') // Path can be only /name new Elysia({ strictPath: true }).get('/name', 'elysia') ``` ## serve Customize HTTP server behavior. Bun serve configuration. ```ts import { Elysia } from 'elysia' new Elysia({ serve: { hostname: 'elysiajs.com', tls: { cert: Bun.file('cert.pem'), key: Bun.file('key.pem') } }, }) ``` This configuration extends [Bun Serve API](https://bun.sh/docs/api/http) and [Bun TLS](https://bun.sh/docs/api/http#tls) ### Example: Max body size We can set the maximum body size by setting [`serve.maxRequestBodySize`](#serve-maxrequestbodysize) in the `serve` configuration. ```ts import { Elysia } from 'elysia' new Elysia({ serve: { maxRequestBodySize: 1024 * 1024 * 256 // 256MB } }) ``` By default the maximum request body size is 128MB (1024 * 1024 * 128). Define body size limit. ```ts import { Elysia } from 'elysia' new Elysia({ serve: { // Maximum message size (in bytes) maxPayloadLength: 64 * 1024, } }) ``` ### Example: HTTPS / TLS We can enable TLS (known as successor of SSL) by passing in a value for key and cert; both are required to enable TLS. ```ts import { Elysia, file } from 'elysia' new Elysia({ serve: { tls: { cert: file('cert.pem'), key: file('key.pem') } } }) ``` Elysia extends Bun configuration which supports TLS out of the box, powered by BoringSSL. See [serve.tls](#serve-tls) for available configuration. ### serve.hostname @default `0.0.0.0` Set the hostname which the server listens on ### serve.id Uniquely identify a server instance with an ID This string will be used to hot reload the server without interrupting pending requests or websockets. If not provided, a value will be generated. To disable hot reloading, set this value to `null`. ### serve.maxRequestBodySize @default `1024 * 1024 * 128` (128MB) Set the maximum size of a request body (in bytes) ### serve.port @default `3000` Port to listen on ### serve.rejectUnauthorized @default `NODE_TLS_REJECT_UNAUTHORIZED` environment variable If set to `false`, any certificate is accepted. ### serve.reusePort @default `true` If the `SO_REUSEPORT` flag should be set This allows multiple processes to bind to the same port, which is useful for load balancing This configuration is override and turns on by default by Elysia ### serve.unix If set, the HTTP server will listen on a unix socket instead of a port. (Cannot be used with hostname+port) ### serve.tls We can enable TLS (known as successor of SSL) by passing in a value for key and cert; both are required to enable TLS. ```ts import { Elysia, file } from 'elysia' new Elysia({ serve: { tls: { cert: file('cert.pem'), key: file('key.pem') } } }) ``` Elysia extends Bun configuration which supports TLS out of the box, powered by BoringSSL. ### serve.tls.ca Optionally override the trusted CA certificates. Default is to trust the well-known CAs curated by Mozilla. Mozilla's CAs are completely replaced when CAs are explicitly specified using this option. ### serve.tls.cert Cert chains in PEM format. One cert chain should be provided per private key. Each cert chain should consist of the PEM formatted certificate for a provided private key, followed by the PEM formatted intermediate certificates (if any), in order, and not including the root CA (the root CA must be pre-known to the peer, see ca). When providing multiple cert chains, they do not have to be in the same order as their private keys in key. If the intermediate certificates are not provided, the peer will not be able to validate the certificate, and the handshake will fail. ### serve.tls.dhParamsFile File path to a .pem file custom Diffie Helman parameters ### serve.tls.key Private keys in PEM format. PEM allows the option of private keys being encrypted. Encrypted keys will be decrypted with options.passphrase. Multiple keys using different algorithms can be provided either as an array of unencrypted key strings or buffers, or an array of objects in the form . The object form can only occur in an array. **object.passphrase** is optional. Encrypted keys will be decrypted with **object.passphrase** if provided, or **options.passphrase** if it is not. ### serve.tls.lowMemoryMode @default `false` This sets `OPENSSL_RELEASE_BUFFERS` to 1. It reduces overall performance but saves some memory. ### serve.tls.passphrase Shared passphrase for a single private key and/or a PFX. ### serve.tls.requestCert @default `false` If set to `true`, the server will request a client certificate. ### serve.tls.secureOptions Optionally affect the OpenSSL protocol behavior, which is not usually necessary. This should be used carefully if at all! Value is a numeric bitmask of the SSL_OP_* options from OpenSSL Options ### serve.tls.serverName Explicitly set a server name ## tags Define an tags for OpenAPI schema for all routes of an instance similar to [detail](#detail) ```ts twoslash import { Elysia } from 'elysia' new Elysia({ tags: ['elysia'] }) ``` ## websocket Override websocket configuration Recommended to leave this as default as Elysia will generate suitable configuration for handling WebSocket automatically This configuration extends [Bun's WebSocket API](https://bun.sh/docs/api/websockets) #### Example ```ts import { Elysia } from 'elysia' new Elysia({ websocket: { // enable compression and decompression perMessageDeflate: true } }) ``` ---
# Experimental Try out an experimental feature which might be available in the future version of Elysia. ## experimental.encodeSchema Handle custom `t.Transform` schema with custom `Encode` before returning the response to client. This allows us to create custom encode function for your data before sending response to the client. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia({ experimental: { encodeSchema: true } }) ``` #### Options - @default `false` - `true` - Run `Encode` before sending the response to client - `false` - Skip `Encode` entirely --- --- url: /plugins/cors.md --- # CORS Plugin This plugin adds support for customizing [Cross-Origin Resource Sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) behavior. Install with: ```bash bun add @elysiajs/cors ``` Then use it: ```typescript twoslash import { Elysia } from 'elysia' import { cors } from '@elysiajs/cors' new Elysia().use(cors()).listen(3000) ``` This will set Elysia to accept requests from any origin. ## Config Below is a config which is accepted by the plugin ### origin @default `true` Indicates whether the response can be shared with the requesting code from the given origins. Value can be one of the following: - **string** - Name of origin which will directly assign to [Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) header. - **boolean** - If set to true, [Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) will be set to `*` (any origins) - **RegExp** - Pattern to match request's URL, allowed if matched. - **Function** - Custom logic to allow resource sharing, allow if `true` is returned. - Expected to have the type of: ```typescript cors(context: Context) => boolean | void ``` - **Array** - iterate through all cases above in order, allowed if any of the values are `true`. --- ### methods @default `*` Allowed methods for cross-origin requests. Assign [Access-Control-Allow-Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods) header. Value can be one of the following: - **undefined | null | ''** - Ignore all methods. - **\*** - Allows all methods. - **string** - Expects either a single method or a comma-delimited string - (eg: `'GET, PUT, POST'`) - **string[]** - Allow multiple HTTP methods. - eg: `['GET', 'PUT', 'POST']` --- ### allowedHeaders @default `*` Allowed headers for an incoming request. Assign [Access-Control-Allow-Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) header. Value can be one of the following: - **string** - Expects either a single header or a comma-delimited string - eg: `'Content-Type, Authorization'`. - **string[]** - Allow multiple HTTP headers. - eg: `['Content-Type', 'Authorization']` --- ### exposeHeaders @default `*` Response CORS with specified headers. Assign [Access-Control-Expose-Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers) header. Value can be one of the following: - **string** - Expects either a single header or a comma-delimited string. - eg: `'Content-Type, X-Powered-By'`. - **string[]** - Allow multiple HTTP headers. - eg: `['Content-Type', 'X-Powered-By']` --- ### credentials @default `true` The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to the frontend JavaScript code when the request's credentials mode [Request.credentials](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) is `include`. When a request's credentials mode [Request.credentials](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) is `include`, browsers will only expose the response to the frontend JavaScript code if the Access-Control-Allow-Credentials value is true. Credentials are cookies, authorization headers, or TLS client certificates. Assign [Access-Control-Allow-Credentials](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials) header. --- ### maxAge @default `5` Indicates how long the results of a [preflight request](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request) (that is the information contained in the [Access-Control-Allow-Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods) and [Access-Control-Allow-Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) headers) can be cached. Assign [Access-Control-Max-Age](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age) header. --- ### preflight The preflight request is a request sent to check if the CORS protocol is understood and if a server is aware of using specific methods and headers. Response with **OPTIONS** request with 3 HTTP request headers: - **Access-Control-Request-Method** - **Access-Control-Request-Headers** - **Origin** This config indicates if the server should respond to preflight requests. ## Pattern Below you can find the common patterns to use the plugin. ## Allow CORS by top-level domain ```typescript twoslash import { Elysia } from 'elysia' import { cors } from '@elysiajs/cors' const app = new Elysia() .use( cors({ origin: /.*\.saltyaom\.com$/ }) ) .get('/', () => 'Hi') .listen(3000) ``` This will allow requests from top-level domains with `saltyaom.com` --- --- url: /plugins/cron.md --- # Cron Plugin This plugin adds support for running cronjob in the Elysia server. Install with: ```bash bun add @elysiajs/cron ``` Then use it: ```typescript twoslash import { Elysia } from 'elysia' import { cron } from '@elysiajs/cron' new Elysia() .use( cron({ name: 'heartbeat', pattern: '*/10 * * * * *', run() { console.log('Heartbeat') } }) ) .listen(3000) ``` The above code will log `heartbeat` every 10 seconds. ## cron Create a cronjob for the Elysia server. type: ``` cron(config: CronConfig, callback: (Instance['store']) => void): this ``` `CronConfig` accepts the parameters specified below: ### name Job name to register to `store`. This will register the cron instance to `store` with a specified name, which can be used to reference in later processes eg. stop the job. ### pattern Time to run the job as specified by [cron syntax](https://en.wikipedia.org/wiki/Cron) specified as below: ``` ┌────────────── second (optional) │ ┌──────────── minute │ │ ┌────────── hour │ │ │ ┌──────── day of the month │ │ │ │ ┌────── month │ │ │ │ │ ┌──── day of week │ │ │ │ │ │ * * * * * * ``` This can be generated by tools like [Crontab Guru](https://crontab.guru/) --- This plugin extends the cron method to Elysia using [cronner](https://github.com/hexagon/croner). Below are the configs accepted by cronner. ### timezone Time zone in Europe/Stockholm format ### startAt Schedule start time for the job ### stopAt Schedule stop time for the job ### maxRuns Maximum number of executions ### catch Continue execution even if an unhandled error is thrown by a triggered function. ### interval The minimum interval between executions, in seconds. ## Pattern Below you can find the common patterns to use the plugin. ## Stop cronjob You can stop cronjob manually by accessing the cronjob name registered to `store`. ```typescript import { Elysia } from 'elysia' import { cron } from '@elysiajs/cron' const app = new Elysia() .use( cron({ name: 'heartbeat', pattern: '*/1 * * * * *', run() { console.log('Heartbeat') } }) ) .get( '/stop', ({ store: { cron: { heartbeat } } }) => { heartbeat.stop() return 'Stop heartbeat' } ) .listen(3000) ``` ## Predefined patterns You can use predefined patterns from `@elysiajs/cron/schedule` ```typescript import { Elysia } from 'elysia' import { cron, Patterns } from '@elysiajs/cron' const app = new Elysia() .use( cron({ name: 'heartbeat', pattern: Patterns.everySecond(), run() { console.log('Heartbeat') } }) ) .get( '/stop', ({ store: { cron: { heartbeat } } }) => { heartbeat.stop() return 'Stop heartbeat' } ) .listen(3000) ``` ### Functions | Function | Description | | ---------------------------------------- | ----------------------------------------------------- | | `.everySeconds(2)` | Run the task every 2 seconds | | `.everyMinutes(5)` | Run the task every 5 minutes | | `.everyHours(3)` | Run the task every 3 hours | | `.everyHoursAt(3, 15)` | Run the task every 3 hours at 15 minutes | | `.everyDayAt('04:19')` | Run the task every day at 04:19 | | `.everyWeekOn(Patterns.MONDAY, '19:30')` | Run the task every Monday at 19:30 | | `.everyWeekdayAt('17:00')` | Run the task every day from Monday to Friday at 17:00 | | `.everyWeekendAt('11:00')` | Run the task on Saturday and Sunday at 11:00 | ### Function aliases to constants | Function | Constant | | ----------------- | ---------------------------------- | | `.everySecond()` | EVERY_SECOND | | `.everyMinute()` | EVERY_MINUTE | | `.hourly()` | EVERY_HOUR | | `.daily()` | EVERY_DAY_AT_MIDNIGHT | | `.everyWeekday()` | EVERY_WEEKDAY | | `.everyWeekend()` | EVERY_WEEKEND | | `.weekly()` | EVERY_WEEK | | `.monthly()` | EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT | | `.everyQuarter()` | EVERY_QUARTER | | `.yearly()` | EVERY_YEAR | ### Constants | Constant | Pattern | | ---------------------------------------- | -------------------- | | `.EVERY_SECOND` | `* * * * * *` | | `.EVERY_5_SECONDS` | `*/5 * * * * *` | | `.EVERY_10_SECONDS` | `*/10 * * * * *` | | `.EVERY_30_SECONDS` | `*/30 * * * * *` | | `.EVERY_MINUTE` | `*/1 * * * *` | | `.EVERY_5_MINUTES` | `0 */5 * * * *` | | `.EVERY_10_MINUTES` | `0 */10 * * * *` | | `.EVERY_30_MINUTES` | `0 */30 * * * *` | | `.EVERY_HOUR` | `0 0-23/1 * * *` | | `.EVERY_2_HOURS` | `0 0-23/2 * * *` | | `.EVERY_3_HOURS` | `0 0-23/3 * * *` | | `.EVERY_4_HOURS` | `0 0-23/4 * * *` | | `.EVERY_5_HOURS` | `0 0-23/5 * * *` | | `.EVERY_6_HOURS` | `0 0-23/6 * * *` | | `.EVERY_7_HOURS` | `0 0-23/7 * * *` | | `.EVERY_8_HOURS` | `0 0-23/8 * * *` | | `.EVERY_9_HOURS` | `0 0-23/9 * * *` | | `.EVERY_10_HOURS` | `0 0-23/10 * * *` | | `.EVERY_11_HOURS` | `0 0-23/11 * * *` | | `.EVERY_12_HOURS` | `0 0-23/12 * * *` | | `.EVERY_DAY_AT_1AM` | `0 01 * * *` | | `.EVERY_DAY_AT_2AM` | `0 02 * * *` | | `.EVERY_DAY_AT_3AM` | `0 03 * * *` | | `.EVERY_DAY_AT_4AM` | `0 04 * * *` | | `.EVERY_DAY_AT_5AM` | `0 05 * * *` | | `.EVERY_DAY_AT_6AM` | `0 06 * * *` | | `.EVERY_DAY_AT_7AM` | `0 07 * * *` | | `.EVERY_DAY_AT_8AM` | `0 08 * * *` | | `.EVERY_DAY_AT_9AM` | `0 09 * * *` | | `.EVERY_DAY_AT_10AM` | `0 10 * * *` | | `.EVERY_DAY_AT_11AM` | `0 11 * * *` | | `.EVERY_DAY_AT_NOON` | `0 12 * * *` | | `.EVERY_DAY_AT_1PM` | `0 13 * * *` | | `.EVERY_DAY_AT_2PM` | `0 14 * * *` | | `.EVERY_DAY_AT_3PM` | `0 15 * * *` | | `.EVERY_DAY_AT_4PM` | `0 16 * * *` | | `.EVERY_DAY_AT_5PM` | `0 17 * * *` | | `.EVERY_DAY_AT_6PM` | `0 18 * * *` | | `.EVERY_DAY_AT_7PM` | `0 19 * * *` | | `.EVERY_DAY_AT_8PM` | `0 20 * * *` | | `.EVERY_DAY_AT_9PM` | `0 21 * * *` | | `.EVERY_DAY_AT_10PM` | `0 22 * * *` | | `.EVERY_DAY_AT_11PM` | `0 23 * * *` | | `.EVERY_DAY_AT_MIDNIGHT` | `0 0 * * *` | | `.EVERY_WEEK` | `0 0 * * 0` | | `.EVERY_WEEKDAY` | `0 0 * * 1-5` | | `.EVERY_WEEKEND` | `0 0 * * 6,0` | | `.EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT` | `0 0 1 * *` | | `.EVERY_1ST_DAY_OF_MONTH_AT_NOON` | `0 12 1 * *` | | `.EVERY_2ND_HOUR` | `0 */2 * * *` | | `.EVERY_2ND_HOUR_FROM_1AM_THROUGH_11PM` | `0 1-23/2 * * *` | | `.EVERY_2ND_MONTH` | `0 0 1 */2 *` | | `.EVERY_QUARTER` | `0 0 1 */3 *` | | `.EVERY_6_MONTHS` | `0 0 1 */6 *` | | `.EVERY_YEAR` | `0 0 1 1 *` | | `.EVERY_30_MINUTES_BETWEEN_9AM_AND_5PM` | `0 */30 9-17 * * *` | | `.EVERY_30_MINUTES_BETWEEN_9AM_AND_6PM` | `0 */30 9-18 * * *` | | `.EVERY_30_MINUTES_BETWEEN_10AM_AND_7PM` | `0 */30 10-19 * * *` | --- --- url: /integrations/drizzle.md --- # Drizzle Drizzle ORM is a headless TypeScript ORM with a focus on type safety and developer experience. We may convert Drizzle schema to Elysia validation models using `drizzle-typebox` ### Drizzle Typebox [Elysia.t](/essential/validation.html#elysia-type) is a fork of TypeBox, allowing us to use any TypeBox type in Elysia directly. We can convert Drizzle schema into TypeBox schema using ["drizzle-typebox"](https://npmjs.org/package/drizzle-typebox), and use it directly on Elysia's schema validation. ### Here's how it works: 1. Define your database schema in Drizzle. 2. Convert Drizzle schema into Elysia validation models using `drizzle-typebox`. 3. Use the converted Elysia validation models to ensure type validation. 4. OpenAPI schema is generated from Elysia validation models. 5. Add [Eden Treaty](/eden/overview) to add type-safety to your frontend. ``` * ——————————————— * | | | -> | Documentation | * ————————— * * ———————— * OpenAPI | | | | | drizzle- | | ——————— | * ——————————————— * | Drizzle | —————————-> | Elysia | | | -typebox | | ——————— | * ——————————————— * * ————————— * * ———————— * Eden | | | | -> | Frontend Code | | | * ——————————————— * ``` ## Installation To install Drizzle, run the following command: ```bash bun add drizzle-orm drizzle-typebox ``` Then you need to pin `@sinclair/typebox` as there might be a mismatch version between `drizzle-typebox` and `Elysia`, this may cause conflict of Symbols between two versions. We recommend pinning the version of `@sinclair/typebox` to the **minimum version** used by `elysia` by using: ```bash grep "@sinclair/typebox" node_modules/elysia/package.json ``` We may use `overrides` field in `package.json` to pin the version of `@sinclair/typebox`: ```json { "overrides": { "@sinclair/typebox": "0.32.4" } } ``` ## Drizzle schema Assuming we have a `user` table in our codebase as follows: ::: code-group ```ts [src/database/schema.ts] import { relations } from 'drizzle-orm' import { pgTable, varchar, timestamp } from 'drizzle-orm/pg-core' import { createId } from '@paralleldrive/cuid2' export const user = pgTable( 'user', { id: varchar('id') .$defaultFn(() => createId()) .primaryKey(), username: varchar('username').notNull().unique(), password: varchar('password').notNull(), email: varchar('email').notNull().unique(), salt: varchar('salt', { length: 64 }).notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), } ) export const table = { user } as const export type Table = typeof table ``` ::: ## drizzle-typebox We may convert the `user` table into TypeBox models by using `drizzle-typebox`: ::: code-group ```ts [src/index.ts] import { createInsertSchema } from 'drizzle-typebox' import { table } from './database/schema' const _createUser = createInsertSchema(table.user, { // Replace email with Elysia's email type email: t.String({ format: 'email' }) }) new Elysia() .post('/sign-up', ({ body }) => { // Create a new user }, { body: t.Omit( _createUser, ['id', 'salt', 'createdAt'] ) }) ``` ::: This allows us to reuse the database schema in Elysia validation models ## Type instantiation is possibly infinite If you got an error like **Type instantiation is possibly infinite** this is because of the circular reference between `drizzle-typebox` and `Elysia`. If we nested a type from drizzle-typebox into Elysia schema, it will cause an infinite loop of type instantiation. To prevent this, we need to **explicitly define a type between `drizzle-typebox` and `Elysia`** schema: ```ts import { t } from 'elysia' import { createInsertSchema } from 'drizzle-typebox' import { table } from './database/schema' const _createUser = createInsertSchema(table.user, { email: t.String({ format: 'email' }) }) // ✅ This works, by referencing the type from `drizzle-typebox` const createUser = t.Omit( _createUser, ['id', 'salt', 'createdAt'] ) // ❌ This will cause an infinite loop of type instantiation const createUser = t.Omit( createInsertSchema(table.user, { email: t.String({ format: 'email' }) }), ['id', 'salt', 'createdAt'] ) ``` Always declare a variable for `drizzle-typebox` and reference it if you want to use Elysia type ## Utility As we are likely going to use `t.Pick` and `t.Omit` to exclude or include certain fields, it may be cumbersome to repeat the process: We recommend using these utility functions **(copy as-is)** to simplify the process: ::: code-group ```ts [src/database/utils.ts] /** * @lastModified 2025-02-04 * @see https://elysiajs.com/recipe/drizzle.html#utility */ import { Kind, type TObject } from '@sinclair/typebox' import { createInsertSchema, createSelectSchema, BuildSchema, } from 'drizzle-typebox' import { table } from './schema' import type { Table } from 'drizzle-orm' type Spread< T extends TObject | Table, Mode extends 'select' | 'insert' | undefined, > = T extends TObject ? { [K in keyof Fields]: Fields[K] } : T extends Table ? Mode extends 'select' ? BuildSchema< 'select', T['_']['columns'], undefined >['properties'] : Mode extends 'insert' ? BuildSchema< 'insert', T['_']['columns'], undefined >['properties'] : {} : {} /** * Spread a Drizzle schema into a plain object */ export const spread = < T extends TObject | Table, Mode extends 'select' | 'insert' | undefined, >( schema: T, mode?: Mode, ): Spread => { const newSchema: Record = {} let table switch (mode) { case 'insert': case 'select': if (Kind in schema) { table = schema break } table = mode === 'insert' ? createInsertSchema(schema) : createSelectSchema(schema) break default: if (!(Kind in schema)) throw new Error('Expect a schema') table = schema } for (const key of Object.keys(table.properties)) newSchema[key] = table.properties[key] return newSchema as any } const a = spread(table.user, 'insert') /** * Spread a Drizzle Table into a plain object * * If `mode` is 'insert', the schema will be refined for insert * If `mode` is 'select', the schema will be refined for select * If `mode` is undefined, the schema will be spread as is, models will need to be refined manually */ export const spreads = < T extends Record, Mode extends 'select' | 'insert' | undefined, >( models: T, mode?: Mode, ): { [K in keyof T]: Spread } => { const newSchema: Record = {} const keys = Object.keys(models) for (const key of keys) newSchema[key] = spread(models[key], mode) return newSchema as any } ``` ::: This utility function will convert Drizzle schema into a plain object, which can pick by property name as plain object: ```ts // ✅ Using spread utility function const user = spread(table.user, 'insert') const createUser = t.Object({ id: user.id, // { type: 'string' } username: user.username, // { type: 'string' } password: user.password // { type: 'string' } }) // ⚠️ Using t.Pick const _createUser = createInsertSchema(table.user) const createUser = t.Pick( _createUser, ['id', 'username', 'password'] ) ``` ### Table Singleton We recommend using a singleton pattern to store the table schema, this will allow us to access the table schema from anywhere in the codebase: ::: code-group ```ts [src/database/model.ts] import { table } from './schema' import { spreads } from './utils' export const db = { insert: spreads({ user: table.user, }, 'insert'), select: spreads({ user: table.user, }, 'select') } as const ``` ::: This will allow us to access the table schema from anywhere in the codebase: ::: code-group ```ts [src/index.ts] import { Elysia } from 'elysia' import { db } from './database/model' const { user } = db.insert new Elysia() .post('/sign-up', ({ body }) => { // Create a new user }, { body: t.Object({ id: user.username, username: user.username, password: user.password }) }) ``` ::: ### Refinement If type refinement is needed, you may use `createInsertSchema` and `createSelectSchema` to refine the schema directly. ::: code-group ```ts [src/database/model.ts] import { t } from 'elysia' import { createInsertSchema, createSelectSchema } from 'drizzle-typebox' import { table } from './schema' import { spreads } from './utils' export const db = { insert: spreads({ user: createInsertSchema(table.user, { email: t.String({ format: 'email' }) }), }, 'insert')), select: spreads({ user: createSelectSchema(table.user, { email: t.String({ format: 'email' }) }) }, 'select') } as const ``` ::: In the code above, we refine a `user.email` schema to include a `format` property The `spread` utility function will skip a refined schema, so you can use it as is. --- For more information, please refer to the [Drizzle ORM](https://orm.drizzle.team) and [Drizzle TypeBox](https://orm.drizzle.team/docs/typebox) documentation. --- --- url: /eden/fetch.md --- # Eden Fetch A fetch-like alternative to Eden Treaty . With Eden Fetch can interact with Elysia server in a type-safe manner using Fetch API. --- First export your existing Elysia server type: ```typescript // server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/hi', () => 'Hi Elysia') .get('/id/:id', ({ params: { id } }) => id) .post('/mirror', ({ body }) => body, { body: t.Object({ id: t.Number(), name: t.String() }) }) .listen(3000) export type App = typeof app ``` Then import the server type, and consume the Elysia API on client: ```typescript import { edenFetch } from '@elysiajs/eden' import type { App } from './server' const fetch = edenFetch('http://localhost:3000') // response type: 'Hi Elysia' const pong = await fetch('/hi', {}) // response type: 1895 const id = await fetch('/id/:id', { params: { id: '1895' } }) // response type: { id: 1895, name: 'Skadi' } const nendoroid = await fetch('/mirror', { method: 'POST', body: { id: 1895, name: 'Skadi' } }) ``` ## Error Handling You can handle errors the same way as Eden Treaty: ```typescript import { edenFetch } from '@elysiajs/eden' import type { App } from './server' const fetch = edenFetch('http://localhost:3000') // response type: { id: 1895, name: 'Skadi' } const { data: nendoroid, error } = await fetch('/mirror', { method: 'POST', body: { id: 1895, name: 'Skadi' } }) if(error) { switch(error.status) { case 400: case 401: throw error.value break case 500: case 502: throw error.value break default: throw error.value break } } const { id, name } = nendoroid ``` ## When should I use Eden Fetch over Eden Treaty Unlike Elysia < 1.0, Eden Fetch is not faster than Eden Treaty anymore. The preference is base on you and your team agreement, however we recommend to use [Eden Treaty](/eden/treaty/overview) instead. For Elysia < 1.0: Using Eden Treaty requires a lot of down-level iteration to map all possible types in a single go, while in contrast, Eden Fetch can be lazily executed until you pick a route. With complex types and a lot of server routes, using Eden Treaty on a low-end development device can lead to slow type inference and auto-completion. But as Elysia has tweaked and optimized a lot of types and inference, Eden Treaty can perform very well in the considerable amount of routes. If your single process contains **more than 500 routes**, and you need to consume all of the routes **in a single frontend codebase**, then you might want to use Eden Fetch as it has a significantly better TypeScript performance than Eden Treaty. --- --- url: /eden/installation.md --- # Eden Installation Start by installing Eden on your frontend: ```bash bun add @elysiajs/eden bun add -d elysia # `@types/bun` must be installed even if you aren't using bun in the client bun add -d @types/bun ``` ::: tip Eden needs Elysia to infer utilities type. Make sure to install Elysia with the version matching on the server. ::: First, export your existing Elysia server type: ```typescript // server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/', () => 'Hi Elysia') .get('/id/:id', ({ params: { id } }) => id) .post('/mirror', ({ body }) => body, { body: t.Object({ id: t.Number(), name: t.String() }) }) .listen(3000) export type App = typeof app // [!code ++] ``` Then consume the Elysia API on client side: ```typescript twoslash // @filename: server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/', 'Hi Elysia') .get('/id/:id', ({ params: { id } }) => id) .post('/mirror', ({ body }) => body, { body: t.Object({ id: t.Number(), name: t.String() }) }) .listen(3000) export type App = typeof app // [!code ++] // @filename: index.ts // ---cut--- // client.ts import { treaty } from '@elysiajs/eden' import type { App } from './server' // [!code ++] const client = treaty('localhost:3000') // [!code ++] // response: Hi Elysia const { data: index } = await client.index.get() // response: 1895 const { data: id } = await client.id({ id: 1895 }).get() // response: { id: 1895, name: 'Skadi' } const { data: nendoroid } = await client.mirror.post({ id: 1895, name: 'Skadi' }) // @noErrors client. // ^| ``` ## Gotcha Sometimes Eden may not infer type from Elysia correctly, the following are the most common workaround to fix Eden type inference. ### Type Strict Make sure to enable strict mode in **tsconfig.json** ```json { "compilerOptions": { "strict": true // [!code ++] } } ``` ### Unmatch Elysia version Eden depends Elysia class to import Elysia instance and infers type correctly. Make sure that both client and server have a matching Elysia version. You can check it with [`npm why`](https://docs.npmjs.com/cli/v10/commands/npm-explain) command: ```bash npm why elysia ``` And output should contain only one elysia version on top-level: ``` elysia@1.1.12 node_modules/elysia elysia@"1.1.25" from the root project peer elysia@">= 1.1.0" from @elysiajs/html@1.1.0 node_modules/@elysiajs/html dev @elysiajs/html@"1.1.1" from the root project peer elysia@">= 1.1.0" from @elysiajs/opentelemetry@1.1.2 node_modules/@elysiajs/opentelemetry dev @elysiajs/opentelemetry@"1.1.7" from the root project peer elysia@">= 1.1.0" from @elysiajs/swagger@1.1.0 node_modules/@elysiajs/swagger dev @elysiajs/swagger@"1.1.6" from the root project peer elysia@">= 1.1.0" from @elysiajs/eden@1.1.2 node_modules/@elysiajs/eden dev @elysiajs/eden@"1.1.3" from the root project ``` ### TypeScript version Elysia uses newer features and syntax of TypeScript to infer types in a the most performant way. Features like Const Generic and Template Literal are heavily used. Make sure your client has a **minimum TypeScript version if >= 5.0** ### Method Chaining To make Eden works, Elysia must be using **method chaining** Elysia's type system is complex, methods usually introduce a new type to the instance. Using method chaining will help save that new type reference. For example: ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .state('build', 1) // Store is strictly typed // [!code ++] .get('/', ({ store: { build } }) => build) .listen(3000) ``` Using this, **state** now returns a new **ElysiaInstance** type, introducing **build** into store and replace the current one. Without using method chaining, Elysia doesn't save the new type when introduced, leading to no type inference. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' const app = new Elysia() app.state('build', 1) app.get('/', ({ store: { build } }) => build) app.listen(3000) ``` ### Type Definitions Sometimes, if you are using a Bun specific feature like `Bun.file` or similar API, you may need to install Bun type definitions to the client as well. ```bash bun add -d @types/bun ``` ### Path alias (monorepo) If you are using path alias in your monorepo, make sure that frontend are able to resolve the path as same as backend. For example, if you have the following path alias for your backend in **tsconfig.json**: ```json { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } } ``` And your backend code is like this: ```typescript import { Elysia } from 'elysia' import { a, b } from '@/controllers' const app = new Elysia() .use(a) .use(b) .listen(3000) export type app = typeof app ``` You **must** make sure that your frontend code is able to resolve the same path alias otherwise type inference will be resolved as any. ```typescript import { treaty } from '@elysiajs/eden' import type { app } from '@/index' const client = treaty('localhost:3000') // This should be able to resolve the same module both frontend and backend, and not `any` import { a, b } from '@/controllers' ``` To fix this, you must make sure that path alias is resolved to the same file in both frontend and backend. So you must change the path alias in **tsconfig.json** to: ```json { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["../apps/backend/src/*"] } } } ``` If configured correctly, you should be able to resolve the same module in both frontend and backend. ```typescript // This should be able to resolve the same module both frontend and backend, and not `any` import { a, b } from '@/controllers' ``` #### Scope We recommended to add a **scope** prefix for each modules in your monorepo to avoid any confusion and conflict that may happen. ```json { "compilerOptions": { "baseUrl": ".", "paths": { "@frontend/*": ["./apps/frontend/src/*"], "@backend/*": ["./apps/backend/src/*"] } } } ``` Then you can import the module like this: ```typescript // Should work in both frontend and backend and not return `any` import { a, b } from '@backend/controllers' ``` We recommended creating a **single tsconfig.json** that define a `baseUrl` as the root of your repo, provide a path according to the module location, and create a **tsconfig.json** for each module that inherits the root **tsconfig.json** which has the path alias. You may find a working example of in this [path alias example repo](https://github.com/SaltyAom/elysia-monorepo-path-alias). --- --- url: /eden/test.md --- # Eden Test Using Eden, we can create an integration test with end-to-end type safety and auto-completion. > Using Eden Treaty to create tests by [irvilerodrigues on Twitter](https://twitter.com/irvilerodrigues/status/1724836632300265926) ## Setup We can use [Bun test](https://bun.sh/guides/test/watch-mode) to create tests. Create **test/index.test.ts** in the root of project directory with the following: ```typescript // test/index.test.ts import { describe, expect, it } from 'bun:test' import { edenTreaty } from '@elysiajs/eden' const app = new Elysia() .get('/', () => 'hi') .listen(3000) const api = edenTreaty('http://localhost:3000') describe('Elysia', () => { it('return a response', async () => { const { data } = await api.get() expect(data).toBe('hi') }) }) ``` Then we can perform tests by running **bun test** ```bash bun test ``` This allows us to perform integration tests programmatically instead of manual fetch while supporting type checking automatically. --- --- url: /eden/treaty/config.md --- # Config Eden Treaty accepts 2 parameters: - **urlOrInstance** - URL endpoint or Elysia instance - **options** (optional) - Customize fetch behavior ## urlOrInstance Accept either URL endpoint as string or a literal Elysia instance. Eden will change the behavior based on type as follows: ### URL Endpoint (string) If URL endpoint is passed, Eden Treaty will use `fetch` or `config.fetcher` to create a network request to an Elysia instance. ```typescript import { treaty } from '@elysiajs/eden' import type { App } from './server' const api = treaty('localhost:3000') ``` You may or may not specified a protocol for URL endpoint. Elysia will appends the endpoints automatically as follows: 1. If protocol is specified, use the URL directly 2. If the URL is localhost and ENV is not production, use http 3. Otherwise use https This also apply to Web Socket as well for determining between **ws://** or **wss://**. --- ### Elysia Instance If Elysia instance is passed, Eden Treaty will create a `Request` class and pass to `Elysia.handle` directly without creating a network request. This allows us to interact with Elysia server directly without request overhead, or the need start a server. ```typescript import { Elysia } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .get('/hi', 'Hi Elysia') .listen(3000) const api = treaty(app) ``` If an instance is passed, generic is not needed to be pass as Eden Treaty can infers the type from a parameter directly. This patterns is recommended for performing unit tests, or creating a type-safe reverse proxy server or micro-services. ## Options 2nd optional parameters for Eden Treaty to customize fetch behavior, accepting parameters as follows: - [fetch](#fetch) - add default parameters to fetch intialization (RequestInit) - [headers](#headers) - define default headers - [fetcher](#fetcher) - custom fetch function eg. Axios, unfetch - [onRequest](#onrequest) - Intercept and modify fetch request before firing - [onResponse](#onresponse) - Intercept and modify fetch's response ## Fetch Default parameters append to 2nd parameters of fetch extends type of **Fetch.RequestInit**. ```typescript export type App = typeof app // [!code ++] import { treaty } from '@elysiajs/eden' // ---cut--- treaty('localhost:3000', { fetch: { credentials: 'include' } }) ``` All parameters that passed to fetch, will be passed to fetcher, which is an equivalent to: ```typescript fetch('http://localhost:3000', { credentials: 'include' }) ``` ## Headers Provide an additional default headers to fetch, a shorthand of `options.fetch.headers`. ```typescript treaty('localhost:3000', { headers: { 'X-Custom': 'Griseo' } }) ``` All parameters that passed to fetch, will be passed to fetcher, which is an equivalent to: ```typescript twoslash fetch('localhost:3000', { headers: { 'X-Custom': 'Griseo' } }) ``` headers may accepts the following as parameters: - Object - Function ### Headers Object If object is passed, then it will be passed to fetch directly ```typescript treaty('localhost:3000', { headers: { 'X-Custom': 'Griseo' } }) ``` ### Function You may specify a headers as a function to return custom headers based on condition ```typescript treaty('localhost:3000', { headers(path, options) { if(path.startsWith('user')) return { authorization: 'Bearer 12345' } } }) ``` You may return object to append its value to fetch headers. headers function accepts 2 parameters: - path `string` - path which will be sent to parameter - note: hostname will be **exclude** eg. (/user/griseo) - options `RequestInit`: Parameters that passed through 2nd parameter of fetch ### Array You may define a headers function as an array if multiple conditions are need. ```typescript treaty('localhost:3000', { headers: [ (path, options) => { if(path.startsWith('user')) return { authorization: 'Bearer 12345' } } ] }) ``` Eden Treaty will **run all functions** even if the value is already returned. ## Headers Priority Eden Treaty will prioritize the order headers if duplicated as follows: 1. Inline method - Passed in method function directly 2. headers - Passed in `config.headers` - If `config.headers` is array, parameters that come after will be prioritize 3. fetch - Passed in `config.fetch.headers` For example, for the following example: ```typescript const api = treaty('localhost:3000', { headers: { authorization: 'Bearer Aponia' } }) api.profile.get({ headers: { authorization: 'Bearer Griseo' } }) ``` This will be results in: ```typescript fetch('http://localhost:3000', { headers: { authorization: 'Bearer Griseo' } }) ``` If inline function doesn't specified headers, then the result will be "**Bearer Aponia**" instead. ## Fetcher Provide a custom fetcher function instead of using an environment's default fetch. ```typescript treaty('localhost:3000', { fetcher(url, options) { return fetch(url, options) } }) ``` It's recommended to replace fetch if you want to use other client other than fetch, eg. Axios, unfetch. ## OnRequest Intercept and modify fetch request before firing. You may return object to append the value to **RequestInit**. ```typescript treaty('localhost:3000', { onRequest(path, options) { if(path.startsWith('user')) return { headers: { authorization: 'Bearer 12345' } } } }) ``` If value is returned, Eden Treaty will perform a **shallow merge** for returned value and `value.headers`. **onRequest** accepts 2 parameters: - path `string` - path which will be sent to parameter - note: hostname will be **exclude** eg. (/user/griseo) - options `RequestInit`: Parameters that passed through 2nd parameter of fetch ### Array You may define an onRequest function as an array if multiples conditions are need. ```typescript treaty('localhost:3000', { onRequest: [ (path, options) => { if(path.startsWith('user')) return { headers: { authorization: 'Bearer 12345' } } } ] }) ``` Eden Treaty will **run all functions** even if the value is already returned. ## onResponse Intercept and modify fetch's response or return a new value. ```typescript treaty('localhost:3000', { onResponse(response) { if(response.ok) return response.json() } }) ``` **onRequest** accepts 1 parameter: - response `Response` - Web Standard Response normally returned from `fetch` ### Array You may define an onResponse function as an array if multiple conditions are need. ```typescript treaty('localhost:3000', { onResponse: [ (response) => { if(response.ok) return response.json() } ] }) ``` Unlike [headers](#headers) and [onRequest](#onrequest), Eden Treaty will loop through functions until a returned value is found or error thrown, the returned value will be use as a new response. --- --- url: /eden/treaty/legacy.md --- # Eden Treaty Legacy ::: tip NOTE This is a documentation for Eden Treaty 1 or (edenTreaty) For a new project, we recommended starting with Eden Treaty 2 (treaty) instead. ::: Eden Treaty is an object-like representation of an Elysia server. Providing accessor like a normal object with type directly from the server, helping us to move faster, and make sure that nothing break --- To use Eden Treaty, first export your existing Elysia server type: ```typescript // server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/', () => 'Hi Elysia') .get('/id/:id', ({ params: { id } }) => id) .post('/mirror', ({ body }) => body, { body: t.Object({ id: t.Number(), name: t.String() }) }) .listen(3000) export type App = typeof app // [!code ++] ``` Then import the server type, and consume the Elysia API on client: ```typescript // client.ts import { edenTreaty } from '@elysiajs/eden' import type { App } from './server' // [!code ++] const app = edenTreaty('http://localhost:') // response type: 'Hi Elysia' const { data: pong, error } = app.get() // response type: 1895 const { data: id, error } = app.id['1895'].get() // response type: { id: 1895, name: 'Skadi' } const { data: nendoroid, error } = app.mirror.post({ id: 1895, name: 'Skadi' }) ``` ::: tip Eden Treaty is fully type-safe with auto-completion support. ::: ## Anatomy Eden Treaty will transform all existing paths to object-like representation, that can be described as: ```typescript EdenTreaty.<1>.<2>..({ ...body, $query?: {}, $fetch?: RequestInit }) ``` ### Path Eden will transform `/` into `.` which can be called with a registered `method`, for example: - **/path** -> .path - **/nested/path** -> .nested.path ### Path parameters Path parameters will be mapped automatically by their name in the URL. - **/id/:id** -> .id.`` - eg: .id.hi - eg: .id['123'] ::: tip If a path doesn't support path parameters, TypeScript will show an error. ::: ### Query You can append queries to path with `$query`: ```typescript app.get({ $query: { name: 'Eden', code: 'Gold' } }) ``` ### Fetch Eden Treaty is a fetch wrapper, you can add any valid [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) parameters to Eden by passing it to `$fetch`: ```typescript app.post({ $fetch: { headers: { 'x-organization': 'MANTIS' } } }) ``` ## Error Handling Eden Treaty will return a value of `data` and `error` as a result, both fully typed. ```typescript // response type: { id: 1895, name: 'Skadi' } const { data: nendoroid, error } = app.mirror.post({ id: 1895, name: 'Skadi' }) if(error) { switch(error.status) { case 400: case 401: warnUser(error.value) break case 500: case 502: emergencyCallDev(error.value) break default: reportError(error.value) break } throw error } const { id, name } = nendoroid ``` Both **data**, and **error** will be typed as nullable until you can confirm their statuses with a type guard. To put it simply, if fetch is successful, data will have a value and error will be null, and vice-versa. ::: tip Error is wrapped with an `Error` with its value return from the server can be retrieve from `Error.value` ::: ### Error type based on status Both Eden Treaty and Eden Fetch can narrow down an error type based on status code if you explicitly provided an error type in the Elysia server. ```typescript // server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .model({ nendoroid: t.Object({ id: t.Number(), name: t.String() }), error: t.Object({ message: t.String() }) }) .get('/', () => 'Hi Elysia') .get('/id/:id', ({ params: { id } }) => id) .post('/mirror', ({ body }) => body, { body: 'nendoroid', response: { 200: 'nendoroid', // [!code ++] 400: 'error', // [!code ++] 401: 'error' // [!code ++] } }) .listen(3000) export type App = typeof app ``` An on the client side: ```typescript const { data: nendoroid, error } = app.mirror.post({ id: 1895, name: 'Skadi' }) if(error) { switch(error.status) { case 400: case 401: // narrow down to type 'error' described in the server warnUser(error.value) break default: // typed as unknown reportError(error.value) break } throw error } ``` ## WebSocket Eden supports WebSocket using the same API as a normal route. ```typescript // Server import { Elysia, t } from 'elysia' const app = new Elysia() .ws('/chat', { message(ws, message) { ws.send(message) }, body: t.String(), response: t.String() }) .listen(3000) type App = typeof app ``` To start listening to real-time data, call the `.subscribe` method: ```typescript // Client import { edenTreaty } from '@elysiajs/eden' const app = edenTreaty('http://localhost:') const chat = app.chat.subscribe() chat.subscribe((message) => { console.log('got', message) }) chat.send('hello from client') ``` We can use [schema](/integrations/cheat-sheet#schema) to enforce type-safety on WebSockets, just like a normal route. --- **Eden.subscribe** returns **EdenWebSocket** which extends the [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket) class with type-safety. The syntax is identical with the WebSocket If more control is need, **EdenWebSocket.raw** can be accessed to interact with the native WebSocket API. ## File Upload You may either pass one of the following to the field to attach file: - **File** - **FileList** - **Blob** Attaching a file will results **content-type** to be **multipart/form-data** Suppose we have the server as the following: ```typescript // server.ts import { Elysia } from 'elysia' const app = new Elysia() .post('/image', ({ body: { image, title } }) => title, { body: t.Object({ title: t.String(), image: t.Files(), }) }) .listen(3000) export type App = typeof app ``` We may use the client as follows: ```typescript // client.ts import { edenTreaty } from '@elysia/eden' import type { Server } from './server' export const client = edenTreaty('http://localhost:3000') const id = (id: string) => document.getElementById(id)! as T const { data } = await client.image.post({ title: "Misono Mika", image: id('picture').files!, }) ``` --- --- url: /eden/treaty/parameters.md --- # Parameters We need to send a payload to server eventually. To handle this, Eden Treaty's methods accept 2 parameters to send data to server. Both parameters is type safe and will be guided by TypeScript automatically: 1. body 2. additional parameters - query - headers - fetch ```typescript import { Elysia, t } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .post('/user', ({ body }) => body, { body: t.Object({ name: t.String() }) }) .listen(3000) const api = treaty('localhost:3000') // ✅ works api.user.post({ name: 'Elysia' }) // ✅ also works api.user.post({ name: 'Elysia' }, { // This is optional as not specified in schema headers: { authorization: 'Bearer 12345' }, query: { id: 2 } }) ``` Unless if the method doesn't accept body, then body will be omitted and left with single parameter only. If the method **"GET"** or **"HEAD"**: 1. Additional parameters - query - headers - fetch ```typescript import { Elysia } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .get('/hello', () => 'hi') .listen(3000) const api = treaty('localhost:3000') // ✅ works api.hello.get({ // This is optional as not specified in schema headers: { hello: 'world' } }) ``` ## Empty body If body is optional or not need but query or headers is required, you may pass the body as `null` or `undefined` instead. ```typescript import { Elysia, t } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .post('/user', () => 'hi', { query: t.Object({ name: t.String() }) }) .listen(3000) const api = treaty('localhost:3000') api.user.post(null, { query: { name: 'Ely' } }) ``` ## Fetch parameters Eden Treaty is a fetch wrapper, we may add any valid [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) parameters to Eden by passing it to `$fetch`: ```typescript import { Elysia, t } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .get('/hello', () => 'hi') .listen(3000) const api = treaty('localhost:3000') const controller = new AbortController() const cancelRequest = setTimeout(() => { controller.abort() }, 5000) await api.hello.get({ fetch: { signal: controller.signal } }) clearTimeout(cancelRequest) ``` ## File Upload We may either pass one of the following to attach file(s): - **File** - **File[]** - **FileList** - **Blob** Attaching a file will results **content-type** to be **multipart/form-data** Suppose we have the server as the following: ```typescript import { Elysia, t } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .post('/image', ({ body: { image, title } }) => title, { body: t.Object({ title: t.String(), image: t.Files() }) }) .listen(3000) export const api = treaty('localhost:3000') const images = document.getElementById('images') as HTMLInputElement const { data } = await api.image.post({ title: "Misono Mika", image: images.files!, }) ``` --- --- url: /eden/treaty/response.md --- # Response Once the fetch method is called, Eden Treaty return a promise containing an object with the following properties: - data - returned value of the response (2xx) - error - returned value from the response (>= 3xx) - response `Response` - Web Standard Response class - status `number` - HTTP status code - headers `FetchRequestInit['headers']` - response headers Once returned, you must provide error handling to ensure that the response data value is unwrapped, otherwise the value will be nullable. Elysia provides a `error()` helper function to handle the error, and Eden will provide type narrowing for the error value. ```typescript import { Elysia, t } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .post('/user', ({ body: { name }, error }) => { if(name === 'Otto') return error(400, 'Bad Request') return name }, { body: t.Object({ name: t.String() }) }) .listen(3000) const api = treaty('localhost:3000') const submit = async (name: string) => { const { data, error } = await api.user.post({ name }) // type: string | null console.log(data) if (error) switch(error.status) { case 400: // Error type will be narrow down throw error.value default: throw error.value } // Once the error is handled, type will be unwrapped // type: string return data } ``` By default, Elysia infers `error` and `response` type to TypeScript automatically, and Eden will be providing auto-completion and type narrowing for accurate behavior. ::: tip If the server responds with an HTTP status >= 300, then value will be always be null, and `error` will have a returned value instead. Otherwise, response will be passed to data. ::: ## Stream response Eden will interpret a stream response as `AsyncGenerator` allowing us to use `for await` loop to consume the stream. ```typescript twoslash 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) // ^? ``` --- --- url: /eden/treaty/unit-test.md --- # Unit Test According to [Eden Treaty config](/eden/treaty/config.html#urlorinstance) and [Unit Test](/patterns/unit-test), we may pass an Elysia instance to Eden Treaty directly to interact with Elysia server directly without sending a network request. We may use this patterns to create a unit test with end-to-end type safety and type-level test all at once. ```typescript twoslash // 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') // ^? }) }) ``` ## Type safety test To perform a type safety test, simply run **tsc** to test folders. ```bash tsc --noEmit test/**/*.ts ``` This is useful to ensure type integrity for both client and server, especially during migrations. --- --- url: /eden/treaty/websocket.md --- # WebSocket Eden Treaty supports WebSocket using `subscribe` method. ```typescript twoslash import { Elysia, t } from "elysia"; import { treaty } from "@elysiajs/eden"; const app = new Elysia() .ws("/chat", { body: t.String(), response: t.String(), message(ws, message) { ws.send(message); }, }) .listen(3000); const api = treaty("localhost:3000"); const chat = api.chat.subscribe(); chat.subscribe((message) => { console.log("got", message); }); chat.on("open", () => { chat.send("hello from client"); }); ``` **.subscribe** accepts the same parameter as `get` and `head`. ## Response **Eden.subscribe** returns **EdenWS** which extends the [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket) results in identical syntax. If more control is need, **EdenWebSocket.raw** can be accessed to interact with the native WebSocket API. --- --- url: /index.md --- --- --- url: /midori.md --- --- --- url: /blog/elysia-02.md --- 「[Blessing](https://youtu.be/3eytpBOkOFA)」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: ```typescript 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`: ```typescript 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` ```typescript const app = new Elysia() .setModel({ sign: t.Object({ username: t.String(), password: t.String() }) }) .post('/sign', ({ body }) => body, { schema: { body: 'sign', response: 'sign' } }) ``` This will bring auto-completion of known models. Screenshot 2566-01-23 at 13 24 28 And type reference stopping you from accidentally returning invalid type. Screenshot 2566-01-23 at 13 26 00 Using `@elysiajs/swagger` will also create a separate `Model` section for listing available models. Screenshot 2566-01-23 at 13 23 41 Reference also handles validation as you expected. In short, it's as same as inline schema but now you only need to type the name of the schema to handle validation and typing instead of a long list of imports. ## OpenAPI Detail field Introducing new field `schema.detail` for customizing detail for the route following the standard of OpenAPI Schema V2 with auto-completion. Screenshot 2566-01-23 at 13 54 11 This allows you to write better documentation and fully editable Swagger as you want: Screenshot 2566-01-23 at 13 23 41 ## Union Type The previous version of Elysia sometime has a problem with distinct Union types, as Elysia tries to catch the response to create a full type reference for Eden. Results in invalidation of possible types, ## Union Response Made possible by Union Type, now returning multiple response status for `schema` now available using `schema.response[statusCode]` ```typescript 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](https://discord.gg/eaFJ2KDJck) 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 > --- --- url: /blog/elysia-03.md --- Named after Camellia's song[「大地の閾を探して [Looking for Edge of Ground]」](https://youtu.be/oyJf72je2U0)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. 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](/blog/elysia-03/narrowed-key.webp) And narrowing the scope of property programmatically also narrow down the type of parameters, or in other words, full type-safety. ![Narrowed Params](/blog/elysia-03/narrowed-param.webp) ### 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](/plugins/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](https://twitter.com/saltyAom/status/1629876280517869568?s=20) ## 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](/plugins/eden/treaty) `eden/treaty`: Original Eden syntax you know and love - [Eden Fn](/plugins/eden/fn) `eden/fn`: Access to Eden Fn - [Eden Fetch](/plugins/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](/blog/elysia-03/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](https://discord.gg/eaFJ2KDJck) 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) > --- --- url: /blog/elysia-04.md --- Named after the opening music of ["The Liar Princess and the Blind Prince" trailer](https://youtu.be/UdBespMvxaA), [「月夜の音楽会」(Moonlit Night Concert)](https://youtu.be/o8b-IQulh1c) 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.](https://github.com/sinclairzx81/typebox/blob/master/changelog/0.26.0.md) ## 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: ```ts 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. ```ts 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](https://github.com/elysiajs/elysia/pull/31) which allows Elysia to access TypeBox's error property, for a better programmatically error response. ```ts 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(3000) ``` 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 --- --- url: /blog/elysia-05.md --- Named after Arknights' original music, 「[Radiant](https://youtu.be/QhUjD--UUV4)」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 over head 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: ```ts 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. ```ts // 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** ```ts 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: ```ts app.post('/', ({ body }) => body, { type: 'json' }) ``` `type` may be one of the following: ```ts 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 the [explicit body](/life-cycle/parse.html#explicit-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. ```ts 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. ```ts 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](/validation/elysia-type.html#numeric) 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. ```ts // ? 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. ```ts 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. ```ts 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. ```ts 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. ```ts // ? 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. ```ts // ✅ 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`: ```ts // 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](https://github.com/bogeychan/elysia-polyfills) 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** --- --- url: /blog/elysia-06.md --- Named after the opening of the legendary anime, **"No Game No Life"**, 「[This Game](https://youtu.be/kJ04dMmimn8)」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) ## New Plugin Model This Game introduce a new syntax for plugin registration, and come up with a new plugin model internally. Previously you can register plugin by defining a callback function for Elysia instance like this: ```ts const plugin = (app: Elysia) => app.get('/', () => 'hello') ``` With the new plugin, you can now turns and Elysia instance into a plugin: ```ts const plugin = new Elysia() .get('/', () => 'hello') ``` This allows any Elysia instance and even existing one to be used across application, removing any possible addition callback and tab spacing. This improved Developer Experience significantly when working and nested group ```ts // < 0.6 const group = (app: Elysia) => app .group('/v1', (app) => app .get('/hello', () => 'Hello World') ) // >= 0.6 const group = new Elysia({ prefix: '/v1' }) .get('/hello', () => 'Hello World') ``` We encourage you to use the new model of Elysia plugin instance, as we can take advantage of Plugin Checksum and new possible features in the future. However, we are **NOT deprecating** the callback function method as there's some case function model is useful like: - Inline function - Plugins that required an information of main instance (for example accessing OpenAPI schema) With this new plugin model, we hope that you can make your codebase even easier to maintain. ## Plugin Checksum By default, Elysia plugin use function callback to register plugin. This means that if you register a plugin for type declaration, it will duplicate itself for just providing a type support, leading to duplication of plugin used in production. Which is why Plugin Checksum is introduced, to de-duplicated plugin registered for type declaration. To opt-in to Plugin Checksum, you need to use a new plugin model, and provide a `name` property to tell Elysia to prevent the plugin from being deduplicate ```ts const plugin = new Elysia({ name: 'plugin' }) ``` This allows Elysia to identify the plugin and deduplicated based on name. Any duplicated name will be registered only once but type-safety will be provided after registration even if the plugin is deduplicated. In case your plugin needs configuration, you can provide the configuration into a **seed** property to generate a checksum for deduplicating the plugin. ```ts const plugin = (config) = new Elysia({ name: 'plugin', seed: config }) ``` Name and seed will be used to generate a checksum to de-duplicated registration, which leads to even better performance improvement. This update also fixed the deduplication of the plugin's lifecycle accidentally inline lifecycle when Elysia is not sure if plugin is local and global event. As always, means performance improvement for an app that's larger than "Hello World". ## Mount and WinterCG Compliance WinterCG is a standard for web-interoperable runtimes supports by Cloudflare, Deno, Vercel Edge Runtime, Netlify Function and various more. WinterCG is a standard to allows web server to runs interoperable across runtime, which use Web Standard definitions like Fetch, Request, and Response. By this, Elysia is partially follows WinterCG compliance as we are optimized to Bun but also openly support other runtime if possible. This allows any framework and code that is WinterCG compliance to be run together in theory, an implementation is proved by [Hono](https://honojs.dev) which introduce the **.mount** method to [runs multiple framework together in one codebase](https://twitter.com/honojs/status/1684839623355490304), including Remix, Elysia, Itty Router, and Hono itself in a simple function. By this, we implemented the same logic for Elysia by introducing `.mount` method to runs any framework or code that is WinterCG compliant. To use `.mount`, [simply pass a `fetch` function](https://twitter.com/saltyAom/status/1684786233594290176): ```ts const app = new Elysia() .get('/', () => 'Hello from Elysia') .mount('/hono', hono.fetch) ``` A **fetch** function is a function that accept Web Standard Request and return Web Standard Response as the definition of: ```ts // Web Standard Request-like object // Web Standard Response type fetch = (request: RequestLike) => Response ``` By default, this declaration is used by: - Bun - Deno - Vercel Edge Runtime - Cloudflare Worker - Netlify Edge Function - Remix Function Handler Which means you can run all of the above code to interlop with Elysia all in a single server, or re-used and existing function all in one deployment, no need to setting up Reverse Proxy for handling multiple server. If the framework also support a **.mount** function, you can deeply nested a framework that support it infinitely. ```ts const elysia = new Elysia() .get('/Hello from Elysia inside Hono inside Elysia') const hono = new Hono() .get('/', (c) => c.text('Hello from Hono!')) .mount('/elysia', elysia.fetch) const main = new Elysia() .get('/', () => 'Hello from Elysia') .mount('/hono', hono.fetch) .listen(3000) ``` You can even re-used multiple existing Elysia project in your server. ```ts import A from 'project-a/elysia' import B from 'project-b/elysia' import C from 'project-c/elysia' new Elysia() .mount(A) .mount(B) .mount(C) ``` If an instance passed to mount is an Elysia instance, it will resolve to `use` automatically, providing type-safety and support for Eden by default. This made the possibility of interlopable framework and runtime to a reality. ## Improved starts up time Starts up time is an important metric in a serverless environment which Elysia excels it incredibly, but we have taken it even further. By default, Elysia generates OpenAPI schema for every route automatically and stored it internally when if not used. In this version, Elysia defers the compilation and moved to `@elysiajs/swagger` instead allowing Elysia starts up time to be even faster. And with various micro-optimization, and made possible by new Plugin model, starts up time is now up to 35% faster. ## Dynamic Mode Elysia introduces Static Code Analysis and Ahead of Time compilation to push the boundary of performance. Static Code Analysis allow Elysia to read your code then produce the most optimized version code, allowing Elysia to push the performance to its limit. Even if Elysia is WinterCG compliance, environment like Cloudflare worker doesn't support function composition. This means that Ahead of Time Compliation isn't possible, leading us to create a dynamic mode which use JIT compilation instead of AoT, allowing Elysia to run in Cloudflare Worker as well. To enable dynamic mode, set `aot` to false. ```ts new Elysia({ aot: false }) ``` Dynamic Mode is enabled by default in cloudflare worker. #### It's worth noting that, enabling Dynamic Mode will disable some feature like dynamic injected code like `t.Numeric` which parse strings to numbers automatically. Ahead of Time compilation can read, detect and optimize your code in exchange for startup time in exchange for extra performance gain, while dynamic mode uses JIT compilation, allowing start up time to be even faster up to 6x. But it should be noted that startup time in Elysia is fast enough by default. Elysia is able to register 10,000 routes in just 78ms which means in an average of 0.0079 ms/route That being said, we are leaving a choice for you to decided yourself. ## Declarative Custom Error This update adds support for adding type support for handling custom error. ```ts class CustomError extends Error { constructor(public message: string) { super(message) } } new Elysia() .addError({ MyError: CustomError }) .onError(({ code, error }) => { switch(code) { // With auto-completion case 'MyError': // With type narrowing // Error is typed as CustomError return error } }) ``` This allows us to handle custom types with type narrowing for handling custom errors and auto-completion for error codes to narrow down the correct types, fully type-safe declaratively. This fulfills one of our main philosophies is focused on Developer Experience especially with types. Elysia Type System is complex, yet we try to refrained our users need to write a custom type or passing a custom generic, retaining all the code to look just like JavaScript. It just works, and all the code looks just like JavaScript. ## TypeBox 0.30 TypeBox is a core library that powers Elysia's strict type system known as **Elysia.t**. In this update, we update TypeBox from 0.28 to 0.30 to make even more fine-grained Type System nearly strictly typed language. These updates introduce new features and many interesting changes, for example **Iterator** type, reducing packages size, TypeScript code generation. And with support for Utility Types like: - `t.Awaited` - `t.Uppercase` - `t.Capitlized` ## Strict Path We got a lot of requests for handling loose path. By default, Elysia handle path strictly, which means that if you have to support path with or without optional `/` , it will not be resolved and you have to duplicate the pathname twice. ```ts new Elysia() .group('/v1', (app) => app // Handle /v1 .get('', handle) // Handle /v1/ .get('/', handle) ) ``` By this, many have been requesting that `/v1/` should also resolved `/v1` as well. With this update, we add support for loose path matching by default, to opt-in into this feature automatically. ```ts new Elysia() .group('/v1', (app) => app // Handle /v1 and /v1/ .get('/', handle) ) ``` To disable loosePath mapping, you can set `strictPath` to true to used the previous behavior: ```ts new Elysia({ strictPath: false }) ``` We hope that this will clear any questions regards to path matching and its expected behavior ## onResponse This update introduce a new lifecycle hook called `onResponse`. This is a proposal proposed by [elysia#67](https://github.com/elysiajs/elysia/issues/67) Previously Elysia life-cycle works as illustrated in this diagram. ![Elysia life-cycle diagram](/blog/elysia-06/lifecycle-05.webp) For any metric, data-collection or logging purpose, you can use `onAfterHandle` to run the function to collect metrics, however, this lifecycle isn't executed when handler runs into an error, whether it's a routing error or a custom error provided. Which is why we introduced `onResponse` to handle all cases of Response. You can use `onRequest`, and `onResponse` together to measure a metric of performance or any required logging. Quoted > However, the onAfterHandle function only fires on successful responses. For instance, if the route is not found, or the body is invalid, or an error is thrown, it is not fired. How can I listen to both successful and non-successful requests? This is why I suggested onResponse. > > Based on the drawing, I would suggest the following: > ![Elysia life-cycle diagram with onResponse hook](/blog/elysia-06/lifecycle-06.webp) --- ### Notable Improvement: - Added an error field to the Elysia type system for adding custom error messages - Support Cloudflare worker with Dynamic Mode (and ENV) - AfterHandle now automatically maps the value - Using bun build to target Bun environment, improving the overall performance by 5-10% - Deduplicated inline lifecycle when using plugin registration - Support for setting `prefix` - Recursive path typing - Slightly improved type checking speed - Recursive schema collision causing infinite types ### Change: - Moved **registerSchemaPath** to @elysiajs/swagger - [Internal] Add qi (queryIndex) to context ### Breaking Change: - [Internal] Removed Elysia Symbol - [Internal] Refactored `getSchemaValidator`, `getResponseSchemaValidator` to named parameters - [Internal] Moved `registerSchemaPath` to `@elysiajs/swagger` ## Afterward We have just passed a one year milestone, and really excited how Elysia and Bun have improved over the year! Pushing the performance boundaries of JavaScript with Bun, and developer experience with Elysia, we are thrilled to have kept in touch with you and our community. Every updates, keeps making Elysia even more stable, and gradually providing a better developer experience without a drop in performance and features. We're thrilled to see our community of open-source developers bringing Elysia to life with their projects like. - [Elysia Vite Plugin SSR](https://github.com/timnghg/elysia-vite-plugin-ssr) allowing us to use Vite Server Side Rendering using Elysia as the server. - [Elysia Connect](https://github.com/timnghg/elysia-connect) which made Connect's plugin compatible with Elysia And much more developers that choose Elysia for their next big project. With our commitment, we also recently introduced [Mobius](https://github.com/saltyaom/mobius), and open-source TypeScript library to parse GraphQL to TypeScript type without relying on code generation by using TypeScript template literal type entirely to be the very first framework to achieve end-to-end type safety without relying on code generation. We incredibly thankful for your overwhelming continous support for Elysia, and we hope to see you pushing the boundaries together in the next release. > As this whole new world cheers my name > > I will never leave it to fate > > and when I see a chance, I will pave the way > > I calls checkmate > > This is the time to breakthrough > > So I will rewrite the story and finally change all the rule > > We are maverick > > We won't give in, until we win this game > > Though I don't know what tomorrow holds > > I'll make a bet any play my cards to win this game > > Unlike the rest, I'll do my best, and I won't ever lose > > To give up this chance would be a deadly since, so let's bet it all > > I put all my fate in used let **the game begin** --- --- url: /blog/elysia-07.md --- Name after our never giving up spirit, our beloved Virtual YouTuber, ~~Suicopath~~ Hoshimachi Suisei, and her brilliance voice: 「[Stellar Stellar](https://youtu.be/AAsRtnbDs-0)」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](https://github.com/saltyaom/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:
Elysia 0.6 Elysia 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](/blog/elysia-07/inference-comparison.webp) 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](/blog/elysia-07/trace.webp) 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 of usage of Reactive Cookie](/blog/elysia-07/cookie.webp) 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: ```typescript 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. ```typescript 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: ```typescript 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. ```typescript new Elysia({ cookie: { secrets: ['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 dynamic 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: ```typescript Numeric: (property?: NumericOptions) => 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. ```typescript 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](https://github.com/bogeychan) for providing the test case for Elysia Web Socket, helping us to rewrite Web Socket with confidence. ## Definitions Remap Proposed on [#83](https://github.com/elysiajs/elysia/issues/83) by [Bogeychan](https://github.com/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. ```typescript 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: ```typescript 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 To provide a smoother experience, some plugins might have a lot of property value which can be overwhelming to remap one-by-one. The **Affix** function, which consists of a **prefix** and **suffix**, allows us to remap all properties of an instance, preventing the name collision of the plugin. ```typescript 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: ```typescript 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. ```typescript 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 I admit that sometime we also forget the status we want to use. This is why we ship 64 HTTP Status codes in text-based form with autocompletion for you. ![Example of using text-base status code](/blog/elysia-07/teapot.webp) 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 feature. ![Text-base status code showing auto-completion](/blog/elysia-07/teapot-autocompletion.webp) 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](https://github.com/elysiajs/elysia/pull/179) WebSocket - added unit tests and fixed example & api by @bogeychan - [#179](https://github.com/elysiajs/elysia/pull/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](https://github.com/elysiajs/elysia/pull/187) InternalServerError message fixed to "INTERNAL_SERVER_ERROR" instead of "NOT_FOUND" by @bogeychan - [#167](https://github.com/elysiajs/elysia/pull/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: - [Ethan Niser with his amazing BETH Stack](https://youtu.be/aDYYn9R-JyE?si=hgvGgbywu_-jsmhR) - Being mentioned by [Fireship](https://youtu.be/dWqNgzZwVJQ?si=AeCmcMsTZtNwmhm2) - Having official integration for [Lucia Auth](https://github.com/pilcrowOnPaper/lucia) 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
--- --- url: /blog/elysia-08.md --- Named after the ending song of Steins;Gate Zero, [**"Gate of Steiner"**](https://youtu.be/S5fnglcM5VI). 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-api) - [New Lifecycle: resolve, mapResponse](#new-life-cycle) - [Error Function](#error-function) - [Static Content](#static-content) - [Default Property](#default-property) - [Default Header](#default-header) - [Performance and Notable Improvement](#performance-and-notable-improvement) ## 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. ```typescript 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. ```typescript 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](/patterns/macro) 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](/essential/context.html#derive). Designed to append new value to context after validation process storing in the same stack as **beforeHandle**. Resolve syntax is identical to [derive](/life-cycle/before-handle#derive), below is an example of retrieving a bearer header from Authorization plugin. ```typescript 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. ```typescript 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. ```typescript 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. ```typescript import { Elysia, error } from 'elysia' // [!code ++] new Elysia() .get('/', () => error(418, "I'm a teapot")) // [!code ++] .listen(3000) ``` Which is an equivalent to: ```typescript 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. ```typescript 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. ```typescript 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](https://github.com/sinclairzx81/typebox/blob/index/changelog/0.32.0.md) 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. ```typescript 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. ```typescript 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](https://github.com/bogeychan/elysia-etag) 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](https://datatracker.ietf.org/doc/html/rfc7233#section-4.2) 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](https://github.com/elysiajs/elysia/discussions/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: ```json { "type": "query", "at": "password", "message": "Required property", "expected": { "email": "eden@elysiajs.com", "password": "" }, "found": { "email": "eden@elysiajs.com" }, "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` ## Afterword It has been a great journey since the first release. Elysia evolved from a generic REST API framework to an ergonomic framework to support End-to-end type safety, OpenAPI documentation generation, we we would like to keep introduce more exciting features in the future.
We are happy to have you, and the developers to build amazing stuff with Elysia and your overwhelming continuous support for Elysia encourages us to keep going. It's exciting to see Elysia grow more as a community: - [Scalar's Elysia theme](https://x.com/saltyAom/status/1737468941696421908?s=20) for new documentation instead of Swagger UI. - [pkgx](https://pkgx.dev/) supports Elysia out of the box. - Community submitted Elysia to [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r22&hw=ph&test=composite) 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](/integrations/nextj), [Astro](/integrations/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
--- --- url: /blog/elysia-10.md --- 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. - [Sucrose](#sucrose) - Rewritten pattern matching static analysis instead of RegEx - [Improved startup time](#improved-startup-time) up to 14x - [Remove ~40 routes/instance TypeScript limitation](#remove-40-routesinstance-limit) - [Faster type inference](#type-inference-improvement) up to ~3.8x - [Treaty 2](#treaty-2) - [Hook type](#hook-type-breaking-change) (breaking changes) - [Inline error](#inline-error) for strict error check --- 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"](https://youtu.be/v1sd5CzR504). Animated short from **"Honkai Impact 3rd"** from my favorite arc, and my favorite character, **"Raiden Mei"** featuring her theme song, ["Honkai World Diva"](https://youtu.be/s_ZLfaZMpe0). 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"**. ```typescript 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. ```typescript 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) ```typescript 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](/blog/elysia-10/558-ts-limit.webp) 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. **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](/blog/elysia-10/ely-comparison.webp) 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. ```typescript // 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](/eden/treaty/overview.html), and for Treaty 1 in [Treaty legacy](/eden/treaty/legacy.html) ## 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. ```typescript 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. ```typescript 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. ```typescript 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. ```typescript 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. ```typescript // ? 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. ```typescript // 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](https://github.com/elysiajs/elysia/issues/513). For the documentation of hook type, see [Lifecycle#hook-type](https://beta.elysiajs.com/essential/scope.html#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](/blog/elysia-10/error-fn.webp) Inline error can be destructured from handler as follows: ```typescript import { Elysia } from 'elysia' new Elysia() .get('/hello', ({ error }) => { if(Math.random() > 0.5) return error(418, 'Nagisa') return 'Azusa' }, { response: t.Object({ 200: t.Literal('Azusa'), 418: t.Literal('Nagisa') }) }) ``` Inline error can produce a fine-grained type from a schema, providing type narrowing, auto-completion, and type checking to the accuracy of value, 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](/blog/elysia-10/inline-error-fn.webp) 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](https://bun.sh/docs/bundler/loaders)) 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](https://github.com/bogeychan/elysia-polyfills) maintained by Bogeychan, one of an active contributor to Elysia. Additionally, we have rewrote [Eden documentation](/eden/overview) 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](https://github.com/elysiajs/documentation/pull/282/files). And finally, if you have problems with migration and additional questions related to Elysia, feels free to ask one in Elysia's Discord server.