--- 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 number 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.Number() }) }) .listen(3000) ``` This code ensures that our path parameter **id** will always be a number on 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. *** 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, status }) { if (!bearer) { set.headers[ 'WWW-Authenticate' ] = `Bearer realm='sign', error="invalid_request"` return status(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. ## Folder Structure Elysia is unopinionated about folder structure, leaving you to **decide** how to organize your code yourself. However, **if you doesn't have a specific structure in mind**, we recommended a feature-based folder structure. Where each feature has its own folder containing controllers, services, models. ``` | src | modules | auth | index.ts (Elysia controller) | service.ts (service) | model.ts (model) | user | index.ts (Elysia controller) | service.ts (service) | model.ts (model) | utils | a | index.ts | b | index.ts ``` This structure allows you to easily find and manage your code, and keep related code together. Here's an example code of how to distribute your code into a feature-based folder structure: ::: code-group ```typescript [auth/index.ts] // Controller handle HTTP related eg. routing, request validation import { Elysia } from 'elysia' import { Auth } from './service' import { AuthModel } from './model' export const auth = new Elysia({ prefix: '/auth' }) .get( '/sign-in', ({ body, cookie: { session } }) => { const response = await Auth.signIn(body) // Set session cookie session.value = response.token return response }, { body: AuthModel.signInBody, response: { 200: AuthModel.signInResponse, 400: AuthModel.signInInvalid } } ) ``` ```typescript [auth/service.ts] // Service handle business logic, decoupled from Elysia controller import { status } from 'elysia' import type { AuthModel } from './service' // If the class doesn't need to store a property, // you may use `abstract class` to avoid class allocation abstract class Auth { static async signIn({ username, password }: AuthModel.SignIn) { const user = await sql` SELECT password FROM users WHERE username = ${username} LIMIT 1` if (await Bun.password.verify(password, user.password)) // You can throw an HTTP error directly throw status( 400, 'Invalid username or password' satisfies AuthModel.SignInInvalid ) return { username, token: await generateAndSaveTokenToDB(user.id) } } } ``` ```typescript [auth/model.ts] // Model define the data structure and validation for the request and response import { t } from 'elysia' export namespace AuthModel { // Define a DTO for Elysia validation export const signInBody = t.Object({ username: t.String(), password: t.String(), }) // Define it as TypeScript type export type signInBody = typeof signIn.static // Repeat for other models export const signInResponse = t.Object({ username: t.String(), token: t.String(), }) export type signInResponse = typeof signIn.static export const signInInvalid = t.Literal('Invalid username or password') export type signInInvalid = typeof signInInvalid.static } ``` ::: Each file has its own responsibility as follows: * **Controller**: Handle HTTP routing, request validation, and cookie. * **Service**: Handle business logic, decoupled from Elysia controller if possible. * **Model**: Define the data structure and validation for the request and response. Feels free to abstract this structure to your own needs, and use 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: Abstract away non-request dependent service We recommended to abstract a service class/function away from Elysia. If the service or function doesn't tied to HTTP request or access a `Context`. It's recommended to abstract it as a static class or function. ```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. ### ✅ Do: Request dependent service as Elysia instance **If the service is a request dependent service** or need to process HTTP request. We recommended to abstract it as Elysia instance to ensure type integrity and inference: ```typescript import { Elysia } from 'elysia' // ✅ Do const AuthService = new Elysia({ name: 'Auth.Service' }) .macro({ isSignIn: { resolve({ cookie, status }) { if (!cookie.session.value) return status(401) return { session: cookie.session.value, } } } }) 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. ::: ### ✅ Do: Decorate only request dependent property It's recommended to `decorate` only request dependent property, such as `requestIP`, `requestTime`, or `session`. Overusing decorator may tied your code to Elysia making it harder to test, and reuse. ```typescript import { Elysia } from 'elysia' new Elysia() .decorate('requestIP', ({ request }) => request.headers.get('x-forwarded-for') || request.ip) .decorate('requestTime', () => Date.now()) .decorate('session', ({ cookie }) => cookie.session.value) .get('/', ({ requestIP, requestTime, session }) => { return { requestIP, requestTime, session } }) ``` ### ❌ 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({ status, cookie: { session } }: Context) { if (session.value) return status(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. ### ⚠️ 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({ status, cookie: { session } }: InferContext) { if (session.value) return status(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. We recommended going through [Better Auth basic setup](https://www.better-auth.com/docs/installation) before going through this page. Our basic setup will look like this: ```ts [auth.ts] import { betterAuth } from 'better-auth' import { Pool } from 'pg' export const auth = betterAuth({ database: new Pool() }) ``` ## Handler After setting up Better Auth instance, we can mount to Elysia via [mount](/patterns/mount.html). We need to mount the handler to Elysia endpoint. ```ts [index.ts] import { Elysia } from 'elysia' import { auth } from './auth' const app = new Elysia() .mount(auth.handler) // [!code ++] .listen(3000) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` Then we can access Better Auth with `http://localhost:3000/api/auth`. ### Custom endpoint We recommended setting a prefix path for when using [mount](/patterns/mount.html). ```ts [index.ts] import { Elysia } from 'elysia' const app = new Elysia() .mount('/auth', auth.handler) // [!code ++] .listen(3000) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` Then we can access Better Auth with `http://localhost:3000/auth/api/auth`. But the URL looks redundant, we can customize the `/api/auth` prefix to something else in Better Auth instance. ```ts import { betterAuth } from 'better-auth' import { openAPI } from 'better-auth/plugins' import { passkey } from 'better-auth/plugins/passkey' import { Pool } from 'pg' export const auth = betterAuth({ basePath: '/api' // [!code ++] }) ``` Then we can access Better Auth with `http://localhost:3000/auth/api`. Unfortunately, we can't set `basePath` of a Better Auth instance to be empty or `/`. ## Swagger / OpenAPI Better Auth support `openapi` with `better-auth/plugins`. However if we are using [@elysiajs/swagger](/plugins/swagger), you might want to extract the documentation from Better Auth instance. We may do that with the following code: ```ts import { openAPI } from 'better-auth/plugins' let _schema: ReturnType const getSchema = async () => (_schema ??= auth.api.generateOpenAPISchema()) export const OpenAPI = { getPaths: (prefix = '/auth/api') => getSchema().then(({ paths }) => { const reference: typeof paths = Object.create(null) for (const path of Object.keys(paths)) { const key = prefix + path reference[key] = paths[path] for (const method of Object.keys(paths[path])) { const operation = (reference[key] as any)[method] operation.tags = ['Better Auth'] } } return reference }) as Promise, components: getSchema().then(({ components }) => components) as Promise } as const ``` Then in our Elysia instance that use `@elysiajs/swagger`. ```ts import { Elysia } from 'elysia' import { swagger } from '@elysiajs/swagger' import { OpenAPI } from './auth' const app = new Elysia().use( swagger({ documentation: { components: await OpenAPI.components, paths: await OpenAPI.getPaths() } }) ) ``` ## CORS To configure cors, you can use the `cors` plugin from `@elysiajs/cors`. ```ts import { Elysia } from 'elysia' import { cors } from '@elysiajs/cors' import { auth } from './auth' const app = new Elysia() .use( cors({ origin: 'http://localhost:3001', methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], credentials: true, allowedHeaders: ['Content-Type', 'Authorization'] }) ) .mount(auth.handler) .listen(3000) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` ## Macro You can use [macro](https://elysiajs.com/patterns/macro.html#macro) with [resolve](https://elysiajs.com/essential/handler.html#resolve) to provide session and user information before pass to view. ```ts import { Elysia } from 'elysia' import { auth } from './auth' // user middleware (compute user and session and pass to routes) const betterAuth = new Elysia({ name: 'better-auth' }) .mount(auth.handler) .macro({ auth: { async resolve({ status, request: { headers } }) { const session = await auth.api.getSession({ headers }) if (!session) return status(401) return { user: session.user, session: session.session } } } }) const app = new Elysia() .use(betterAuth) .get('/user', ({ user }) => user, { auth: true }) .listen(3000) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` This will allow you to access the `user` and `session` object in all of your routes. --- --- 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, status }) => { set.headers['x-powered-by'] = 'Elysia' return status(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 }) ``` 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 ## 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'] } }) ``` ## 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 import { Elysia, t } from 'elysia' new Elysia({ encodeSchema: true }) ``` #### Options - @default `true` * `true` - Run `Encode` before sending the response to client * `false` - Skip `Encode` entirely ## 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 using [exact mirror](/blog/elysia-13.html#exact-mirror) * `typebox`: Elysia will coerce fields into a specified schema using [TypeBox's Value.Clean](https://github.com/sinclairzx81/typebox) * `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 ``` ## santize A function or an array of function that calls and intercepts on every `t.String` while validation. Allowing us to read and transform a string into a new value. ```ts import { Elysia, t } from 'elysia' new Elysia({ santize: (value) => Bun.escapeHTML(value) }) ``` ## 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'] }) ``` ### systemRouter Use runtime/framework provided router if possible. On Bun, Elysia will use [Bun.serve.routes](https://bun.sh/docs/api/http#routing) and fallback to Elysia's own router. ## 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 } }) ``` *** --- --- 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: /patterns/deploy.md --- # Deploy to production This page is a guide on how to deploy Elysia to production. ## Compile to binary We recommended running a build command before deploying to production as it could potentially reduce memory usage and file size significantly. We recommended compile Elysia into a single binary using the command as follows: ```bash bun build \ --compile \ --minify-whitespace \ --minify-syntax \ --target bun \ --outfile server \ ./src/index.ts ``` This will generate a portable binary `server` which we can run to start our server. Compiling server to binary usually significantly reduce memory usage by 2-3x compared to development environment. This command is a bit long, so let's break it down: 1. `--compile` - Compile TypeScript to binary 2. `--minify-whitespace` - Remove unnecessary whitespace 3. `--minify-syntax` - Minify JavaScript syntax to reduce file size 4. `--target bun` - Target the `bun` platform, this can optimize the binary for the target platform 5. `--outfile server` - Output the binary as `server` 6. `./src/index.ts` - The entry file of our server (codebase) To start our server, simly run the binary. ```bash ./server ``` Once binary is compiled, you don't need `Bun` installed on the machine to run the server. This is great as the deployment server doesn't need to install an extra runtime to run making binary portable. ### Why not --minify Bun does have `--minify` flag that will minify the binary. However if we are using [OpenTelemetry](/plugins/opentelemetry), it's going to reduce a function name to a single character. This make tracing harder than it should as OpenTelemetry rely on a function name. However, if you're not using OpenTelemetry, you may opt in for `--minify` instead ```bash bun build \ --compile \ --minify \ --target bun \ --outfile server \ ./src/index.ts ``` ### Permission Some Linux distro might not be able to run the binary, we suggest enable executable permission to a binary if you're on Linux: ```bash chmod +x ./server ./server ``` ### Unknown random Chinese error If you're trying to deploy a binary to your server but unable to run with random chinese character error. It means that the machine you're running on **doesn't support AVX2**. Unfortunately, Bun require machine that has an `AVX2` hardware support. There's no workaround as far as we know. ## Compile to JavaScript If you are unable to compile to binary or you are deploying on a Windows server. You may bundle your server to a JavaScript file instead. ```bash bun build \ --compile \ // [!code --] --minify-whitespace \ --minify-syntax \ --target bun \ --outfile ./dist/index.js \ ./src/index.ts ``` This will generate a single portable JavaScript file that you can deploy on your server. ```bash NODE_ENV=production bun ./dist/index.js ``` ## Docker On Docker, we recommended to always compile to binary to reduce base image overhead. Here's an example image using Distroless image using binary. ```dockerfile [Dockerfile] FROM oven/bun AS build WORKDIR /app # Cache packages installation COPY package.json package.json COPY bun.lock bun.lock RUN bun install COPY ./src ./src ENV NODE_ENV=production RUN bun build \ --compile \ --minify-whitespace \ --minify-syntax \ --target bun \ --outfile server \ ./src/index.ts FROM gcr.io/distroless/base WORKDIR /app COPY --from=build /app/server server ENV NODE_ENV=production CMD ["./server"] EXPOSE 3000 ``` ### OpenTelemetry If you are using [OpenTelemetry](/integrations/opentelemetry) to deploys production server. As OpenTelemetry rely on monkey-patching `node_modules/`. It's required that make instrumentations works properly, we need to specify that libraries to be instrument is an external module to exclude it from being bundled. For example, if you are using `@opentelemetry/instrumentation-pg` to instrument `pg` library. We need to exclude `pg` from being bundled and make sure that it is importing `node_modules/pg`. To make this works, we may specified `pg` as an external module with `--external pg` ```bash bun build --compile --external pg --outfile server src/index.ts ``` This tells bun to not `pg` bundled into the final output file, and will be imported from the `node_modules` directory at runtime. So on a production server, you must also keeps the `node_modules` directory. It's recommended to specify packages that should be available in a production server as `dependencies` in `package.json` and use `bun install --production` to install only production dependencies. ```json { "dependencies": { "pg": "^8.15.6" }, "devDependencies": { "@elysiajs/opentelemetry": "^1.2.0", "@opentelemetry/instrumentation-pg": "^0.52.0", "@types/pg": "^8.11.14", "elysia": "^1.2.25" } } ``` Then after running a build command, on a production server ```bash bun install --production ``` If the node\_modules directory still includes development dependencies, you may remove the node\_modules directory and reinstall production dependencies again. ### Monorepo If you are using Elysia with Monorepo, you may need to include dependent `packages`. If you are using Turborepo, you may place a Dockerfile inside an your apps directory like **apps/server/Dockerfile**. This may apply to other monorepo manager such as Lerna, etc. Assume that our monorepo are using Turborepo with structure as follows: * apps * server * **Dockerfile (place a Dockerfile here)** * packages * config Then we can build our Dockerfile on monorepo root (not app root): ```bash docker build -t elysia-mono . ``` With Dockerfile as follows: ```dockerfile [apps/server/Dockerfile] FROM oven/bun:1 AS build WORKDIR /app # Cache packages COPY package.json package.json COPY bun.lock bun.lock COPY /apps/server/package.json ./apps/server/package.json COPY /packages/config/package.json ./packages/config/package.json RUN bun install COPY /apps/server ./apps/server COPY /packages/config ./packages/config ENV NODE_ENV=production RUN bun build \ --compile \ --minify-whitespace \ --minify-syntax \ --target bun \ --outfile server \ ./src/index.ts FROM gcr.io/distroless/base WORKDIR /app COPY --from=build /app/server server ENV NODE_ENV=production CMD ["./server"] EXPOSE 3000 ``` ## Railway [Railway](https://railway.app) is one of the popular deployment platform. Railway assign **random port** to expose for each deployment that can be access via `PORT` environment variable. We need to modify our Elysia server to accept `PORT` environment to comply with Railway port. Instead of a fixed port, we may use `process.env.PORT` and provide a fallback on development instead. ```ts new Elysia() .listen(3000) // [!code --] .listen(process.env.PORT ?? 3000) // [!code ++] ``` This should allows Elysia to intercept port provided by Railway. ::: tip Elysia assign hostname to `0.0.0.0` automatically, which works with Railway ::: --- --- 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 ``` ::: 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.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 types from Elysia correctly, the following are the most common workarounds 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 on Elysia class to import Elysia instance and infer types correctly. Make sure that both client and server have the 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 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 work, Elysia must use **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 replacing the current one. Without 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 If you are using a Bun specific feature, like `Bun.file` or similar API and return it from a handler, 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 is able to resolve the path as same as backend. ::: tip Setting up path alias in monorepo is a bit tricky, you can fork our example template: [Kozeki Template](https://github.com/SaltyAom/kozeki-template) and modify it to your needs. ::: 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 adding a **scope** prefix for each module 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 recommend creating a **single tsconfig.json** that defines 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) or [Kozeki Template](https://github.com/SaltyAom/kozeki-template). --- --- 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 are 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 returns 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 }, status }) => { if(name === 'Otto') return status(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` types 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 the 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: /midori.md --- ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/', 'Hello World') .get('/json', { hello: 'world' }) .get('/id/:id', ({ params: { id } }) => id) .listen(3000) ``` ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .post( '/profile', // ↓ hover me ↓ ({ body }) => body, { body: t.Object({ username: t.String() }) } ) .listen(3000) ``` ```ts twoslash // @filename: controllers.ts import { Elysia } from 'elysia' export const users = new Elysia() .get('/users', 'Dreamy Euphony') export const feed = new Elysia() .get('/feed', ['Hoshino', 'Griseo', 'Astro']) // @filename: server.ts // ---cut--- import { Elysia, t } from 'elysia' import { swagger } from '@elysiajs/swagger' import { users, feed } from './controllers' new Elysia() .use(swagger()) .use(users) .use(feed) .listen(3000) ``` ```typescript twoslash // @filename: server.ts // ---cut--- // server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .patch( '/user/profile', ({ body, status }) => { if(body.age < 18) return status(400, "Oh no") if(body.name === 'Nagisa') return status(418) return body }, { body: t.Object({ name: t.String(), age: t.Number() }) } ) .listen(80) export type App = typeof app ``` ```typescript twoslash // @errors: 2322 1003 // @filename: server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .patch( '/user/profile', ({ body, status }) => { if(body.age < 18) return status(400, "Oh no") if(body.name === 'Nagisa') return status(418) return body }, { body: t.Object({ name: t.String(), age: t.Number() }) } ) .listen(80) export type App = typeof app // @filename: client.ts // ---cut--- // client.ts import { treaty } from '@elysiajs/eden' import type { App } from './server' const api = treaty('localhost') const { data, error } = await api.user.profile.patch({ name: 'saltyaom', age: '21' }) if(error) switch(error.status) { case 400: throw error.value // ^? case 418: throw error.value // ^? } data // ^? ``` --- --- url: /eden/overview.md --- # End-to-End Type Safety Imagine you have a toy train set. Each piece of the train track has to fit perfectly with the next one, like puzzle pieces. End-to-end type safety is like making sure all the pieces of the track match up correctly so the train doesn't fall off or get stuck. For a framework to have end-to-end type safety means you can connect client and server in a type-safe manner. Elysia provide end-to-end type safety **without code generation** out of the box with RPC-like connector, **Eden** Others framework that support e2e type safety: * tRPC * Remix * SvelteKit * Nuxt * TS-Rest Elysia allows you to change the type on the server and it will be instantly reflected on the client, helping with auto-completion and type-enforcement. ## Eden Eden is a RPC-like client to connect Elysia **end-to-end type safety** using only TypeScript's type inference instead of code generation. Allowing you to sync client and server types effortlessly, weighing less than 2KB. Eden is consists of 2 modules: 1. Eden Treaty **(recommended)**: an improved version RFC version of Eden Treaty 2. Eden Fetch: Fetch-like client with type safety. Below is an overview, use-case and comparison for each module. ## Eden Treaty (Recommended) Eden Treaty is an object-like representation of an Elysia server providing end-to-end type safety and a significantly improved developer experience. With Eden Treaty we can connect interact Elysia server with full-type support and auto-completion, error handling with type narrowing, and creating type safe unit test. Example usage of Eden Treaty: ```typescript twoslash // @filename: server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/', 'hi') .get('/users', () => 'Skadi') .put('/nendoroid/:id', ({ body }) => body, { body: t.Object({ name: t.String(), from: t.String() }) }) .get('/nendoroid/:id/name', () => 'Skadi') .listen(3000) export type App = typeof app // @filename: index.ts // ---cut--- import { treaty } from '@elysiajs/eden' import type { App } from './server' const app = treaty('localhost:3000') // @noErrors app. // ^| // Call [GET] at '/' const { data } = await app.get() // Call [PUT] at '/nendoroid/:id' const { data: nendoroid, error } = await app.nendoroid({ id: 1895 }).put({ name: 'Skadi', from: 'Arknights' }) ``` ## Eden Fetch A fetch-like alternative to Eden Treaty for developers that prefers fetch syntax. ```typescript import { edenFetch } from '@elysiajs/eden' import type { App } from './server' const fetch = edenFetch('http://localhost:3000') const { data } = await fetch('/name/:name', { method: 'POST', params: { name: 'Saori' }, body: { branch: 'Arius', type: 'Striker' } }) ``` ::: tip NOTE Unlike Eden Treaty, Eden Fetch doesn't provide Web Socket implementation for Elysia server ::: --- --- url: /plugins/graphql-yoga.md --- # GraphQL Yoga Plugin This plugin integrates GraphQL yoga with Elysia Install with: ```bash bun add @elysiajs/graphql-yoga ``` Then use it: ```typescript twoslash 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) ``` Accessing `/graphql` in the browser (GET request) would show you a GraphiQL instance for the GraphQL-enabled Elysia server. optional: you can install a custom version of optional peer dependencies as well: ```bash bun add graphql graphql-yoga ``` ## Resolver Elysia uses [Mobius](https://github.com/saltyaom/mobius) to infer type from **typeDefs** field automatically, allowing you to get full type-safety and auto-complete when typing **resolver** types. ## Context You can add custom context to the resolver function by adding **context** ```ts import { Elysia } from 'elysia' import { yoga } from '@elysiajs/graphql-yoga' const app = new Elysia() .use( yoga({ typeDefs: /* GraphQL */ ` type Query { hi: String } `, context: { name: 'Mobius' }, // If context is a function on this doesn't present // for some reason it won't infer context type useContext(_) {}, resolvers: { Query: { hi: async (parent, args, context) => context.name } } }) ) .listen(3000) ``` ## Config This plugin extends [GraphQL Yoga's createYoga options, please refer to the GraphQL Yoga documentation](https://the-guild.dev/graphql/yoga-server/docs) with inlining `schema` config to root. Below is a config which is accepted by the plugin ### path @default `/graphql` Endpoint to expose GraphQL handler --- --- url: /essential/handler.md --- # Handler Handler is a function that responds to the request for each route. Accepting request information and returning a response to the client. Altenatively, handler is also known as a **Controller** in other frameworks. ```typescript import { Elysia } from 'elysia' new Elysia() // the function `() => 'hello world'` is a handler .get('/', () => 'hello world') .listen(3000) ``` Handler maybe a literal value, and can be inlined. ```typescript import { Elysia, file } from 'elysia' new Elysia() .get('/', 'Hello Elysia') .get('/video', file('kyuukurarin.mp4')) .listen(3000) ``` Using an inline value always returns the same value which is useful to optimize performance for static resource like file. This allows Elysia to compile the response ahead of time to optimize performance. ::: tip Providing an inline value is not a cache. Static Resource value, headers and status can be mutate dynamically using lifecycle. ::: ## Context **Context** contains a request information which unique for each request, and is not shared except for `store` (global mutable state). ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/', (context) => context.path) // ^ This is a context ``` **Context** can only be retrieved in a route handler. It consists of: * **path** - Pathname of the request * **body** - [HTTP message](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages), form or file upload. * **query** - [Query String](https://en.wikipedia.org/wiki/Query_string), include additional parameters for search query as JavaScript Object. (Query is extracted from a value after pathname starting from '?' question mark sign) * **params** - Elysia's path parameters parsed as JavaScript object * **headers** - [HTTP Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers), additional information about the request like User-Agent, Content-Type, Cache Hint. * **request** - [Web Standard Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) * **redirect** - A function to redirect a response * **store** - A global mutable store for Elysia instance * **cookie** - A global mutable signal store for interacting with Cookie (including get/set) * **set** - Property to apply to Response: * **status** - [HTTP status](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status), defaults to 200 if not set. * **headers** - Response headers * **redirect** - Response as a path to redirect to * **error** - A function to return custom status code * **server** - Bun server instance ## Set **set** is a mutable property that form a response accessible via `Context.set`. * **set.status** - Set custom status code * **set.headers** - Append custom headers * **set.redirect** - Append redirect ```ts twoslash import { Elysia } from 'elysia' new Elysia() .get('/', ({ set, status }) => { set.headers = { 'X-Teapot': 'true' } return status(418, 'I am a teapot') }) .listen(3000) ``` ### status We can return a custom status code by using either: * **status** function (recommended) * **set.status** (legacy) ```typescript import { Elysia } from 'elysia' new Elysia() .get('/error', ({ error }) => error(418, 'I am a teapot')) .get('/set.status', ({ set }) => { set.status = 418 return 'I am a teapot' }) .listen(3000) ``` ### status function A dedicated `status` function for returning status code with response. ```typescript import { Elysia } from 'elysia' new Elysia() .get('/', ({ status }) => status(418, "Kirifuji Nagisa")) .listen(3000) ``` It's recommend to use `status` inside main handler as it has better inference: * allows TypeScript to check if a return value is correctly type to response schema * autocompletion for type narrowing base on status code * type narrowing for error handling using End-to-end type safety ([Eden](/eden/overview)) ### set.status Set a default status code if not provided. It's recommended to use this in a plugin that only needs to return a specific status code while allowing the user to return a custom value. For example, HTTP 201/206 or 403/405, etc. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .onBeforeHandle(({ set }) => { set.status = 418 return 'Kirifuji Nagisa' }) .get('/', () => 'hi') .listen(3000) ``` Unlike `status` function, `set.status` cannot infer the return value type, therefore it can't check if the return value is correctly type to response schema. ::: tip HTTP Status indicates the type of response. If the route handler is executed successfully without error, Elysia will return the status code 200. ::: You can also set a status code using the common name of the status code instead of using a number. ```typescript twoslash // @errors 2322 import { Elysia } from 'elysia' new Elysia() .get('/', ({ set }) => { set.status // ^? return 'Kirifuji Nagisa' }) .listen(3000) ``` ### set.headers Allowing us to append or delete a response headers represent as Object. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/', ({ set }) => { set.headers['x-powered-by'] = 'Elysia' return 'a mimir' }) .listen(3000) ``` ::: warning The names of headers should be lowercase to force case-sensitivity consistency for HTTP headers and auto-completion, eg. use `set-cookie` rather than `Set-Cookie`. ::: ### redirect Redirect a request to another resource. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/', ({ redirect }) => { return redirect('https://youtu.be/whpVWVWBW4U?&t=8') }) .get('/custom-status', ({ redirect }) => { // You can also set custom status to redirect return redirect('https://youtu.be/whpVWVWBW4U?&t=8', 302) }) .listen(3000) ``` When using redirect, returned value is not required and will be ignored. As response will be from another resource. ## Server Server instance is accessible via `Context.server` to interact with the server. Server could be nullable as it could be running in a different environment (test). If server is running (allocating) using Bun, `server` will be available (not null). ```typescript import { Elysia } from 'elysia' new Elysia() .get('/port', ({ server }) => { return server?.port }) .listen(3000) ``` ### Request IP We can get request IP by using `server.requestIP` method ```typescript import { Elysia } from 'elysia' new Elysia() .get('/ip', ({ server, request }) => { return server?.requestIP(request) }) .listen(3000) ``` ## Response Elysia is built on top of Web Standard Request/Response. To comply with the Web Standard, a value returned from route handler will be mapped into a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) by Elysia. Letting you focus on business logic rather than boilerplate code. ```typescript import { Elysia } from 'elysia' new Elysia() // Equivalent to "new Response('hi')" .get('/', () => 'hi') .listen(3000) ``` If you prefer an explicit Response class, Elysia also handles that automatically. ```typescript import { Elysia } from 'elysia' new Elysia() .get('/', () => new Response('hi')) .listen(3000) ``` ::: tip Using a primitive value or `Response` has near identical performance (+- 0.1%), so pick the one you prefer, regardless of performance. ::: ## Formdata We may return a `FormData` by using returning `form` utility directly from the handler. ```typescript import { Elysia, form, file } from 'elysia' new Elysia() .get('/', () => form({ name: 'Tea Party', images: [file('nagi.web'), file('mika.webp')] })) .listen(3000) ``` This pattern is useful if even need to return a file or multipart form data. ### Return a single file Or alternatively, you can return a single file by returning `file` directly without `form`. ```typescript import { Elysia, file } from 'elysia' new Elysia() .get('/', file('nagi.web')) .listen(3000) ``` ## Handle As Elysia is built on top of Web Standard Request, we can programmatically test it using `Elysia.handle`. ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/', () => 'hello') .post('/hi', () => 'hi') .listen(3000) app.handle(new Request('http://localhost/')).then(console.log) ``` **Elysia.handle** is a function to process an actual request sent to the server. ::: tip Unlike unit test's mock, **you can expect it to behave like an actual request** sent to the server. But also useful for simulating or creating unit tests. ::: ## Stream To return a response streaming out of the box by using a generator function with `yield` keyword. ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/ok', function* () { yield 1 yield 2 yield 3 }) ``` This this example, we may stream a response by using `yield` keyword. ### Set headers Elysia will defers returning response headers until the first chunk is yielded. This allows us to set headers before the response is streamed. ```typescript twoslash import { Elysia } from 'elysia' const app = new Elysia() .get('/ok', function* ({ set }) { // This will set headers set.headers['x-name'] = 'Elysia' yield 1 yield 2 // This will do nothing set.headers['x-id'] = '1' yield 3 }) ``` Once the first chunk is yielded, Elysia will send the headers and the first chunk in the same response. Setting headers after the first chunk is yielded will do nothing. ### Conditional Stream If the response is returned without yield, Elysia will automatically convert stream to normal response instead. ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/ok', function* () { if (Math.random() > 0.5) return 'ok' yield 1 yield 2 yield 3 }) ``` This allows us to conditionally stream a response or return a normal response if necessary. ### Abort While streaming a response, it's common that request may be cancelled before the response is fully streamed. Elysia will automatically stop the generator function when the request is cancelled. ### Eden [Eden](/eden/overview) 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) ``` ## Extending context As Elysia only provides essential information, we can customize Context for our specific need for instance: * extracting user ID as variable * inject a common pattern repository * add a database connection We may extend Elysia's context by using the following APIs to customize the Context: * [state](#state) - a global mutable state * [decorate](#decorate) - additional property assigned to **Context** * [derive](#derive) / [resolve](#resolve) - create a new value from existing property ### When to extend context You should only extend context when: * A property is a global mutable state, and shared across multiple routes using [state](#state) * A property is associated with a request or response using [decorate](#decorate) * A property is derived from an existing property using [derive](#derive) / [resolve](#resolve) Otherwise, we recommend defining a value or function separately than extending the context. ::: tip It's recommended to assign properties related to request and response, or frequently used functions to Context for separation of concerns. ::: ## State **State** is a global mutable object or state shared across the Elysia app. Once **state** is called, value will be added to **store** property **once at call time**, and can be used in handler. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .state('version', 1) .get('/a', ({ store: { version } }) => version) // ^? .get('/b', ({ store }) => store) .get('/c', () => 'still ok') .listen(3000) ``` ### When to use * When you need to share a primitive mutable value across multiple routes * If you want to use a non-primitive or a `wrapper` value or class that mutate an internal state, use [decorate](#decorate) instead. ### Key takeaway * **store** is a representation of a single-source-of-truth global mutable object for the entire Elysia app. * **state** is a function to assign an initial value to **store**, which could be mutated later. * Make sure to assign a value before using it in a handler. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' new Elysia() // ❌ TypeError: counter doesn't exist in store .get('/error', ({ store }) => store.counter) .state('counter', 0) // ✅ Because we assigned a counter before, we can now access it .get('/', ({ store }) => store.counter) ``` ::: tip Beware that we cannot use a state value before assign. Elysia registers state values into the store automatically without explicit type or additional TypeScript generic needed. ::: ## Decorate **decorate** assigns an additional property to **Context** directly **at call time**. ```typescript twoslash import { Elysia } from 'elysia' class Logger { log(value: string) { console.log(value) } } new Elysia() .decorate('logger', new Logger()) // ✅ defined from the previous line .get('/', ({ logger }) => { logger.log('hi') return 'hi' }) ``` ### When to use * A constant or readonly value object to **Context** * Non primitive value or class that may contain internal mutable state * Additional functions, singleton, or immutable property to all handlers. ### Key takeaway * Unlike **state**, decorated value **SHOULD NOT** be mutated although it's possible * Make sure to assign a value before using it in a handler. ## Derive Retrieve values from existing properties in **Context** and assign new properties. Derive assigns when request happens **at transform lifecycle** allowing us to "derive" (create new properties from existing properties). ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .derive(({ headers }) => { const auth = headers['authorization'] return { bearer: auth?.startsWith('Bearer ') ? auth.slice(7) : null } }) .get('/', ({ bearer }) => bearer) ``` Because **derive** is assigned once a new request starts, **derive** can access request properties like **headers**, **query**, **body** where **store**, and **decorate** can't. ### When to use * Create a new property from existing properties in **Context** without validation or type checking * When you need to access request properties like **headers**, **query**, **body** without validation ### Key takeaway * Unlike **state** and **decorate** instead of assign **at call time**, **derive** is assigned once a new request starts. * **derive is called at transform, or before validation** happens, Elysia cannot safely confirm the type of request property resulting in as **unknown**. If you want to assign a new value from typed request properties, you may want to use [resolve](#resolve) instead. ## Resolve Same as [derive](#derive), resolve allow us to assign a new property to context. Resolve is called at **beforeHandle** lifecycle or **after validation**, allowing us to **derive** request properties safely. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .guard({ headers: t.Object({ bearer: t.String({ pattern: '^Bearer .+$' }) }) }) .resolve(({ headers }) => { return { bearer: headers.bearer.slice(7) } }) .get('/', ({ bearer }) => bearer) ``` ### When to use * Create a new property from existing properties in **Context** with type integrity (type checked) * When you need to access request properties like **headers**, **query**, **body** with validation ### Key takeaway * **resolve is called at beforeHandle, or after validation** happens. Elysia can safely confirm the type of request property resulting in as **typed**. ### Error from resolve/derive As resolve and derive is based on **transform** and **beforeHandle** lifecycle, we can return an error from resolve and derive. If error is returned from **derive**, Elysia will return early exit and return the error as response. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .derive(({ headers, status }) => { const auth = headers['authorization'] if(!auth) return status(400) return { bearer: auth?.startsWith('Bearer ') ? auth.slice(7) : null } }) .get('/', ({ bearer }) => bearer) ``` ## Pattern **state**, **decorate** offers a similar APIs pattern for assigning property to Context as the following: * key-value * object * remap Where **derive** can be only used with **remap** because it depends on existing value. ### key-value We can use **state**, and **decorate** to assign a value using a key-value pattern. ```typescript import { Elysia } from 'elysia' class Logger { log(value: string) { console.log(value) } } new Elysia() .state('counter', 0) .decorate('logger', new Logger()) ``` This pattern is great for readability for setting a single property. ### Object Assigning multiple properties is better contained in an object for a single assignment. ```typescript import { Elysia } from 'elysia' new Elysia() .decorate({ logger: new Logger(), trace: new Trace(), telemetry: new Telemetry() }) ``` The object offers a less repetitive API for setting multiple values. ### Remap Remap is a function reassignment. Allowing us to create a new value from existing value like renaming or removing a property. By providing a function, and returning an entirely new object to reassign the value. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' new Elysia() .state('counter', 0) .state('version', 1) .state(({ version, ...store }) => ({ ...store, elysiaVersion: 1 })) // ✅ Create from state remap .get('/elysia-version', ({ store }) => store.elysiaVersion) // ❌ Excluded from state remap .get('/version', ({ store }) => store.version) ``` It's a good idea to use state remap to create a new initial value from the existing value. However, it's important to note that Elysia doesn't offer reactivity from this approach, as remap only assigns an initial value. ::: tip Using remap, Elysia will treat a returned object as a new property, removing any property that is missing from the object. ::: ## 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 **prefix** and **suffix**, allowing us to remap all property of an instance. ```ts twoslash import { Elysia } from 'elysia' const setup = new Elysia({ name: 'setup' }) .decorate({ argon: 'a', boron: 'b', carbon: 'c' }) const app = new Elysia() .use( setup .prefix('decorator', 'setup') ) .get('/', ({ setupCarbon, ...rest }) => 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, we can also remap `all` property of the plugin: ```ts twoslash import { Elysia } from 'elysia' const setup = new Elysia({ name: 'setup' }) .decorate({ argon: 'a', boron: 'b', carbon: 'c' }) const app = new Elysia() .use(setup.prefix('all', 'setup')) // [!code ++] .get('/', ({ setupCarbon, ...rest }) => setupCarbon) ``` ## Reference and value To mutate the state, it's recommended to use **reference** to mutate rather than using an actual value. When accessing the property from JavaScript, if we define a primitive value from an object property as a new value, the reference is lost, the value is treated as new separate value instead. For example: ```typescript const store = { counter: 0 } store.counter++ console.log(store.counter) // ✅ 1 ``` We can use **store.counter** to access and mutate the property. However, if we define a counter as a new value ```typescript const store = { counter: 0 } let counter = store.counter counter++ console.log(store.counter) // ❌ 0 console.log(counter) // ✅ 1 ``` Once a primitive value is redefined as a new variable, the reference **"link"** will be missing, causing unexpected behavior. This can apply to `store`, as it's a global mutable object instead. ```typescript import { Elysia } from 'elysia' new Elysia() .state('counter', 0) // ✅ Using reference, value is shared .get('/', ({ store }) => store.counter++) // ❌ Creating a new variable on primitive value, the link is lost .get('/error', ({ store: { counter } }) => counter) ``` ## TypeScript Elysia automatically type context base on various of factors like store, decorators, schema. It's recommended to leave Elysia to type context instead of manually define one. However, Elysia also offers some utility type to help you define a handler type. * [InferContext](#infercontext) * [InferHandle](#inferhandler) ### InferContext Infer context is a utility type to help you define a context type based on Elysia instance. ```typescript twoslash import { Elysia, type InferContext } from 'elysia' const setup = new Elysia() .state('a', 'a') .decorate('b', 'b') type Context = InferContext const handler = ({ store }: Context) => store.a ``` ### InferHandler Infer handler is a utility type to help you define a handler type based on Elysia instance, path, and schema. ```typescript twoslash import { Elysia, type InferHandler } from 'elysia' const setup = new Elysia() .state('a', 'a') .decorate('b', 'b') type Handler = InferHandler< // Elysia instance to based on typeof setup, // path '/path', // schema { body: string response: { 200: string } } > const handler: Handler = ({ body }) => body const app = new Elysia() .get('/', handler) ``` Unlike `InferContext`, `InferHandler` requires a path and schema to define a handler type and can safely ensure type safety of a return type. --- --- url: /plugins/html.md --- # HTML Plugin Allows you to use [JSX](#jsx) and HTML with proper headers and support. Install with: ```bash bun add @elysiajs/html ``` Then use it: ```tsx twoslash import React from 'react' // ---cut--- import { Elysia } from 'elysia' import { html, Html } from '@elysiajs/html' new Elysia() .use(html()) .get( '/html', () => ` Hello World

Hello World

` ) .get('/jsx', () => ( Hello World

Hello World

)) .listen(3000) ``` This plugin will automatically add `Content-Type: text/html; charset=utf8` header to the response, add ``, and convert it into a Response object. ## JSX Elysia HTML is based on [@kitajs/html](https://github.com/kitajs/html) allowing us to define JSX to string in compile time to achieve high performance. Name your file that needs to use JSX to end with affix **"x"**: * .js -> .jsx * .ts -> .tsx To register the TypeScript type, please append the following to **tsconfig.json**: ```jsonc // tsconfig.json { "compilerOptions": { "jsx": "react", "jsxFactory": "Html.createElement", "jsxFragmentFactory": "Html.Fragment" } } ``` That's it, now you can use JSX as your template engine: ```tsx twoslash import React from 'react' // ---cut--- import { Elysia } from 'elysia' import { html, Html } from '@elysiajs/html' // [!code ++] new Elysia() .use(html()) // [!code ++] .get('/', () => ( Hello World

Hello World

)) .listen(3000) ``` If the error `Cannot find name 'Html'. Did you mean 'html'?` occurs, this import must be added to the JSX template: ```tsx import { Html } from '@elysiajs/html' ``` It is important that it is written in uppercase. ## XSS Elysia HTML is based use of the Kita HTML plugin to detect possible XSS attacks in compile time. You can use a dedicated `safe` attribute to sanitize user value to prevent XSS vulnerability. ```tsx import { Elysia, t } from 'elysia' import { html, Html } from '@elysiajs/html' new Elysia() .use(html()) .post( '/', ({ body }) => ( Hello World

{body}

), { body: t.String() } ) .listen(3000) ``` However, when are building a large-scale app, it's best to have a type reminder to detect possible XSS vulnerabilities in your codebase. To add a type-safe reminder, please install: ```sh bun add @kitajs/ts-html-plugin ``` Then appends the following **tsconfig.json** ```jsonc // tsconfig.json { "compilerOptions": { "jsx": "react", "jsxFactory": "Html.createElement", "jsxFragmentFactory": "Html.Fragment", "plugins": [{ "name": "@kitajs/ts-html-plugin" }] } } ``` ## Options ### contentType * Type: `string` * Default: `'text/html; charset=utf8'` The content-type of the response. ### autoDetect * Type: `boolean` * Default: `true` Whether to automatically detect HTML content and set the content-type. ### autoDoctype * Type: `boolean | 'full'` * Default: `true` Whether to automatically add `` to a response starting with ``, if not found. Use `full` to also automatically add doctypes on responses returned without this plugin ```ts // without the plugin app.get('/', () => '') // With the plugin app.get('/', ({ html }) => html('')) ``` ### isHtml * Type: `(value: string) => boolean` * Default: `isHtml` (exported function) The function is used to detect if a string is a html or not. Default implementation if length is greater than 7, starts with `<` and ends with `>`. Keep in mind there's no real way to validate HTML, so the default implementation is a best guess. --- --- url: /integrations/astro.md --- # Integration with Astro With [Astro Endpoint](https://docs.astro.build/en/core-concepts/endpoints/), we can run Elysia on Astro directly. 1. Set **output** to **server** in **astro.config.mjs** ```javascript // astro.config.mjs import { defineConfig } from 'astro/config' // https://astro.build/config export default defineConfig({ output: 'server' // [!code ++] }) ``` 2. Create **pages/\[...slugs].ts** 3. Create or import an existing Elysia server in **\[...slugs].ts** 4. Export the handler with the name of method you want to expose ```typescript // pages/[...slugs].ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/api', () => 'hi') .post('/api', ({ body }) => body, { body: t.Object({ name: t.String() }) }) const handle = ({ request }: { request: Request }) => app.handle(request) // [!code ++] export const GET = handle // [!code ++] export const POST = handle // [!code ++] ``` Elysia will work normally as expected because of WinterCG compliance. We recommended running [Astro on Bun](https://docs.astro.build/en/recipes/bun) as Elysia is designed to be run on Bun ::: tip You can run Elysia server without running Astro on Bun thanks to WinterCG support. However some plugins like **Elysia Static** may not work if you are running Astro on Node. ::: With this approach, you can have co-location of both frontend and backend in a single repository and have End-to-end type-safety with Eden. Please refer to [Astro Endpoint](https://docs.astro.build/en/core-concepts/endpoints/) for more information. ## Prefix If you place an Elysia server not in the root directory of the app router, you need to annotate the prefix to the Elysia server. For example, if you place Elysia server in **pages/api/\[...slugs].ts**, you need to annotate prefix as **/api** to Elysia server. ```typescript // pages/api/[...slugs].ts import { Elysia, t } from 'elysia' const app = new Elysia({ prefix: '/api' }) // [!code ++] .get('/', () => 'hi') .post('/', ({ body }) => body, { body: t.Object({ name: t.String() }) }) const handle = ({ request }: { request: Request }) => app.handle(request) // [!code ++] export const GET = handle // [!code ++] export const POST = handle // [!code ++] ``` This will ensure that Elysia routing will work properly in any location you place it. --- --- 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: /integrations/expo.md --- # Integration with Expo Starting from Expo SDK 50, and App Router v3, Expo allows us to create API route directly in an Expo app. 1. Create an Expo app if it doesn't exist with: ```typescript bun create expo-app --template tabs ``` 2. Create **app/\[...slugs]+api.ts** 3. In **\[...slugs]+api.ts**, create or import an existing Elysia server 4. Export the handler with the name of method you want to expose ```typescript // app/[...slugs]+api.ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/', () => 'hello Next') .post('/', ({ body }) => body, { body: t.Object({ name: t.String() }) }) export const GET = app.handle // [!code ++] export const POST = app.handle // [!code ++] ``` Elysia will work normally as expected because of WinterCG compliance, however, some plugins like **Elysia Static** may not work if you are running Expo on Node. You can treat the Elysia server as if normal Expo API route. With this approach, you can have co-location of both frontend and backend in a single repository and have [End-to-end type safety with Eden](https://elysiajs.com/eden/overview.html) with both client-side and server action. Please refer to [API route](https://docs.expo.dev/router/reference/api-routes/) for more information. ## Prefix If you place an Elysia server not in the root directory of the app router, you need to annotate the prefix to the Elysia server. For example, if you place Elysia server in **app/api/\[...slugs]+api.ts**, you need to annotate prefix as **/api** to Elysia server. ```typescript // app/api/[...slugs]+api.ts import { Elysia, t } from 'elysia' const app = new Elysia({ prefix: '/api' }) .get('/', () => 'hi') .post('/', ({ body }) => body, { body: t.Object({ name: t.String() }) }) export const GET = app.handle export const POST = app.handle ``` This will ensure that Elysia routing will work properly in any location you place in. ## Deployment You can either directly use API route using Elysia and deploy as normal Elysia app normally if need or using [experimental Expo server runtime](https://docs.expo.dev/router/reference/api-routes/#deployment). If you are using Expo server runtime, you may use `expo export` command to create optimized build for your expo app, this will include an Expo function which is using Elysia at **dist/server/\_expo/functions/\[...slugs]+api.js** ::: tip Please note that Expo Functions are treated as Edge functions instead of normal server, so running the Edge function directly will not allocate any port. ::: You may use the Expo function adapter provided by Expo to deploy your Edge Function. Currently Expo support the following adapter: * [Express](https://docs.expo.dev/router/reference/api-routes/#express) * [Netlify](https://docs.expo.dev/router/reference/api-routes/#netlify) * [Vercel](https://docs.expo.dev/router/reference/api-routes/#vercel) --- --- url: /integrations/nextjs.md --- # Integration with Nextjs With Nextjs App Router, we can run Elysia on Nextjs route. 1. Create **api/\[\[...slugs]]/route.ts** inside app router 2. In **route.ts**, create or import an existing Elysia server 3. Export the handler with the name of method you want to expose ```typescript // app/api/[[...slugs]]/route.ts import { Elysia, t } from 'elysia' const app = new Elysia({ prefix: '/api' }) .get('/', () => 'hello Next') .post('/', ({ body }) => body, { body: t.Object({ name: t.String() }) }) export const GET = app.handle // [!code ++] export const POST = app.handle // [!code ++] ``` Elysia will work normally as expected because of WinterCG compliance, however, some plugins like **Elysia Static** may not work if you are running Nextjs on Node. You can treat the Elysia server as a normal Nextjs API route. With this approach, you can have co-location of both frontend and backend in a single repository and have [End-to-end type safety with Eden](https://elysiajs.com/eden/overview.html) with both client-side and server action Please refer to [Nextjs Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers#static-route-handlers) for more information. ## Prefix Because our Elysia server is not in the root directory of the app router, you need to annotate the prefix to the Elysia server. For example, if you place Elysia server in **app/user/\[\[...slugs]]/route.ts**, you need to annotate prefix as **/user** to Elysia server. ```typescript // app/user/[[...slugs]]/route.ts import { Elysia, t } from 'elysia' const app = new Elysia({ prefix: '/user' }) // [!code ++] .get('/', () => 'hi') .post('/', ({ body }) => body, { body: t.Object({ name: t.String() }) }) export const GET = app.handle export const POST = app.handle ``` This will ensure that Elysia routing will work properly in any location you place it. --- --- url: /integrations/nuxt.md --- # Integration with Nuxt We can use [nuxt-elysia](https://github.com/tkesgar/nuxt-elysia), a community plugin for Nuxt, to setup Elysia on Nuxt API route with Eden Treaty. 1. Install the plugin with the following command: ```bash bun add elysia @elysiajs/eden bun add -d nuxt-elysia ``` 2. Add `nuxt-elysia` to your Nuxt config: ```ts export default defineNuxtConfig({ modules: [ // [!code ++] 'nuxt-elysia' // [!code ++] ] // [!code ++] }) ``` 3. Create `api.ts` in the project root: ```typescript [api.ts] export default () => new Elysia() // [!code ++] .get('/hello', () => ({ message: 'Hello world!' })) // [!code ++] ``` 4. Use Eden Treaty in your Nuxt app: ```vue ``` This will automatically setup Elysia to run on Nuxt API route automatically. ## Prefix By default, Elysia will be mounted on **/\_api** but we can customize it with `nuxt-elysia` config. ```ts export default defineNuxtConfig({ nuxtElysia: { path: '/api' // [!code ++] } }) ``` This will mount Elysia on **/api** instead of **/\_api**. For more configuration, please refer to [nuxt-elysia](https://github.com/tkesgar/nuxt-elysia) --- --- url: /integrations/prisma.md --- # Prisma [Prisma](https://prisma.io) is an ORM that allows us to interact with databases in a type-safe manner. It provides a way to define your database schema using a Prisma schema file, and then generates TypeScript types based on that schema. ### Prismabox [Prismabox](https://github.com/m1212e/prismabox) is a library that generate TypeBox or Elysia validation models from Prisma schema. We can use Prismabox to convert Prisma schema into Elysia validation models, which can then be used to ensure type validation in Elysia. ### Here's how it works: 1. Define your database schema in Prisma Schema. 2. Add `prismabox` generator to generate Elysia schema. 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 | | | | | prismabox | | ——————— | * ——————————————— * | Prisma | —————————-> | Elysia | | | | | ——————— | * ——————————————— * * ————————— * * ———————— * Eden | | | | -> | Frontend Code | | | * ——————————————— * ``` ## Installation To install Prisma, run the following command: ```bash bun add @prisma/client prismabox && \ bun add -d prisma ``` ## Prisma schema Assuming you already have a `prisma/schema.prisma`. We can add a `prismabox` generator to the Prisma schema file as follows: ::: code-group ```ts [prisma/schema.prisma] generator client { provider = "prisma-client-js" output = "../generated/prisma" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } generator prismabox { // [!code ++] provider = "prismabox" // [!code ++] typeboxImportDependencyName = "elysia" // [!code ++] typeboxImportVariableName = "t" // [!code ++] inputModel = true // [!code ++] output = "../generated/prismabox" // [!code ++] } // [!code ++] model User { id String @id @default(cuid()) email String @unique name String? posts Post[] } model Post { id String @id @default(cuid()) title String content String? published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId String } ``` ::: This will generate Elysia validation models in the `generated/prismabox` directory. Each model will have its own file, and the models will be named based on the Prisma model names. For example: * `User` model will be generated to `generated/prismabox/User.ts` * `Post` model will be generated to `generated/prismabox/Post.ts` ## Using generated models Then we can import the generated models in our Elysia application: ::: code-group ```ts [src/index.ts] import { Elysia, t } from 'elysia' import { PrismaClient } from '../generated/prisma' // [!code ++] import { UserPlain, UserPlainInputCreate } from '../generated/prismabox/User' // [!code ++] const prisma = new PrismaClient() const app = new Elysia() .put( '/', async ({ body }) => prisma.user.create({ data: body }), { body: UserPlainInputCreate, // [!code ++] response: UserPlain // [!code ++] } ) .get( '/id/:id', async ({ params: { id }, status }) => { const user = await prisma.user.findUnique({ where: { id } }) if (!user) return status(404, 'User not found') return user }, { response: { 200: UserPlain, // [!code ++] 404: t.String() // [!code ++] } } ) .listen(3000) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` ::: This allows us to reuse the database schema in Elysia validation models. *** For more information, please refer to the [Prisma](https://prisma.io), and [Prismabox](https://github.com/m1212e/prismabox) documentation. --- --- url: /integrations/sveltekit.md --- # Integration with SvelteKit With SvelteKit, you can run Elysia on server routes. 1. Create **src/routes/\[...slugs]/+server.ts**. 2. In **+server.ts**, create or import an existing Elysia server 3. Export the handler with the name of method you want to expose ```typescript // src/routes/[...slugs]/+server.ts import { Elysia, t } from 'elysia'; const app = new Elysia() .get('/', () => 'hello SvelteKit') .post('/', ({ body }) => body, { body: t.Object({ name: t.String() }) }) type RequestHandler = (v: { request: Request }) => Response | Promise export const GET: RequestHandler = ({ request }) => app.handle(request) export const POST: RequestHandler = ({ request }) => app.handle(request) ``` You can treat the Elysia server as a normal SvelteKit server route. With this approach, you can have co-location of both frontend and backend in a single repository and have [End-to-end type-safety with Eden](https://elysiajs.com/eden/overview.html) with both client-side and server action Please refer to [SvelteKit Routing](https://kit.svelte.dev/docs/routing#server) for more information. ## Prefix If you place an Elysia server not in the root directory of the app router, you need to annotate the prefix to the Elysia server. For example, if you place Elysia server in **src/routes/api/\[...slugs]/+server.ts**, you need to annotate prefix as **/api** to Elysia server. ```typescript twoslash // src/routes/api/[...slugs]/+server.ts import { Elysia, t } from 'elysia'; const app = new Elysia({ prefix: '/api' }) // [!code ++] .get('/', () => 'hi') .post('/', ({ body }) => body, { body: t.Object({ name: t.String() }) }) type RequestHandler = (v: { request: Request }) => Response | Promise export const GET: RequestHandler = ({ request }) => app.handle(request) export const POST: RequestHandler = ({ request }) => app.handle(request) ``` This will ensure that Elysia routing will work properly in any location you place it. --- --- url: /plugins/jwt.md --- # JWT Plugin This plugin adds support for using JWT in Elysia handler Install with: ```bash bun add @elysiajs/jwt ``` Then use it: ::: code-group ```typescript [cookie] import { Elysia } from 'elysia' import { jwt } from '@elysiajs/jwt' const app = new Elysia() .use( jwt({ name: 'jwt', secret: 'Fischl von Luftschloss Narfidort' }) ) .get('/sign/:name', async ({ jwt, params: { name }, cookie: { auth } }) => { const value = await jwt.sign({ name }) auth.set({ value, httpOnly: true, maxAge: 7 * 86400, path: '/profile', }) return `Sign in as ${value}` }) .get('/profile', async ({ jwt, status, cookie: { auth } }) => { const profile = await jwt.verify(auth.value) if (!profile) return status(401, 'Unauthorized') return `Hello ${profile.name}` }) .listen(3000) ``` ```typescript [headers] import { Elysia } from 'elysia' import { jwt } from '@elysiajs/jwt' const app = new Elysia() .use( jwt({ name: 'jwt', secret: 'Fischl von Luftschloss Narfidort' }) ) .get('/sign/:name', ({ jwt, params: { name } }) => { return jwt.sign({ name }) }) .get('/profile', async ({ jwt, error, headers: { authorization } }) => { const profile = await jwt.verify(authorization) if (!profile) return status(401, 'Unauthorized') return `Hello ${profile.name}` }) .listen(3000) ``` ::: ## Config This plugin extends config from [jose](https://github.com/panva/jose). Below is a config that is accepted by the plugin. ### name Name to register `jwt` function as. For example, `jwt` function will be registered with a custom name. ```typescript app .use( jwt({ name: 'myJWTNamespace', secret: process.env.JWT_SECRETS! }) ) .get('/sign/:name', ({ myJWTNamespace, params }) => { return myJWTNamespace.sign(params) }) ``` Because some might need to use multiple `jwt` with different configs in a single server, explicitly registering the JWT function with a different name is needed. ### secret The private key to sign JWT payload with. ### schema Type strict validation for JWT payload. *** Below is a config that extends from [cookie](https://npmjs.com/package/cookie) ### alg @default `HS256` Signing Algorithm to sign JWT payload with. Possible properties for jose are: HS256 HS384 HS512 PS256 PS384 PS512 RS256 RS384 RS512 ES256 ES256K ES384 ES512 EdDSA ### iss The issuer claim identifies the principal that issued the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.1) TLDR; is usually (the domain) name of the signer. ### sub The subject claim identifies the principal that is the subject of the JWT. The claims in a JWT are normally statements about the subject as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2) ### aud The audience claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3) ### jti JWT ID claim provides a unique identifier for the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7) ### nbf The "not before" claim identifies the time before which the JWT must not be accepted for processing as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.5) ### exp The expiration time claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.4) ### iat The "issued at" claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6) ### b64 This JWS Extension Header Parameter modifies the JWS Payload representation and the JWS Signing input computation as per [RFC7797](https://www.rfc-editor.org/rfc/rfc7797). ### kid A hint indicating which key was used to secure the JWS. This parameter allows originators to explicitly signal a change of key to recipients as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.4) ### x5t (X.509 certificate SHA-1 thumbprint) header parameter is a base64url-encoded SHA-1 digest of the DER encoding of the X.509 certificate [RFC5280](https://www.rfc-editor.org/rfc/rfc5280) corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.7) ### x5c (X.509 certificate chain) header parameter contains the X.509 public key certificate or certificate chain [RFC5280](https://www.rfc-editor.org/rfc/rfc5280) corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.6) ### x5u (X.509 URL) header parameter is a URI [RFC3986](https://www.rfc-editor.org/rfc/rfc3986) that refers to a resource for the X.509 public key certificate or certificate chain \[RFC5280] corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.5) ### jwk The "jku" (JWK Set URL) Header Parameter is a URI \[RFC3986] that refers to a resource for a set of JSON-encoded public keys, one of which corresponds to the key used to digitally sign the JWS. The keys MUST be encoded as a JWK Set \[JWK] as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.2) ### typ The `typ` (type) Header Parameter is used by JWS applications to declare the media type \[IANA.MediaTypes] of this complete JWS. This is intended for use by the application when more than one kind of object could be present in an application data structure that can contain a JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.9) ### ctr Content-Type parameter is used by JWS applications to declare the media type \[IANA.MediaTypes] of the secured content (the payload). This is intended for use by the application when more than one kind of object could be present in the JWS Payload as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.9) ## Handler Below are the value added to the handler. ### jwt.sign A dynamic object of collection related to use with JWT registered by the JWT plugin. Type: ```typescript sign: (payload: JWTPayloadSpec): Promise ``` `JWTPayloadSpec` accepts the same value as [JWT config](#config) ### jwt.verify Verify payload with the provided JWT config Type: ```typescript verify(payload: string) => Promise ``` `JWTPayloadSpec` accepts the same value as [JWT config](#config) ## Pattern Below you can find the common patterns to use the plugin. ## Set JWT expiration date By default, the config is passed to `setCookie` and inherits its value. ```typescript const app = new Elysia() .use( jwt({ name: 'jwt', secret: 'kunikuzushi', exp: '7d' }) ) .get('/sign/:name', async ({ jwt, params }) => jwt.sign(params)) ``` This will sign JWT with an expiration date of the next 7 days. --- --- url: /key-concept.md --- # Key Concept Although Elysia is a simple library, it has some key concepts that you need to understand to use it effectively. This page covers most important concepts of Elysia that you should know. ::: tip We **highly recommend** you to read this page before learn more about Elysia. ::: ## Everything is a component Every Elysia instance is a component. A component is a plugin that could plug into other instances. It could be a router, a store, a service, or anything else. ```ts twoslash import { Elysia } from 'elysia' const store = new Elysia() .state({ visitor: 0 }) const router = new Elysia() .use(store) .get('/increase', ({ store }) => store.visitor++) const app = new Elysia() .use(router) .get('/', ({ store }) => store) .listen(3000) ``` This forces you to break down your application into small pieces, making it easy for you to add or remove features. Learn more about this in [plugin](/essential/plugin.html). ## 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 typed `build` property. ### 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. ## Scope By default, event/life-cycle in each instance is isolated from each other. ```ts twoslash // @errors: 2339 import { Elysia } from 'elysia' const ip = new Elysia() .derive(({ server, request }) => ({ ip: server?.requestIP(request) })) .get('/ip', ({ ip }) => ip) const server = new Elysia() .use(ip) .get('/ip', ({ ip }) => ip) .listen(3000) ``` In this example, the `ip` property is only shared in its own instance but not in the `server` instance. To share the lifecycle, in our case, an `ip` property with `server` instance, we need to **explicitly say** that it could be shared. ```ts twoslash import { Elysia } from 'elysia' const ip = new Elysia() .derive( { as: 'global' }, // [!code ++] ({ server, request }) => ({ ip: server?.requestIP(request) }) ) .get('/ip', ({ ip }) => ip) const server = new Elysia() .use(ip) .get('/ip', ({ ip }) => ip) .listen(3000) ``` In this example, `ip` property is shared between `ip` and `server` instance because we define it as `global`. This forces you to think about the scope of each property, preventing you from accidentally sharing the property between instances. Learn more about this in [scope](/essential/plugin.html#scope). ## Dependency By default, each instance will be re-executed every time it's applied to another instance. This can cause a duplication of the same method being applied multiple times, whereas some methods, like **lifecycle** or **routes**, should only be called once. To prevent lifecycle methods from being duplicated, we can add **a unique identifier** to the instance. ```ts twoslash import { Elysia } from 'elysia' const ip = new Elysia({ name: 'ip' }) // [!code ++] .derive( { as: 'global' }, ({ server, request }) => ({ ip: server?.requestIP(request) }) ) .get('/ip', ({ ip }) => ip) const router1 = new Elysia() .use(ip) .get('/ip-1', ({ ip }) => ip) const router2 = new Elysia() .use(ip) .get('/ip-2', ({ ip }) => ip) const server = new Elysia() .use(router1) .use(router2) ``` This will prevent the `ip` property from being called multiple times by applying deduplication using a unique name. This allows us to reuse the same instance multiple times without the performance penalty. Forcing you to think about the dependencies of each instance. Learn more about this in [plugin deduplication](/essential/plugin.html#plugin-deduplication). ### Service Locator When you apply a plugin with state/decorators to an instance, the instance will gain type safety. But if you don't apply the plugin to another instance, it will not be able to infer the type. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' const child = new Elysia() // ❌ 'a' is missing .get('/', ({ a }) => a) const main = new Elysia() .decorate('a', 'a') .use(child) ``` Elysia introduces the **Service Locator** pattern to counteract this. We simply provide the plugin reference for Elysia to find the service to add type safety. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' const setup = new Elysia({ name: 'setup' }) .decorate('a', 'a') // Without 'setup', type will be missing const error = new Elysia() .get('/', ({ a }) => a) const main = new Elysia() // With `setup`, type will be inferred .use(setup) // [!code ++] .get('/', ({ a }) => a) // ^? ``` As mentioned in [dependencies](#dependencies), we can use the `name` property to deduplicate the instance so it will not have any performance penalty or lifecycle duplication. ## Order of code The order of Elysia's life-cycle code is very important. Because event will only apply to routes **after** it is registered. If you put the onError before plugin, plugin will not inherit the onError event. ```typescript import { Elysia } from 'elysia' new Elysia() .onBeforeHandle(() => { console.log('1') }) .get('/', () => 'hi') .onBeforeHandle(() => { console.log('2') }) .listen(3000) ``` Console should log the following: ```bash 1 ``` Notice that it doesn't log **2**, because the event is registered after the route so it is not applied to the route. Learn more about this in [order of code](/essential/life-cycle.html#order-of-code). ## Type Inference Elysia has a complex type system that allows you to infer types from the instance. ```ts twoslash import { Elysia, t } from 'elysia' const app = new Elysia() .post('/', ({ body }) => body, { // ^? body: t.Object({ name: t.String() }) }) ``` If possible, **always use an inline function** to provide an accurate type inference. If you need to apply a separate function, eg. MVC's controller pattern, it's recommended to destructure properties from inline function to prevent unnecessary type inference. ```ts twoslash import { Elysia, t } from 'elysia' abstract class Controller { static greet({ name }: { name: string }) { return 'hello ' + name } } const app = new Elysia() .post('/', ({ body }) => Controller.greet(body), { body: t.Object({ name: t.String() }) }) ``` ### TypeScript We can get a type definitions of every Elysia/TypeBox's type by accessing `static` property as follows: ```ts twoslash import { t } from 'elysia' const MyType = t.Object({ hello: t.Literal('Elysia') }) type MyType = typeof MyType.static // ^? ``` This allows Elysia to infer and provide type automatically, reducing the need to declare duplicate schema A single Elysia/TypeBox schema can be used for: * Runtime validation * Data coercion * TypeScript type * OpenAPI schema This allows us to make a schema as a **single source of truth**. Learn more about this in [Best practice: MVC Controller](/essential/best-practice.html#controller). --- --- url: /essential/life-cycle.md --- # Lifecycle Life Cycle allows us to intercept an important event at the predefined point allowing us to customize the behavior of our server as needed. Elysia's Life Cycle event can be illustrated as the following. ![Elysia Life Cycle Graph](/assets/lifecycle-chart.svg) > Click on image to enlarge Below are the request life cycle available in Elysia: ## Why Imagine we want to return some HTML. We need to set **"Content-Type"** headers as **"text/html"** for the browser to render HTML. Explicitly specifying that the response is HTML could be repetitive if there are a lot of handlers, say ~200 endpoints. This can lead to a lot of duplicated code just to specify the **"text/html"** **"Content-Type"** But what if after we send a response, we could detect that the response is an HTML string then append the header automatically? That's when the concept of Life Cycle comes into play. ## Hook We refer to each function that intercepts the life cycle event as **"hook"**, as the function hooks into the lifecycle event. Hooks can be categorized into 2 types: 1. Local Hook: Execute on a specific route 2. Interceptor Hook: Execute on every route ::: tip The hook will accept the same Context as a handler, you can imagine adding a route handler but at a specific point. ::: ## Local Hook The local hook is executed on a specific route. To use a local hook, you can inline hook into a route handler: ```typescript import { Elysia } from 'elysia' import { isHtml } from '@elysiajs/html' new Elysia() .get('/', () => '

Hello World

', { afterHandle({ response, set }) { if (isHtml(response)) set.headers['Content-Type'] = 'text/html; charset=utf8' } }) .get('/hi', () => '

Hello World

') .listen(3000) ``` The response should be listed as follows: | Path | Content-Type | | ---- | ------------------------ | | / | text/html; charset=utf8 | | /hi | text/plain; charset=utf8 | ## Interceptor Hook Register hook into every handler **of the current instance** that came after. To add an interceptor hook, you can use `.on` followed by a life cycle event in camelCase: ```typescript import { Elysia } from 'elysia' import { isHtml } from '@elysiajs/html' new Elysia() .get('/none', () => '

Hello World

') .onAfterHandle(({ response, set }) => { if (isHtml(response)) set.headers['Content-Type'] = 'text/html; charset=utf8' }) .get('/', () => '

Hello World

') .get('/hi', () => '

Hello World

') .listen(3000) ``` The response should be listed as follows: | Path | Content-Type | | ----- | ------------------------ | | / | text/html; charset=utf8 | | /hi | text/html; charset=utf8 | | /none | text/plain; charset=utf8 | Events from other plugins are also applied to the route so the order of code is important. ::: tip The code above will only apply to the current instance, not applying to parent. See [scope](/essential/plugin#scope) to find out why ::: ## Order of code The order of Elysia's life-cycle code is very important. Because event will only apply to routes **after** it is registered. If you put the onError before plugin, plugin will not inherit the onError event. ```typescript import { Elysia } from 'elysia' new Elysia() .onBeforeHandle(() => { console.log('1') }) .get('/', () => 'hi') .onBeforeHandle(() => { console.log('2') }) .listen(3000) ``` Console should log the following: ```bash 1 ``` Notice that it doesn't log **2**, because the event is registered after the route so it is not applied to the route. This also applies to the plugin. ```typescript import { Elysia } from 'elysia' new Elysia() .onBeforeHandle(() => { console.log('1') }) .use(someRouter) .onBeforeHandle(() => { console.log('2') }) .listen(3000) ``` In the code above, only **1** will be logged, because the event is registered after the plugin. This is because each events will be inline into a route handler to create a true encapsulation scope and static code analysis. The only exception is `onRequest` which is executed before the route handler so it couldn't be inlined and tied to the routing process instead. ## Request The first life-cycle event to get executed for every new request is recieved. As `onRequest` is designed to provide only the most crucial context to reduce overhead, it is recommended to use in the following scenario: * Caching * Rate Limiter / IP/Region Lock * Analytic * Provide custom header, eg. CORS #### Example Below is a pseudo code to enforce rate-limit on a certain IP address. ```typescript import { Elysia } from 'elysia' new Elysia() .use(rateLimiter) .onRequest(({ rateLimiter, ip, set, status }) => { if (rateLimiter.check(ip)) return status(420, 'Enhance your calm') }) .get('/', () => 'hi') .listen(3000) ``` If a value is returned from `onRequest`, it will be used as the response and the rest of the life-cycle will be skipped. ### Pre Context Context's onRequest is typed as `PreContext`, a minimal representation of `Context` with the attribute on the following: request: `Request` * set: `Set` * store * decorators Context doesn't provide `derived` value because derive is based on `onTransform` event. ## Parse Parse is an equivalent of **body parser** in Express. A function to parse body, the return value will be append to `Context.body`, if not, Elysia will continue iterating through additional parser functions assigned by `onParse` until either body is assigned or all parsers have been executed. By default, Elysia will parse the body with content-type of: * `text/plain` * `application/json` * `multipart/form-data` * `application/x-www-form-urlencoded` It's recommended to use the `onParse` event to provide a custom body parser that Elysia doesn't provide. #### Example Below is an example code to retrieve value based on custom headers. ```typescript import { Elysia } from 'elysia' new Elysia().onParse(({ request, contentType }) => { if (contentType === 'application/custom-type') return request.text() }) ``` The returned value will be assigned to Context.body. If not, Elysia will continue iterating through additional parser functions from **onParse** stack until either body is assigned or all parsers have been executed. ### Context `onParse` Context is extends from `Context` with additional properties of the following: * contentType: Content-Type header of the request All of the context is based on normal context and can be used like normal context in route handler. ### Parser By default, Elysia will try to determine body parsing function ahead of time and pick the most suitable function to speed up the process. Elysia is able to determine that body function by reading `body`. Take a look at this example: ```typescript import { Elysia, t } from 'elysia' new Elysia().post('/', ({ body }) => body, { body: t.Object({ username: t.String(), password: t.String() }) }) ``` Elysia read the body schema and found that, the type is entirely an object, so it's likely that the body will be JSON. Elysia then picks the JSON body parser function ahead of time and tries to parse the body. Here's a criteria that Elysia uses to pick up type of body parser * `application/json`: body typed as `t.Object` * `multipart/form-data`: body typed as `t.Object`, and is 1 level deep with `t.File` * `application/x-www-form-urlencoded`: body typed as `t.URLEncoded` * `text/plain`: other primitive type This allows Elysia to optimize body parser ahead of time, and reduce overhead in compile time. ### Explicit Parser However, in some scenario if Elysia fails to pick the correct body parser function, we can explicitly tell Elysia to use a certain function by specifying `type` ```typescript import { Elysia } from 'elysia' new Elysia().post('/', ({ body }) => body, { // Short form of application/json parse: 'json' }) ``` Allowing us to control Elysia behavior for picking body parser function to fit our needs in a complex scenario. `type` may be one of the following: ```typescript 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' // Skip body parsing entirely | 'none' | 'text/plain' | 'application/json' | 'multipart/form-data' | 'application/x-www-form-urlencoded' ``` ### Skip Body Parsing When you need to integrate a third-party library with HTTP handler like `trpc`, `orpc`, and it throw `Body is already used`. This is because Web Standard Request can be parsed only once. Both Elysia and the third-party library both has its own body parser, so you can skip body parsing on Elysia side by specifying `parse: 'none'` ```typescript import { Elysia } from 'elysia' new Elysia() .post( '/', ({ request }) => library.handle(request), { parse: 'none' } ) ``` ### Custom Parser You can provide register a custom parser with `parser`: ```typescript import { Elysia } from 'elysia' new Elysia() .parser('custom', ({ request, contentType }) => { if (contentType === 'application/elysia') return request.text() }) .post('/', ({ body }) => body, { parse: ['custom', 'json'] }) ``` ## Transform Executed just before **Validation** process, designed to mutate context to conform with the validation or appending new value. It's recommended to use transform for the following: * Mutate existing context to conform with validation. * `derive` is based on `onTransform` with support for providing type. #### Example Below is an example of using transform to mutate params to be numeric values. ```typescript import { Elysia, t } from 'elysia' new Elysia() .get('/id/:id', ({ params: { id } }) => id, { params: t.Object({ id: t.Number() }), transform({ params }) { const id = +params.id if (!Number.isNaN(id)) params.id = id } }) .listen(3000) ``` ## Derive Append new value to context directly **before validation**. It's stored in the same stack as **transform**. Unlike **state** and **decorate** that assigned value before the server started. **derive** assigns a property when each request happens. Allowing us to extract a piece of information into a property instead. ```typescript import { Elysia } from 'elysia' new Elysia() .derive(({ headers }) => { const auth = headers['Authorization'] return { bearer: auth?.startsWith('Bearer ') ? auth.slice(7) : null } }) .get('/', ({ bearer }) => bearer) ``` Because **derive** is assigned once a new request starts, **derive** can access Request properties like **headers**, **query**, **body** where **store**, and **decorate** can't. Unlike **state**, and **decorate**. Properties which assigned by **derive** is unique and not shared with another request. ### Queue `derive` and `transform` is stored in the same queue. ```typescript import { Elysia } from 'elysia' new Elysia() .onTransform(() => { console.log(1) }) .derive(() => { console.log(2) return {} }) ``` The console should log as the following: ```bash 1 2 ``` ## Before Handle Execute after validation and before the main route handler. Designed to provide a custom validation to provide a specific requirement before running the main handler. If a value is returned, the route handler will be skipped. It's recommended to use Before Handle in the following situations: * Restricted access check: authorization, user sign-in * Custom request requirement over data structure #### Example Below is an example of using the before handle to check for user sign-in. ```typescript import { Elysia } from 'elysia' import { validateSession } from './user' new Elysia() .get('/', () => 'hi', { beforeHandle({ set, cookie: { session }, status }) { if (!validateSession(session.value)) return status(401) } }) .listen(3000) ``` The response should be listed as follows: | Is signed in | Response | | ------------ | ------------ | | ❌ | Unauthorized | | ✅ | Hi | ### Guard When we need to apply the same before handle to multiple routes, we can use `guard` to apply the same before handle to multiple routes. ```typescript import { Elysia } from 'elysia' import { signUp, signIn, validateSession, isUserExists } from './user' new Elysia() .guard( { beforeHandle({ set, cookie: { session }, status }) { if (!validateSession(session.value)) return status(401) } }, (app) => app .get('/user/:id', ({ body }) => signUp(body)) .post('/profile', ({ body }) => signIn(body), { beforeHandle: isUserExists }) ) .get('/', () => 'hi') .listen(3000) ``` ## Resolve Append new value to context **after validation**. It's stored in the same stack as **beforeHandle**. Resolve syntax is identical to [derive](#derive), below is an example of retrieving a bearer header from the Authorization plugin. ```typescript import { Elysia, t } 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) ``` Using `resolve` and `onBeforeHandle` is stored in the same queue. ```typescript import { Elysia } from 'elysia' new Elysia() .onBeforeHandle(() => { console.log(1) }) .resolve(() => { console.log(2) return {} }) .onBeforeHandle(() => { console.log(3) }) ``` The console should log as the following: ```bash 1 2 3 ``` Same as **derive**, properties which assigned by **resolve** is unique and not shared with another request. ### Guard resolve As resolve is not available in local hook, it's recommended to use guard to encapsulate the **resolve** event. ```typescript import { Elysia } from 'elysia' import { isSignIn, findUserById } from './user' new Elysia() .guard( { beforeHandle: isSignIn }, (app) => app .resolve(({ cookie: { session } }) => ({ userId: findUserById(session.value) })) .get('/profile', ({ userId }) => userId) ) .listen(3000) ``` ## After Handle Execute after the main handler, for mapping a returned value of **before handle** and **route handler** into a proper response. It's recommended to use After Handle in the following situations: * Transform requests into a new value, eg. Compression, Event Stream * Add custom headers based on the response value, eg. **Content-Type** #### Example Below is an example of using the after handle to add HTML content type to response headers. ```typescript import { Elysia } from 'elysia' import { isHtml } from '@elysiajs/html' new Elysia() .get('/', () => '

Hello World

', { afterHandle({ response, set }) { if (isHtml(response)) set.headers['content-type'] = 'text/html; charset=utf8' } }) .get('/hi', () => '

Hello World

') .listen(3000) ``` The response should be listed as follows: | Path | Content-Type | | ---- | ------------------------ | | / | text/html; charset=utf8 | | /hi | text/plain; charset=utf8 | ### Returned Value If a value is returned After Handle will use a return value as a new response value unless the value is **undefined** The above example could be rewritten as the following: ```typescript import { Elysia } from 'elysia' import { isHtml } from '@elysiajs/html' new Elysia() .get('/', () => '

Hello World

', { afterHandle({ response, set }) { if (isHtml(response)) { set.headers['content-type'] = 'text/html; charset=utf8' return new Response(response) } } }) .get('/hi', () => '

Hello World

') .listen(3000) ``` Unlike **beforeHandle**, after a value is returned from **afterHandle**, the iteration of afterHandle **will **NOT** be skipped.** ### Context `onAfterHandle` context extends from `Context` with the additional property of `response`, which is the response to return to the client. The `onAfterHandle` context is based on the normal context and can be used like the normal context in route handlers. ## Map Response Executed just after **"afterHandle"**, designed to provide custom response mapping. It's recommended to use transform for the following: * Compression * Map value into a Web Standard Response #### Example Below is an example of using mapResponse to provide Response compression. ```typescript import { Elysia } from 'elysia' const encoder = new TextEncoder() new Elysia() .mapResponse(({ response, set }) => { const isJson = typeof response === 'object' const text = isJson ? JSON.stringify(response) : (response?.toString() ?? '') set.headers['Content-Encoding'] = 'gzip' return new Response(Bun.gzipSync(encoder.encode(text)), { headers: { 'Content-Type': `${ isJson ? 'application/json' : 'text/plain' }; charset=utf-8` } }) }) .get('/text', () => 'mapResponse') .get('/json', () => ({ map: 'response' })) .listen(3000) ``` Like **parse** and **beforeHandle**, after a value is returned, the next iteration of **mapResponse** will be skipped. Elysia will handle the merging process of **set.headers** from **mapResponse** automatically. We don't need to worry about appending **set.headers** to Response manually. ## On Error (Error Handling) Designed for error-handling. It will be executed when an error is thrown in any life-cycle. Its recommended to use on Error in the following situation: * To provide custom error message * Fail safe or an error handler or retrying a request * Logging and analytic #### Example Elysia catches all the errors thrown in the handler, classifies the error code, and pipes them to `onError` middleware. ```typescript import { Elysia } from 'elysia' new Elysia() .onError(({ code, error }) => { return new Response(error.toString()) }) .get('/', () => { throw new Error('Server is during maintenance') return 'unreachable' }) ``` With `onError` we can catch and transform the error into a custom error message. ::: tip It's important that `onError` must be called before the handler we want to apply it to. ::: ### Custom 404 message For example, returning custom 404 messages: ```typescript import { Elysia, NotFoundError } from 'elysia' new Elysia() .onError(({ code, status, set }) => { if (code === 'NOT_FOUND') return status(404, 'Not Found :(') }) .post('/', () => { throw new NotFoundError() }) .listen(3000) ``` ### Context `onError` Context is extends from `Context` with additional properties of the following: * **error**: A value that was thrown * **code**: *Error Code* ### Error Code Elysia error code consists of: "UNKNOWN" | "VALIDATION" | "NOT\_FOUND" | "PARSE" | "INTERNAL\_SERVER\_ERROR" | "INVALID\_COOKIE\_SIGNATURE" | "INVALID\_FILE\_TYPE" * **NOT\_FOUND** * **PARSE** * **VALIDATION** * **INTERNAL\_SERVER\_ERROR** * **INVALID\_COOKIE\_SIGNATURE** * **INVALID\_FILE\_TYPE** * **UNKNOWN** * **number** (based on HTTP Status) By default, the thrown error code is `UNKNOWN`. ::: tip If no error response is returned, the error will be returned using `error.name`. ::: ### To Throw or To Return `Elysia.error` is a shorthand for returning an error with a specific HTTP status code. It could either be **return** or **throw** based on your specific needs. * If an `status` is **throw**, it will be caught by `onError` middleware. * If an `status` is **return**, it will be **NOT** caught by `onError` middleware. See the following code: ```typescript import { Elysia, file } from 'elysia' new Elysia() .onError(({ code, error, path }) => { if (code === 418) return 'caught' }) .get('/throw', ({ status }) => { // This will be caught by onError throw status(418) }) .get('/return', ({ status }) => { // This will NOT be caught by onError return status(418) }) ``` ### Custom Error Elysia supports custom error both in the type-level and implementation level. To provide a custom error code, we can use `Elysia.error` to add a custom error code, helping us to easily classify and narrow down the error type for full type safety with auto-complete as the following: ```typescript twoslash import { Elysia } from 'elysia' class MyError extends Error { constructor(public message: string) { super(message) } } new Elysia() .error({ MyError }) .onError(({ code, error }) => { switch (code) { // With auto-completion case 'MyError': // With type narrowing // Hover to see error is typed as `CustomError` return error } }) .get('/', () => { throw new MyError('Hello Error') }) ``` ### Local Error Same as others life-cycle, we provide an error into an [scope](/essential/plugin.html#scope) using guard: ```typescript import { Elysia } from 'elysia' new Elysia() .get('/', () => 'Hello', { beforeHandle({ set, request: { headers }, error }) { if (!isSignIn(headers)) throw error(401) }, error({ error }) { return 'Handled' } }) .listen(3000) ``` ## After Response Executed after the response sent to the client. It's recommended to use **After Response** in the following situations: * Clean up response * Logging and analytics #### Example Below is an example of using the response handle to check for user sign-in. ```typescript import { Elysia } from 'elysia' new Elysia() .onAfterResponse(() => { console.log('Response', performance.now()) }) .listen(3000) ``` Console should log as the following: ```bash Response 0.0000 Response 0.0001 Response 0.0002 ``` ### Response Similar to [Map Response](#map-resonse), `afterResponse` also accept a `response` value. ```typescript import { Elysia } from 'elysia' new Elysia() .onAfterResponse(({ response }) => { console.log(response) }) .get('/', () => 'Hello') .listen(3000) ``` `response` from `onAfterResponse`, is not a Web-Standard's `Response` but is a value that is returned from the handler. To get a headers, and status returned from the handler, we can access `set` from the context. ```typescript import { Elysia } from 'elysia' new Elysia() .onAfterResponse(({ set }) => { console.log(set.status, set.headers) }) .get('/', () => 'Hello') .listen(3000) ``` --- --- url: /patterns/macro.md --- # Macro Macro allows us to define a custom field to the hook. \ Macro v1 uses functional callback with event listener function. **Elysia.macro** allows us to compose custom heavy logic into a simple configuration available in hook, and **guard** with full type safety. ```typescript twoslash import { Elysia } from 'elysia' const plugin = new Elysia({ name: 'plugin' }) .macro(({ onBeforeHandle }) => ({ hi(word: string) { onBeforeHandle(() => { console.log(word) }) } })) const app = new Elysia() .use(plugin) .get('/', () => 'hi', { hi: 'Elysia' }) ``` Accessing the path should log **"Elysia"** as the result. ### API **macro** should return an object, each key is reflected to the hook, and the provided value inside the hook will be sent back as the first parameter. In previous example, we create **hi** accepting a **string**. We then assigned **hi** to **"Elysia"**, the value was then sent back to the **hi** function, and then the function added a new event to **beforeHandle** stack. Which is an equivalent of pushing function to **beforeHandle** as the following: ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/', () => 'hi', { beforeHandle() { console.log('Elysia') } }) ``` **macro** shine when a logic is more complex than accepting a new function, for example creating an authorization layer for each route. ```typescript twoslash // @filename: auth.ts import { Elysia } from 'elysia' export const auth = new Elysia() .macro(() => { return { isAuth(isAuth: boolean) {}, role(role: 'user' | 'admin') {}, } }) // @filename: index.ts // ---cut--- import { Elysia } from 'elysia' import { auth } from './auth' const app = new Elysia() .use(auth) .get('/', () => 'hi', { isAuth: true, role: 'admin' }) ``` The field can accept anything ranging from string to function, allowing us to create a custom life cycle event. **macro** will be executed in order from top-to-bottom according to definition in hook, ensure that the stack should be handle in correct order. ### Parameters **Elysia.macro** parameters to interact with the life cycle event as the following: * onParse * onTransform * onBeforeHandle * onAfterHandle * onError * onResponse * events - Life cycle store * global: Life cycle of a global stack * local: Life cycle of an inline hook (route) Parameters start with **on** is a function to appends function into a life cycle stack. While **events** is an actual stack that stores an order of the life-cycle event. You may mutate the stack directly or using the helper function provided by Elysia. ### Options The life cycle function of an extension API accepts additional **options** to ensure control over life cycle events. * **options** (optional) - determine which stack * **function** - function to execute on the event ```typescript import { Elysia } from 'elysia' const plugin = new Elysia({ name: 'plugin' }) .macro(({ onBeforeHandle }) => { return { hi(word: string) { onBeforeHandle( { insert: 'before' }, // [!code ++] () => { console.log(word) } ) } } }) ``` **Options** may accept the following parameter: * **insert** * Where should the function be added * value: **'before' | 'after'** * @default: **'after'** * **stack** * Determine which type of stack should be added * value: **'global' | 'local'** * @default: **'local'** Macro v2 use an object syntax with return lifecycle like inline hook. **Elysia.macro** allows us to compose custom heavy logic into a simple configuration available in hook, and **guard** with full type safety. ```typescript twoslash import { Elysia } from 'elysia' const plugin = new Elysia({ name: 'plugin' }) .macro({ hi(word: string) { return { beforeHandle() { console.log(word) } } } }) const app = new Elysia() .use(plugin) .get('/', () => 'hi', { hi: 'Elysia' }) ``` Accessing the path should log **"Elysia"** as the results. ### API **macro** has the same API as hook. In previous example, we create a **hi** macro accepting a **string**. We then assigned **hi** to **"Elysia"**, the value was then sent back to the **hi** function, and then the function added a new event to **beforeHandle** stack. Which is an equivalent of pushing function to **beforeHandle** as the following: ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/', () => 'hi', { beforeHandle() { console.log('Elysia') } }) ``` **macro** shine when a logic is more complex than accepting a new function, for example creating an authorization layer for each route. ```typescript twoslash // @filename: auth.ts import { Elysia } from 'elysia' export const auth = new Elysia() .macro({ isAuth: { resolve() { return { user: 'saltyaom' } } }, role(role: 'admin' | 'user') { return {} } }) // @filename: index.ts // ---cut--- import { Elysia } from 'elysia' import { auth } from './auth' const app = new Elysia() .use(auth) .get('/', ({ user }) => user, { // ^? isAuth: true, role: 'admin' }) ``` Macro v2 can also register a new property to the context, allowing us to access the value directly from the context. The field can accept anything ranging from string to function, allowing us to create a custom life cycle event. **macro** will be executed in order from top-to-bottom according to definition in hook, ensure that the stack is handled in the correct order. ## Resolve You add a property to the context by returning an object with a [**resolve**](/essential/life-cycle.html#resolve) function. ```ts twoslash import { Elysia } from 'elysia' new Elysia() .macro({ user: (enabled: true) => ({ resolve: () => ({ user: 'Pardofelis' }) }) }) .get('/', ({ user }) => user, { // ^? user: true }) ``` In the example above, we add a new property **user** to the context by returning an object with a **resolve** function. Here's an example that macro resolve could be useful: * perform authentication and add user to the context * run an additional database query and add data to the context * add a new property to the context ## Property shorthand Starting from Elysia 1.2.10, each property in the macro object can be a function or an object. If the property is an object, it will be translated to a function that accept a boolean parameter, and will be executed if the parameter is true. ```typescript import { Elysia } from 'elysia' export const auth = new Elysia() .macro({ // This property shorthand isAuth: { resolve() { return { user: 'saltyaom' } } }, // is equivalent to isAuth(enabled: boolean) { if(!enabled) return return { resolve() { return { user } } } } }) ``` --- --- url: /migrate/from-express.md --- # From Express to Elysia This guide is for Express users who want to see a differences from Express including syntax, and how to migrate your application from Express to Elysia by example. **Express** is a popular web framework for Node.js, and widely used for building web applications and APIs. It is known for its simplicity and flexibility. **Elysia** is an ergonomic web framework for Bun, Node.js, and runtime that support Web Standard API. Designed to be ergonomic and developer-friendly with a focus on **sounds type safety** and performance. ## Performance Elysia has significant performance improvements over Express thanks to native Bun implementation, and static code analysis. ## Routing Express and Elysia has similar routing syntax, using `app.get()` and `app.post()` methods to define routes and similar path parameters syntax. ::: code-group ```ts [Express] import express from 'express' const app = express() app.get('/', (req, res) => { res.send('Hello World') }) app.post('/id/:id', (req, res) => { res.status(201).send(req.params.id) }) app.listen(3000) ``` ::: > Express use `req` and `res` as request and response objects ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia() .get('/', 'Hello World') .post( '/id/:id', ({ status, params: { id } }) => { return status(201, id) } ) .listen(3000) ``` ::: > Elysia use a single `context` and returns the response directly There is a slight different in style guide, Elysia recommends usage of method chaining and object destructuring. Elysia also supports an inline value for the response if you don't need to use the context. ## Handler Both has a simliar property for accessing input parameters like `headers`, `query`, `params`, and `body`. ::: code-group ```ts [Express] import express from 'express' const app = express() app.use(express.json()) app.post('/user', (req, res) => { const limit = req.query.limit const name = req.body.name const auth = req.headers.authorization res.json({ limit, name, auth }) }) ``` ::: > Express needs `express.json()` middleware to parse JSON body ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia() .post('/user', (ctx) => { const limit = ctx.query.limit const name = ctx.body.name const auth = ctx.headers.authorization return { limit, name, auth } }) ``` ::: > Elysia parse JSON, URL-encoded data, and formdata by default ## Subrouter Express use a dedicated `express.Router()` for declaring a sub router while Elysia treats every instances as a component that can be plug and play together. ::: code-group ```ts [Express] import express from 'express' const subRouter = express.Router() subRouter.get('/user', (req, res) => { res.send('Hello User') }) const app = express() app.use('/api', subRouter) ``` ::: > Express use `express.Router()` to create a sub router ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia({ prefix: '/api' }) .get('/user', 'Hello User') const app = new Elysia() .use(subRouter) ``` ::: > Elysia treat every instances as a component ## Validation Elysia has a built-in support for request validation with sounds type safety out of the box, while Express doesn't offers a built-in validation, and require a manual type declaration based on each validation library. ::: code-group ```ts [Express] import express from 'express' import { z } from 'zod' const app = express() app.use(express.json()) const paramSchema = z.object({ id: z.coerce.number() }) const bodySchema = z.object({ name: z.string() }) app.patch('/user/:id', (req, res) => { const params = paramSchema.safeParse(req.params) if (!params.success) return res.status(422).json(result.error) const body = bodySchema.safeParse(req.body) if (!body.success) return res.status(422).json(result.error) res.json({ params: params.id.data, body: body.data }) }) ``` ::: > Express require external validation library like `zod` or `joi` to validate request body ::: code-group ```ts twoslash [Elysia] import { Elysia, t } from 'elysia' const app = new Elysia() .patch('/user/:id', ({ params, body }) => ({ // ^? params, body // ^? }), { params: t.Object({ id: t.Number() }), body: t.Object({ name: t.String() }) }) ``` ::: > Elysia use TypeBox for validation, and coerce type automatically ## File upload Express use an external library `multer` to handle file upload, while Elysia has a built-in support for file and formdata, mimetype valiation using declarative API. ::: code-group ```ts [Express] import express from 'express' import multer from 'multer' import { fileTypeFromFile } from 'file-type' import path from 'path' const app = express() const upload = multer({ dest: 'uploads/' }) app.post('/upload', upload.single('image'), async (req, res) => { const file = req.file if (!file) return res .status(422) .send('No file uploaded') const type = await fileTypeFromFile(file.path) if (!type || !type.mime.startsWith('image/')) return res .status(422) .send('File is not a valid image') const filePath = path.resolve(file.path) res.sendFile(filePath) }) ``` ::: > Express needs `express.json()` middleware to parse JSON body ::: code-group ```ts [Elysia] import { Elysia, t } from 'elysia' const app = new Elysia() .post('/upload', ({ body }) => body.file, { body: t.Object({ file: t.File({ type: 'image' }) }) }) ``` ::: > Elysia handle file, and mimetype validation declaratively As **multer** doesn't validate mimetype, you need to validate the mimetype manually using **file-type** or similar library. Elysia validate file upload, and use **file-type** to validate mimetype automatically. ## Middleware Express middleware use a single queue-based order while Elysia give you a more granular control using an **event-based** lifecycle. Elysia's Life Cycle event can be illustrated as the following. ![Elysia Life Cycle Graph](/assets/lifecycle-chart.svg) > Click on image to enlarge While Express has a single flow for request pipeline in order, Elysia can intercept each event in a request pipeline. ::: code-group ```ts [Express] import express from 'express' const app = express() // Global middleware app.use((req, res, next) => { console.log(`${req.method} ${req.url}`) next() }) app.get( '/protected', // Route-specific middleware (req, res, next) => { const token = req.headers.authorization if (!token) return res.status(401).send('Unauthorized') next() }, (req, res) => { res.send('Protected route') } ) ``` ::: > Express use a single queue-based order for middleware which execute in order ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia() // Global middleware .onRequest('/user', ({ method, path }) => { console.log(`${method} ${path}`) }) // Route-specific middleware .get('/protected', () => 'protected', { beforeHandle({ status, headers }) { if (!headers.authorizaton) return status(401) } }) ``` ::: > Elysia use a specific event interceptor for each point in the request pipeline While Hono has a `next` function to call the next middleware, Elysia does not has one. ## Sounds type safety Elysia is designed to be sounds type safety. For example, you can customize context in a **type safe** manner using [derive](/essential/life-cycle.html#derive) and [resolve](/essential/life-cycle.html#resolve) while Express doesn't not. ::: code-group ```ts twoslash [Express] // @errors: 2339 import express from 'express' import type { Request, Response } from 'express' const app = express() const getVersion = (req: Request, res: Response, next: Function) => { // @ts-ignore req.version = 2 next() } app.get('/version', getVersion, (req, res) => { res.send(req.version) // ^? }) const authenticate = (req: Request, res: Response, next: Function) => { const token = req.headers.authorization if (!token) return res.status(401).send('Unauthorized') // @ts-ignore req.token = token.split(' ')[1] next() } app.get('/token', getVersion, authenticate, (req, res) => { req.version // ^? res.send(req.token) // ^? }) ``` ::: > Express use a single queue-based order for middleware which execute in order ::: code-group ```ts twoslash [Elysia] import { Elysia } from 'elysia' const app = new Elysia() .decorate('version', 2) .get('/version', ({ version }) => version) .resolve(({ status, headers: { authorization } }) => { if(!authorization?.startsWith('Bearer ')) return status(401) return { token: authorization.split(' ')[1] } }) .get('/token', ({ token, version }) => { version // ^? return token // ^? }) ``` ::: > Elysia use a specific event interceptor for each point in the request pipeline While Express can, use `declare module` to extend the `Request` interface, it is globally available and doesn't have sounds type safety, and doesn't garantee that the property is available in all request handlers. ```ts declare module 'express' { interface Request { version: number token: string } } ``` > This is required for the above Express example to work, which doesn't offers sounds type safety ## Middleware parameter Express use a function to return a plugin to define a reusable route-specific middleware, while Elysia use [macro](/patterns/macro) to define a custom hook. ::: code-group ```ts twoslash [Express] const findUser = (authorization?: string) => { return { name: 'Jane Doe', role: 'admin' as const } } // ---cut--- // @errors: 2339 import express from 'express' import type { Request, Response } from 'express' const app = express() const role = (role: 'user' | 'admin') => (req: Request, res: Response, next: Function) => { const user = findUser(req.headers.authorization) if (user.role !== role) return res.status(401).send('Unauthorized') // @ts-ignore req.user = user next() } app.get('/token', role('admin'), (req, res) => { res.send(req.user) // ^? }) ``` ::: > Express use a function callback to accept custom argument for middleware ::: code-group ```ts twoslash [Elysia] const findUser = (authorization?: string) => { return { name: 'Jane Doe', role: 'admin' as const } } // ---cut--- import { Elysia } from 'elysia' const app = new Elysia() .macro({ role: (role: 'user' | 'admin') => ({ resolve({ status, headers: { authorization } }) { const user = findUser(authorization) if(user.role !== role) return status(401) return { user } } }) }) .get('/token', ({ user }) => user, { // ^? role: 'admin' }) ``` ::: > Elysia use macro to pass custom argument to custom middleware ## Error handling Express use a single error handler for all routes, while Elysia provides a more granular control over error handling. ::: code-group ```ts import express from 'express' const app = express() class CustomError extends Error { constructor(message: string) { super(message) this.name = 'CustomError' } } // global error handler app.use((error, req, res, next) => { if(error instanceof CustomError) { res.status(500).json({ message: 'Something went wrong!', error }) } }) // route-specific error handler app.get('/error', (req, res) => { throw new CustomError('oh uh') }) ``` ::: > Express use middleware to handle error, a single error handler for all routes ::: code-group ```ts twoslash [Elysia] import { Elysia } from 'elysia' class CustomError extends Error { // Optional: custom HTTP status code status = 500 constructor(message: string) { super(message) this.name = 'CustomError' } // Optional: what should be sent to the client toResponse() { return { message: "If you're seeing this, our dev forgot to handle this error", error: this } } } const app = new Elysia() // Optional: register custom error class .error({ CUSTOM: CustomError, }) // Global error handler .onError(({ error, code }) => { if(code === 'CUSTOM') // ^? return { message: 'Something went wrong!', error } }) .get('/error', () => { throw new CustomError('oh uh') }, { // Optional: route specific error handler error({ error }) { return { message: 'Only for this route!', error } } }) ``` ::: > Elysia provide more granular control over error handling, and scoping mechanism While Express offers error handling using middleware, Elysia provide: 1. Both global and route specific error handler 2. Shorthand for mapping HTTP status and `toResponse` for mapping error to a response 3. Provide a custom error code for each error The error code is useful for logging and debugging, and is important when differentiating between different error types extending the same class. ## Encapsulation Express middleware is registered globally, while Elysia give you a control over side-effect of a plugin via explicit scoping mechanism, and order-of-code. ::: code-group ```ts [Express] import express from 'express' const app = express() app.get('/', (req, res) => { res.send('Hello World') }) const subRouter = express.Router() subRouter.use((req, res, next) => { const token = req.headers.authorization if (!token) return res.status(401).send('Unauthorized') next() }) app.use(subRouter) // has side-effect from subRouter app.get('/side-effect', (req, res) => { res.send('hi') }) ``` ::: > Express doesn't handle side-effect of middleware, and requires a prefix to separate the side-effect ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia() .onBeforeHandle(({ status, headers: { authorization } }) => { if(!authorization?.startsWith('Bearer ')) return status(401) }) const app = new Elysia() .get('/', 'Hello World') .use(subRouter) // doesn't have side-effect from subRouter .get('/side-effect', () => 'hi') ``` ::: > Elysia encapsulate a side-effect into a plugin By default, Elysia will encapsulate lifecycle events and context to the instance that is used, so that the side-effect of a plugin will not affect parent instance unless explicitly stated. ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia() .onBeforeHandle(({ status, headers: { authorization } }) => { if(!authorization?.startsWith('Bearer ')) return status(401) }) // Scoped to parent instance but not beyond .as('scoped') // [!code ++] const app = new Elysia() .get('/', 'Hello World') .use(subRouter) // [!code ++] // now have side-effect from subRouter .get('/side-effect', () => 'hi') ``` Elysia offers 3 type of scoping mechanism: 1. **local** - Apply to current instance only, no side-effect (default) 2. **scoped** - Scoped side-effect to the parent instance but not beyond 3. **global** - Affects every instances While Express can scope the middleware side-effect by adding a prefix, it isn't a true encapsulation. The side-effect is still there but separated to any routes starts with said prefix, adding a mental overhead to the developer to memorize which prefix has side-effect. Which you can do the following: 1. Move order of code from but only if there are a single instance with side-effects. 2. Add a prefix, but the side-effects are still there. If other instance has the same prefix, then it has the side-effects. This can leads to a nightmarish scenario to debug as Express doesn't offers true encapsulation. ## Cookie Express use an external library `cookie-parser` to parse cookies, while Elysia has a built-in support for cookie and use a signal-based approach to handle cookies. ::: code-group ```ts [Express] import express from 'express' import cookieParser from 'cookie-parser' const app = express() app.use(cookieParser('secret')) app.get('/', function (req, res) { req.cookies.name req.signedCookies.name res.cookie('name', 'value', { signed: true, maxAge: 1000 * 60 * 60 * 24 }) }) ``` ::: > Express use `cookie-parser` to parse cookies ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia({ cookie: { secret: 'secret' } }) .get('/', ({ cookie: { name } }) => { // signature verification is handle automatically name.value // cookie signature is signed automatically name.value = 'value' name.maxAge = 1000 * 60 * 60 * 24 }) ``` ::: > Elysia use signal-based approach to handle cookies ## OpenAPI Express require a separate configuration for OpenAPI, validation, and type safety while Elysia has a built-in support for OpenAPI using schema as a **single source of truth**. ::: code-group ```ts [Express] import express from 'express' import swaggerUi from 'swagger-ui-express' const app = express() app.use(express.json()) app.post('/users', (req, res) => { // TODO: validate request body res.status(201).json(req.body) }) const swaggerSpec = { openapi: '3.0.0', info: { title: 'My API', version: '1.0.0' }, paths: { '/users': { post: { summary: 'Create user', requestBody: { content: { 'application/json': { schema: { type: 'object', properties: { name: { type: 'string', description: 'First name only' }, age: { type: 'integer' } }, required: ['name', 'age'] } } } }, responses: { '201': { description: 'User created' } } } } } } app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)) ``` ::: > Express requires a separate configuration for OpenAPI, validation, and type safety ::: code-group ```ts twoslash [Elysia] import { Elysia, t } from 'elysia' import { swagger } from '@elysiajs/swagger' // [!code ++] const app = new Elysia() .use(swagger()) // [!code ++] .model({ user: t.Object({ name: t.String(), age: t.Number() }) }) .post('/users', ({ body }) => body, { // ^? body: 'user[]', response: { 201: 'user[]' }, detail: { summary: 'Create user' } }) ``` ::: > Elysia use a schema as a single source of truth Elysia will generate OpenAPI specification based on the schema you provided, and validate the request and response based on the schema, and infer type automatically. Elysia also appends the schema registered in `model` to the OpenAPI spec, allowing you to reference the model in a dedicated section in Swagger or Scalar UI. ## Testing Express use a single `supertest` library to test the application, while Elysia is built on top of Web Standard API allowing it be used with any testing library. ::: code-group ```ts [Express] import express from 'express' import request from 'supertest' import { describe, it, expect } from 'vitest' const app = express() app.get('/', (req, res) => { res.send('Hello World') }) describe('GET /', () => { it('should return Hello World', async () => { const res = await request(app).get('/') expect(res.status).toBe(200) expect(res.text).toBe('Hello World') }) }) ``` ::: > Express use `supertest` library to test the application ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' import { describe, it, expect } from 'vitest' const app = new Elysia() .get('/', 'Hello World') describe('GET /', () => { it('should return Hello World', async () => { const res = await app.handle( new Request('http://localhost') ) expect(res.status).toBe(200) expect(await res.text()).toBe('Hello World') }) }) ``` ::: > Elysia use Web Standard API to handle request and response Alternatively, Elysia also offers a helper library called [Eden](/eden/overview) for End-to-end type safety, allowing us to test with auto-completion, and full type safety. ```ts twoslash [Elysia] import { Elysia } from 'elysia' import { treaty } from '@elysiajs/eden' import { describe, expect, it } from 'bun:test' const app = new Elysia().get('/hello', 'Hello World') const api = treaty(app) describe('GET /', () => { it('should return Hello World', async () => { const { data, error, status } = await api.hello.get() expect(status).toBe(200) expect(data).toBe('Hello World') // ^? }) }) ``` ## End-to-end type safety Elysia offers a built-in support for **end-to-end type safety** without code generation using [Eden](/eden/overview), Express doesn't offers one. ::: code-group ```ts twoslash [Elysia] import { Elysia, t } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .post('/mirror', ({ body }) => body, { body: t.Object({ message: t.String() }) }) const api = treaty(app) const { data, error } = await api.mirror.post({ message: 'Hello World' }) if(error) throw error // ^? console.log(data) // ^? // ---cut-after--- console.log('ok') ``` ::: If end-to-end type safety is important for you then Elysia is the right choice. *** Elysia offers a more ergonomic and developer-friendly experience with a focus on performance, type safety, and simplicity while Express is a popular web framework for Node.js, but it has some limitations when it comes to performance and simplicity. If you are looking for a framework that is easy to use, has a great developer experience, and is built on top of Web Standard API, Elysia is the right choice for you. Alternatively, if you are coming from a different framework, you can check out: --- --- url: /migrate/from-fastify.md --- # From Fastify to Elysia This guide is for Fastify users who want to see a differences from Fastify including syntax, and how to migrate your application from Fastify to Elysia by example. **Fastify** is a fast and low overhead web framework for Node.js, designed to be simple and easy to use. It is built on top of the HTTP module and provides a set of features that make it easy to build web applications. **Elysia** is an ergonomic web framework for Bun, Node.js, and runtime that support Web Standard API. Designed to be ergonomic and developer-friendly with a focus on **sounds type safety** and performance. ## Performance Elysia has significant performance improvements over Fastify thanks to native Bun implementation, and static code analysis. ## Routing Fastify and Elysia has similar routing syntax, using `app.get()` and `app.post()` methods to define routes and similar path parameters syntax. ::: code-group ```ts [Fastify] import fastify from 'fastify' const app = fastify() app.get('/', (request, reply) => { res.send('Hello World') }) app.post('/id/:id', (request, reply) => { reply.status(201).send(req.params.id) }) app.listen({ port: 3000 }) ``` ::: > Fastify use `request` and `reply` as request and response objects ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia() .get('/', 'Hello World') .post( '/id/:id', ({ status, params: { id } }) => { return status(201, id) } ) .listen(3000) ``` ::: > Elysia use a single `context` and returns the response directly There is a slight different in style guide, Elysia recommends usage of method chaining and object destructuring. Elysia also supports an inline value for the response if you don't need to use the context. ## Handler Both has a simliar property for accessing input parameters like `headers`, `query`, `params`, and `body`, and automatically parse the request body to JSON, URL-encoded data, and formdata. ::: code-group ```ts [Fastify] import fastify from 'fastify' const app = fastify() app.post('/user', (request, reply) => { const limit = request.query.limit const name = request.body.name const auth = request.headers.authorization reply.send({ limit, name, auth }) }) ``` ::: > Fastify parse data and put it into `request` object ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia() .post('/user', (ctx) => { const limit = ctx.query.limit const name = ctx.body.name const auth = ctx.headers.authorization return { limit, name, auth } }) ``` ::: > Elysia parse data and put it into `context` object ## Subrouter Fastify use a function callback to define a subrouter while Elysia treats every instances as a component that can be plug and play together. ::: code-group ```ts [Fastify] import fastify, { FastifyPluginCallback } from 'fastify' const subRouter: FastifyPluginCallback = (app, opts, done) => { app.get('/user', (request, reply) => { reply.send('Hello User') }) } const app = fastify() app.register(subRouter, { prefix: '/api' }) ``` ::: > Fsatify use a function callback to declare a sub router ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia({ prefix: '/api' }) .get('/user', 'Hello User') const app = new Elysia() .use(subRouter) ``` ::: > Elysia treat every instances as a component While Elysia set the prefix in the constructor, Fastify requires you to set the prefix in the options. ## Validation Elysia has a built-in support for request validation with sounds type safety out of the box using **TypeBox** while Fastify use JSON Schema for declaring schema, and **ajv** for validation. However, doesn't infer type automatically, and you need to use a type provider like `@fastify/type-provider-json-schema-to-ts` to infer type. ::: code-group ```ts [Fastify] import fastify from 'fastify' import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts' const app = fastify().withTypeProvider() app.patch( '/user/:id', { schema: { params: { type: 'object', properties: { id: { type: 'string', pattern: '^[0-9]+$' } }, required: ['id'] }, body: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, } }, (request, reply) => { // map string to number request.params.id = +request.params.id reply.send({ params: request.params, body: request.body }) } }) ``` ::: > Fastify use JSON Schema for validation ::: code-group ```ts twoslash [Elysia] import { Elysia, t } from 'elysia' const app = new Elysia() .patch('/user/:id', ({ params, body }) => ({ // ^? params, body // ^? }), { params: t.Object({ id: t.Number() }), body: t.Object({ name: t.String() }) }) ``` ::: > Elysia use TypeBox for validation, and coerce type automatically Alternatively, Fastify can also use **TypeBox** or **Zod** for validation using `@fastify/type-provider-typebox` to infer type automatically. While Elysia **prefers TypeBox** for validation, Elysia also supports **Zod**, and **Valibot** via [TypeMap](https://github.com/sinclairzx81/typemap). ## File upload Fastify use a `fastify-multipart` to handle file upload which use `Busboy` under the hood while Elysia use Web Standard API for handling formdata, mimetype valiation using declarative API. However, Fastify doesn't offers a straight forward way for file validation, eg. file size and mimetype, and required some workarounds to validate the file. ::: code-group ```ts [Fastify] import fastify from 'fastify' import multipart from '@fastify/multipart' import { fileTypeFromBuffer } from 'file-type' const app = fastify() app.register(multipart, { attachFieldsToBody: 'keyValues' }) app.post( '/upload', { schema: { body: { type: 'object', properties: { file: { type: 'object' } }, required: ['file'] } } }, async (req, res) => { const file = req.body.file if (!file) return res.status(422).send('No file uploaded') const type = await fileTypeFromBuffer(file) if (!type || !type.mime.startsWith('image/')) return res.status(422).send('File is not a valid image') res.header('Content-Type', type.mime) res.send(file) } ) ``` ::: > Fastift use `fastify-multipart` to handle file upload, and fake `type: object` to allow Buffer ::: code-group ```ts [Elysia] import { Elysia, t } from 'elysia' const app = new Elysia() .post('/upload', ({ body }) => body.file, { body: t.Object({ file: t.File({ type: 'image' }) }) }) ``` ::: > Elysia handle file, and mimetype validation using `t.File` As **multer** doesn't validate mimetype, you need to validate the mimetype manually using **file-type** or similar library. While Elysia, validate file upload, and use **file-type** to validate mimetype automatically. ## Lifecycle Event Both Fastify and Elysia has some what similar lifecycle event using event-based approach. ### Elysia Lifecycle Elysia's Life Cycle event can be illustrated as the following. ![Elysia Life Cycle Graph](/assets/lifecycle-chart.svg) > Click on image to enlarge ### Fastify Lifecycle Fastify's Life Cycle event can be illustrated as the following. ``` Incoming Request │ └─▶ Routing │ └─▶ Instance Logger │ 4**/5** ◀─┴─▶ onRequest Hook │ 4**/5** ◀─┴─▶ preParsing Hook │ 4**/5** ◀─┴─▶ Parsing │ 4**/5** ◀─┴─▶ preValidation Hook │ 400 ◀─┴─▶ Validation │ 4**/5** ◀─┴─▶ preHandler Hook │ 4**/5** ◀─┴─▶ User Handler │ └─▶ Reply │ 4**/5** ◀─┴─▶ preSerialization Hook │ └─▶ onSend Hook │ 4**/5** ◀─┴─▶ Outgoing Response │ └─▶ onResponse Hook ``` Both also has somewhat similar syntax for intercepting the request and response lifecycle events, however Elysia doesn't require you to call `done` to continue the lifecycle event. ::: code-group ```ts [Fastify] import fastify from 'fastify' const app = fastify() // Global middleware app.addHook('onRequest', (request, reply, done) => { console.log(`${request.method} ${request.url}`) done() }) app.get( '/protected', { // Route-specific middleware preHandler(request, reply, done) { const token = request.headers.authorization if (!token) reply.status(401).send('Unauthorized') done() } }, (request, reply) => { reply.send('Protected route') } ) ``` ::: > Fastify use `addHook` to register a middleware, and requires you to call `done` to continue the lifecycle event ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia() // Global middleware .onRequest('/user', ({ method, path }) => { console.log(`${method} ${path}`) }) // Route-specific middleware .get('/protected', () => 'protected', { beforeHandle({ status, headers }) { if (!headers.authorizaton) return status(401) } }) ``` ::: > Elysia detects the lifecycle event automatically, and doesn't require you to call `done` to continue the lifecycle event ## Sounds type safety Elysia is designed to be sounds type safety. For example, you can customize context in a **type safe** manner using [derive](/essential/life-cycle.html#derive) and [resolve](/essential/life-cycle.html#resolve) while Fastify doesn't not. ::: code-group ```ts twoslash [Fastify] // @errors: 2339 import fastify from 'fastify' const app = fastify() app.decorateRequest('version', 2) app.get('/version', (req, res) => { res.send(req.version) // ^? }) app.get( '/token', { preHandler(req, res, done) { const token = req.headers.authorization if (!token) return res.status(401).send('Unauthorized') // @ts-ignore req.token = token.split(' ')[1] done() } }, (req, res) => { req.version // ^? res.send(req.token) // ^? } ) app.listen({ port: 3000 }) ``` ::: > Fastify use `decorateRequest` but doesn't offers sounds type safety ::: code-group ```ts twoslash [Elysia] import { Elysia } from 'elysia' const app = new Elysia() .decorate('version', 2) .get('/version', ({ version }) => version) .resolve(({ status, headers: { authorization } }) => { if(!authorization?.startsWith('Bearer ')) return status(401) return { token: authorization.split(' ')[1] } }) .get('/token', ({ token, version }) => { version // ^? return token // ^? }) ``` ::: > Elysia use `decorate` to extend the context, and `resolve` to add custom properties to the context While Fastify can, use `declare module` to extend the `FastifyRequest` interface, it is globally available and doesn't have sounds type safety, and doesn't garantee that the property is available in all request handlers. ```ts declare module 'fastify' { interface FastifyRequest { version: number token: string } } ``` > This is required for the above Fastify example to work, which doesn't offers sounds type safety ## Middleware parameter Fastify use a function to return Fastify plugin to define a named middleware, while Elysia use [macro](/patterns/macro) to define a custom hook. ::: code-group ```ts twoslash [Fastify] const findUser = (authorization?: string) => { return { name: 'Jane Doe', role: 'admin' as const } } // ---cut--- // @errors: 2339 import fastify from 'fastify' import type { FastifyRequest, FastifyReply } from 'fastify' const app = fastify() const role = (role: 'user' | 'admin') => (request: FastifyRequest, reply: FastifyReply, next: Function) => { const user = findUser(request.headers.authorization) if (user.role !== role) return reply.status(401).send('Unauthorized') // @ts-ignore request.user = user next() } app.get( '/token', { preHandler: role('admin') }, (request, reply) => { reply.send(request.user) // ^? } ) ``` ::: > Fastify use a function callback to accept custom argument for middleware ::: code-group ```ts twoslash [Elysia] const findUser = (authorization?: string) => { return { name: 'Jane Doe', role: 'admin' as const } } // ---cut--- import { Elysia } from 'elysia' const app = new Elysia() .macro({ role: (role: 'user' | 'admin') => ({ resolve({ status, headers: { authorization } }) { const user = findUser(authorization) if(user.role !== role) return status(401) return { user } } }) }) .get('/token', ({ user }) => user, { // ^? role: 'admin' }) ``` ::: > Elysia use macro to pass custom argument to custom middleware While Fastify use a function callback, it needs to return a function to be placed in an event handler or an object represented as a hook which can be hard to handle when there are need for multiple custom functions as you need to reconcile them into a single object. ## Error handling Both Fastify and Elysia offers a lifecycle event to handle error. ::: code-group ```ts import fastify from 'fastify' const app = fastify() class CustomError extends Error { constructor(message: string) { super(message) this.name = 'CustomError' } } // global error handler app.setErrorHandler((error, request, reply) => { if (error instanceof CustomError) reply.status(500).send({ message: 'Something went wrong!', error }) }) app.get( '/error', { // route-specific error handler errorHandler(error, request, reply) { reply.send({ message: 'Only for this route!', error }) } }, (request, reply) => { throw new CustomError('oh uh') } ) ``` ::: > Fastify use `setErrorHandler` for global error handler, and `errorHandler` for route-specific error handler ::: code-group ```ts twoslash [Elysia] import { Elysia } from 'elysia' class CustomError extends Error { // Optional: custom HTTP status code status = 500 constructor(message: string) { super(message) this.name = 'CustomError' } // Optional: what should be sent to the client toResponse() { return { message: "If you're seeing this, our dev forgot to handle this error", error: this } } } const app = new Elysia() // Optional: register custom error class .error({ CUSTOM: CustomError, }) // Global error handler .onError(({ error, code }) => { if(code === 'CUSTOM') // ^? return { message: 'Something went wrong!', error } }) .get('/error', () => { throw new CustomError('oh uh') }, { // Optional: route specific error handler error({ error }) { return { message: 'Only for this route!', error } } }) ``` ::: > Elysia offers a custom error code, a shorthand for status and `toResponse` for mapping error to a response. While Both offers error handling using lifecycle event, Elysia also provide: 1. Custom error code 2. Shorthand for mapping HTTP status and `toResponse` for mapping error to a response The error code is useful for logging and debugging, and is important when differentiating between different error types extending the same class. ## Encapsulation Fastify encapsulate plugin side-effect, while Elysia give you a control over side-effect of a plugin via explicit scoping mechanism, and order-of-code. ::: code-group ```ts [Fastify] import fastify from 'fastify' import type { FastifyPluginCallback } from 'fastify' const subRouter: FastifyPluginCallback = (app, opts, done) => { app.addHook('preHandler', (request, reply) => { if (!request.headers.authorization?.startsWith('Bearer ')) reply.code(401).send({ error: 'Unauthorized' }) }) done() } const app = fastify() .get('/', (request, reply) => { reply.send('Hello World') }) .register(subRouter) // doesn't have side-effect from subRouter .get('/side-effect', () => 'hi') ``` ::: > Fastify encapsulate side-effect of a plugin ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia() .onBeforeHandle(({ status, headers: { authorization } }) => { if(!authorization?.startsWith('Bearer ')) return status(401) }) const app = new Elysia() .get('/', 'Hello World') .use(subRouter) // doesn't have side-effect from subRouter .get('/side-effect', () => 'hi') ``` ::: > Elysia encapsulate side-effect of a plugin unless explicitly stated Both has a encapsulate mechanism of a plugin to prevent side-effect. However, Elysia can explicitly stated which plugin should have side-effect by declaring a scoped while Fastify always encapsulate it. ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia() .onBeforeHandle(({ status, headers: { authorization } }) => { if(!authorization?.startsWith('Bearer ')) return status(401) }) // Scoped to parent instance but not beyond .as('scoped') // [!code ++] const app = new Elysia() .get('/', 'Hello World') .use(subRouter) // [!code ++] // now have side-effect from subRouter .get('/side-effect', () => 'hi') ``` Elysia offers 3 type of scoping mechanism: 1. **local** - Apply to current instance only, no side-effect (default) 2. **scoped** - Scoped side-effect to the parent instance but not beyond 3. **global** - Affects every instances *** As Fastify doesn't offers a scoping mechanism, we need to either: 1. Create a function for each hooks and append them manually 2. Use higher-order-function, and apply it to instance that need the effect However, this can caused a duplicated side-effect if not handled carefully. ```ts import fastify from 'fastify' import type { FastifyRequest, FastifyReply, FastifyPluginCallback } from 'fastify' const log = (request: FastifyRequest, reply: FastifyReply, done: Function) => { console.log('Middleware executed') done() } const app = fastify() app.addHook('onRequest', log) app.get('/main', (request, reply) => { reply.send('Hello from main!') }) const subRouter: FastifyPluginCallback = (app, opts, done) => { app.addHook('onRequest', log) // This would log twice app.get('/sub', (request, reply) => { return reply.send('Hello from sub router!') }) done() } app.register(subRouter, { prefix: '/sub' }) app.listen({ port: 3000 }) ``` In this scenario, Elysia offers a plugin deduplication mechanism to prevent duplicated side-effect. ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia({ name: 'subRouter' }) // [!code ++] .onBeforeHandle(({ status, headers: { authorization } }) => { if(!authorization?.startsWith('Bearer ')) return status(401) }) .as('scoped') const app = new Elysia() .get('/', 'Hello World') .use(subRouter) .use(subRouter) // [!code ++] .use(subRouter) // [!code ++] .use(subRouter) // [!code ++] // side-effect only called once .get('/side-effect', () => 'hi') ``` By using a unique `name`, Elysia will apply the plugin only once, and will not cause duplicated side-effect. ## Cookie Fastify use `@fastify/cookie` to parse cookies, while Elysia has a built-in support for cookie and use a signal-based approach to handle cookies. ::: code-group ```ts [Fastify] import fastify from 'fastify' import cookie from '@fastify/cookie' const app = fastify() app.use(cookie, { secret: 'secret', hook: 'onRequest' }) app.get('/', function (request, reply) { request.unsignCookie(request.cookies.name) reply.setCookie('name', 'value', { path: '/', signed: true }) }) ``` ::: > Fastify use `unsignCookie` to verify the cookie signature, and `setCookie` to set the cookie ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia({ cookie: { secret: 'secret' } }) .get('/', ({ cookie: { name } }) => { // signature verification is handle automatically name.value // cookie signature is signed automatically name.value = 'value' name.maxAge = 1000 * 60 * 60 * 24 }) ``` ::: > Elysia use a signal-based approach to handle cookies, and signature verification is handle automatically ## OpenAPI Both offers OpenAPI documentation using Swagger, however Elysia default to Scalar UI which is a more modern and user-friendly interface for OpenAPI documentation. ::: code-group ```ts [Fastify] import fastify from 'fastify' import swagger from '@fastify/swagger' const app = fastify() app.register(swagger, { openapi: '3.0.0', info: { title: 'My API', version: '1.0.0' } }) app.addSchema({ $id: 'user', type: 'object', properties: { name: { type: 'string', description: 'First name only' }, age: { type: 'integer' } }, required: ['name', 'age'] }) app.post( '/users', { schema: { summary: 'Create user', body: { $ref: 'user#' }, response: { '201': { $ref: 'user#' } } } }, (req, res) => { res.status(201).send(req.body) } ) await fastify.ready() fastify.swagger() ``` ::: > Fastify use `@fastify/swagger` for OpenAPI documentation using Swagger ::: code-group ```ts twoslash [Elysia] import { Elysia, t } from 'elysia' import { swagger } from '@elysiajs/swagger' // [!code ++] const app = new Elysia() .use(swagger()) // [!code ++] .model({ user: t.Object({ name: t.String(), age: t.Number() }) }) .post('/users', ({ body }) => body, { // ^? body: 'user[]', response: { 201: 'user[]' }, detail: { summary: 'Create user' } }) ``` ::: > Elysia use `@elysiajs/swagger` for OpenAPI documentation using Scalar by default, or optionally Swagger Both offers model reference using `$ref` for OpenAPI documentation, however Fastify doesn't offers type-safety, and auto-completion for specifying model name while Elysia does. ## Testing Fastify has a built-in support for testing using `fastify.inject()` to **simulate** network request while Elysia use a Web Standard API to do an **actual** request. ::: code-group ```ts [Fastify] import fastify from 'fastify' import request from 'supertest' import { describe, it, expect } from 'vitest' function build(opts = {}) { const app = fastify(opts) app.get('/', async function (request, reply) { reply.send({ hello: 'world' }) }) return app } describe('GET /', () => { it('should return Hello World', async () => { const app = build() const response = await app.inject({ url: '/', method: 'GET', }) expect(res.status).toBe(200) expect(res.text).toBe('Hello World') }) }) ``` ::: > Fastify use `fastify.inject()` to simulate network request ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' import { describe, it, expect } from 'vitest' const app = new Elysia() .get('/', 'Hello World') describe('GET /', () => { it('should return Hello World', async () => { const res = await app.handle( new Request('http://localhost') ) expect(res.status).toBe(200) expect(await res.text()).toBe('Hello World') }) }) ``` ::: > Elysia use Web Standard API to handle **actual** request Alternatively, Elysia also offers a helper library called [Eden](/eden/overview) for End-to-end type safety, allowing us to test with auto-completion, and full type safety. ```ts twoslash [Elysia] import { Elysia } from 'elysia' import { treaty } from '@elysiajs/eden' import { describe, expect, it } from 'bun:test' const app = new Elysia().get('/hello', 'Hello World') const api = treaty(app) describe('GET /', () => { it('should return Hello World', async () => { const { data, error, status } = await api.hello.get() expect(status).toBe(200) expect(data).toBe('Hello World') // ^? }) }) ``` ## End-to-end type safety Elysia offers a built-in support for **end-to-end type safety** without code generation using [Eden](/eden/overview), while Fastify doesn't offers one. ::: code-group ```ts twoslash [Elysia] import { Elysia, t } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .post('/mirror', ({ body }) => body, { body: t.Object({ message: t.String() }) }) const api = treaty(app) const { data, error } = await api.mirror.post({ message: 'Hello World' }) if(error) throw error // ^? console.log(data) // ^? // ---cut-after--- console.log('ok') ``` ::: If end-to-end type safety is important for you then Elysia is the right choice. *** Elysia offers a more ergonomic and developer-friendly experience with a focus on performance, type safety, and simplicity while Fastify is one of the established framework for Node.js, but doesn't has **sounds type safety** and **end-to-end type safety** offers by next generation framework. If you are looking for a framework that is easy to use, has a great developer experience, and is built on top of Web Standard API, Elysia is the right choice for you. Alternatively, if you are coming from a different framework, you can check out: --- --- url: /migrate/from-hono.md --- # From Hono to Elysia This guide is for Hono users who want to see a differences from Elysia including syntax, and how to migrate your application from Hono to Elysia by example. **Hono** is a fast and lightweight built on Web Standard. It has broad compatibility with multiple runtime like Deno, Bun, Cloudflare Workers, and Node.js. **Elysia** is an ergonomic web framework. Designed to be ergonomic and developer-friendly with a focus on **sounds type safety** and performance. Both frameworks are built on top of Web Standard API, and has slight different syntax. Hono offers more compatability with multiple runtimes while Elysia focus on specific set of runtimes. ## Performance Elysia has significant performance improvements over Hono thanks to static code analysis. ## Routing Hono and Elysia has similar routing syntax, using `app.get()` and `app.post()` methods to define routes and similar path parameters syntax. Both use a single `Context` parameters to handle request and response, and return a response directly. ::: code-group ```ts [Hono] import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => { return c.text('Hello World') }) app.post('/id/:id', (c) => { c.status(201) return c.text(req.params.id) }) export default app ``` ::: > Hono use helper `c.text`, `c.json` to return a response ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia() .get('/', 'Hello World') .post( '/id/:id', ({ status, params: { id } }) => { return status(201, id) } ) .listen(3000) ``` ::: > Elysia use a single `context` and returns the response directly While Hono use a `c.text`, and `c.json` to warp a response, Elysia map a value to a response automatically. There is a slight different in style guide, Elysia recommends usage of method chaining and object destructuring. Hono port allocation is depends on runtime, and adapter while Elysia use a single `listen` method to start the server. ## Handler Hono use a function to parse query, header, and body manually while Elysia automatically parse properties. ::: code-group ```ts [Hono] import { Hono } from 'hono' const app = new Hono() app.post('/user', async (c) => { const limit = c.req.query('limit') const { name } = await c.body() const auth = c.req.header('authorization') return c.json({ limit, name, auth }) }) ``` ::: > Hono parse body automatically but it doesn't apply to query and headers ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia() .post('/user', (ctx) => { const limit = ctx.query.limit const name = ctx.body.name const auth = ctx.headers.authorization return { limit, name, auth } }) ``` ::: > Elysia use static code analysis to analyze what to parse Elysia use **static code analysis** to determine what to parse, and only parse the required properties. This is useful for performance and type safety. ## Subrouter Both can inherits another instance as a router, but Elysia treat every instances as a component which can be used as a subrouter. ::: code-group ```ts [Hono] import { Hono } from 'hono' const subRouter = new Hono() subRouter.get('/user', (c) => { return c.text('Hello User') }) const app = new Hono() app.route('/api', subRouter) ``` ::: > Hono **require** a prefix to separate the subrouter ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia({ prefix: '/api' }) .get('/user', 'Hello User') const app = new Elysia() .use(subRouter) ``` ::: > Elysia use optional prefix constructor to define one While Hono requires a prefix to separate the subrouter, Elysia doesn't require a prefix to separate the subrouter. ## Validation While Hono supports for **zod**, Elysia focus on deep integration with **TypeBox** to offers seamless integration with OpenAPI, validation, and advanced feature behind the scene. ::: code-group ```ts [Hono] import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' const app = new Hono() app.patch( '/user/:id', zValidator( 'param', z.object({ id: z.coerce.number() }) ), zValidator( 'json', z.object({ name: z.string() }) ), (c) => { return c.json({ params: c.req.param(), body: c.req.json() }) } ) ``` ::: > Hono use pipe based ::: code-group ```ts twoslash [Elysia] import { Elysia, t } from 'elysia' const app = new Elysia() .patch('/user/:id', ({ params, body }) => ({ // ^? params, body // ^? }), { params: t.Object({ id: t.Number() }), body: t.Object({ name: t.String() }) }) ``` ::: > Elysia use TypeBox for validation, and coerce type automatically Both offers type inference from schema to context automatically. ## File upload Both Hono, and Elysia use Web Standard API to handle file upload, but Elysia has a built-in declarative support for file validation using **file-type** to validate mimetype. ::: code-group ```ts [Hono] import { Hono } from 'hono' import { z } from 'zod' import { zValidator } from '@hono/zod-validator' import { fileTypeFromBlob } from 'file-type' const app = new Hono() app.post( '/upload', zValidator( 'form', z.object({ file: z.instanceof(File) }) ), async (c) => { const body = await c.req.parseBody() const type = await fileTypeFromBlob(body.image as File) if (!type || !type.mime.startsWith('image/')) { c.status(422) return c.text('File is not a valid image') } return new Response(body.image) } ) ``` ::: > Hono needs a separate `file-type` library to validate mimetype ::: code-group ```ts [Elysia] import { Elysia, t } from 'elysia' const app = new Elysia() .post('/upload', ({ body }) => body.file, { body: t.Object({ file: t.File({ type: 'image' }) }) }) ``` ::: > Elysia handle file, and mimetype validation declaratively As Web Standard API doesn't validate mimetype, it is a security risk to trust `content-type` provided by the client so external library is required for Hono, while Elysia use `file-type` to validate mimetype automatically. ## Middleware Hono middleware use a single queue-based order similar to Express while Elysia give you a more granular control using an **event-based** lifecycle. Elysia's Life Cycle event can be illustrated as the following. ![Elysia Life Cycle Graph](/assets/lifecycle-chart.svg) > Click on image to enlarge While Hono has a single flow for request pipeline in order, Elysia can intercept each event in a request pipeline. ::: code-group ```ts [Hono] import { Hono } from 'hono' const app = new Hono() // Global middleware app.use(async (c, next) => { console.log(`${c.method} ${c.url}`) await next() }) app.get( '/protected', // Route-specific middleware async (c, next) => { const token = c.headers.authorization if (!token) { c.status(401) return c.text('Unauthorized') } await next() }, (req, res) => { res.send('Protected route') } ) ``` ::: > Hono use a single queue-based order for middleware which execute in order ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia() // Global middleware .onRequest('/user', ({ method, path }) => { console.log(`${method} ${path}`) }) // Route-specific middleware .get('/protected', () => 'protected', { beforeHandle({ status, headers }) { if (!headers.authorizaton) return status(401) } }) ``` ::: > Elysia use a specific event interceptor for each point in the request pipeline While Hono has a `next` function to call the next middleware, Elysia does not has one. ## Sounds type safety Elysia is designed to be sounds type safety. For example, you can customize context in a **type safe** manner using [derive](/essential/life-cycle.html#derive) and [resolve](/essential/life-cycle.html#resolve) while Hono doesn't not. ::: code-group ```ts twoslash [Hono] // @errors: 2339, 2769 import { Hono } from 'hono' import { createMiddleware } from 'hono/factory' const app = new Hono() const getVersion = createMiddleware(async (c, next) => { c.set('version', 2) await next() }) app.use(getVersion) app.get('/version', getVersion, (c) => { return c.text(c.get('version') + '') }) const authenticate = createMiddleware(async (c, next) => { const token = c.req.header('authorization') if (!token) { c.status(401) return c.text('Unauthorized') } c.set('token', token.split(' ')[1]) await next() }) app.post('/user', authenticate, async (c) => { c.get('version') return c.text(c.get('token')) }) ``` ::: > Hono use a middleware to extend the context, but is not type safe ::: code-group ```ts twoslash [Elysia] import { Elysia } from 'elysia' const app = new Elysia() .decorate('version', 2) .get('/version', ({ version }) => version) .resolve(({ status, headers: { authorization } }) => { if(!authorization?.startsWith('Bearer ')) return status(401) return { token: authorization.split(' ')[1] } }) .get('/token', ({ token, version }) => { version // ^? return token // ^? }) ``` ::: > Elysia use a specific event interceptor for each point in the request pipeline While Hono can, use `declare module` to extend the `ContextVariableMap` interface, it is globally available and doesn't have sounds type safety, and doesn't garantee that the property is available in all request handlers. ```ts declare module 'hono' { interface ContextVariableMap { version: number token: string } } ``` > This is required for the above Hono example to work, which doesn't offers sounds type safety ## Middleware parameter Hono use a callback function to define a reusable route-specific middleware, while Elysia use [macro](/patterns/macro) to define a custom hook. ::: code-group ```ts twoslash [Hono] const findUser = (authorization?: string) => { return { name: 'Jane Doe', role: 'admin' as const } } // ---cut--- // @errors: 2339 2589 2769 import { Hono } from 'hono' import { createMiddleware } from 'hono/factory' const app = new Hono() const role = (role: 'user' | 'admin') => createMiddleware(async (c, next) => { const user = findUser(c.req.header('Authorization')) if(user.role !== role) { c.status(401) return c.text('Unauthorized') } c.set('user', user) await next() }) app.get('/user/:id', role('admin'), (c) => { return c.json(c.get('user')) }) ``` ::: > Hono use callback to return `createMiddleware` to create a reusable middleware, but is not type safe ::: code-group ```ts twoslash [Elysia] const findUser = (authorization?: string) => { return { name: 'Jane Doe', role: 'admin' as const } } // ---cut--- import { Elysia } from 'elysia' const app = new Elysia() .macro({ role: (role: 'user' | 'admin') => ({ resolve({ status, headers: { authorization } }) { const user = findUser(authorization) if(user.role !== role) return status(401) return { user } } }) }) .get('/token', ({ user }) => user, { // ^? role: 'admin' }) ``` ::: > Elysia use macro to pass custom argument to custom middleware ## Error handling Hono provide a `onError` function which apply to all routes while Elysia provides a more granular control over error handling. ::: code-group ```ts import { Hono } from 'hono' const app = new Hono() class CustomError extends Error { constructor(message: string) { super(message) this.name = 'CustomError' } } // global error handler app.onError((error, c) => { if(error instanceof CustomError) { c.status(500) return c.json({ message: 'Something went wrong!', error }) } }) // route-specific error handler app.get('/error', (req, res) => { throw new CustomError('oh uh') }) ``` ::: > Hono use `onError` funcition to handle error, a single error handler for all routes ::: code-group ```ts twoslash [Elysia] import { Elysia } from 'elysia' class CustomError extends Error { // Optional: custom HTTP status code status = 500 constructor(message: string) { super(message) this.name = 'CustomError' } // Optional: what should be sent to the client toResponse() { return { message: "If you're seeing this, our dev forgot to handle this error", error: this } } } const app = new Elysia() // Optional: register custom error class .error({ CUSTOM: CustomError, }) // Global error handler .onError(({ error, code }) => { if(code === 'CUSTOM') // ^? return { message: 'Something went wrong!', error } }) .get('/error', () => { throw new CustomError('oh uh') }, { // Optional: route specific error handler error({ error }) { return { message: 'Only for this route!', error } } }) ``` ::: > Elysia provide more granular control over error handling, and scoping mechanism While Hono offers error handling using middleware-like, Elysia provide: 1. Both global and route specific error handler 2. Shorthand for mapping HTTP status and `toResponse` for mapping error to a response 3. Provide a custom error code for each error The error code is useful for logging and debugging, and is important when differentiating between different error types extending the same class. ## Encapsulation Hono encapsulate plugin side-effect, while Elysia give you a control over side-effect of a plugin via explicit scoping mechanism, and order-of-code. ::: code-group ```ts [Hono] import { Hono } from 'hono' const subRouter = new Hono() subRouter.get('/user', (c) => { return c.text('Hello User') }) const app = new Hono() app.route('/api', subRouter) ``` ::: > Hono encapsulate side-effect of a plugin ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia() .onBeforeHandle(({ status, headers: { authorization } }) => { if(!authorization?.startsWith('Bearer ')) return status(401) }) const app = new Elysia() .get('/', 'Hello World') .use(subRouter) // doesn't have side-effect from subRouter .get('/side-effect', () => 'hi') ``` ::: > Elysia encapsulate side-effect of a plugin unless explicitly stated Both has a encapsulate mechanism of a plugin to prevent side-effect. However, Elysia can explicitly stated which plugin should have side-effect by declaring a scoped while Fastify always encapsulate it. ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia() .onBeforeHandle(({ status, headers: { authorization } }) => { if(!authorization?.startsWith('Bearer ')) return status(401) }) // Scoped to parent instance but not beyond .as('scoped') // [!code ++] const app = new Elysia() .get('/', 'Hello World') .use(subRouter) // [!code ++] // now have side-effect from subRouter .get('/side-effect', () => 'hi') ``` Elysia offers 3 type of scoping mechanism: 1. **local** - Apply to current instance only, no side-effect (default) 2. **scoped** - Scoped side-effect to the parent instance but not beyond 3. **global** - Affects every instances *** As Hono doesn't offers a scoping mechanism, we need to either: 1. Create a function for each hooks and append them manually 2. Use higher-order-function, and apply it to instance that need the effect However, this can caused a duplicated side-effect if not handled carefully. ```ts [Hono] import { Hono } from 'hono' import { createMiddleware } from 'hono/factory' const middleware = createMiddleware(async (c, next) => { console.log('called') await next() }) const app = new Hono() const subRouter = new Hono() app.use(middleware) app.get('/main', (c) => c.text('Hello from main!')) subRouter.use(middleware) // This would log twice subRouter.get('/sub', (c) => c.text('Hello from sub router!')) app.route('/sub', subRouter) export default app ``` In this scenario, Elysia offers a plugin deduplication mechanism to prevent duplicated side-effect. ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia({ name: 'subRouter' }) // [!code ++] .onBeforeHandle(({ status, headers: { authorization } }) => { if(!authorization?.startsWith('Bearer ')) return status(401) }) .as('scoped') const app = new Elysia() .get('/', 'Hello World') .use(subRouter) .use(subRouter) // [!code ++] .use(subRouter) // [!code ++] .use(subRouter) // [!code ++] // side-effect only called once .get('/side-effect', () => 'hi') ``` By using a unique `name`, Elysia will apply the plugin only once, and will not cause duplicated side-effect. ## Cookie Hono has a built-in cookie utility functions under `hono/cookie`, while Elysia use a signal-based approach to handle cookies. ::: code-group ```ts [Hono] import { Hono } from 'hono' import { getSignedCookie, setSignedCookie } from 'hono/cookie' const app = new Hono() app.get('/', async (c) => { const name = await getSignedCookie(c, 'secret', 'name') await setSignedCookie( c, 'name', 'value', 'secret', { maxAge: 1000, } ) }) ``` ::: > Hono use utility functions to handle cookies ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia({ cookie: { secret: 'secret' } }) .get('/', ({ cookie: { name } }) => { // signature verification is handle automatically name.value // cookie signature is signed automatically name.value = 'value' name.maxAge = 1000 * 60 * 60 * 24 }) ``` ::: > Elysia use signal-based approach to handle cookies ## OpenAPI Hono require additional effort to describe the specification, while Elysia seamless integrate the specification into the schema. ::: code-group ```ts [Hono] import { Hono } from 'hono' import { describeRoute, openAPISpecs } from 'hono-openapi' import { resolver, validator as zodValidator } from 'hono-openapi/zod' import { swaggerUI } from '@hono/swagger-ui' import { z } from '@hono/zod-openapi' const app = new Hono() const model = z.array( z.object({ name: z.string().openapi({ description: 'first name only' }), age: z.number() }) ) const detail = await resolver(model).builder() console.log(detail) app.post( '/', zodValidator('json', model), describeRoute({ validateResponse: true, summary: 'Create user', requestBody: { content: { 'application/json': { schema: detail.schema } } }, responses: { 201: { description: 'User created', content: { 'application/json': { schema: resolver(model) } } } } }), (c) => { c.status(201) return c.json(c.req.valid('json')) } ) app.get('/ui', swaggerUI({ url: '/doc' })) app.get( '/doc', openAPISpecs(app, { documentation: { info: { title: 'Hono API', version: '1.0.0', description: 'Greeting API' }, components: { ...detail.components } } }) ) export default app ``` ::: > Hono require additional effort to describe the specification ::: code-group ```ts twoslash [Elysia] import { Elysia, t } from 'elysia' import { swagger } from '@elysiajs/swagger' // [!code ++] const app = new Elysia() .use(swagger()) // [!code ++] .model({ user: t.Object({ name: t.String(), age: t.Number() }) }) .post('/users', ({ body }) => body, { // ^? body: 'user[]', response: { 201: 'user[]' }, detail: { summary: 'Create user' } }) ``` ::: > Elysia seamlessly integrate the specification into the schema Hono has separate function to describe route specification, validation, and require some effort to setup properly. Elysia use schema you provide to generate the OpenAPI specification, and validate the request/response, and infer type automatically all from a **single source of truth**. Elysia also appends the schema registered in `model` to the OpenAPI spec, allowing you to reference the model in a dedicated section in Swagger or Scalar UI while Hono inline the schema to the route. ## Testing Both is built on top of Web Standard API allowing it be used with any testing library. ::: code-group ```ts [Hono] import { Hono } from 'hono' import { describe, it, expect } from 'vitest' const app = new Hono() .get('/', (c) => c.text('Hello World')) describe('GET /', () => { it('should return Hello World', async () => { const res = await app.request('/') expect(res.status).toBe(200) expect(await res.text()).toBe('Hello World') }) }) ``` ::: > Hono has a built-in `request` method to run the request ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' import { describe, it, expect } from 'vitest' const app = new Elysia() .get('/', 'Hello World') describe('GET /', () => { it('should return Hello World', async () => { const res = await app.handle( new Request('http://localhost') ) expect(res.status).toBe(200) expect(await res.text()).toBe('Hello World') }) }) ``` ::: > Elysia use Web Standard API to handle request and response Alternatively, Elysia also offers a helper library called [Eden](/eden/overview) for End-to-end type safety, allowing us to test with auto-completion, and full type safety. ```ts twoslash [Elysia] import { Elysia } from 'elysia' import { treaty } from '@elysiajs/eden' import { describe, expect, it } from 'bun:test' const app = new Elysia().get('/hello', 'Hello World') const api = treaty(app) describe('GET /', () => { it('should return Hello World', async () => { const { data, error, status } = await api.hello.get() expect(status).toBe(200) expect(data).toBe('Hello World') // ^? }) }) ``` ## End-to-end type safety Both offers end-to-end type safety, however Hono doesn't seems to offers type-safe error handling based on status code. ::: code-group ```ts twoslash [Hono] import { Hono } from 'hono' import { hc } from 'hono/client' import { z } from 'zod' import { zValidator } from '@hono/zod-validator' const app = new Hono() .post( '/mirror', zValidator( 'json', z.object({ message: z.string() }) ), (c) => c.json(c.req.valid('json')) ) const client = hc('/') const response = await client.mirror.$post({ json: { message: 'Hello, world!' } }) const data = await response.json() // ^? console.log(data) ``` ::: > Hono use `hc` to run the request, and offers end-to-end type safety ::: code-group ```ts twoslash [Elysia] import { Elysia, t } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .post('/mirror', ({ body }) => body, { body: t.Object({ message: t.String() }) }) const api = treaty(app) const { data, error } = await api.mirror.post({ message: 'Hello World' }) if(error) throw error // ^? console.log(data) // ^? // ---cut-after--- console.log('ok') ``` ::: > Elysia use `treaty` to run the request, and offers end-to-end type safety While both offers end-to-end type safety, Elysia offers a more type-safe error handling based on status code while Hono doesn't. Using the same purpose code for each framework to measure type inference speed, Elysia is 2.3x faster than Hono for type checking. ![Elysia eden type inference performance](/migrate/elysia-type-infer.webp) > Elysia take 536ms to infer both Elysia, and Eden (click to enlarge) ![Hono HC type inference performance](/migrate/hono-type-infer.webp) > Hono take 1.27s to infer both Hono, and HC with error (aborted) (click to enlarge) The 1.27 seconds doesn't reflect the entire duration of the inference, but a duration from start to aborted by error **"Type instantiation is excessively deep and possibly infinite."** which happens when there are too large schema. ![Hono HC code showing excessively deep error](/migrate/hono-hc-infer.webp) > Hono HC showing excessively deep error This is caused by the large schema, and Hono doesn't support over a 100 routes with complex body, and response validation while Elysia doesn't have this issue. ![Elysia Eden code showing type inference without error](/migrate/elysia-eden-infer.webp) > Elysia Eden code showing type inference without error Elysia has a faster type inference performance, and doesn't have **"Type instantiation is excessively deep and possibly infinite."** *at least* up to 2,000 routes with complex body, and response validation. If end-to-end type safety is important for you then Elysia is the right choice. *** Both are the next generation web framework built on top of Web Standard API with slight differences. Elysia is designed to be ergonomic and developer-friendly with a focus on **sounds type safety**, and has beter performance than Hono. While Hono offers a broad compatibility with multiple runtimes, especially with Cloudflare Workers, and a larger user base. Alternatively, if you are coming from a different framework, you can check out: --- --- url: /patterns/mount.md --- # Mount WinterCG is a standard for web-interoperable runtimes. Supported by Cloudflare, Deno, Vercel Edge Runtime, Netlify Function, and various others, it allows web servers to run interoperably across runtimes that use Web Standard definitions like `Fetch`, `Request`, and `Response`. Elysia is WinterCG compliant. We are optimized to run on Bun but also openly support other runtimes if possible. In theory, this allows any framework or code that is WinterCG compliant to be run together, allowing frameworks like Elysia, Hono, Remix, Itty Router to run together in a simple function. Adhering to this, we implemented the same logic for Elysia by introducing `.mount` method to run with any framework or code that is WinterCG compliant. ## Mount To use **.mount**, [simply pass a `fetch` function](https://twitter.com/saltyAom/status/1684786233594290176): ```ts import { Elysia } from 'elysia' const app = new Elysia() .get('/', () => 'Hello from Elysia') .mount('/hono', hono.fetch) ``` A **fetch** function is a function that accepts a Web Standard Request and returns a Web Standard Response with 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 * etc. This allows you to execute all the aforementioned code in a single server environment, making it possible to interact seamlessly with Elysia. You can also reuse existing functions within a single deployment, eliminating the need for a reverse proxy to manage multiple servers. If the framework also supports a **.mount** function, you can deeply nest a framework that supports it. ```ts import { Elysia } from 'elysia' import { Hono } from 'hono' 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) ``` ## Reusing Elysia Moreover, you can re-use multiple existing Elysia projects on your server. ```ts import { Elysia } from 'elysia' 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 be resolved with `use` automatically, providing type-safety and support for Eden by default. This makes the possibility of an interoperable framework and runtime a reality. --- --- url: /integrations/openapi.md --- # OpenAPI Elysia has first-class support and follows OpenAPI schema by default. Elysia can automatically generate an API documentation page by providing a Swagger plugin. To generate the Swagger page, install the plugin: ```bash bun add @elysiajs/swagger ``` And register the plugin to the server: ```typescript import { Elysia } from 'elysia' import { swagger } from '@elysiajs/swagger' const app = new Elysia() .use(swagger()) ``` By default, Elysia use OpenAPI V3 schema and [Scalar UI](http://scalar.com) by default For Swagger plugin configuration, see the [Swagger plugin page](/plugins/swagger). ## Route definitions We add route information by providing a schema type. However, sometime defining a type only isn't clear what the route might work. You can use `schema.detail` fields to explictly define what the route is all about. ```typescript import { Elysia, t } from 'elysia' import { swagger } from '@elysiajs/swagger' new Elysia() .use(swagger()) .post('/sign-in', ({ body }) => body, { body: t.Object( { username: t.String(), password: t.String({ minLength: 8, description: 'User password (at least 8 characters)' // [!code ++] }) }, { // [!code ++] description: 'Expected an username and password' // [!code ++] } // [!code ++] ), detail: { // [!code ++] summary: 'Sign in the user', // [!code ++] tags: ['authentication'] // [!code ++] } // [!code ++] }) ``` The detail fields follows an OpenAPI V3 definition with auto-completion and type-safety by default. Detail is then passed to Swagger to put the description to Swagger route. ### detail `detail` extends the [OpenAPI Operation Object](https://swagger.io/specification#operation-object) The detail field is an object that can be use to describe information about the route for API documentation. Which may contains the following fields: ### tags An array of tags for the operation. Tags can be used for logical grouping of operations by resources or any other qualifier. ### summary A short summary of what the operation does. ### description A verbose explanation of the operation behavior. ### externalDocs Additional external documentation for this operation. ### operationId A unique string used to identify the operation. The id MUST be unique among all operations described in the API. The operationId value is case-sensitive. ### deprecated Declares this operation to be deprecated. Consumers SHOULD refrain from usage of the declared operation. Default value is `false`. ### security A declaration of which security mechanisms can be used for this operation. The list of values includes alternative security requirement objects that can be used. Only one of the security requirement objects need to be satisfied to authorize a request. To make security optional, an empty security requirement (`{}`) can be included in the array. ## Hide You can hide the route from the Swagger page by setting `detail.hide` to `true` ```typescript import { Elysia, t } from 'elysia' import { swagger } from '@elysiajs/swagger' new Elysia() .use(swagger()) .post('/sign-in', ({ body }) => body, { body: t.Object( { username: t.String(), password: t.String() }, { description: 'Expected an username and password' } ), detail: { // [!code ++] hide: true // [!code ++] } // [!code ++] }) ``` ## Tags group Elysia may accept tags to add an entire instance or group of routes to a specific tag. ```typescript import { Elysia, t } from 'elysia' new Elysia({ tags: ['user'] }) .get('/user', 'user') .get('/admin', 'admin') ``` ## Guard Alternatively, Elysia may accept guards to add an entire instance or group of routes to a specific guard. ```typescript import { Elysia, t } from 'elysia' new Elysia() .guard({ detail: { description: 'Require user to be logged in' } }) .get('/user', 'user') .get('/admin', 'admin') ``` --- --- url: /integrations/opentelemetry.md --- # OpenTelemetry To start using OpenTelemetry, install `@elysiajs/opentelemetry` and apply plugin to any instance. ```typescript import { Elysia } from 'elysia' import { opentelemetry } from '@elysiajs/opentelemetry' import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node' import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' new Elysia().use( opentelemetry({ spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter())] }) ) ``` ![jaeger showing collected trace automatically](/blog/elysia-11/jaeger.webp) Elysia OpenTelemetry is will **collect span of any library compatible OpenTelemetry standard**, and will apply parent and child span automatically. In the code above, we apply `Prisma` to trace how long each query took. By applying OpenTelemetry, Elysia will then: * collect telemetry data * Grouping relevant lifecycle together * Measure how long each function took * Instrument HTTP request and response * Collect error and exception You may export telemetry data to Jaeger, Zipkin, New Relic, Axiom or any other OpenTelemetry compatible backend. ![axiom showing collected trace from OpenTelemetry](/blog/elysia-11/axiom.webp) Here's an example of exporting telemetry to [Axiom](https://axiom.co) ```typescript import { Elysia } from 'elysia' import { opentelemetry } from '@elysiajs/opentelemetry' import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node' import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' new Elysia().use( opentelemetry({ spanProcessors: [ new BatchSpanProcessor( new OTLPTraceExporter({ url: 'https://api.axiom.co/v1/traces', // [!code ++] headers: { // [!code ++] Authorization: `Bearer ${Bun.env.AXIOM_TOKEN}`, // [!code ++] 'X-Axiom-Dataset': Bun.env.AXIOM_DATASET // [!code ++] } // [!code ++] }) ) ] }) ) ``` ## Instrumentations Many instrumentation libraries required that the SDK **MUST** run before importing the module. For example, to use `PgInstrumentation`, the `OpenTelemetry SDK` must run before importing the `pg` module. To achieve this in Bun, we can 1. Separate an OpenTelemetry setup into a different file 2. create `bunfig.toml` to preload the OpenTelemetry setup file Let's create a new file in `src/instrumentation.ts` ```ts [src/instrumentation.ts] import { opentelemetry } from '@elysiajs/opentelemetry' import { PgInstrumentation } from '@opentelemetry/instrumentation-pg' export const instrumentation = opentelemetry({ instrumentations: [new PgInstrumentation()] }) ``` Then we can apply this `instrumentaiton` plugin into our main instance in `src/index.ts` ```ts [src/index.ts] import { Elysia } from 'elysia' import { instrumentation } from './instrumentation.ts' new Elysia().use(instrumentation).listen(3000) ``` Then create a `bunfig.toml` with the following: ```toml [bunfig.toml] preload = ["./src/instrumentation.ts"] ``` This will tell Bun to load and setup `instrumentation` before running the `src/index.ts` allowing OpenTelemetry to do its setup as needed. ### Deploying to production If you are using `bun build` or other bundlers. As OpenTelemetry rely on monkey-patching `node_modules/`. It's required that make instrumentations works properly, we need to specify that libraries to be instrument is an external module to exclude it from being bundled. For example, if you are using `@opentelemetry/instrumentation-pg` to instrument `pg` library. We need to exclude `pg` from being bundled and make sure that it is importing `node_modules/pg`. To make this works, we may specified `pg` as an external module with `--external pg` ```bash bun build --compile --external pg --outfile server src/index.ts ``` This tells bun to not `pg` bundled into the final output file, and will be imported from the **node\_modules** directory at runtime. So on a production server, you must also keeps the **node\_modules** directory. It's recommended to specify packages that should be available in a production server as **dependencies** in **package.json** and use `bun install --production` to install only production dependencies. ```json { "dependencies": { "pg": "^8.15.6" }, "devDependencies": { "@elysiajs/opentelemetry": "^1.2.0", "@opentelemetry/instrumentation-pg": "^0.52.0", "@types/pg": "^8.11.14", "elysia": "^1.2.25" } } ``` Then after running a build command, on a production server ```bash bun install --production ``` If the node\_modules directory still includes development dependencies, you may remove the node\_modules directory and reinstall production dependencies again. ## OpenTelemetry SDK Elysia OpenTelemetry is for applying OpenTelemetry to Elysia server only. You may use OpenTelemetry SDK normally, and the span is run under Elysia's request span, it will be automatically appear in Elysia trace. However, we also provide a `getTracer`, and `record` utility to collect span from any part of your application. ```typescript import { Elysia } from 'elysia' import { record } from '@elysiajs/opentelemetry' export const plugin = new Elysia().get('', () => { return record('database.query', () => { return db.query('SELECT * FROM users') }) }) ``` ## Record utility `record` is an equivalent to OpenTelemetry's `startActiveSpan` but it will handle auto-closing and capture exception automatically. You may think of `record` as a label for your code that will be shown in trace. ### Prepare your codebase for observability Elysia OpenTelemetry will group lifecycle and read the **function name** of each hook as the name of the span. It's a good time to **name your function**. If your hook handler is an arrow function, you may refactor it to named function to understand the trace better, otherwise your trace span will be named as `anonymous`. ```typescript const bad = new Elysia() // ⚠️ span name will be anonymous .derive(async ({ cookie: { session } }) => { return { user: await getProfile(session) } }) const good = new Elysia() // ✅ span name will be getProfile .derive(async function getProfile({ cookie: { session } }) { return { user: await getProfile(session) } }) ``` ## getCurrentSpan `getCurrentSpan` is a utility to get the current span of the current request when you are outside of the handler. ```typescript import { getCurrentSpan } from '@elysiajs/opentelemetry' function utility() { const span = getCurrentSpan() span.setAttributes({ 'custom.attribute': 'value' }) } ``` This works outside of the handler by retriving current span from `AsyncLocalStorage` ## setAttribute `setAttribute` is a utility to set attribute to the current span. ```typescript import { setAttribute } from '@elysiajs/opentelemetry' function utility() { setAttribute('custom.attribute', 'value') } ``` This is a syntax sugar for `getCurrentSpan().setAttributes` ## Configuration See [opentelemetry plugin](/plugins/opentelemetry) for configuration option and definition. --- --- url: /plugins/opentelemetry.md --- # OpenTelemetry ::: tip This page is a **config reference** for **OpenTelemetry**, if you're looking to setup and integrate with OpenTelemetry, we recommended taking a look at [Integrate with OpenTelemetry](/integrations/opentelemetry) instead. ::: To start using OpenTelemetry, install `@elysiajs/opentelemetry` and apply plugin to any instance. ```typescript twoslash import { Elysia } from 'elysia' import { opentelemetry } from '@elysiajs/opentelemetry' import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node' import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' new Elysia() .use( opentelemetry({ spanProcessors: [ new BatchSpanProcessor( new OTLPTraceExporter() ) ] }) ) ``` ![jaeger showing collected trace automatically](/blog/elysia-11/jaeger.webp) Elysia OpenTelemetry is will **collect span of any library compatible OpenTelemetry standard**, and will apply parent and child span automatically. ## Usage See [opentelemetry](/integrations/opentelemetry) for usage and utilities ## Config This plugin extends OpenTelemetry SDK parameters options. Below is a config which is accepted by the plugin ### autoDetectResources - boolean Detect resources automatically from the environment using the default resource detectors. default: `true` ### contextManager - ContextManager Use a custom context manager. default: `AsyncHooksContextManager` ### textMapPropagator - TextMapPropagator Use a custom propagator. default: `CompositePropagator` using W3C Trace Context and Baggage ### metricReader - MetricReader Add a MetricReader that will be passed to the MeterProvider. ### views - View\[] A list of views to be passed to the MeterProvider. Accepts an array of View-instances. This parameter can be used to configure explicit bucket sizes of histogram metrics. ### instrumentations - (Instrumentation | Instrumentation\[])\[] Configure instrumentations. By default `getNodeAutoInstrumentations` is enabled, if you want to enable them you can use either metapackage or configure each instrumentation individually. default: `getNodeAutoInstrumentations()` ### resource - IResource Configure a resource. Resources may also be detected by using the autoDetectResources method of the SDK. ### resourceDetectors - Array\ Configure resource detectors. By default, the resource detectors are \[envDetector, processDetector, hostDetector]. NOTE: In order to enable the detection, the parameter autoDetectResources has to be true. If resourceDetectors was not set, you can also use the environment variable OTEL\_NODE\_RESOURCE\_DETECTORS to enable only certain detectors, or completely disable them: * env * host * os * process * serviceinstance (experimental) * all - enable all resource detectors above * none - disable resource detection For example, to enable only the env, host detectors: ```bash export OTEL_NODE_RESOURCE_DETECTORS="env,host" ``` ### sampler - Sampler Configure a custom sampler. By default, all traces will be sampled. ### serviceName - string Namespace to be identify as. ### spanProcessors - SpanProcessor\[] An array of span processors to register to the tracer provider. ### traceExporter - SpanExporter Configure a trace exporter. If an exporter is configured, it will be used with a `BatchSpanProcessor`. If an exporter OR span processor is not configured programmatically, this package will auto setup the default otlp exporter with http/protobuf protocol with a BatchSpanProcessor. ### spanLimits - SpanLimits Configure tracing parameters. These are the same trace parameters used to configure a tracer. --- --- url: /eden/treaty/overview.md --- # Eden Treaty Eden Treaty is an object representation to interact with a server and features type safety, auto-completion, and error handling. 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', () => '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 the client: ```typescript twoslash // @filename: 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 // [!code ++] // @filename: client.ts // ---cut--- // client.ts import { treaty } from '@elysiajs/eden' import type { App } from './server' // [!code ++] const app = treaty('localhost:3000') // response type: 'Hi Elysia' const { data, error } = await app.hi.get() // ^? ``` ## Tree like syntax HTTP Path is a resource indicator for a file system tree. File system consists of multiple levels of folders, for example: * /documents/elysia * /documents/kalpas * /documents/kelvin Each level is separated by **/** (slash) and a name. However in JavaScript, instead of using **"/"** (slash) we use **"."** (dot) to access deeper resources. Eden Treaty turns an Elysia server into a tree-like file system that can be accessed in the JavaScript frontend instead. | Path | Treaty | | ------------ | ------------ | | / | | | /hi | .hi | | /deep/nested | .deep.nested | Combined with the HTTP method, we can interact with the Elysia server. | Path | Method | Treaty | | ------------ | ------ | ------------------- | | / | GET | .get() | | /hi | GET | .hi.get() | | /deep/nested | GET | .deep.nested.get() | | /deep/nested | POST | .deep.nested.post() | ## Dynamic path However, dynamic path parameters cannot be expressed using notation. If they are fully replaced, we don't know what the parameter name is supposed to be. ```typescript // ❌ Unclear what the value is supposed to represent? treaty.item['skadi'].get() ``` To handle this, we can specify a dynamic path using a function to provide a key value instead. ```typescript // ✅ Clear that value is dynamic path is 'name' treaty.item({ name: 'Skadi' }).get() ``` | Path | Treaty | | --------------- | -------------------------------- | | /item | .item | | /item/:name | .item({ name: 'Skadi' }) | | /item/:name/id | .item({ name: 'Skadi' }).id | --- --- url: /essential/plugin.md --- # Plugin Plugin is a pattern that decouples functionality into smaller parts. Creating reusable components for our web server. Defining a plugin is to define a separate instance. ```typescript twoslash import { Elysia } from 'elysia' const plugin = new Elysia() .decorate('plugin', 'hi') .get('/plugin', ({ plugin }) => plugin) const app = new Elysia() .use(plugin) .get('/', ({ plugin }) => plugin) // ^? .listen(3000) ``` We can use the plugin by passing an instance to **Elysia.use**. The plugin will inherit all properties of the plugin instance, including **state**, **decorate**, **derive**, **route**, **lifecycle**, etc. Elysia will also handle the type inference automatically as well, so you can imagine as if you call all of the other instances on the main one. ::: tip Notice that the plugin doesn't contain **.listen**, because **.listen** will allocate a port for the usage, and we only want the main instance to allocate the port. ::: ## Plugin Every Elysia instance can be a plugin. We can decouple our logic into a new separate Elysia instance and use it as a plugin. First, we define an instance in a difference file: ```typescript twoslash // plugin.ts import { Elysia } from 'elysia' export const plugin = new Elysia() .get('/plugin', () => 'hi') ``` And then we import the instance into the main file: ```typescript import { Elysia } from 'elysia' import { plugin } from './plugin' const app = new Elysia() .use(plugin) .listen(3000) ``` ### Config To make the plugin more useful, allowing customization via config is recommended. You can create a function that accepts parameters that may change the behavior of the plugin to make it more reusable. ```typescript import { Elysia } from 'elysia' const version = (version = 1) => new Elysia() .get('/version', version) const app = new Elysia() .use(version(1)) .listen(3000) ``` ### Functional callback It's recommended to define a new plugin instance instead of using a function callback. Functional callback allows us to access the existing property of the main instance. For example, checking if specific routes or stores existed. To define a functional callback, create a function that accepts Elysia as a parameter. ```typescript twoslash import { Elysia } from 'elysia' const plugin = (app: Elysia) => app .state('counter', 0) .get('/plugin', () => 'Hi') const app = new Elysia() .use(plugin) .get('/counter', ({ store: { counter } }) => counter) .listen(3000) ``` Once passed to `Elysia.use`, functional callback behaves as a normal plugin except the property is assigned directly to the main instance. ::: tip You shall not worry about the performance difference between a functional callback and creating an instance. Elysia can create 10k instances in a matter of milliseconds, the new Elysia instance has even better type inference performance than the functional callback. ::: ## Plugin Deduplication By default, Elysia will register any plugin and handle type definitions. Some plugins may be used multiple times to provide type inference, resulting in duplication of setting initial values or routes. Elysia avoids this by differentiating the instance by using **name** and **optional seeds** to help Elysia identify instance duplication: ```typescript import { Elysia } from 'elysia' const plugin = (config: { prefix: T }) => new Elysia({ name: 'my-plugin', // [!code ++] seed: config, // [!code ++] }) .get(`${config.prefix}/hi`, () => 'Hi') const app = new Elysia() .use( plugin({ prefix: '/v2' }) ) .listen(3000) ``` Elysia will use **name** and **seed** to create a checksum to identify if the instance has been registered previously or not, if so, Elysia will skip the registration of the plugin. If seed is not provided, Elysia will only use **name** to differentiate the instance. This means that the plugin is only registered once even if you registered it multiple times. ```typescript import { Elysia } from 'elysia' const plugin = new Elysia({ name: '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. ::: tip Seed could be anything, varying from a string to a complex object or class. If the provided value is class, Elysia will then try to use the `.toString` method to generate a checksum. ::: ### Service Locator When you apply a plugin with state/decorators to an instance, the instance will gain type safety. But if you don't apply the plugin to another instance, it will not be able to infer the type. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' const child = new Elysia() // ❌ 'a' is missing .get('/', ({ a }) => a) const main = new Elysia() .decorate('a', 'a') .use(child) ``` Elysia introduces the **Service Locator** pattern to counteract this. Elysia will lookup the plugin checksum and get the value or register a new one. Infer the type from the plugin. So we have to provide the plugin reference for Elysia to find the service to add type safety. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' const setup = new Elysia({ name: 'setup' }) .decorate('a', 'a') // Without 'setup', type will be missing const error = new Elysia() .get('/', ({ a }) => a) const main = new Elysia() // With `setup`, type will be inferred .use(setup) // [!code ++] .get('/', ({ a }) => a) // ^? ``` ## Guard Guard allows us to apply hook and schema into multiple routes all at once. ```typescript twoslash const signUp = (a: T) => a const signIn = (a: T) => a const isUserExists = (a: T) => a // ---cut--- import { Elysia, t } from 'elysia' new Elysia() .guard( { // [!code ++] body: t.Object({ // [!code ++] username: t.String(), // [!code ++] password: t.String() // [!code ++] }) // [!code ++] }, // [!code ++] (app) => // [!code ++] app .post('/sign-up', ({ body }) => signUp(body)) .post('/sign-in', ({ body }) => signIn(body), { // ^? beforeHandle: isUserExists }) ) .get('/', 'hi') .listen(3000) ``` This code applies validation for `body` to both '/sign-in' and '/sign-up' instead of inlining the schema one by one but applies not to '/'. We can summarize the route validation as the following: | Path | Has validation | | ------- | ------------- | | /sign-up | ✅ | | /sign-in | ✅ | | / | ❌ | Guard accepts the same parameter as inline hook, the only difference is that you can apply hook to multiple routes in the scope. This means that the code above is translated into: ```typescript twoslash const signUp = (a: T) => a const signIn = (a: T) => a const isUserExists = (a: any) => a // ---cut--- import { Elysia, t } from 'elysia' new Elysia() .post('/sign-up', ({ body }) => signUp(body), { body: t.Object({ username: t.String(), password: t.String() }) }) .post('/sign-in', ({ body }) => body, { beforeHandle: isUserExists, body: t.Object({ username: t.String(), password: t.String() }) }) .get('/', () => 'hi') .listen(3000) ``` ### Grouped Guard We can use a group with prefixes by providing 3 parameters to the group. 1. Prefix - Route prefix 2. Guard - Schema 3. Scope - Elysia app callback With the same API as guard apply to the 2nd parameter, instead of nesting group and guard together. Consider the following example: ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .group('/v1', (app) => app.guard( { body: t.Literal('Rikuhachima Aru') }, (app) => app.post('/student', ({ body }) => body) // ^? ) ) .listen(3000) ``` From nested groupped guard, we may merge group and guard together by providing guard scope to 2nd parameter of group: ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .group( '/v1', (app) => app.guard( // [!code --] { body: t.Literal('Rikuhachima Aru') }, (app) => app.post('/student', ({ body }) => body) ) // [!code --] ) .listen(3000) ``` Which results in the follows syntax: ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .group( '/v1', { body: t.Literal('Rikuhachima Aru') }, (app) => app.post('/student', ({ body }) => body) // ^? ) .listen(3000) ``` ## Scope By default, hook and schema will apply to **current instance only**. Elysia has an encapsulation scope for to prevent unintentional side effects. Scope type is to specify the scope of hook whether is should be encapsulated or global. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' const plugin = new Elysia() .derive(() => { return { hi: 'ok' } }) .get('/child', ({ hi }) => hi) const main = new Elysia() .use(plugin) // ⚠️ Hi is missing .get('/parent', ({ hi }) => hi) ``` From the above code, we can see that `hi` is missing from the parent instance because the scope is local by default if not specified, and will not apply to parent. To apply the hook to the parent instance, we can use the `as` to specify scope of the hook. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' const plugin = new Elysia() .derive({ as: 'scoped' }, () => { // [!code ++] return { hi: 'ok' } }) .get('/child', ({ hi }) => hi) const main = new Elysia() .use(plugin) // ✅ Hi is now available .get('/parent', ({ hi }) => hi) ``` ### Scope level Elysia has 3 levels of scope as the following: Scope type are as the following: * **local** (default) - apply to only current instance and descendant only * **scoped** - apply to parent, current instance and descendants * **global** - apply to all instance that apply the plugin (all parents, current, and descendants) Let's review what each scope type does by using the following example: ```typescript import { Elysia } from 'elysia' // ? Value base on table value provided below const type = 'local' const child = new Elysia() .get('/child', 'hi') const current = new Elysia() .onBeforeHandle({ as: type }, () => { // [!code ++] console.log('hi') }) .use(child) .get('/current', 'hi') const parent = new Elysia() .use(current) .get('/parent', 'hi') const main = new Elysia() .use(parent) .get('/main', 'hi') ``` By changing the `type` value, the result should be as follows: | type | child | current | parent | main | | ---------- | ----- | ------- | ------ | ---- | | 'local' | ✅ | ✅ | ❌ | ❌ | | 'scoped' | ✅ | ✅ | ✅ | ❌ | | 'global' | ✅ | ✅ | ✅ | ✅ | ### Scope cast To apply hook to parent may use one of the following: 1. `inline as` apply only to a single hook 2. `guard as` apply to all hook in a guard 3. `instance as` apply to all hook in an instance ### 1. Inline as Every event listener will accept `as` parameter to specify the scope of the hook. ```typescript twoslash import { Elysia } from 'elysia' const plugin = new Elysia() .derive({ as: 'scoped' }, () => { // [!code ++] return { hi: 'ok' } }) .get('/child', ({ hi }) => hi) const main = new Elysia() .use(plugin) // ✅ Hi is now available .get('/parent', ({ hi }) => hi) ``` However, this method is apply to only a single hook, and may not be suitable for multiple hooks. ### 2. Guard as Every event listener will accept `as` parameter to specify the scope of the hook. ```typescript import { Elysia, t } from 'elysia' const plugin = new Elysia() .guard({ as: 'scoped', // [!code ++] response: t.String(), beforeHandle() { console.log('ok') } }) .get('/child', 'ok') const main = new Elysia() .use(plugin) .get('/parent', 'hello') ``` Guard alllowing us to apply `schema` and `hook` to multiple routes all at once while specifying the scope. However, it doesn't support `derive` and `resolve` method. ### 3. Instance as `as` will read all hooks and schema scope of the current instance, modify. ```typescript twoslash import { Elysia } from 'elysia' const plugin = new Elysia() .derive(() => { return { hi: 'ok' } }) .get('/child', ({ hi }) => hi) .as('scoped') // [!code ++] const main = new Elysia() .use(plugin) // ✅ Hi is now available .get('/parent', ({ hi }) => hi) ``` Sometimes we want to reapply plugin to parent instance as well but as it's limited by `scoped` mechanism, it's limited to 1 parent only. To apply to the parent instance, we need to **lift the scope up** to the parent instance, and `as` is the perfect method to do so. Which means if you have `local` scope, and want to apply it to the parent instance, you can use `as('scoped')` to lift it up. ```typescript twoslash // @errors: 2304 2345 import { Elysia, t } from 'elysia' const plugin = new Elysia() .guard({ response: t.String() }) .onBeforeHandle(() => { console.log('called') }) .get('/ok', () => 'ok') .get('/not-ok', () => 1) .as('scoped') // [!code ++] const instance = new Elysia() .use(plugin) .get('/no-ok-parent', () => 2) .as('scoped') // [!code ++] const parent = new Elysia() .use(instance) // This now error because `scoped` is lifted up to parent .get('/ok', () => 3) ``` ### Descendant By default plugin will **apply hook to itself and descendants** only. If the hook is registered in a plugin, instances that inherit the plugin will **NOT** inherit hooks and schema. ```typescript import { Elysia } from 'elysia' const plugin = new Elysia() .onBeforeHandle(() => { console.log('hi') }) .get('/child', 'log hi') const main = new Elysia() .use(plugin) .get('/parent', 'not log hi') ``` To apply hook to globally, we need to specify hook as global. ```typescript import { Elysia } from 'elysia' const plugin = new Elysia() .onBeforeHandle(() => { return 'hi' }) .get('/child', 'child') .as('scoped') const main = new Elysia() .use(plugin) .get('/parent', 'parent') ``` ## Lazy Load Modules are eagerly loaded by default. Elysia loads all modules then registers and indexes all of them before starting the server. This enforces that all the modules have loaded before it starts accepting requests. While this is fine for most applications, it may become a bottleneck for a server running in a serverless environment or an edge function, in which the startup time is important. Lazy-loading can help decrease startup time by deferring modules to be gradually indexed after the server start. Lazy-loading modules are a good option when some modules are heavy and importing startup time is crucial. By default, any async plugin without await is treated as a deferred module and the import statement as a lazy-loading module. Both will be registered after the server is started. ### Deferred Module The deferred module is an async plugin that can be registered after the server is started. ```typescript // plugin.ts import { Elysia, file } from 'elysia' import { loadAllFiles } from './files' export const loadStatic = async (app: Elysia) => { const files = await loadAllFiles() files.forEach((asset) => app .get(asset, file(file)) ) return app } ``` And in the main file: ```typescript import { Elysia } from 'elysia' import { loadStatic } from './plugin' const app = new Elysia() .use(loadStatic) ``` Elysia static plugin is also a deferred module, as it loads files and registers files path asynchronously. ### Lazy Load Module Same as the async plugin, the lazy-load module will be registered after the server is started. A lazy-load module can be both sync or async function, as long as the module is used with `import` the module will be lazy-loaded. ```typescript import { Elysia } from 'elysia' const app = new Elysia() .use(import('./plugin')) ``` Using module lazy-loading is recommended when the module is computationally heavy and/or blocking. To ensure module registration before the server starts, we can use `await` on the deferred module. ### Testing In a test environment, we can use `await app.modules` to wait for deferred and lazy-loading modules. ```typescript import { describe, expect, it } from 'bun:test' import { Elysia } from 'elysia' describe('Modules', () => { it('inline async', async () => { const app = new Elysia() .use(async (app) => app.get('/async', () => 'async') ) await app.modules const res = await app .handle(new Request('http://localhost/async')) .then((r) => r.text()) expect(res).toBe('async') }) }) ``` --- --- url: /plugins/overview.md --- # Overview Elysia is designed to be modular and lightweight. Following the same idea as Arch Linux (btw, I use Arch): > Design decisions are made on a case-by-case basis through developer consensus This is to ensure developers end up with a performant web server they intend to create. By extension, Elysia includes pre-built common pattern plugins for convenient developer usage: ## Official plugins: * [Bearer](/plugins/bearer) - retrieve [Bearer](https://swagger.io/docs/specification/authentication/bearer-authentication/) token automatically * [CORS](/plugins/cors) - set up [Cross-origin resource sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) * [Cron](/plugins/cron) - set up [cron](https://en.wikipedia.org/wiki/Cron) job * [Eden](/eden/overview) - end-to-end type safety client for Elysia * [GraphQL Apollo](/plugins/graphql-apollo) - run [Apollo GraphQL](https://www.apollographql.com/) on Elysia * [GraphQL Yoga](/plugins/graphql-yoga) - run [GraphQL Yoga](https://github.com/dotansimha/graphql-yoga) on Elysia * [HTML](/plugins/html) - handle HTML responses * [JWT](/plugins/jwt) - authenticate with [JWTs](https://jwt.io/) * [OpenTelemetry](/plugins/opentelemetry) - add support for OpenTelemetry * [Server Timing](/plugins/server-timing) - audit performance bottlenecks with the [Server-Timing API](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing) * [Static](/plugins/static) - serve static files/folders * [Stream](/plugins/stream) - integrate response streaming and [server-sent events (SSEs)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) * [Swagger](/plugins/swagger) - generate [Swagger](https://swagger.io/) documentation * [tRPC](/plugins/trpc) - support [tRPC](https://trpc.io/) * [WebSocket](/patterns/websocket) - support [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) ## Community plugins: * [Create ElysiaJS](https://github.com/kravetsone/create-elysiajs) - scaffolding your Elysia project with the environment with easy (help with ORM, Linters and Plugins)! * [Lucia Auth](https://github.com/pilcrowOnPaper/lucia) - authentication, simple and clean * [Elysia Clerk](https://github.com/wobsoriano/elysia-clerk) - unofficial Clerk authentication plugin * [Elysia Polyfills](https://github.com/bogeychan/elysia-polyfills) - run Elysia ecosystem on Node.js and Deno * [Vite server](https://github.com/kravetsone/elysia-vite-server) - plugin which start and decorate [`vite`](https://vitejs.dev/) dev server in `development` and in `production` mode serve static (if it needed) * [Vite](https://github.com/timnghg/elysia-vite) - serve entry HTML file with Vite's scripts injected * [Nuxt](https://github.com/trylovetom/elysiajs-nuxt) - easily integrate elysia with nuxt! * [Remix](https://github.com/kravetsone/elysia-remix) - use [Remix](https://remix.run/) with `HMR` support (powered by [`vite`](https://vitejs.dev/))! Close a really long-standing plugin request [#12](https://github.com/elysiajs/elysia/issues/12) * [Sync](https://github.com/johnny-woodtke/elysiajs-sync) - a lightweight offline-first data synchronization framework powered by [Dexie.js](https://dexie.org/) * [Connect middleware](https://github.com/kravetsone/elysia-connect-middleware) - plugin which allows you to use [`express`](https://www.npmjs.com/package/express)/[`connect`](https://www.npmjs.com/package/connect) middleware directly in Elysia! * [Elysia Helmet](https://github.com/DevTobias/elysia-helmet) - secure Elysia apps with various HTTP headers * [Vite Plugin SSR](https://github.com/timnghg/elysia-vite-plugin-ssr) - Vite SSR plugin using Elysia server * [OAuth 2.0](https://github.com/kravetsone/elysia-oauth2) - a plugin for [OAuth 2.0](https://en.wikipedia.org/wiki/OAuth) Authorization Flow with more than **42** providers and **type-safety**! * [OAuth2](https://github.com/bogeychan/elysia-oauth2) - handle OAuth 2.0 authorization code flow * [OAuth2 Resource Server](https://github.com/ap-1/elysia-oauth2-resource-server) - a plugin for validating JWT tokens from OAuth2 providers against JWKS endpoints with support for issuer, audience, and scope verification * [Elysia OpenID Client](https://github.com/macropygia/elysia-openid-client) - OpenID client based on [openid-client](https://github.com/panva/node-openid-client) * [Rate Limit](https://github.com/rayriffy/elysia-rate-limit) - simple, lightweight rate limiter * [Logysia](https://github.com/tristanisham/logysia) - classic logging middleware * [Logestic](https://github.com/cybercoder-naj/logestic) - an advanced and customisable logging library for ElysiaJS * [Logger](https://github.com/bogeychan/elysia-logger) - [pino](https://github.com/pinojs/pino)-based logging middleware * [Elylog](https://github.com/eajr/elylog) - simple stdout logging library with some customization * [Logify for Elysia.js](https://github.com/0xrasla/logify) - a beautiful, fast, and type-safe logging middleware for Elysia.js applications * [Nice Logger](https://github.com/tanishqmanuja/nice-logger) - not the nicest, but a pretty nice and sweet logger for Elysia. * [Sentry](https://github.com/johnny-woodtke/elysiajs-sentry) - capture traces and errors with this [Sentry](https://docs.sentry.io/) plugin * [Elysia Lambda](https://github.com/TotalTechGeek/elysia-lambda) - deploy on AWS Lambda * [Decorators](https://github.com/gaurishhs/elysia-decorators) - use TypeScript decorators * [Autoload](https://github.com/kravetsone/elysia-autoload) - filesystem router based on a directory structure that generates types for [Eden](https://elysiajs.com/eden/overview.html) with [`Bun.build`](https://github.com/kravetsone/elysia-autoload?tab=readme-ov-file#bun-build-usage) support * [Msgpack](https://github.com/kravetsone/elysia-msgpack) - allows you to work with [MessagePack](https://msgpack.org) * [XML](https://github.com/kravetsone/elysia-xml) - allows you to work with XML * [Autoroutes](https://github.com/wobsoriano/elysia-autoroutes) - filesystem routes * [Group Router](https://github.com/itsyoboieltr/elysia-group-router) - filesystem and folder-based router for groups * [Basic Auth](https://github.com/itsyoboieltr/elysia-basic-auth) - basic HTTP authentication * [ETag](https://github.com/bogeychan/elysia-etag) - automatic HTTP [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) generation * [CDN Cache](https://github.com/johnny-woodtke/elysiajs-cdn-cache) - Cache-Control plugin for Elysia - no more manually setting HTTP headers * [Basic Auth](https://github.com/eelkevdbos/elysia-basic-auth) - basic HTTP authentication (using `request` event) * [i18n](https://github.com/eelkevdbos/elysia-i18next) - [i18n](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n) wrapper based on [i18next](https://www.i18next.com/) * [Elysia Request ID](https://github.com/gtramontina/elysia-requestid) - add/forward request IDs (`X-Request-ID` or custom) * [Elysia HTMX](https://github.com/gtramontina/elysia-htmx) - context helpers for [HTMX](https://htmx.org/) * [Elysia HMR HTML](https://github.com/gtrabanco/elysia-hmr-html) - reload HTML files when changing any file in a directory * [Elysia Inject HTML](https://github.com/gtrabanco/elysia-inject-html) - inject HTML code in HTML files * [Elysia HTTP Error](https://github.com/yfrans/elysia-http-error) - return HTTP errors from Elysia handlers * [Elysia Http Status Code](https://github.com/sylvain12/elysia-http-status-code) - integrate HTTP status codes * [NoCache](https://github.com/gaurishhs/elysia-nocache) - disable caching * [Elysia Tailwind](https://github.com/gtramontina/elysia-tailwind) - compile [Tailwindcss](https://tailwindcss.com/) in a plugin. * [Elysia Compression](https://github.com/gusb3ll/elysia-compression) - compress response * [Elysia IP](https://github.com/gaurishhs/elysia-ip) - get the IP Address * [OAuth2 Server](https://github.com/myazarc/elysia-oauth2-server) - developing an OAuth2 Server with Elysia * [Elysia Flash Messages](https://github.com/gtramontina/elysia-flash-messages) - enable flash messages * [Elysia AuthKit](https://github.com/gtramontina/elysia-authkit) - unnoficial [WorkOS' AuthKit](https://www.authkit.com/) authentication * [Elysia Error Handler](https://github.com/gtramontina/elysia-error-handler) - simpler error handling * [Elysia env](https://github.com/yolk-oss/elysia-env) - typesafe environment variables with typebox * [Elysia Drizzle Schema](https://github.com/Edsol/elysia-drizzle-schema) - helps to use Drizzle ORM schema inside elysia swagger model. * [Unify-Elysia](https://github.com/qlaffont/unify-elysia) - unify error code for Elysia * [Unify-Elysia-GQL](https://github.com/qlaffont/unify-elysia-gql) - unify error code for Elysia GraphQL Server (Yoga & Apollo) * [Elysia Auth Drizzle](https://github.com/qlaffont/elysia-auth-drizzle) - library who handle authentification with JWT (Header/Cookie/QueryParam). * [graceful-server-elysia](https://github.com/qlaffont/graceful-server-elysia) - library inspired by [graceful-server](https://github.com/gquittet/graceful-server). * [Logixlysia](https://github.com/PunGrumpy/logixlysia) - a beautiful and simple logging middleware for ElysiaJS with colors and timestamps. * [Elysia Fault](https://github.com/vitorpldev/elysia-fault) - a simple and customizable error handling middleware with the possibility of creating your own HTTP errors * [Elysia Compress](https://github.com/vermaysha/elysia-compress) - ElysiaJS plugin to compress responses inspired by [@fastify/compress](https://github.com/fastify/fastify-compress) * [@labzzhq/compressor](https://github.com/labzzhq/compressor/) - Compact Brilliance, Expansive Results: HTTP Compressor for Elysia and Bunnyhop with gzip, deflate and brotli support. * [Elysia Accepts](https://github.com/morigs/elysia-accepts) - Elysia plugin for accept headers parsing and content negotiation * [Elysia Compression](https://github.com/chneau/elysia-compression) - Elysia plugin for compressing responses * [Elysia Logger](https://github.com/chneau/elysia-logger) - Elysia plugin for logging HTTP requests and responses inspired by [hono/logger](https://hono.dev/docs/middleware/builtin/logger) * [Elysia CQRS](https://github.com/jassix/elysia-cqrs) - Elysia plugin for CQRS pattern * [Elysia Supabase](https://github.com/mastermakrela/elysia-supabase) - Seamlessly integrate [Supabase](https://supabase.com/) authentication and database functionality into Elysia, allowing easy access to authenticated user data and Supabase client instance. Especially useful for [Edge Functions](https://supabase.com/docs/guides/functions). * [Elysia XSS](https://www.npmjs.com/package/elysia-xss) - a plugin for Elysia.js that provides XSS (Cross-Site Scripting) protection by sanitizing request body data. * [Elysiajs Helmet](https://www.npmjs.com/package/elysiajs-helmet) - a comprehensive security middleware for Elysia.js applications that helps secure your apps by setting various HTTP headers. * [Decorators for Elysia.js](https://github.com/Ateeb-Khan-97/better-elysia) - seamlessly develop and integrate APIs, Websocket and Streaming APIs with this small library. * [Elysia Protobuf](https://github.com/ilyhalight/elysia-protobuf) - support protobuf for Elysia. * [Elysia Prometheus](https://github.com/m1handr/elysia-prometheus) - Elysia plugin for exposing HTTP metrics for Prometheus. * [Elysia Remote DTS](https://github.com/rayriffy/elysia-remote-dts) - A plugin that provide .d.ts types remotely for Eden Treaty to consume. * [Cap Checkpoint plugin for Elysia](https://capjs.js.org/guide/middleware/elysia.html) - Cloudflare-like middleware for Cap, a lightweight, modern open-source CAPTCHA alternative designed using SHA-256 PoW. * [Elysia Background](https://github.com/staciax/elysia-background) - A background task processing plugin for Elysia.js ## Complementary projects: * [prismabox](https://github.com/m1212e/prismabox) - Generator for typebox schemes based on your database models, works well with elysia *** If you have a plugin written for Elysia, feel free to add your plugin to the list by **clicking Edit this page on GitHub** below 👇 --- --- url: /quick-start.md --- # Quick Start Elysia is a TypeScript backend framework with multiple runtime support but optimized for Bun. However, you can use Elysia with other runtimes like Node.js. \ Elysia is optimized for Bun which is a JavaScript runtime that aims to be a drop-in replacement for Node.js. You can install Bun with the command below: ::: code-group ```bash [MacOS/Linux] curl -fsSL https://bun.sh/install | bash ``` ```bash [Windows] powershell -c "irm bun.sh/install.ps1 | iex" ``` ::: \ We recommend starting a new Elysia server using `bun create elysia`, which sets up everything automatically. ```bash bun create elysia app ``` Once done, you should see the folder name `app` in your directory. ```bash cd app ``` Start a development server by: ```bash bun dev ``` Navigate to [localhost:3000](http://localhost:3000) should greet you with "Hello Elysia". ::: tip Elysia ships you with `dev` command to automatically reload your server on file change. ::: To manually create a new Elysia app, install Elysia as a package: ```typescript bun add elysia bun add -d @types/bun ``` This will install Elysia and Bun type definitions. Create a new file `src/index.ts` and add the following code: ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/', () => 'Hello Elysia') .listen(3000) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` Open your `package.json` file and add the following scripts: ```json { "scripts": { "dev": "bun --watch src/index.ts", "build": "bun build src/index.ts --target bun --outdir ./dist", "start": "NODE_ENV=production bun dist/index.js", "test": "bun test" } } ``` These scripts refer to the different stages of developing an application: * **dev** - Start Elysia in development mode with auto-reload on code change. * **build** - Build the application for production usage. * **start** - Start an Elysia production server. If you are using TypeScript, make sure to create, and update `tsconfig.json` to include `compilerOptions.strict` to `true`: ```json { "compilerOptions": { "strict": true } } ``` Node.js is a JavaScript runtime for server-side applications, the most popular runtime for JavaScript which Elysia supports. You can install Node.js with the command below: ::: code-group ```bash [MacOS] brew install node ``` ```bash [Windows] choco install nodejs ``` ```bash [apt (Linux)] sudo apt install nodejs ``` ```bash [pacman (Arch)] pacman -S nodejs npm ``` ::: ## Setup We recommended using TypeScript for your Node.js project. \ To create a new Elysia app with TypeScript, we recommended install Elysia with `tsx`: ::: code-group ```bash [bun] bun add elysia @elysiajs/node && \ bun add -d tsx @types/node typescript ``` ```bash [pnpm] pnpm add elysia @elysiajs/node && \ pnpm add -D tsx @types/node typescript ``` ```bash [npm] npm install elysia @elysiajs/node && \ npm install --save-dev tsx @types/node typescript ``` ```bash [yarn] yarn add elysia @elysiajs/node && \ yarn add -D tsx @types/node typescript ``` ::: This will install Elysia, TypeScript, and `tsx`. `tsx` is a CLI that transpiles TypeScript to JavaScript with hot-reload and several more feature you expected from a modern development environment. Create a new file `src/index.ts` and add the following code: ```typescript import { Elysia } from 'elysia' import { node } from '@elysiajs/node' const app = new Elysia({ adapter: node() }) .get('/', () => 'Hello Elysia') .listen(3000, ({ hostname, port }) => { console.log( `🦊 Elysia is running at ${hostname}:${port}` ) }) ``` Open your `package.json` file and add the following scripts: ```json { "scripts": { "dev": "tsx watch src/index.ts", "build": "tsc src/index.ts --outDir dist", "start": "NODE_ENV=production node dist/index.js" } } ``` These scripts refer to the different stages of developing an application: * **dev** - Start Elysia in development mode with auto-reload on code change. * **build** - Build the application for production usage. * **start** - Start an Elysia production server. Make sure to create `tsconfig.json` ```bash npx tsc --init ``` Don't forget to update `tsconfig.json` to include `compilerOptions.strict` to `true`: ```json { "compilerOptions": { "strict": true } } ``` ::: warning If you use Elysia without TypeScript you may miss out on some features like auto-completion, advanced type checking and end-to-end type safety, which are the core features of Elysia. ::: To create a new Elysia app with JavaScript, starts by installing Elysia: ::: code-group ```bash [pnpm] bun add elysia @elysiajs/node ``` ```bash [pnpm] pnpm add elysia @elysiajs/node ``` ```bash [npm] npm install elysia @elysiajs/node ``` ```bash [yarn] yarn add elysia @elysiajs/node ``` ::: This will install Elysia, TypeScript, and `tsx`. `tsx` is a CLI that transpiles TypeScript to JavaScript with hot-reload and several more feature you expected from a modern development environment. Create a new file `src/index.ts` and add the following code: ```javascript import { Elysia } from 'elysia' import { node } from '@elysiajs/node' const app = new Elysia({ adapter: node() }) .get('/', () => 'Hello Elysia') .listen(3000, ({ hostname, port }) => { console.log( `🦊 Elysia is running at ${hostname}:${port}` ) }) ``` Open your `package.json` file and add the following scripts: ```json { "type", "module", "scripts": { "dev": "node src/index.ts", "start": "NODE_ENV=production node src/index.js" } } ``` These scripts refer to the different stages of developing an application: * **dev** - Start Elysia in development mode with auto-reload on code change. * **start** - Start an Elysia production server. Make sure to create `tsconfig.json` ```bash npx tsc --init ``` Don't forget to update `tsconfig.json` to include `compilerOptions.strict` to `true`: ```json { "compilerOptions": { "strict": true } } ``` Elysia is a WinterCG compliance library, which means if a framework or runtime supports Web Standard Request/Response, it can run Elysia. First, install Elysia with the command below: ::: code-group ```bash [bun] bun install elysia ``` ```bash [pnpm] pnpm install elysia ``` ```bash [npm] npm install elysia ``` ```bash [yarn] yarn add elysia ``` ::: Next, select a runtime that supports Web Standard Request/Response. We have a few recommendations: ### Not on the list? If you are using a custom runtime, you may access `app.fetch` to handle the request and response manually. ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/', () => 'Hello Elysia') .listen(3000) export default app.fetch console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` ## Next Steps We recommend checking out the either one of the following: If you have any questions, feel free to ask in our [Discord](https://discord.gg/eaFJ2KDJck) community. --- --- url: /integrations/react-email.md --- # React Email React Email is a library that allows you to use React components to create emails. As Elysia is using Bun as runtime environment, we can directly write a React Email component and import the JSX directly to our code to send emails. ## Installation To install React Email, run the following command: ```bash bun add -d react-email bun add @react-email/components react react-dom ``` Then add this script to `package.json`: ```json { "scripts": { "email": "email dev --dir src/emails" } } ``` We recommend adding email templates into the `src/emails` directory as we can directly import the JSX files. ### TypeScript If you are using TypeScript, you may need to add the following to your `tsconfig.json`: ```json { "compilerOptions": { "jsx": "react" } } ``` ## Your first email Create file `src/emails/otp.tsx` with the following code: ```tsx import * as React from 'react' import { Tailwind, Section, Text } from '@react-email/components' export default function OTPEmail({ otp }: { otp: number }) { return (
Verify your Email Address Use the following code to verify your email address {otp} This code is valid for 10 minutes Thank you joining us
) } OTPEmail.PreviewProps = { otp: 123456 } ``` You may notice that we are using `@react-email/components` to create the email template. This library provides a set of components including **styling with Tailwind** that are compatible with email clients like Gmail, Outlook, etc. We also added a `PreviewProps` to the `OTPEmail` function. This is only apply when previewing the email on our playground. ## Preview your email To preview your email, run the following command: ```bash bun email ``` This will open a browser window with the preview of your email. ![React Email playground showing an OTP email we have just written](/recipe/react-email/email-preview.webp) ## Sending email To send an email, we can use `react-dom/server` to render the the email then submit using a preferred provider: ::: code-group ```tsx [Nodemailer] import { Elysia, t } from 'elysia' import * as React from 'react' import { renderToStaticMarkup } from 'react-dom/server' import OTPEmail from './emails/otp' import nodemailer from 'nodemailer' // [!code ++] const transporter = nodemailer.createTransport({ // [!code ++] host: 'smtp.gehenna.sh', // [!code ++] port: 465, // [!code ++] auth: { // [!code ++] user: 'makoto', // [!code ++] pass: '12345678' // [!code ++] } // [!code ++] }) // [!code ++] new Elysia() .get('/otp', async ({ body }) => { // Random between 100,000 and 999,999 const otp = ~~(Math.random() * (900_000 - 1)) + 100_000 const html = renderToStaticMarkup() await transporter.sendMail({ // [!code ++] from: 'ibuki@gehenna.sh', // [!code ++] to: body, // [!code ++] subject: 'Verify your email address', // [!code ++] html, // [!code ++] }) // [!code ++] return { success: true } }, { body: t.String({ format: 'email' }) }) .listen(3000) ``` ```tsx [Resend] import { Elysia, t } from 'elysia' import OTPEmail from './emails/otp' import Resend from 'resend' // [!code ++] const resend = new Resend('re_123456789') // [!code ++] new Elysia() .get('/otp', ({ body }) => { // Random between 100,000 and 999,999 const otp = ~~(Math.random() * (900_000 - 1)) + 100_000 await resend.emails.send({ // [!code ++] from: 'ibuki@gehenna.sh', // [!code ++] to: body, // [!code ++] subject: 'Verify your email address', // [!code ++] html: , // [!code ++] }) // [!code ++] return { success: true } }, { body: t.String({ format: 'email' }) }) .listen(3000) ``` ```tsx [AWS SES] import { Elysia, t } from 'elysia' import * as React from 'react' import { renderToStaticMarkup } from 'react-dom/server' import OTPEmail from './emails/otp' import { type SendEmailCommandInput, SES } from '@aws-sdk/client-ses' // [!code ++] import { fromEnv } from '@aws-sdk/credential-providers' // [!code ++] const ses = new SES({ // [!code ++] credentials: // [!code ++] process.env.NODE_ENV === 'production' ? fromEnv() : undefined // [!code ++] }) // [!code ++] new Elysia() .get('/otp', ({ body }) => { // Random between 100,000 and 999,999 const otp = ~~(Math.random() * (900_000 - 1)) + 100_000 const html = renderToStaticMarkup() await ses.sendEmail({ // [!code ++] Source: 'ibuki@gehenna.sh', // [!code ++] Destination: { // [!code ++] ToAddresses: [body] // [!code ++] }, // [!code ++] Message: { // [!code ++] Body: { // [!code ++] Html: { // [!code ++] Charset: 'UTF-8', // [!code ++] Data: html // [!code ++] } // [!code ++] }, // [!code ++] Subject: { // [!code ++] Charset: 'UTF-8', // [!code ++] Data: 'Verify your email address' // [!code ++] } // [!code ++] } // [!code ++] } satisfies SendEmailCommandInput) // [!code ++] return { success: true } }, { body: t.String({ format: 'email' }) }) .listen(3000) ``` ```tsx [Sendgrid] import { Elysia, t } from 'elysia' import OTPEmail from './emails/otp' import sendgrid from "@sendgrid/mail" // [!code ++] sendgrid.setApiKey(process.env.SENDGRID_API_KEY) // [!code ++] new Elysia() .get('/otp', ({ body }) => { // Random between 100,000 and 999,999 const otp = ~~(Math.random() * (900_000 - 1)) + 100_000 const html = renderToStaticMarkup() await sendgrid.send({ // [!code ++] from: 'ibuki@gehenna.sh', // [!code ++] to: body, // [!code ++] subject: 'Verify your email address', // [!code ++] html // [!code ++] }) // [!code ++] return { success: true } }, { body: t.String({ format: 'email' }) }) .listen(3000) ``` ::: ::: tip Notice that we can directly import the email component out of the box thanks to Bun ::: You may see all of the available integration with React Email in the [React Email Integration](https://react.email/docs/integrations/overview), and learn more about React Email in [React Email documentation](https://react.email/docs) --- --- url: /patterns/cookie.md --- # Cookie To use Cookie, you can extract the cookie property and access its name and value directly. There's no get/set, you can extract the cookie name and retrieve or update its value directly. ```ts import { Elysia } from 'elysia' new Elysia() .get('/', ({ cookie: { name } }) => { // Get name.value // Set name.value = "New Value" }) ``` By default, Reactive Cookie can encode/decode type of object automatically allowing us to treat cookie as an object without worrying about the encoding/decoding. **It just works**. ## Reactivity The Elysia cookie is reactive. This means that when you change the cookie value, the cookie will be updated automatically based on approach like signal. A single source of truth for handling cookies is provided by Elysia cookies, which have the ability to automatically set headers and sync cookie values. Since cookies are Proxy-dependent objects by default, the extract value can never be **undefined**; instead, it will always be a value of `Cookie`, which can be obtained by invoking the **.value** property. We can treat the cookie jar as a regular object, iteration over it will only iterate over an already-existing cookie value. ## Cookie Attribute To use Cookie attribute, you can either use one of the following: 1. Setting the property directly 2. Using `set` or `add` to update cookie property. See [cookie attribute config](/patterns/cookie.html#config) for more information. ### Assign Property You can get/set the property of a cookie like any normal object, the reactivity model synchronizes the cookie value automatically. ```ts import { Elysia } from 'elysia' new Elysia() .get('/', ({ cookie: { name } }) => { // get name.domain // set name.domain = 'millennium.sh' name.httpOnly = true }) ``` ## set **set** permits updating multiple cookie properties all at once through **reset all property** and overwrite the property with a new value. ```ts import { Elysia } from 'elysia' new Elysia() .get('/', ({ cookie: { name } }) => { name.set({ domain: 'millennium.sh', httpOnly: true }) }) ``` ## add Like **set**, **add** allow us to update multiple cookie properties at once, but instead, will only overwrite the property defined instead of resetting. ## remove To remove a cookie, you can use either: 1. name.remove 2. delete cookie.name ```ts import { Elysia } from 'elysia' new Elysia() .get('/', ({ cookie, cookie: { name } }) => { name.remove() delete cookie.name }) ``` ## Cookie Schema You can strictly validate cookie type and providing type inference for cookie by using cookie schema with `t.Cookie`. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/', ({ cookie: { name } }) => { // Set name.value = { id: 617, name: 'Summoning 101' } }, { cookie: t.Cookie({ name: t.Object({ id: t.Numeric(), name: t.String() }) }) }) ``` ## Nullable Cookie To handle nullable cookie value, you can use `t.Optional` on the cookie name you want to be nullable. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/', ({ cookie: { name } }) => { // Set name.value = { id: 617, name: 'Summoning 101' } }, { cookie: t.Cookie({ name: t.Optional( t.Object({ id: t.Numeric(), name: t.String() }) ) }) }) ``` ## Cookie Signature With an introduction of Cookie Schema, and `t.Cookie` type, we can 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. ## Using Cookie Signature By provide a cookie secret, and `sign` property to indicate which cookie should have a signature verification. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/', ({ cookie: { profile } }) => { profile.value = { id: 617, name: 'Summoning 101' } }, { cookie: t.Cookie({ profile: t.Object({ id: t.Numeric(), name: t.String() }) }, { secrets: 'Fischl von Luftschloss Narfidort', sign: ['profile'] }) }) ``` Elysia then sign and unsign cookie value automatically. ## Constructor You can use Elysia constructor to set global cookie `secret`, and `sign` value to apply to all route globally instead of inlining to every route you need. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia({ cookie: { secrets: 'Fischl von Luftschloss Narfidort', sign: ['profile'] } }) .get('/', ({ cookie: { profile } }) => { profile.value = { id: 617, name: 'Summoning 101' } }, { cookie: t.Cookie({ profile: t.Object({ id: t.Numeric(), name: t.String() }) }) }) ``` ## Cookie Rotation Elysia handle Cookie's secret rotation automatically. Cookie Rotation is a migration technique to sign a cookie with a newer secret, while also be able to verify the old signature of the cookie. ```ts import { Elysia } from 'elysia' new Elysia({ cookie: { secrets: ['Vengeance will be mine', 'Fischl von Luftschloss Narfidort'] } }) ``` ## Config Below is a cookie config accepted by Elysia. ### secret The secret key for signing/un-signing cookies. If an array is passed, will use Key Rotation. Key rotation is when an encryption key is retired and replaced by generating a new cryptographic key. *** Below is a config that extends from [cookie](https://npmjs.com/package/cookie) ### domain Specifies the value for the [Domain Set-Cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.3). By default, no domain is set, and most clients will consider the cookie to apply to only the current domain. ### encode @default `encodeURIComponent` Specifies a function that will be used to encode a cookie's value. Since the value of a cookie has a limited character set (and must be a simple string), this function can be used to encode a value into a string suited for a cookie's value. The default function is the global `encodeURIComponent`, which will encode a JavaScript string into UTF-8 byte sequences and then URL-encode any that fall outside of the cookie range. ### expires Specifies the Date object to be the value for the [Expires Set-Cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.1). By default, no expiration is set, and most clients will consider this a "non-persistent cookie" and will delete it on a condition like exiting a web browser application. ::: tip The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but not all clients may obey this, so if both are set, they should point to the same date and time. ::: ### httpOnly @default `false` Specifies the boolean value for the [HttpOnly Set-Cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6). When truthy, the HttpOnly attribute is set, otherwise, it is not. By default, the HttpOnly attribute is not set. ::: tip be careful when setting this to true, as compliant clients will not allow client-side JavaScript to see the cookie in `document.cookie`. ::: ### maxAge @default `undefined` Specifies the number (in seconds) to be the value for the [Max-Age Set-Cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.2). The given number will be converted to an integer by rounding down. By default, no maximum age is set. ::: tip The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but not all clients may obey this, so if both are set, they should point to the same date and time. ::: ### path Specifies the value for the [Path Set-Cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.4). By default, the path handler is considered the default path. ### priority Specifies the string to be the value for the [Priority Set-Cookie attribute](https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1). `low` will set the Priority attribute to Low. `medium` will set the Priority attribute to Medium, the default priority when not set. `high` will set the Priority attribute to High. More information about the different priority levels can be found in [the specification](https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1). ::: tip This is an attribute that has not yet been fully standardized and may change in the future. This also means many clients may ignore this attribute until they understand it. ::: ### sameSite Specifies the boolean or string to be the value for the [SameSite Set-Cookie attribute](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7). `true` will set the SameSite attribute to Strict for strict same-site enforcement. `false` will not set the SameSite attribute. `'lax'` will set the SameSite attribute to Lax for lax same-site enforcement. `'none'` will set the SameSite attribute to None for an explicit cross-site cookie. `'strict'` will set the SameSite attribute to Strict for strict same-site enforcement. More information about the different enforcement levels can be found in [the specification](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7). ::: tip This is an attribute that has not yet been fully standardized and may change in the future. This also means many clients may ignore this attribute until they understand it. ::: ### secure Specifies the boolean value for the [Secure Set-Cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5). When truthy, the Secure attribute is set, otherwise, it is not. By default, the Secure attribute is not set. ::: tip Be careful when setting this to true, as compliant clients will not send the cookie back to the server in the future if the browser does not have an HTTPS connection. ::: --- --- url: /essential/route.md --- # Routing Web servers use the request's **path and HTTP method** to look up the correct resource, refers as **"routing"**. We can define a route by calling a **method named after HTTP verbs**, passing a path and a function to execute when matched. ```typescript import { Elysia } from 'elysia' new Elysia() .get('/', 'hello') .get('/hi', 'hi') .listen(3000) ``` We can access the web server by going to **http://localhost:3000** By default, web browsers will send a GET method when visiting a page. ::: tip Using an interactive browser above, hover on a blue highlight area to see difference result between each path ::: ## Path type Path in Elysia can be grouped into 3 types: * **static paths** - static string to locate the resource * **dynamic paths** - segment can be any value * **wildcards** - path until a specific point can be anything You can use all of the path types together to compose a behavior for your web server. The priorities are as follows: 1. static paths 2. dynamic paths 3. wildcards If the path is resolved as the static wild dynamic path is presented, Elysia will resolve the static path rather than the dynamic path ```typescript import { Elysia } from 'elysia' new Elysia() .get('/id/1', 'static path') .get('/id/:id', 'dynamic path') .get('/id/*', 'wildcard path') .listen(3000) ``` Here the server will respond as follows: | Path | Response | | ------- | ------------- | | /id/1 | static path | | /id/2 | dynamic path | | /id/2/a | wildcard path | ## Static Path A path or pathname is an identifier to locate resources of a server. ```bash http://localhost:/path/page ``` Elysia uses the path and method to look up the correct resource. A path starts after the origin. Prefix with **/** and ends before search query **(?)** We can categorize the URL and path as follows: | URL | Path | | ------------------------------- | ------------ | | http://example.com/ | / | | http://example.com/hello | /hello | | http://example.com/hello/world | /hello/world | | http://example.com/hello?name=salt | /hello | | http://example.com/hello#title | /hello | ::: tip If the path is not specified, the browser and web server will treat the path as '/' as a default value. ::: Elysia will look up each request for [route](/essential/route) and response using [handler](/essential/handler) function. ## Dynamic path URLs can be both static and dynamic. Static paths are hardcoded strings that can be used to locate resources of the server, while dynamic paths match some part and captures the value to extract extra information. For instance, we can extract the user ID from the pathname. For example: ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/id/:id', ({ params: { id } }) => id) // ^? .listen(3000) ``` Here dynamic path is created with `/id/:id` which tells Elysia to match any path up until `/id`. What comes after that is then stored as **params** object. When requested, the server should return the response as follows: | Path | Response | | ---------------------- | --------- | | /id/1 | 1 | | /id/123 | 123 | | /id/anything | anything | | /id/anything?name=salt | anything | | /id | Not Found | | /id/anything/rest | Not Found | Dynamic paths are great to include things like IDs, which then can be used later. We refer to the named variable path as **path parameter** or **params** for short. ## Segment URL segments are each path that is composed into a full path. Segments are separated by `/`. ![Representation of URL segments](/essential/url-segment.webp) Path parameters in Elysia are represented by prefixing a segment with ':' followed by a name. ![Representation of path parameter](/essential/path-parameter.webp) Path parameters allow Elysia to capture a specific segment of a URL. The named path parameter will then be stored in `Context.params`. | Route | Path | Params | | --------- | ------ | ------- | | /id/:id | /id/1 | id=1 | | /id/:id | /id/hi | id=hi | | /id/:name | /id/hi | name=hi | ## Multiple path parameters You can have as many path parameters as you like, which will then be stored into a `params` object. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/id/:id', ({ params: { id } }) => id) .get('/id/:id/:name', ({ params: { id, name } }) => id + ' ' + name) // ^? .listen(3000) ``` The server will respond as follows: | Path | Response | | ---------------------- | ------------- | | /id/1 | 1 | | /id/123 | 123 | | /id/anything | anything | | /id/anything?name=salt | anything | | /id | Not Found | | /id/anything/rest | anything rest | ## Optional path parameters Sometime we might want a static and dynamic path to resolve the same handler. We can make a path parameter optional by adding a question mark `?` after the parameter name. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/id/:id?', ({ params: { id } }) => `id ${id}`) // ^? .listen(3000) ``` The server will respond as follows: | Path | Response | | ---------------------- | ------------- | | /id | id undefined | | /id/1 | id 1 | ## Wildcards Dynamic paths allow capturing certain segments of the URL. However, when you need a value of the path to be more dynamic and want to capture the rest of the URL segment, a wildcard can be used. Wildcards can capture the value after segment regardless of amount by using "\*". ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/id/*', ({ params }) => params['*']) // ^? .listen(3000) ``` In this case the server will respond as follows: | Path | Response | | ---------------------- | ------------- | | /id/1 | 1 | | /id/123 | 123 | | /id/anything | anything | | /id/anything?name=salt | anything | | /id | Not Found | | /id/anything/rest | anything/rest | Wildcards are useful for capturing a path until a specific point. ::: tip You can use a wildcard with a path parameter. ::: ## HTTP Verb HTTP defines a set of request methods to indicate the desired action to be performed for a given resource There are several HTTP verbs, but the most common ones are: ### GET Requests using GET should only retrieve data. ### POST Submits a payload to the specified resource, often causing state change or side effect. ### PUT Replaces all current representations of the target resource using the request's payload. ### PATCH Applies partial modifications to a resource. ### DELETE Deletes the specified resource. *** To handle each of the different verbs, Elysia has a built-in API for several HTTP verbs by default, similar to `Elysia.get` ```typescript import { Elysia } from 'elysia' new Elysia() .get('/', 'hello') .post('/hi', 'hi') .listen(3000) ``` Elysia HTTP methods accepts the following parameters: * **path**: Pathname * **function**: Function to respond to the client * **hook**: Additional metadata You can read more about the HTTP methods on [HTTP Request Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods). ## Custom Method We can accept custom HTTP Methods with `Elysia.route`. ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/get', 'hello') .post('/post', 'hi') .route('M-SEARCH', '/m-search', 'connect') // [!code ++] .listen(3000) ``` **Elysia.route** accepts the following: * **method**: HTTP Verb * **path**: Pathname * **function**: Function to response to the client * **hook**: Additional metadata When navigating to each method, you should see the results as the following: | Path | Method | Result | | --------- | -------- | ------- | | /get | GET | hello | | /post | POST | hi | | /m-search | M-SEARCH | connect | ::: tip Based on [RFC 7231](https://www.rfc-editor.org/rfc/rfc7231#section-4.1), HTTP Verb is case-sensitive. It's recommended to use the UPPERCASE convention for defining a custom HTTP Verb with Elysia. ::: ## Elysia.all Elysia provides an `Elysia.all` for handling any HTTP method for a specified path using the same API like **Elysia.get** and **Elysia.post** ```typescript import { Elysia } from 'elysia' new Elysia() .all('/', 'hi') .listen(3000) ``` Any HTTP method that matches the path, will be handled as follows: | Path | Method | Result | | ---- | -------- | ------ | | / | GET | hi | | / | POST | hi | | / | DELETE | hi | ## Handle Most developers use REST clients like Postman, Insomnia or Hoppscotch to test their API. However, Elysia can be programmatically test using `Elysia.handle`. ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/', 'hello') .post('/hi', 'hi') .listen(3000) app.handle(new Request('http://localhost/')).then(console.log) ``` **Elysia.handle** is a function to process an actual request sent to the server. ::: tip Unlike unit test's mock, **you can expect it to behave like an actual request** sent to the server. But also useful for simulating or creating unit tests. ::: ## 404 If no path matches the defined routes, Elysia will pass the request to [error](/essential/life-cycle.html#on-error) life cycle before returning a **"NOT\_FOUND"** with an HTTP status of 404. We can handle a custom 404 error by returning a value from `error` life cycle like this: ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/', 'hi') .onError(({ code }) => { if (code === 'NOT_FOUND') { return 'Route not found :(' } }) .listen(3000) ``` When navigating to your web server, you should see the result as follows: | Path | Method | Result | | ---- | ------ | ------------------- | | / | GET | hi | | / | POST | Route not found :( | | /hi | GET | Route not found :( | You can learn more about life cycle and error handling in [Life Cycle Events](/essential/life-cycle#events) and [Error Handling](/essential/life-cycle.html#on-error). ::: tip HTTP Status is used to indicate the type of response. By default if everything is correct, the server will return a '200 OK' status code (If a route matches and there is no error, Elysia will return 200 as default) If the server fails to find any route to handle, like in this case, then the server shall return a '404 NOT FOUND' status code. ::: ## Group When creating a web server, you would often have multiple routes sharing the same prefix: ```typescript import { Elysia } from 'elysia' new Elysia() .post('/user/sign-in', 'Sign in') .post('/user/sign-up', 'Sign up') .post('/user/profile', 'Profile') .listen(3000) ``` This can be improved with `Elysia.group`, allowing us to apply prefixes to multiple routes at the same time by grouping them together: ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .group('/user', (app) => app .post('/sign-in', 'Sign in') .post('/sign-up', 'Sign up') .post('/profile', 'Profile') ) .listen(3000) ``` This code behaves the same as our first example and should be structured as follows: | Path | Result | | ------------- | ------- | | /user/sign-in | Sign in | | /user/sign-up | Sign up | | /user/profile | Profile | `.group()` can also accept an optional guard parameter to reduce boilerplate of using groups and guards together: ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .group( '/user', { body: t.Literal('Rikuhachima Aru') }, (app) => app .post('/sign-in', 'Sign in') .post('/sign-up', 'Sign up') .post('/profile', 'Profile') ) .listen(3000) ``` You may find more information about grouped guards in [scope](/essential/plugin.html#scope). ### Prefix We can separate a group into a separate plugin instance to reduce nesting by providing a **prefix** to the constructor. ```typescript import { Elysia } from 'elysia' const users = new Elysia({ prefix: '/user' }) .post('/sign-in', 'Sign in') .post('/sign-up', 'Sign up') .post('/profile', 'Profile') new Elysia() .use(users) .get('/', 'hello world') .listen(3000) ``` --- --- url: /plugins/server-timing.md --- # Server Timing Plugin This plugin adds support for auditing performance bottlenecks with Server Timing API Install with: ```bash bun add @elysiajs/server-timing ``` Then use it: ```typescript twoslash import { Elysia } from 'elysia' import { serverTiming } from '@elysiajs/server-timing' new Elysia() .use(serverTiming()) .get('/', () => 'hello') .listen(3000) ``` Server Timing then will append header 'Server-Timing' with log duration, function name, and detail for each life-cycle function. To inspect, open browser developer tools > Network > \[Request made through Elysia server] > Timing. ![Developer tools showing Server Timing screenshot](/assets/server-timing.webp) Now you can effortlessly audit the performance bottleneck of your server. ## Config Below is a config which is accepted by the plugin ### enabled @default `NODE_ENV !== 'production'` Determine whether or not Server Timing should be enabled ### allow @default `undefined` A condition whether server timing should be log ### trace @default `undefined` Allow Server Timing to log specified life-cycle events: Trace accepts objects of the following: * request: capture duration from request * parse: capture duration from parse * transform: capture duration from transform * beforeHandle: capture duration from beforeHandle * handle: capture duration from the handle * afterHandle: capture duration from afterHandle * total: capture total duration from start to finish ## Pattern Below you can find the common patterns to use the plugin. * [Allow Condition](#allow-condition) ## Allow Condition You may disable Server Timing on specific routes via `allow` property ```ts twoslash import { Elysia } from 'elysia' import { serverTiming } from '@elysiajs/server-timing' new Elysia() .use( serverTiming({ allow: ({ request }) => { return new URL(request.url).pathname !== '/no-trace' } }) ) ``` --- --- url: /plugins/static.md --- # Static Plugin This plugin can serve static files/folders for Elysia Server Install with: ```bash bun add @elysiajs/static ``` Then use it: ```typescript twoslash import { Elysia } from 'elysia' import { staticPlugin } from '@elysiajs/static' new Elysia() .use(staticPlugin()) .listen(3000) ``` By default, the static plugin default folder is `public`, and registered with `/public` prefix. Suppose your project structure is: ``` | - src | - index.ts | - public | - takodachi.png | - nested | - takodachi.png ``` The available path will become: * /public/takodachi.png * /public/nested/takodachi.png ## Config Below is a config which is accepted by the plugin ### assets @default `"public"` Path to the folder to expose as static ### prefix @default `"/public"` Path prefix to register public files ### ignorePatterns @default `[]` List of files to ignore from serving as static files ### staticLimit @default `1024` By default, the static plugin will register paths to the Router with a static name, if the limits are exceeded, paths will be lazily added to the Router to reduce memory usage. Tradeoff memory with performance. ### alwaysStatic @default `false` If set to true, static files path will be registered to Router skipping the `staticLimits`. ### headers @default `{}` Set response headers of files ### indexHTML @default `false` If set to true, the `index.html` file from the static directory will be served for any request that is matching neither a route nor any existing static file. ## Pattern Below you can find the common patterns to use the plugin. * [Single File](#single-file) ## Single file Suppose you want to return just a single file, you can use `file` instead of using the static plugin ```typescript twoslash import { Elysia, file } from 'elysia' new Elysia() .get('/file', file('public/takodachi.png')) ``` --- --- url: /plugins/stream.md --- # Stream Plugin ::: warning This plugin is in maintenance mode and will not receive new features. We recommend using the [Generator Stream instead](/essential/handler#stream) ::: This plugin adds support for streaming response or sending Server-Sent Event back to the client. Install with: ```bash bun add @elysiajs/stream ``` Then use it: ```typescript import { Elysia } from 'elysia' import { Stream } from '@elysiajs/stream' new Elysia() .get('/', () => new Stream(async (stream) => { stream.send('hello') await stream.wait(1000) stream.send('world') stream.close() })) .listen(3000) ``` By default, `Stream` will return `Response` with `content-type` of `text/event-stream; charset=utf8`. ## Constructor Below is the constructor parameter accepted by `Stream`: 1. Stream: * Automatic: Automatically stream response from a provided value * Iterable * AsyncIterable * ReadableStream * Response * Manual: Callback of `(stream: this) => unknown` or `undefined` 2. Options: `StreamOptions` * [event](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event): A string identifying the type of event described * [retry](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#retry): The reconnection time in milliseconds ## Method Below is the method provided by `Stream`: ### send Enqueue data to stream to send back to the client ### close Close the stream ### wait Return a promise that resolves in the provided value in ms ### value Inner value of the `ReadableStream` ## Pattern Below you can find the common patterns to use the plugin. * [OpenAI](#openai) * [Fetch Stream](#fetch-stream) * [Server Sent Event](#server-sent-event) ## OpenAI Automatic mode is triggered when the parameter is either `Iterable` or `AsyncIterable` streaming the response back to the client automatically. Below is an example of integrating ChatGPT into Elysia. ```ts new Elysia() .get( '/ai', ({ query: { prompt } }) => new Stream( openai.chat.completions.create({ model: 'gpt-3.5-turbo', stream: true, messages: [{ role: 'user', content: prompt }] }) ) ) ``` By default [openai](https://npmjs.com/package/openai) chatGPT completion returns `AsyncIterable` so you should be able to wrap the OpenAI in `Stream`. ## Fetch Stream You can pass a fetch from an endpoint that returns the stream to proxy a stream. This is useful for those endpoints that use AI text generation since you can proxy it directly, eg. [Cloudflare AI](https://developers.cloudflare.com/workers-ai/models/llm/#examples---chat-style-with-system-prompt-preferred). ```ts const model = '@cf/meta/llama-2-7b-chat-int8' const endpoint = `https://api.cloudflare.com/client/v4/accounts/${process.env.ACCOUNT_ID}/ai/run/${model}` new Elysia() .get('/ai', ({ query: { prompt } }) => fetch(endpoint, { method: 'POST', headers: { authorization: `Bearer ${API_TOKEN}`, 'content-type': 'application/json' }, body: JSON.stringify({ messages: [ { role: 'system', content: 'You are a friendly assistant' }, { role: 'user', content: prompt } ] }) }) ) ``` ## Server Sent Event Manual mode is triggered when the parameter is either `callback` or `undefined`, allowing you to control the stream. ### callback-based Below is an example of creating a Server-Sent Event endpoint using a constructor callback ```ts new Elysia() .get('/source', () => new Stream((stream) => { const interval = setInterval(() => { stream.send('hello world') }, 500) setTimeout(() => { clearInterval(interval) stream.close() }, 3000) }) ) ``` ### value-based Below is an example of creating a Server-Sent Event endpoint using a value-based ```ts new Elysia() .get('/source', () => { const stream = new Stream() const interval = setInterval(() => { stream.send('hello world') }, 500) setTimeout(() => { clearInterval(interval) stream.close() }, 3000) return stream }) ``` Both callback-based and value-based streams work in the same way but with different syntax for your preference. --- --- url: /essential/structure.md --- #### This page has been moved to [best practice](/essential/best-practice) # Structure 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 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) }) ``` ## 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({ status, cookie: { session } }: Context) { if (session.value) return status(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: Request dependent service as Elysia instance We recommended to abstract service class away from Elysia. However, **if the service is a request dependent service** or need to process HTTP request. We recommended to abstract it as Elysia instance 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, status }) => { if (!Auth?.user || !Auth.user) return status(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({ status, cookie: { session } }: InferContext) { if (session.value) return status(401) } } ``` However we recommend to avoid this if possible, and use [Elysia as a service](✅-do-use-elysia-instance-as-a-service) 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)](/validation/overview.html#data-validation). 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() }) } ``` ### 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.html#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 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 [remapping](/patterns/remapping.html#remapping). 3. Show up as "models" in OpenAPI compliance client, eg. Swagger. 4. Improve TypeScript inference speed as model type will be cached during registration. *** As mentioned, Elysia is a pattern-agnostic framework, and we only provide a recommendation guide for handling Elysia with the MVC pattern. It’s entirely up to you and your team whether to follow this recommendation based on your preferences and agreement. --- --- url: /plugins/swagger.md --- # Swagger Plugin This plugin generates a Swagger endpoint for an Elysia server Install with: ```bash bun add @elysiajs/swagger ``` Then use it: ```typescript twoslash import { Elysia } from 'elysia' import { swagger } from '@elysiajs/swagger' new Elysia() .use(swagger()) .get('/', () => 'hi') .post('/hello', () => 'world') .listen(3000) ``` Accessing `/swagger` would show you a Scalar UI with the generated endpoint documentation from the Elysia server. You can also access the raw OpenAPI spec at `/swagger/json`. ## Config Below is a config which is accepted by the plugin ### provider @default `scalar` UI Provider for documentation. Default to Scalar. ### scalar Configuration for customizing Scalar. Please refer to the [Scalar config](https://github.com/scalar/scalar/blob/main/documentation/configuration.md) ### swagger Configuration for customizing Swagger. Please refer to the [Swagger specification](https://swagger.io/specification/v2/). ### excludeStaticFile @default `true` Determine if Swagger should exclude static files. ### path @default `/swagger` Endpoint to expose Swagger ### exclude Paths to exclude from Swagger documentation. Value can be one of the following: * **string** * **RegExp** * **Array\** ## Pattern Below you can find the common patterns to use the plugin. ## Change Swagger Endpoint You can change the swagger endpoint by setting [path](#path) in the plugin config. ```typescript twoslash import { Elysia } from 'elysia' import { swagger } from '@elysiajs/swagger' new Elysia() .use( swagger({ path: '/v2/swagger' }) ) .listen(3000) ``` ## Customize Swagger info ```typescript twoslash import { Elysia } from 'elysia' import { swagger } from '@elysiajs/swagger' new Elysia() .use( swagger({ documentation: { info: { title: 'Elysia Documentation', version: '1.0.0' } } }) ) .listen(3000) ``` ## Using Tags Elysia can separate the endpoints into groups by using the Swaggers tag system Firstly define the available tags in the swagger config object ```typescript app.use( swagger({ documentation: { tags: [ { name: 'App', description: 'General endpoints' }, { name: 'Auth', description: 'Authentication endpoints' } ] } }) ) ``` Then use the details property of the endpoint configuration section to assign that endpoint to the group ```typescript app.get('/', () => 'Hello Elysia', { detail: { tags: ['App'] } }) app.group('/auth', (app) => app.post( '/sign-up', async ({ body }) => db.user.create({ data: body, select: { id: true, username: true } }), { detail: { tags: ['Auth'] } } ) ) ``` Which will produce a swagger page like the following ## Security Configuration To secure your API endpoints, you can define security schemes in the Swagger configuration. The example below demonstrates how to use Bearer Authentication (JWT) to protect your endpoints: ```typescript app.use( swagger({ documentation: { components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' } } } } }) ) export const addressController = new Elysia({ prefix: '/address', detail: { tags: ['Address'], security: [ { bearerAuth: [] } ] } }) ``` This configuration ensures that all endpoints under the `/address` prefix require a valid JWT token for access. --- --- url: /patterns/unit-test.md --- # Unit Test Being WinterCG compliant, we can use Request / Response classes to test an Elysia server. Elysia provides the **Elysia.handle** method, which accepts a Web Standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and returns [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response), simulating an HTTP Request. Bun includes a built-in [test runner](https://bun.sh/docs/cli/test) that offers a Jest-like API through the `bun:test` module, facilitating the creation of unit 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 { 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') }) }) ``` Then we can perform tests by running **bun test** ```bash bun test ``` New requests to an Elysia server must be a fully valid URL, **NOT** a part of a URL. The request must provide URL as the following: | URL | Valid | | --------------------- | ----- | | http://localhost/user | ✅ | | /user | ❌ | We can also use other testing libraries like Jest to create Elysia unit tests. ## Eden Treaty test We may use Eden Treaty to create an end-to-end type safety test for Elysia server as follows: ```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, error } = await api.hello.get() expect(data).toBe('hi') // ^? }) }) ``` See [Eden Treaty Unit Test](/eden/treaty/unit-test) for setup and more information. --- --- url: /patterns/trace.md --- # Trace Performance is an important aspect for Elysia. We don't want to be fast for benchmarking purposes, we want you to have a real fast server in real-world scenario. There are many factors that can slow down our app - and it's hard to identify them, but **trace** can helps solve that problem by injecting start and stop code to each life-cycle. Trace allows us to inject code to before and after of each life-cycle event, block and interact with the execution of the function. ## Trace Trace use a callback listener to ensure that callback function is finished before moving on to the next lifecycle event. To use `trace`, you need to call `trace` method on the Elysia instance, and pass a callback function that will be executed for each life-cycle event. You may listen to each lifecycle by adding `on` prefix follows by life-cycle name, for example `onHandle` to listen to `handle` event. ```ts twoslash import { Elysia } from 'elysia' const app = new Elysia() .trace(async ({ onHandle }) => { onHandle(({ begin, onStop }) => { onStop(({ end }) => { console.log('handle took', end - begin, 'ms') }) }) }) .get('/', () => 'Hi') .listen(3000) ``` Please refer to [Life Cycle Events](/essential/life-cycle#events) for more information: ![Elysia Life Cycle](/assets/lifecycle-chart.svg) ## Children Every events except `handle` have a children, which is an array of events that are executed inside for each life-cycle event. You can use `onEvent` to listen to each child event in order ```ts twoslash import { Elysia } from 'elysia' const sleep = (time = 1000) => new Promise((resolve) => setTimeout(resolve, time)) const app = new Elysia() .trace(async ({ onBeforeHandle }) => { onBeforeHandle(({ total, onEvent }) => { console.log('total children:', total) onEvent(({ onStop }) => { onStop(({ elapsed }) => { console.log('child took', elapsed, 'ms') }) }) }) }) .get('/', () => 'Hi', { beforeHandle: [ function setup() {}, async function delay() { await sleep() } ] }) .listen(3000) ``` In this example, total children will be `2` because there are 2 children in the `beforeHandle` event. Then we listen to each child event by using `onEvent` and print the duration of each child event. ## Trace Parameter When each lifecycle is called ```ts twoslash import { Elysia } from 'elysia' const app = new Elysia() // This is trace parameter // hover to view the type .trace((parameter) => { }) .get('/', () => 'Hi') .listen(3000) ``` `trace` accept the following parameters: ### id - `number` Randomly generated unique id for each request ### context - `Context` Elysia's [Context](/essential/handler.html#context), eg. `set`, `store`, `query`, `params` ### set - `Context.set` Shortcut for `context.set`, to set a headers or status of the context ### store - `Singleton.store` Shortcut for `context.store`, to access a data in the context ### time - `number` Timestamp of when request is called ### on\[Event] - `TraceListener` An event listener for each life-cycle event. You may listen to the following life-cycle: * **onRequest** - get notified of every new request * **onParse** - array of functions to parse the body * **onTransform** - transform request and context before validation * **onBeforeHandle** - custom requirement to check before the main handler, can skip the main handler if response returned. * **onHandle** - function assigned to the path * **onAfterHandle** - interact with the response before sending it back to the client * **onMapResponse** - map returned value into a Web Standard Response * **onError** - handle error thrown during processing request * **onAfterResponse** - cleanup function after response is sent ## Trace Listener A listener for each life-cycle event ```ts twoslash import { Elysia } from 'elysia' const app = new Elysia() .trace(({ onBeforeHandle }) => { // This is trace listener // hover to view the type onBeforeHandle((parameter) => { }) }) .get('/', () => 'Hi') .listen(3000) ``` Each lifecycle listener accept the following ### name - `string` The name of the function, if the function is anonymous, the name will be `anonymous` ### begin - `number` The time when the function is started ### end - `Promise` The time when the function is ended, will be resolved when the function is ended ### error - `Promise` Error that was thrown in the lifecycle, will be resolved when the function is ended ### onStop - `callback?: (detail: TraceEndDetail) => any` A callback that will be executed when the lifecycle is ended ```ts twoslash import { Elysia } from 'elysia' const app = new Elysia() .trace(({ onBeforeHandle, set }) => { onBeforeHandle(({ onStop }) => { onStop(({ elapsed }) => { set.headers['X-Elapsed'] = elapsed.toString() }) }) }) .get('/', () => 'Hi') .listen(3000) ``` It's recommended to mutate context in this function as there's a lock mechanism to ensure the context is mutate successfully before moving on to the next lifecycle event ## TraceEndDetail A parameter that passed to `onStop` callback ### end - `number` The time when the function is ended ### error - `Error | null` Error that was thrown in the lifecycle ### elapsed - `number` Elapsed time of the lifecycle or `end - begin` --- --- url: /plugins/trpc.md --- # tRPC Plugin This plugin adds support for using [tRPC](https://trpc.io/) Install with: ```bash bun add @elysiajs/trpc @trpc/server @elysiajs/websocket ``` Then use it: ```typescript import { Elysia, t as T } from 'elysia' import { initTRPC } from '@trpc/server' import { compile as c, trpc } from '@elysiajs/trpc' const t = initTRPC.create() const p = t.procedure const router = t.router({ greet: p // 💡 Using Zod //.input(z.string()) // 💡 Using Elysia's T .input(c(T.String())) .query(({ input }) => input) }) export type Router = typeof router const app = new Elysia().use(trpc(router)).listen(3000) ``` ## trpc Accept the tRPC router and register to Elysia's handler. ````ts trpc( router: Router, option?: { endpoint?: string } ): this ``` `Router` is the TRPC Router instance. ### endpoint The path to the exposed TRPC endpoint. ```` --- --- url: /tutorial.md --- # Elysia Tutorial We will be building a small CRUD note-taking API server. There's no database or other "production ready" features. This tutorial is going to only focus on Elysia feature and how to use Elysia only. We expected it to take around 15-20 minutes if you follow along. *** ### From other framework? If you have used other popular frameworks like Express, Fastify, or Hono, you will find Elysia right at home with just a few differences. ### Not a fan of tutorial? If you prefers to a more try-it-yourself approach, you can skip this tutorial and go straight to the [key concept](/key-concept) page to get a good understanding of how Elysia works. ### llms.txt Alternatively, you can download llms.txt or llms-full.txt and feeds it to your favorite LLMs like ChatGPT, Claude or Gemini to get a more interactive experience. ## Setup Elysia is designed to run on [Bun](https://bun.sh), an alternative runtime to Node.js but it can also run on Node.js or any runtime that support Web Standard API. However, in this tutorial we will be using Bun. Install Bun if you haven't already. ::: code-group ```bash [MacOS/Linux] curl -fsSL https://bun.sh/install | bash ``` ```bash [Windows] powershell -c "irm bun.sh/install.ps1 | iex" ``` ::: ### Create a new project ```bash # Create a new project bun create elysia hi-elysia # cd into the project cd hi-elysia # Install dependencies bun install ``` This will create a barebone project with Elysia and basic TypeScript config. ### Start the development server ```bash bun dev ``` Open your browser and go to **http://localhost:3000**, you should see **Hello Elysia** message on the screen. Elysia use Bun with `--watch` flag to automatically reload the server when you make changes. ## Route To add a new route, we specify an HTTP method, a pathname, and a value. Let's start by opening the `src/index.ts` file as follows: ```typescript [index.ts] import { Elysia } from 'elysia' const app = new Elysia() .get('/', () => 'Hello Elysia') .get('/hello', 'Do you miss me?') // [!code ++] .listen(3000) ``` Open **http://localhost:3000/hello**, you should see **Do you miss me?**. There are several HTTP methods we can use, but we will use the following for this tutorial: * get * post * put * patch * delete Other methods are available, use the same syntax as `get` ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/', () => 'Hello Elysia') .get('/hello', 'Do you miss me?') // [!code --] .post('/hello', 'Do you miss me?') // [!code ++] .listen(3000) ``` Elysia accepts both value and function as a response. However, we can use function to access `Context` (route and instance information). ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/', () => 'Hello Elysia') // [!code --] .get('/', ({ path }) => path) // [!code ++] .post('/hello', 'Do you miss me?') .listen(3000) ``` ## Swagger Entering a URL to the browser can only interact with the GET method. To interact with other methods, we need a REST Client like Postman or Insomnia. Luckily, Elysia comes with a **OpenAPI Schema** with [Scalar](https://scalar.com) to interact with our API. ```bash # Install the Swagger plugin bun add @elysiajs/swagger ``` Then apply the plugin to the Elysia instance. ```typescript import { Elysia } from 'elysia' import { swagger } from '@elysiajs/swagger' const app = new Elysia() // Apply the swagger plugin .use(swagger()) // [!code ++] .get('/', ({ path }) => path) .post('/hello', 'Do you miss me?') .listen(3000) ``` Navigate to **http://localhost:3000/swagger**, you should see the documentation like this: ![Scalar Documentation landing](/tutorial/scalar-landing.webp) Now we can interact with all the routes we have created. Scroll to **/hello** and click a blue **Test Request** button to show the form. We can see the result by clicking the black **Send** button. ![Scalar Documentation landing](/tutorial/scalar-request.webp) ## Decorate However, for more complex data we may want to use class for complex data as it allows us to define custom methods and properties. Now, let's create a singleton class to store our notes. ```typescript import { Elysia } from 'elysia' import { swagger } from '@elysiajs/swagger' class Note { // [!code ++] constructor(public data: string[] = ['Moonhalo']) {} // [!code ++] } // [!code ++] const app = new Elysia() .use(swagger()) .decorate('note', new Note()) // [!code ++] .get('/note', ({ note }) => note.data) // [!code ++] .listen(3000) ``` `decorate` allows us to inject a singleton class into the Elysia instance, allowing us to access it in the route handler. Open **http://localhost:3000/note**, we should see **\["Moonhalo"]** on the screen. For Scalar documentation, we may need to reload the page to see the new changes. ![Scalar Documentation landing](/tutorial/scalar-moonhalo.webp) ## Path parameter Now let's retrieve a note by its index. We can define a path parameter by prefixing it with a colon. ```typescript twoslash // @errors: 7015 import { Elysia } from 'elysia' import { swagger } from '@elysiajs/swagger' class Note { constructor(public data: string[] = ['Moonhalo']) {} } const app = new Elysia() .use(swagger()) .decorate('note', new Note()) .get('/note', ({ note }) => note.data) .get('/note/:index', ({ note, params: { index } }) => { // [!code ++] return note.data[index] // [!code ++] }) // [!code ++] .listen(3000) ``` Let's ignore the error for now. Open **http://localhost:3000/note/0**, we should see **Moonhalo** on the screen. The path parameter allows us to retrieve a specific part from the URL. In our case, we retrieve a **"0"** from **/note/0** put into a variable named **index**. ## Validation The error above is a warning that the path parameter can be any string, while an array index should be a number. For example, **/note/0** is valid, but **/note/zero** is not. We can enforce and validate type by declaring a schema: ```typescript import { Elysia, t } from 'elysia' // [!code ++] import { swagger } from '@elysiajs/swagger' class Note { constructor(public data: string[] = ['Moonhalo']) {} } const app = new Elysia() .use(swagger()) .decorate('note', new Note()) .get('/note', ({ note }) => note.data) .get( '/note/:index', ({ note, params: { index } }) => { return note.data[index] }, { // [!code ++] params: t.Object({ // [!code ++] index: t.Number() // [!code ++] }) // [!code ++] } // [!code ++] ) .listen(3000) ``` We import **t** from Elysia to define a schema for the path parameter. Now, if we try to access **http://localhost:3000/note/abc**, we should see an error message. This code resolves the error we saw earlier because of a **TypeScript warning**. Elysia schema doesn't only enforce validation on the runtime, but it also infers a TypeScript type for auto-completion and checking error ahead of time, and a Scalar documentation. Most frameworks provide only one of these features or provide them separately requiring us to update each one separately, but Elysia provides all of them as a **Single Source of Truth**. ### Validation type Elysia provides validation for the following properties: * params - path parameter * query - URL querystring * body - request body * headers - request headers * cookie - cookie * response - response body All of them share the same syntax as the example above. ## Status code By default, Elysia will return a status code of 200 for all routes even if the response is an error. For example, if we try to access **http://localhost:3000/note/1**, we should see **undefined** on the screen which shouldn't be a 200 status code (OK). We can change the status code by returning an error ```typescript import { Elysia, t } from 'elysia' import { swagger } from '@elysiajs/swagger' class Note { constructor(public data: string[] = ['Moonhalo']) {} } const app = new Elysia() .use(swagger()) .decorate('note', new Note()) .get('/note', ({ note }) => note.data) .get( '/note/:index', ({ note, params: { index }, status }) => { // [!code ++] return note.data[index] ?? status(404) // [!code ++] }, { params: t.Object({ index: t.Number() }) } ) .listen(3000) ``` Now, if we try to access **http://localhost:3000/note/1**, we should see **Not Found** on the screen with a status code of 404. We can also return a custom message by passing a string to the error function. ```typescript import { Elysia, t } from 'elysia' import { swagger } from '@elysiajs/swagger' class Note { constructor(public data: string[] = ['Moonhalo']) {} } const app = new Elysia() .use(swagger()) .decorate('note', new Note()) .get('/note', ({ note }) => note.data) .get( '/note/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'oh no :(') // [!code ++] }, { params: t.Object({ index: t.Number() }) } ) .listen(3000) ``` ## Plugin The main instance is starting to get crowded, we can move the route handler to a separate file and import it as a plugin. Create a new file named **note.ts**: ::: code-group ```typescript [note.ts] import { Elysia, t } from 'elysia' class Note { constructor(public data: string[] = ['Moonhalo']) {} } export const note = new Elysia() .decorate('note', new Note()) .get('/note', ({ note }) => note.data) .get( '/note/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'oh no :(') }, { params: t.Object({ index: t.Number() }) } ) ``` ::: Then on the **index.ts**, apply **note** into the main instance: ::: code-group ```typescript [index.ts] import { Elysia, t } from 'elysia' import { swagger } from '@elysiajs/swagger' import { note } from './note' // [!code ++] class Note { // [!code --] constructor(public data: string[] = ['Moonhalo']) {} // [!code --] } // [!code --] const app = new Elysia() .use(swagger()) .use(note) // [!code ++] .decorate('note', new Note()) // [!code --] .get('/note', ({ note }) => note.data) // [!code --] .get( // [!code --] '/note/:index', // [!code --] ({ note, params: { index }, status }) => { // [!code --] return note.data[index] ?? status(404, 'oh no :(') // [!code --] }, // [!code --] { // [!code --] params: t.Object({ // [!code --] index: t.Number() // [!code --] }) // [!code --] } // [!code --] ) // [!code --] .listen(3000) ``` ::: Open **http://localhost:3000/note/1** and you should see **oh no :(** again like before. We have just created a **note** plugin, by declaring a new Elysia instance. Each plugin is a separate instance of Elysia which has its own routes, middlewares, and decorators which can be applied to other instances. ## Applying CRUD We can apply the same pattern to create, update, and delete routes. ::: code-group ```typescript [note.ts] import { Elysia, t } from 'elysia' class Note { constructor(public data: string[] = ['Moonhalo']) {} add(note: string) { // [!code ++] this.data.push(note) // [!code ++] return this.data // [!code ++] } // [!code ++] remove(index: number) { // [!code ++] return this.data.splice(index, 1) // [!code ++] } // [!code ++] update(index: number, note: string) { // [!code ++] return (this.data[index] = note) // [!code ++] } // [!code ++] } export const note = new Elysia() .decorate('note', new Note()) .get('/note', ({ note }) => note.data) .put('/note', ({ note, body: { data } }) => note.add(data), { // [!code ++] body: t.Object({ // [!code ++] data: t.String() // [!code ++] }) // [!code ++] }) // [!code ++] .get( '/note/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'Not Found :(') }, { params: t.Object({ index: t.Number() }) } ) .delete( // [!code ++] '/note/:index', // [!code ++] ({ note, params: { index }, status }) => { // [!code ++] if (index in note.data) return note.remove(index) // [!code ++] return status(422) // [!code ++] }, // [!code ++] { // [!code ++] params: t.Object({ // [!code ++] index: t.Number() // [!code ++] }) // [!code ++] } // [!code ++] ) // [!code ++] .patch( // [!code ++] '/note/:index', // [!code ++] ({ note, params: { index }, body: { data }, status }) => { // [!code ++] if (index in note.data) return note.update(index, data) // [!code ++] return status(422) // [!code ++] }, // [!code ++] { // [!code ++] params: t.Object({ // [!code ++] index: t.Number() // [!code ++] }), // [!code ++] body: t.Object({ // [!code ++] data: t.String() // [!code ++] }) // [!code ++] } // [!code ++] ) // [!code ++] ``` ::: Now let's open **http://localhost:3000/swagger** and try playing around with CRUD operations. ## Group If we look closely, all of the routes in the **note** plugin share a **/note** prefix. We can simplify this by declaring **prefix** ::: code-group ```typescript [note.ts] export const note = new Elysia({ prefix: '/note' }) // [!code ++] .decorate('note', new Note()) .get('/', ({ note }) => note.data) // [!code ++] .put('/', ({ note, body: { data } }) => note.add(data), { body: t.Object({ data: t.String() }) }) .get( '/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'Not Found :(') }, { params: t.Object({ index: t.Number() }) } ) .delete( '/:index', ({ note, params: { index }, status }) => { if (index in note.data) return note.remove(index) return status(422) }, { params: t.Object({ index: t.Number() }) } ) .patch( '/:index', ({ note, params: { index }, body: { data }, status }) => { if (index in note.data) return note.update(index, data) return status(422) }, { params: t.Object({ index: t.Number() }), body: t.Object({ data: t.String() }) } ) ``` ::: ## Guard Now we may notice that there are several routes in plugin that has **params** validation. We may define a **guard** to apply validation to routes in the plugin. ::: code-group ```typescript [note.ts] export const note = new Elysia({ prefix: '/note' }) .decorate('note', new Note()) .get('/', ({ note }) => note.data) .put('/', ({ note, body: { data } }) => note.add(data), { body: t.Object({ data: t.String() }) }) .guard({ // [!code ++] params: t.Object({ // [!code ++] index: t.Number() // [!code ++] }) // [!code ++] }) // [!code ++] .get( '/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'Not Found :(') }, { // [!code --] params: t.Object({ // [!code --] index: t.Number() // [!code --] }) // [!code --] } // [!code --] ) .delete( '/:index', ({ note, params: { index }, status }) => { if (index in note.data) return note.remove(index) return status(422) }, { // [!code --] params: t.Object({ // [!code --] index: t.Number() // [!code --] }) // [!code --] } // [!code --] ) .patch( '/:index', ({ note, params: { index }, body: { data }, status }) => { if (index in note.data) return note.update(index, data) return status(422) }, { params: t.Object({ // [!code --] index: t.Number() // [!code --] }), // [!code --] body: t.Object({ data: t.String() }) } ) ``` ::: Validation will be applied to all routes **after guard** is called and tied to the plugin. ## Lifecycle Now in real-world usage, we may want to do something like logging before the request is processed. Instead of inline `console.log` for each route, we may apply a **lifecycle** that intercepts the request before/after it is processed. There are several lifecycles that we can use, but in this case we will be using `onTransform`. ::: code-group ```typescript [note.ts] export const note = new Elysia({ prefix: '/note' }) .decorate('note', new Note()) .onTransform(function log({ body, params, path, request: { method } }) { // [!code ++] console.log(`${method} ${path}`, { // [!code ++] body, // [!code ++] params // [!code ++] }) // [!code ++] }) // [!code ++] .get('/', ({ note }) => note.data) .put('/', ({ note, body: { data } }) => note.add(data), { body: t.Object({ data: t.String() }) }) .guard({ params: t.Object({ index: t.Number() }) }) .get('/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'Not Found :(') }) .delete('/:index', ({ note, params: { index }, status }) => { if (index in note.data) return note.remove(index) return status(422) }) .patch( '/:index', ({ note, params: { index }, body: { data }, status }) => { if (index in note.data) return note.update(index, data) return status(422) }, { body: t.Object({ data: t.String() }) } ) ``` ::: `onTransform` is called after **routing but before validation**, so we can do something like logging the request that is defined without triggering the **404 Not found** route. This allows us to log the request before it is processed, and we can see the request body and path parameters. ### Scope By default, the **lifecycle hook is encapsulated**. Hook is applied to routes in the same instance, and is not applied to other plugins (routes that are not defined in the same plugin). This means the log function, in the `onTransform` hook, will not be called on other instances, unless we explicitly defined it as `scoped` or `global`. ## Authentication Now we may want to add restrictions to our routes, so only the owner of the note can update or delete it. Let's create a `user.ts` file that will handle the user authentication: ```typescript [user.ts] import { Elysia, t } from 'elysia' // [!code ++] // [!code ++] export const user = new Elysia({ prefix: '/user' }) // [!code ++] .state({ // [!code ++] user: {} as Record, // [!code ++] session: {} as Record // [!code ++] }) // [!code ++] .put( // [!code ++] '/sign-up', // [!code ++] async ({ body: { username, password }, store, status }) => { // [!code ++] if (store.user[username]) // [!code ++] return status(400, { // [!code ++] success: false, // [!code ++] message: 'User already exists' // [!code ++] }) // [!code ++] // [!code ++] store.user[username] = await Bun.password.hash(password) // [!code ++] // [!code ++] return { // [!code ++] success: true, // [!code ++] message: 'User created' // [!code ++] } // [!code ++] }, // [!code ++] { // [!code ++] body: t.Object({ // [!code ++] username: t.String({ minLength: 1 }), // [!code ++] password: t.String({ minLength: 8 }) // [!code ++] }) // [!code ++] } // [!code ++] ) // [!code ++] .post( // [!code ++] '/sign-in', // [!code ++] async ({ // [!code ++] store: { user, session }, // [!code ++] status, // [!code ++] body: { username, password }, // [!code ++] cookie: { token } // [!code ++] }) => { // [!code ++] if ( // [!code ++] !user[username] || // [!code ++] !(await Bun.password.verify(password, user[username])) // [!code ++] ) // [!code ++] return status(400, { // [!code ++] success: false, // [!code ++] message: 'Invalid username or password' // [!code ++] }) // [!code ++] const key = crypto.getRandomValues(new Uint32Array(1))[0] // [!code ++] session[key] = username // [!code ++] token.value = key // [!code ++] return { // [!code ++] success: true, // [!code ++] message: `Signed in as ${username}` // [!code ++] } // [!code ++] }, // [!code ++] { // [!code ++] body: t.Object({ // [!code ++] username: t.String({ minLength: 1 }), // [!code ++] password: t.String({ minLength: 8 }) // [!code ++] }), // [!code ++] cookie: t.Cookie( // [!code ++] { // [!code ++] token: t.Number() // [!code ++] }, // [!code ++] { // [!code ++] secrets: 'seia' // [!code ++] } // [!code ++] ) // [!code ++] } // [!code ++] ) // [!code ++] ``` Now there are a lot of things to unwrap here: 1. We create a new instance with 2 routes for sign up and sign in. 2. In the instance, we define an in-memory store `user` and `session` * 2.1 `user` will hold key-value of `username` and `password` * 2.2 `session` will hold a key-value of `session` and `username` 3. In `/sign-up` we insert a username and hashed password with argon2id 4. In `/sign-in` we do the following: * 4.1 We check if user exists and verify the password * 4.2 If the password matches, then we generate a new session into `session` * 4.3 We set cookie `token` with the value of session * 4.4 We append `secret` to cookie to add hash and block an attacker from tampering with the cookie ::: tip As we are using an in-memory store, the data are wiped out every reload or every time we edit the code. We will fix that in the later part of the tutorial. ::: Now if we want to check if a user is signed in, we could check for value of `token` cookie and check with the `session` store. ## Reference Model However, we can recognize that both `/sign-in` and `/sign-up` both share the same `body` model. Instead of copy-pasting the model all over the place, we could use a **reference model** to reuse the model by specifying a name. To create a **reference model**, we may use `.model` and pass the name and the value of models: ```typescript [user.ts] import { Elysia, t } from 'elysia' export const user = new Elysia({ prefix: '/user' }) .state({ user: {} as Record, session: {} as Record }) .model({ // [!code ++] signIn: t.Object({ // [!code ++] username: t.String({ minLength: 1 }), // [!code ++] password: t.String({ minLength: 8 }) // [!code ++] }), // [!code ++] session: t.Cookie( // [!code ++] { // [!code ++] token: t.Number() // [!code ++] }, // [!code ++] { // [!code ++] secrets: 'seia' // [!code ++] } // [!code ++] ), // [!code ++] optionalSession: t.Cookie( { token: t.Optional(t.Number()) }, { secrets: 'seia' } ) // [!code ++] }) // [!code ++] .put( '/sign-up', async ({ body: { username, password }, store, status }) => { if (store.user[username]) return status(400, { success: false, message: 'User already exists' }) store.user[username] = await Bun.password.hash(password) return { success: true, message: 'User created' } }, { body: 'signIn' // [!code ++] } ) .post( '/sign-in', async ({ store: { user, session }, status, body: { username, password }, cookie: { token } }) => { if ( !user[username] || !(await Bun.password.verify(password, user[username])) ) return status(400, { success: false, message: 'Invalid username or password' }) const key = crypto.getRandomValues(new Uint32Array(1))[0] session[key] = username token.value = key return { success: true, message: `Signed in as ${username}` } }, { body: 'signIn', // [!code ++] cookie: 'session' // [!code ++] } ) ``` After adding a model/models, we can reuse them by referencing their name in the schema instead of providing a literal type while providing the same functionality and type safety. `Elysia.model` could accept multiple overloads: 1. Providing an object, the register all key-value as models 2. Providing a function, then access all previous models then return new models Finally, we could add the `/profile` and `/sign-out` routes as follows: ```typescript [user.ts] import { Elysia, t } from 'elysia' export const user = new Elysia({ prefix: '/user' }) .state({ user: {} as Record, session: {} as Record }) .model({ signIn: t.Object({ username: t.String({ minLength: 1 }), password: t.String({ minLength: 8 }) }), session: t.Cookie( { token: t.Number() }, { secrets: 'seia' } ), optionalSession: t.Cookie( { token: t.Optional(t.Number()) }, { secrets: 'seia' } ) }) .put( '/sign-up', async ({ body: { username, password }, store, status }) => { if (store.user[username]) return status(400, { success: false, message: 'User already exists' }) store.user[username] = await Bun.password.hash(password) return { success: true, message: 'User created' } }, { body: 'signIn' } ) .post( '/sign-in', async ({ store: { user, session }, status, body: { username, password }, cookie: { token } }) => { if ( !user[username] || !(await Bun.password.verify(password, user[username])) ) return status(400, { success: false, message: 'Invalid username or password' }) const key = crypto.getRandomValues(new Uint32Array(1))[0] session[key] = username token.value = key return { success: true, message: `Signed in as ${username}` } }, { body: 'signIn', cookie: 'optionalSession' } ) .get( // [!code ++] '/sign-out', // [!code ++] ({ cookie: { token } }) => { // [!code ++] token.remove() // [!code ++] // [!code ++] return { // [!code ++] success: true, // [!code ++] message: 'Signed out' // [!code ++] } // [!code ++] }, // [!code ++] { // [!code ++] cookie: 'optionalSession' // [!code ++] } // [!code ++] ) // [!code ++] .get( // [!code ++] '/profile', // [!code ++] ({ cookie: { token }, store: { session }, status }) => { // [!code ++] const username = session[token.value] // [!code ++] // [!code ++] if (!username) // [!code ++] return status(401, { // [!code ++] success: false, // [!code ++] message: 'Unauthorized' // [!code ++] }) // [!code ++] // [!code ++] return { // [!code ++] success: true, // [!code ++] username // [!code ++] } // [!code ++] }, // [!code ++] { // [!code ++] cookie: 'session' // [!code ++] } // [!code ++] ) // [!code ++] ``` As we are going to apply `authorization` in the `note`, we are going to need to repeat two things: 1. Checking if user exists 2. Getting user id (in our case 'username') For **1.** instead of using guard, we could use a **macro**. ## Plugin deduplication As we are going to reuse this hook in multiple modules (user, and note), let's extract the service (utility) part out and apply it to both modules. ```ts [user.ts] // @errors: 2538 import { Elysia, t } from 'elysia' export const userService = new Elysia({ name: 'user/service' }) // [!code ++] .state({ // [!code ++] user: {} as Record, // [!code ++] session: {} as Record // [!code ++] }) // [!code ++] .model({ // [!code ++] signIn: t.Object({ // [!code ++] username: t.String({ minLength: 1 }), // [!code ++] password: t.String({ minLength: 8 }) // [!code ++] }), // [!code ++] session: t.Cookie( // [!code ++] { // [!code ++] token: t.Number() // [!code ++] }, // [!code ++] { // [!code ++] secrets: 'seia' // [!code ++] } // [!code ++] ), // [!code ++] optionalSession: t.Cookie( { token: t.Optional(t.Number()) }, { secrets: 'seia' } ) // [!code ++] }) // [!code ++] export const user = new Elysia({ prefix: '/user' }) .use(userService) // [!code ++] .state({ // [!code --] user: {} as Record, // [!code --] session: {} as Record // [!code --] }) // [!code --] .model({ // [!code --] signIn: t.Object({ // [!code --] username: t.String({ minLength: 1 }), // [!code --] password: t.String({ minLength: 8 }) // [!code --] }), // [!code --] session: t.Cookie( // [!code --] { // [!code --] token: t.Number() // [!code --] }, // [!code --] { // [!code --] secrets: 'seia' // [!code --] } // [!code --] ), // [!code --] optionalSession: t.Cookie( { token: t.Optional(t.Number()) }, { secrets: 'seia' } ) // [!code --] }) // [!code --] ``` The `name` property here is very important, as it's a unique identifier for the plugin to prevent duplicate instances (like a singleton). If we were to define the instance without the plugin, hook/lifecycle and routes are going to be registered every time the plugin is used. Our intention is to apply this plugin (service) to multiple modules to provide utility function, this make deduplication very important as life-cycle shouldn't be registered twice. ## Macro Macro allows us to define a custom hook with custom life-cycle management. To define a macro, we could use `.macro` as follows: ```ts [user.ts] import { Elysia, t } from 'elysia' export const userService = new Elysia({ name: 'user/service' }) .state({ user: {} as Record, session: {} as Record }) .model({ signIn: t.Object({ username: t.String({ minLength: 1 }), password: t.String({ minLength: 8 }) }), session: t.Cookie( { token: t.Number() }, { secrets: 'seia' } ), optionalSession: t.Cookie( { token: t.Optional(t.Number()) }, { secrets: 'seia' } ) }) .macro({ isSignIn(enabled: boolean) { // [!code ++] if (!enabled) return // [!code ++] return { beforeHandle({ status, cookie: { token }, store: { session } }) { // [!code ++] if (!token.value) // [!code ++] return status(401, { // [!code ++] success: false, // [!code ++] message: 'Unauthorized' // [!code ++] }) // [!code ++] const username = session[token.value as unknown as number] // [!code ++] if (!username) // [!code ++] return status(401, { // [!code ++] success: false, // [!code ++] message: 'Unauthorized' // [!code ++] }) // [!code ++] } // [!code ++] } // [!code ++] } // [!code ++] }) // [!code ++] ``` We have just created a new macro name `isSignIn` that accepts a `boolean` value, if it is true, then we add an `onBeforeHandle` event that executes **after validation but before the main handler**, allowing us to extract authentication logic here. To use the macro, simply specify `isSignIn: true` as follows: ```ts [user.ts] import { Elysia, t } from 'elysia' export const user = new Elysia({ prefix: '/user' }).use(userService).get( '/profile', ({ cookie: { token }, store: { session }, status }) => { const username = session[token.value] if (!username) // [!code --] return status(401, { // [!code --] success: false, // [!code --] message: 'Unauthorized' // [!code --] }) // [!code --] return { success: true, username } }, { isSignIn: true, // [!code ++] cookie: 'session' } ) ``` As we specified `isSignIn`, we can extract the imperative checking part, and reuse the same logic on multiple routes without copy-pasting the same code all over again. ::: tip This may seem like a small code change to trade for a larger boilerplate, but as the server grows more complex, the user-checking could also grow to be a very complex mechanism. ::: ## Resolve Our last objective is to get the username (id) from the token. We could use `resolve` to define a new property into the same context as `store` but only execute it per request. Unlike `decorate` and `store`, resolve is defined at the `beforeHandle` stage or the value will be available **after validation**. This ensures that the property like `cookie: 'session'` exists before creating a new property. ```ts [user.ts] export const getUserId = new Elysia() // [!code ++] .use(userService) // [!code ++] .guard({ // [!code ++] cookie: 'session' // [!code ++] }) // [!code ++] .resolve(({ store: { session }, cookie: { token } }) => ({ // [!code ++] username: session[token.value] // [!code ++] })) // [!code ++] ``` In this instance, we define a new property `username` by using `resolve`, allowing us to reduce the getting `username` logic into a property instead. We don't define a name in this `getUserId` instance because we want `guard` and `resolve` to reapply into multiple instances. ::: tip Same as macro, `resolve` plays well if the logic for getting the property is complex and might not be worth it for a small operation like this. But since in the real-world we are going to need database-connection, caching, and queuing it might make it fit the narrative. ::: ## Scope Now if we try to apply the use of the `getUserId`, we might notice that the property `username` and `guard` isn't applied. ```ts [user.ts] export const getUserId = new Elysia() .use(userService) .guard({ isSignIn: true, cookie: 'session' }) .resolve(({ store: { session }, cookie: { token } }) => ({ username: session[token.value] })) export const user = new Elysia({ prefix: '/user' }) .use(getUserId) .get('/profile', ({ username }) => ({ success: true, username })) ``` This is because the Elysia **encapsulate lifecycle** does this by default as mentioned in [lifecycle](#lifecycle) This is intentional by design, as we don't want each module to have a side-effect to other modules. Having a side-effect can be very difficult to debug especially in a large codebase with multiple (Elysia) dependencies. If we want lifecycle to be applied to the parent, we can explicitly annotate that it could be applied to the parent by using either: 1. scoped - only apply to parent at 1-level above and not any further 2. global - apply to all parent levels In our case, we want to use **scoped** as it will apply to the controller that uses the service only. To do this, we need to annotate that life-cycle as `scoped`: ```typescript [user.ts] export const getUserId = new Elysia() .use(userService) .guard({ as: 'scoped', // [!code ++] isSignIn: true, cookie: 'session' }) .resolve( { as: 'scoped' }, // [!code ++] ({ store: { session }, cookie: { token } }) => ({ username: session[token.value] }) ) export const user = new Elysia({ prefix: '/user' }) .use(getUserId) .get('/profile', ({ username }) => ({ // ^? success: true, username })) ``` Alternatively, if we have multiple `scoped` defined, we could use `as` to cast multiple life-cycles instead. ```ts [user.ts] export const getUserId = new Elysia() .use(userService) .guard({ as: 'scoped', // [!code --] isSignIn: true, cookie: 'session' }) .resolve( { as: 'scoped' }, // [!code --] ({ store: { session }, cookie: { token } }) => ({ username: session[token.value] }) ) .as('scoped') // [!code ++] export const user = new Elysia({ prefix: '/user' }) .use(getUserId) .get('/profile', ({ username }) => ({ success: true, username })) ``` Both achieve the same effect, the only difference is single or multiple cast instances. ::: tip Encapsulation happens in both runtime, and type-level. This allows us to catch the error ahead of time. ::: Lastly, we can reuse `userService` and `getUserId` to help with authorization in our **note** controller. But first, don't forget to import the `user` in the `index.ts` file: ::: code-group ```typescript [index.ts] import { Elysia, t } from 'elysia' import { swagger } from '@elysiajs/swagger' import { note } from './note' import { user } from './user' // [!code ++] const app = new Elysia() .use(swagger()) .use(user) // [!code ++] .use(note) .listen(3000) ``` ::: ## Authorization First, let's modify the `Note` class to store the user who created the note. But instead of defining the `Memo` type, we can define a memo schema and infer the type from it, allowing us to sync runtime and type-level. ```typescript [note.ts] import { Elysia, t } from 'elysia' const memo = t.Object({ // [!code ++] data: t.String(), // [!code ++] author: t.String() // [!code ++] }) // [!code ++] type Memo = typeof memo.static // [!code ++] class Note { constructor(public data: string[] = ['Moonhalo']) {} // [!code --] constructor( // [!code ++] public data: Memo[] = [ // [!code ++] { // [!code ++] data: 'Moonhalo', // [!code ++] author: 'saltyaom' // [!code ++] } // [!code ++] ] // [!code ++] ) {} // [!code ++] add(note: string) { // [!code --] add(note: Memo) { // [!code ++] this.data.push(note) return this.data } remove(index: number) { return this.data.splice(index, 1) } update(index: number, note: string) { // [!code --] return (this.data[index] = note) // [!code --] } // [!code --] update(index: number, note: Partial) { // [!code ++] return (this.data[index] = { ...this.data[index], ...note }) // [!code ++] } // [!code ++] } export const note = new Elysia({ prefix: '/note' }) .decorate('note', new Note()) .model({ // [!code ++] memo: t.Omit(memo, ['author']) // [!code ++] }) // [!code ++] .onTransform(function log({ body, params, path, request: { method } }) { console.log(`${method} ${path}`, { body, params }) }) .get('/', ({ note }) => note.data) .put('/', ({ note, body: { data } }) => note.add(data), { // [!code --] body: t.Object({ // [!code --] data: t.String() // [!code --] }), // [!code --] }) // [!code --] .put('/', ({ note, body: { data }, username }) => note.add({ data, author: username }), { // [!code ++] body: 'memo' // [!code ++] } ) // [!code ++] .guard({ params: t.Object({ index: t.Number() }) }) .get( '/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'Not Found :(') } ) .delete( '/:index', ({ note, params: { index }, status }) => { if (index in note.data) return note.remove(index) return status(422) } ) .patch( '/:index', ({ note, params: { index }, body: { data }, status }) => { // [!code --] if (index in note.data) return note.update(index, data) // [!code --] ({ note, params: { index }, body: { data }, status, username }) => { // [!code ++] if (index in note.data) // [!code ++] return note.update(index, { data, author: username })) // [!code ++] return status(422) }, { body: t.Object({ // [!code --] data: t.String() // [!code --] }), // [!code --] body: 'memo' } ) ``` Now let's import, and use `userService`, `getUserId` to apply authorization to the **note** controller. ```typescript [note.ts] import { Elysia, t } from 'elysia' import { getUserId, userService } from './user' // [!code ++] const memo = t.Object({ data: t.String(), author: t.String() }) type Memo = typeof memo.static class Note { constructor( public data: Memo[] = [ { data: 'Moonhalo', author: 'saltyaom' } ] ) {} add(note: Memo) { this.data.push(note) return this.data } remove(index: number) { return this.data.splice(index, 1) } update(index: number, note: Partial) { return (this.data[index] = { ...this.data[index], ...note }) } } export const note = new Elysia({ prefix: '/note' }) .use(userService) // [!code ++] .decorate('note', new Note()) .model({ memo: t.Omit(memo, ['author']) }) .onTransform(function log({ body, params, path, request: { method } }) { console.log(`${method} ${path}`, { body, params }) }) .get('/', ({ note }) => note.data) .use(getUserId) // [!code ++] .put( '/', ({ note, body: { data }, username }) => note.add({ data, author: username }), { body: 'memo' } ) .get( '/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'Not Found :(') }, { params: t.Object({ index: t.Number() }) } ) .guard({ params: t.Object({ index: t.Number() }) }) .delete('/:index', ({ note, params: { index }, status }) => { if (index in note.data) return note.remove(index) return status(422) }) .patch( '/:index', ({ note, params: { index }, body: { data }, status, username }) => { if (index in note.data) return note.update(index, { data, author: username }) return status(422) }, { isSignIn: true, body: 'memo' } ) ``` And that's it 🎉 We have just implemented authorization by reusing the service we created earlier. ## Error handling One of the most important aspects of an API is to make sure nothing goes wrong, and if it does, we need to handle it properly. We use use the `onError` lifecycle to catch any error that is thrown in the server. ::: code-group ```typescript [index.ts] import { Elysia, t } from 'elysia' import { swagger } from '@elysiajs/swagger' import { note } from './note' import { user } from './user' const app = new Elysia() .use(swagger()) .onError(({ error, code }) => { // [!code ++] if (code === 'NOT_FOUND') return // [!code ++] console.error(error) // [!code ++] }) // [!code ++] .use(user) .use(note) .listen(3000) ``` ::: We have just added an error listener that will catch any error that is thrown in the server, excluding **404 Not Found** and log it to the console. ::: tip Notice that `onError` is used before `use(note)`. This is important as Elysia applies the method from top-to-bottom. The listener has to be applied before the route. And as `onError` is applied on the root instance, it doesn't need to define a scope as it will apply to all children instances. ::: Returning a truthy value will override a default error response, so we can return a custom error response which inherits the status code. ::: code-group ```typescript [index.ts] import { Elysia, t } from 'elysia' import { swagger } from '@elysiajs/swagger' import { note } from './note' const app = new Elysia() .use(swagger()) .onError(({ error, code }) => { // [!code ++] if (code === 'NOT_FOUND') return 'Not Found :(' // [!code ++] console.error(error) // [!code ++] }) // [!code ++] .use(note) .listen(3000) ``` ::: ### Observability Now we have a working API, a final touch is to make sure everything is working after we deployed our server. Elysia supports OpenTelemetry by default with the `@elysiajs/opentelemetry` plugin. ```bash bun add @elysiajs/opentelemetry ``` Make sure to have an OpenTelemetry collector running otherwise we will be using Jaeger from docker. ```bash docker run --name jaeger \ -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ -e COLLECTOR_OTLP_ENABLED=true \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 4317:4317 \ -p 4318:4318 \ -p 14250:14250 \ -p 14268:14268 \ -p 14269:14269 \ -p 9411:9411 \ jaegertracing/all-in-one:latest ``` Now let's apply the OpenTelemetry plugin to our server. ::: code-group ```typescript [index.ts] import { Elysia, t } from 'elysia' import { opentelemetry } from '@elysiajs/opentelemetry' // [!code ++] import { swagger } from '@elysiajs/swagger' import { note } from './note' import { user } from './user' const app = new Elysia() .use(opentelemetry()) // [!code ++] .use(swagger()) .onError(({ error, code }) => { if (code === 'NOT_FOUND') return 'Not Found :(' console.error(error) }) .use(note) .use(user) .listen(3000) ``` ::: Now try out some more requests and open http://localhost:16686 to see traces. Select service **Elysia** and click on **Find Traces**, we should be able to see a list of requests that we have made. ![Jaeger showing list of requests](/tutorial/jaeger-list.webp) Click on any of the requests to see how long each lifecycle hook takes to process the request. ![Jaeger showing request span](/tutorial/jaeger-span.webp) Click on the root parent span to see the request details, this will show you the request and response payload, and errors if have any. ![Jaeger showing request detail](/tutorial/jaeger-detail.webp) Elysia supports OpenTelemetry out of the box, it automatically integrates with other JavaScript libraries that support OpenTelemetry like Prisma, GraphQL Yoga, Effect, etc. You can also use other OpenTelemetry plugins to send traces to other services like Zipkin, Prometheus, etc. ## Codebase recap If you are following along, you should have a codebase that looks like this: ::: code-group ```typescript twoslash [index.ts] // @errors: 2538 // @filename: user.ts import { Elysia, t } from 'elysia' export const userService = new Elysia({ name: 'user/service' }) .state({ user: {} as Record, session: {} as Record }) .model({ signIn: t.Object({ username: t.String({ minLength: 1 }), password: t.String({ minLength: 8 }) }), session: t.Cookie( { token: t.Number() }, { secrets: 'seia' } ), optionalSession: t.Cookie( { token: t.Optional(t.Number()) }, { secrets: 'seia' } ) }) .macro({ isSignIn(enabled: boolean) { if (!enabled) return return { beforeHandle({ status, cookie: { token }, store: { session } }) { if (!token.value) return status(401, { success: false, message: 'Unauthorized' }) const username = session[token.value as unknown as number] if (!username) return status(401, { success: false, message: 'Unauthorized' }) } } } }) export const getUserId = new Elysia() .use(userService) .guard({ isSignIn: true, cookie: 'session' }) .resolve(({ store: { session }, cookie: { token } }) => ({ username: session[token.value] })) .as('scoped') export const user = new Elysia({ prefix: '/user' }) .use(userService) .put( '/sign-up', async ({ body: { username, password }, store, status }) => { if (store.user[username]) return status(400, { success: false, message: 'User already exists' }) store.user[username] = await Bun.password.hash(password) return { success: true, message: 'User created' } }, { body: 'signIn' } ) .post( '/sign-in', async ({ store: { user, session }, status, body: { username, password }, cookie: { token } }) => { if ( !user[username] || !(await Bun.password.verify(password, user[username])) ) return status(400, { success: false, message: 'Invalid username or password' }) const key = crypto.getRandomValues(new Uint32Array(1))[0] session[key] = username token.value = key return { success: true, message: `Signed in as ${username}` } }, { body: 'signIn', cookie: 'optionalSession' } ) .get( '/sign-out', ({ cookie: { token } }) => { token.remove() return { success: true, message: 'Signed out' } }, { cookie: 'optionalSession' } ) .use(getUserId) .get('/profile', ({ username }) => ({ success: true, username })) // @filename: note.ts import { Elysia, t } from 'elysia' import { getUserId, userService } from './user' const memo = t.Object({ data: t.String(), author: t.String() }) type Memo = typeof memo.static class Note { constructor( public data: Memo[] = [ { data: 'Moonhalo', author: 'saltyaom' } ] ) {} add(note: Memo) { this.data.push(note) return this.data } remove(index: number) { return this.data.splice(index, 1) } update(index: number, note: Partial) { return (this.data[index] = { ...this.data[index], ...note }) } } export const note = new Elysia({ prefix: '/note' }) .use(userService) .decorate('note', new Note()) .model({ memo: t.Omit(memo, ['author']) }) .onTransform(function log({ body, params, path, request: { method } }) { console.log(`${method} ${path}`, { body, params }) }) .get('/', ({ note }) => note.data) .use(getUserId) .put( '/', ({ note, body: { data }, username }) => note.add({ data, author: username }), { body: 'memo' } ) .get( '/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'Not Found :(') }, { params: t.Object({ index: t.Number() }) } ) .guard({ params: t.Object({ index: t.Number() }) }) .delete('/:index', ({ note, params: { index }, status }) => { if (index in note.data) return note.remove(index) return status(422) }) .patch( '/:index', ({ note, params: { index }, body: { data }, status, username }) => { if (index in note.data) return note.update(index, { data, author: username }) return status(422) }, { isSignIn: true, body: 'memo' } ) // @filename: index.ts // ---cut--- import { Elysia } from 'elysia' import { swagger } from '@elysiajs/swagger' import { opentelemetry } from '@elysiajs/opentelemetry' import { note } from './note' import { user } from './user' const app = new Elysia() .use(opentelemetry()) .use(swagger()) .onError(({ error, code }) => { if (code === 'NOT_FOUND') return 'Not Found :(' console.error(error) }) .use(user) .use(note) .listen(3000) ``` ```typescript twoslash [user.ts] // @errors: 2538 import { Elysia, t } from 'elysia' export const userService = new Elysia({ name: 'user/service' }) .state({ user: {} as Record, session: {} as Record }) .model({ signIn: t.Object({ username: t.String({ minLength: 1 }), password: t.String({ minLength: 8 }) }), session: t.Cookie( { token: t.Number() }, { secrets: 'seia' } ), optionalSession: t.Cookie( { token: t.Optional(t.Number()) }, { secrets: 'seia' } ) }) .macro({ isSignIn(enabled: boolean) { if (!enabled) return return { beforeHandle({ status, cookie: { token }, store: { session } }) { if (!token.value) return status(401, { success: false, message: 'Unauthorized' }) const username = session[token.value as unknown as number] if (!username) return status(401, { success: false, message: 'Unauthorized' }) } } } }) export const getUserId = new Elysia() .use(userService) .guard({ isSignIn: true, cookie: 'session' }) .resolve(({ store: { session }, cookie: { token } }) => ({ username: session[token.value] })) .as('scoped') export const user = new Elysia({ prefix: '/user' }) .use(userService) .put( '/sign-up', async ({ body: { username, password }, store, status }) => { if (store.user[username]) return status(400, { success: false, message: 'User already exists' }) store.user[username] = await Bun.password.hash(password) return { success: true, message: 'User created' } }, { body: 'signIn' } ) .post( '/sign-in', async ({ store: { user, session }, status, body: { username, password }, cookie: { token } }) => { if ( !user[username] || !(await Bun.password.verify(password, user[username])) ) return status(400, { success: false, message: 'Invalid username or password' }) const key = crypto.getRandomValues(new Uint32Array(1))[0] session[key] = username token.value = key return { success: true, message: `Signed in as ${username}` } }, { body: 'signIn', cookie: 'optionalSession' } ) .get( '/sign-out', ({ cookie: { token } }) => { token.remove() return { success: true, message: 'Signed out' } }, { cookie: 'optionalSession' } ) .use(getUserId) .get('/profile', ({ username }) => ({ success: true, username })) ``` ```typescript twoslash [note.ts] // @errors: 2538 // @filename: user.ts import { Elysia, t } from 'elysia' export const userService = new Elysia({ name: 'user/service' }) .state({ user: {} as Record, session: {} as Record }) .model({ signIn: t.Object({ username: t.String({ minLength: 1 }), password: t.String({ minLength: 8 }) }), session: t.Cookie( { token: t.Number() }, { secrets: 'seia' } ), optionalSession: t.Cookie( { token: t.Optional(t.Number()) }, { secrets: 'seia' } ) }) .macro({ isSignIn(enabled: boolean) { if (!enabled) return return { beforeHandle({ status, cookie: { token }, store: { session } }) { if (!token.value) return status(401, { success: false, message: 'Unauthorized' }) const username = session[token.value as unknown as number] if (!username) return status(401, { success: false, message: 'Unauthorized' }) } } } }) export const getUserId = new Elysia() .use(userService) .guard({ isSignIn: true, cookie: 'session' }) .resolve(({ store: { session }, cookie: { token } }) => ({ username: session[token.value] })) .as('scoped') export const user = new Elysia({ prefix: '/user' }) .use(getUserId) .get('/profile', ({ username }) => ({ success: true, username })) // @filename: note.ts // ---cut--- import { Elysia, t } from 'elysia' import { getUserId, userService } from './user' const memo = t.Object({ data: t.String(), author: t.String() }) type Memo = typeof memo.static class Note { constructor( public data: Memo[] = [ { data: 'Moonhalo', author: 'saltyaom' } ] ) {} add(note: Memo) { this.data.push(note) return this.data } remove(index: number) { return this.data.splice(index, 1) } update(index: number, note: Partial) { return (this.data[index] = { ...this.data[index], ...note }) } } export const note = new Elysia({ prefix: '/note' }) .use(userService) .decorate('note', new Note()) .model({ memo: t.Omit(memo, ['author']) }) .onTransform(function log({ body, params, path, request: { method } }) { console.log(`${method} ${path}`, { body, params }) }) .get('/', ({ note }) => note.data) .use(getUserId) .put( '/', ({ note, body: { data }, username }) => note.add({ data, author: username }), { body: 'memo' } ) .get( '/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'Not Found :(') }, { params: t.Object({ index: t.Number() }) } ) .guard({ params: t.Object({ index: t.Number() }) }) .delete('/:index', ({ note, params: { index }, status }) => { if (index in note.data) return note.remove(index) return status(422) }) .patch( '/:index', ({ note, params: { index }, body: { data }, status, username }) => { if (index in note.data) return note.update(index, { data, author: username }) return status(422) }, { isSignIn: true, body: 'memo' } ) ``` ::: ## Build for production Finally we can bundle our server into a binary for production using `bun build`: ```bash bun build \ --compile \ --minify-whitespace \ --minify-syntax \ --target bun \ --outfile server \ ./src/index.ts ``` This command is a bit long, so let's break it down: 1. `--compile` - Compile TypeScript to binary 2. `--minify-whitespace` - Remove unnecessary whitespace 3. `--minify-syntax` - Minify JavaScript syntax to reduce file size 4. `--target bun` - Target the `bun` platform, this can optimize the binary for the target platform 5. `--outfile server` - Output the binary as `server` 6. `./src/index.ts` - The entry file of our server (codebase) Now we can run the binary using `./server` and it will start the server on port 3000 same as using `bun dev`. ```bash ./server ``` Open your browser and navigate to `http://localhost:3000/swagger`, you should see the same result as using the dev command. By minifying the binary not only have we made our server small and portable, we also significantly reduced the memory usage of it. ::: tip Bun does have the `--minify` flag that will minify the binary, however it includes `--minify-identifiers`, and as we are using OpenTelemetry, it's going to rename function names and make tracing harder than it should. ::: ::: warning Exercise: Try to run the development server and production server, and compare the memory usage. The development server will use a process named 'bun', while the production server will use the name 'server'. ::: ## Wrapping up And- that's it 🎉 We have created a simple API using Elysia, we have learned how to create a simple API, how to handle errors, and how to observe our server using OpenTelemetry. You could to take a step further by trying to connect to a real database, connect to a real frontend or implement real-time communication with WebSocket. This tutorial covered most of the concepts we need to know to create an Elysia server, however there are other several useful concepts you might want to know. ### If you are stuck If you have any further questions, feel free to ask our community on GitHub Discussions, Discord, and Twitter. We wish you well on your journey with Elysia ❤️ --- --- url: /patterns/type.md --- # Type Here's a common patterns for writing validation types in Elysia. ## Primitive Type The TypeBox API is designed around and is similar to TypeScript types. There are many familiar names and behaviors that intersect with TypeScript counterparts, such as **String**, **Number**, **Boolean**, and **Object**, as well as more advanced features like **Intersect**, **KeyOf**, and **Tuple** for versatility. If you are familiar with TypeScript, creating a TypeBox schema behaves the same as writing a TypeScript type, except it provides actual type validation at runtime. To create your first schema, import **Elysia.t** from Elysia and start with the most basic type: ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .post('/', ({ body }) => `Hello ${body}`, { body: t.String() }) .listen(3000) ``` This code tells Elysia to validate an incoming HTTP body, ensuring that the body is a string. If it is a string, it will be allowed to flow through the request pipeline and handler. If the shape doesn't match, it will throw an error into the [Error Life Cycle](/essential/life-cycle.html#on-error). ![Elysia Life Cycle](/assets/lifecycle-chart.svg) ### Basic Type TypeBox provides basic primitive types with the same behavior as TypeScript types. The following table lists the most common basic types: ```typescript t.String() ``` ```typescript string ``` ```typescript t.Number() ``` ```typescript number ``` ```typescript t.Boolean() ``` ```typescript boolean ``` ```typescript t.Array( t.Number() ) ``` ```typescript number[] ``` ```typescript t.Object({ x: t.Number() }) ``` ```typescript { x: number } ``` ```typescript t.Null() ``` ```typescript null ``` ```typescript t.Literal(42) ``` ```typescript 42 ``` Elysia extends all types from TypeBox, allowing you to reference most of the API from TypeBox for use in Elysia. See [TypeBox's Type](https://github.com/sinclairzx81/typebox#json-types) for additional types supported by TypeBox. ### Attribute TypeBox can accept arguments for more comprehensive behavior based on the JSON Schema 7 specification. ```typescript t.String({ format: 'email' }) ``` ```typescript saltyaom@elysiajs.com ``` ```typescript t.Number({ minimum: 10, maximum: 100 }) ``` ```typescript 10 ``` ```typescript t.Array( t.Number(), { /** * Minimum number of items */ minItems: 1, /** * Maximum number of items */ maxItems: 5 } ) ``` ```typescript [1, 2, 3, 4, 5] ``` ```typescript t.Object( { x: t.Number() }, { /** * @default false * Accept additional properties * that not specified in schema * but still match the type */ additionalProperties: true } ) ``` ```typescript x: 100 y: 200 ``` See [JSON Schema 7 specification](https://json-schema.org/draft/2020-12/json-schema-validation) for more explanation of each attribute. ## Honorable Mentions The following are common patterns often found useful when creating a schema. ### Union Allows a field in `t.Object` to have multiple types. ```typescript t.Union([ t.String(), t.Number() ]) ``` ```typescript string | number ``` ``` Hello 123 ``` ### Optional Allows a field in `t.Object` to be undefined or optional. ```typescript t.Object({ x: t.Number(), y: t.Optional(t.Number()) }) ``` ```typescript { x: number, y?: number } ``` ```typescript { x: 123 } ``` ### Partial Allows all fields in `t.Object` to be optional. ```typescript t.Partial( t.Object({ x: t.Number(), y: t.Number() }) ) ``` ```typescript { x?: number, y?: number } ``` ```typescript { y: 123 } ``` ## Elysia Type `Elysia.t` is based on TypeBox with pre-configuration for server usage, providing additional types commonly found in server-side validation. You can find all the source code for Elysia types in `elysia/type-system`. The following are types provided by Elysia: ### UnionEnum `UnionEnum` allows the value to be one of the specified values. ```typescript t.UnionEnum(['rapi', 'anis', 1, true, false]) ``` By default, these value will not automatically ### File A singular file, often useful for **file upload** validation. ```typescript t.File() ``` File extends the attributes of the base schema, with additional properties as follows: #### type Specifies the format of the file, such as image, video, or audio. If an array is provided, it will attempt to validate if any of the formats are valid. ```typescript type?: MaybeArray ``` #### minSize Minimum size of the file. Accepts a number in bytes or a suffix of file units: ```typescript minSize?: number | `${number}${'k' | 'm'}` ``` #### maxSize Maximum size of the file. Accepts a number in bytes or a suffix of file units: ```typescript maxSize?: number | `${number}${'k' | 'm'}` ``` #### File Unit Suffix: The following are the specifications of the file unit: m: MegaByte (1048576 byte) k: KiloByte (1024 byte) ### Files Extends from [File](#file), but adds support for an array of files in a single field. ```typescript t.Files() ``` Files extends the attributes of the base schema, array, and File. ### Cookie Object-like representation of a Cookie Jar extended from the Object type. ```typescript t.Cookie({ name: t.String() }) ``` Cookie extends the attributes of [Object](https://json-schema.org/draft/2020-12/json-schema-validation#name-validation-keywords-for-obj) and [Cookie](https://github.com/jshttp/cookie#options-1) with additional properties as follows: #### secrets The secret key for signing cookies. Accepts a string or an array of strings. ```typescript secrets?: string | string[] ``` If an array is provided, [Key Rotation](https://crypto.stackexchange.com/questions/41796/whats-the-purpose-of-key-rotation) will be used. The newly signed value will use the first secret as the key. ### Nullable Allows the value to be null but not undefined. ```typescript t.Nullable(t.String()) ``` ### MaybeEmpty Allows the value to be null and undefined. ```typescript t.MaybeEmpty(t.String()) ``` For additional information, you can find the full source code of the type system in [`elysia/type-system`](https://github.com/elysiajs/elysia/blob/main/src/type-system.ts). ### Form A syntax sugar our `t.Object` with support for verifying return value of [form](/essential/handler.html#formdata) (FormData). ```typescript t.FormData({ someValue: t.File() }) ``` ### Numeric (legacy) ::: warning This is not need as Elysia type already transforms Number to Numeric automatically since 1.0 ::: Numeric accepts a numeric string or number and then transforms the value into a number. ```typescript t.Numeric() ``` This is useful when an incoming value is a numeric string, for example, a path parameter or query string. Numeric accepts the same attributes as [Numeric Instance](https://json-schema.org/draft/2020-12/json-schema-validation#name-validation-keywords-for-num) ## Elysia behavior Elysia use TypeBox by default. However, to help making handling with HTTP easier. Elysia has some dedicated type and have some behavior difference from TypeBox. ## Optional To make a field optional, use `t.Optional`. This will allows client to optionally provide a query parameter. This behavior also applied to `body`, `headers`. This is different from TypeBox where optional is to mark a field of object as optional. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/optional', ({ query }) => query, { // ^? query: t.Optional( t.Object({ name: t.String() }) ) }) ``` ## Number to Numeric By default, Elysia will convert a `t.Number` to [t.Numeric](#numeric-legacy) when provided as route schema. Because parsed HTTP headers, query, url parameter is always a string. This means that even if a value is number, it will be treated as string. Elysia override this behavior by checking if a string value looks like a number then convert it even appropriate. This is only applied when it is used as a route schema and not in a nested `t.Object`. ```ts import { Elysia, t } from 'elysia' new Elysia() .get('/:id', ({ id }) => id, { params: t.Object({ // Converted to t.Numeric() id: t.Number() }), body: t.Object({ // NOT converted to t.Numeric() id: t.Number() }) }) // NOT converted to t.Numeric() t.Number() ``` ## Boolean to BooleanString Similar to [Number to Numeric](#number-to-numeric) Any `t.Boolean` will be converted to `t.BooleanString`. ```ts import { Elysia, t } from 'elysia' new Elysia() .get('/:id', ({ id }) => id, { params: t.Object({ // Converted to t.Boolean() id: t.Boolean() }), body: t.Object({ // NOT converted to t.Boolean() id: t.Boolean() }) }) // NOT converted to t.BooleanString() t.Boolean() ``` --- --- url: /essential/validation.md --- # Validation The purpose of creating an API server is to take an input and process it. JavaScript allows any data to be any type. Elysia provides a tool to validate data out of the box to ensure that the data is in the correct format. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/id/:id', ({ params: { id } }) => id, { params: t.Object({ id: t.Number() }) }) .listen(3000) ``` ### TypeBox **Elysia.t** is a schema builder based on [TypeBox](https://github.com/sinclairzx81/typebox) that provides type-safety at runtime, compile-time, and for OpenAPI schemas, enabling the generation of OpenAPI/Swagger documentation. TypeBox is a very fast, lightweight, and type-safe runtime validation library for TypeScript. Elysia extends and customizes the default behavior of TypeBox to match server-side validation requirements. We believe that validation should be handled by the framework natively, rather than relying on the user to set up a custom type for every project. ### TypeScript We can get a type definitions of every Elysia/TypeBox's type by accessing `static` property as follows: ```ts twoslash import { t } from 'elysia' const MyType = t.Object({ hello: t.Literal('Elysia') }) type MyType = typeof MyType.static // ^? ``` This allows Elysia to infer and provide type automatically, reducing the need to declare duplicate schema A single Elysia/TypeBox schema can be used for: * Runtime validation * Data coercion * TypeScript type * OpenAPI schema This allows us to make a schema as a **single source of truth**. ## Schema type Elysia supports declarative schemas with the following types: *** These properties should be provided as the third argument of the route handler to validate the incoming request. ```typescript import { Elysia, t } from 'elysia' new Elysia() .get('/id/:id', () => 'Hello World!', { query: t.Object({ name: t.String() }), params: t.Object({ id: t.Number() }) }) .listen(3000) ``` The response should be as follows: | URL | Query | Params | | --- | --------- | ------------ | | /id/a | ❌ | ❌ | | /id/1?name=Elysia | ✅ | ✅ | | /id/1?alias=Elysia | ❌ | ✅ | | /id/a?name=Elysia | ✅ | ❌ | | /id/a?alias=Elysia | ❌ | ❌ | When a schema is provided, the type will be inferred from the schema automatically and an OpenAPI type will be generated for Swagger documentation, eliminating the redundant task of providing the type manually. ## Guard Guard can be used to apply a schema to multiple handlers. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/none', ({ query }) => 'hi') // ^? .guard({ // [!code ++] query: t.Object({ // [!code ++] name: t.String() // [!code ++] }) // [!code ++] }) // [!code ++] .get('/query', ({ query }) => query) // ^? .listen(3000) ``` This code ensures that the query must have **name** with a string value for every handler after it. The response should be listed as follows: The response should be listed as follows: | Path | Response | | ------------- | -------- | | /none | hi | | /none?name=a | hi | | /query | error | | /query?name=a | a | If multiple global schemas are defined for the same property, the latest one will take precedence. If both local and global schemas are defined, the local one will take precedence. ### Guard Schema Type Guard supports 2 types to define a validation. ### **override (default)** Override schema if schema is collide with each others. ![Elysia run with default override guard showing schema gets override](/blog/elysia-13/schema-override.webp) ### **standalone** Separate collided schema, and runs both independently resulting in both being validated. ![Elysia run with standalone merging multiple guard together](/blog/elysia-13/schema-standalone.webp) To define schema type of guard with `schema`: ```ts import { Elysia } from 'elysia' new Elysia() .guard({ schema: 'standalone', // [!code ++] response: t.Object({ title: t.String() }) }) ``` ## Body An incoming [HTTP Message](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages) is the data sent to the server. It can be in the form of JSON, form-data, or any other format. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .post('/body', ({ body }) => body, { // ^? body: t.Object({ name: t.String() }) }) .listen(3000) ``` The validation should be as follows: | Body | Validation | | --- | --------- | | { name: 'Elysia' } | ✅ | | { name: 1 } | ❌ | | { alias: 'Elysia' } | ❌ | | `undefined` | ❌ | Elysia disables body-parser for **GET** and **HEAD** messages by default, following the specs of HTTP/1.1 [RFC2616](https://www.rfc-editor.org/rfc/rfc2616#section-4.3) > If the request method does not include defined semantics for an entity-body, then the message-body SHOULD be ignored when handling the request. Most browsers disable the attachment of the body by default for **GET** and **HEAD** methods. #### Specs Validate an incoming [HTTP Message](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages) (or body). These messages are additional messages for the web server to process. The body is provided in the same way as the `body` in `fetch` API. The content type should be set accordingly to the defined body. ```typescript fetch('https://elysiajs.com', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Elysia' }) }) ``` ### File File is a special type of body that can be used to upload files. ```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) ``` By providing a file type, Elysia will automatically assume that the content-type is `multipart/form-data`. ## Query Query is the data sent through the URL. It can be in the form of `?key=value`. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/query', ({ query }) => query, { // ^? query: t.Object({ name: t.String() }) }) .listen(3000) ``` Query must be provided in the form of an object. The validation should be as follows: | Query | Validation | | ---- | --------- | | /?name=Elysia | ✅ | | /?name=1 | ✅ | | /?alias=Elysia | ❌ | | /?name=ElysiaJS\&alias=Elysia | ✅ | | / | ❌ | #### Specs A query string is a part of the URL that starts with **?** and can contain one or more query parameters, which are key-value pairs used to convey additional information to the server, usually for customized behavior like filtering or searching. ![URL Object](/essential/url-object.svg) Query is provided after the **?** in Fetch API. ```typescript fetch('https://elysiajs.com/?name=Elysia') ``` When specifying query parameters, it's crucial to understand that all query parameter values must be represented as strings. This is due to how they are encoded and appended to the URL. ### Coercion Elysia will coerce applicable schema on `query` to respective type automatically. See [Elysia behavior](/patterns/type#elysia-behavior) for more information. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/', ({ query }) => query, { // ^? query: t.Object({ // [!code ++] name: t.Number() // [!code ++] }) // [!code ++] }) .listen(3000) ``` ### Array By default, Elysia treat query parameters as a single string even if specified multiple time. To use array, we need to explicitly declare it as an array. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/', ({ query }) => query, { // ^? query: t.Object({ name: t.Array(t.String()) // [!code ++] }) }) .listen(3000) ``` Once Elysia detect that a property is assignable to array, Elysia will coerce it to an array of the specified type. By default, Elysia format query array with the following format: #### nuqs This format is used by [nuqs](https://nuqs.47ng.com). By using **,** as a delimiter, a property will be treated as array. ``` http://localhost?name=rapi,anis,neon&squad=counter { name: ['rapi', 'anis', 'neon'], squad: 'counter' } ``` #### HTML form format If a key is assigned multiple time, the key will be treated as an array. This is similar to HTML form format when an input with the same name is specified multiple times. ``` http://localhost?name=rapi&name=anis&name=neon&squad=counter // name: ['rapi', 'anis', 'neon'] ``` ## Params Params or path parameters are the data sent through the URL path. They can be in the form of `/key`. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/id/:id', ({ params }) => params, { // ^? params: t.Object({ id: t.Number() }) }) ``` Params must be provided in the form of an object. The validation should be as follows: | URL | Validation | | --- | --------- | | /id/1 | ✅ | | /id/a | ❌ | #### Specs Path parameter (not to be confused with query string or query parameter). **This field is usually not needed as Elysia can infer types from path parameters automatically**, unless there is a need for a specific value pattern, such as a numeric value or template literal pattern. ```typescript fetch('https://elysiajs.com/id/1') ``` ### Params type inference If a params schema is not provided, Elysia will automatically infer the type as a string. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/id/:id', ({ params }) => params) // ^? ``` ## Headers Headers are the data sent through the request's header. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/headers', ({ headers }) => headers, { // ^? headers: t.Object({ authorization: t.String() }) }) ``` Unlike other types, headers have `additionalProperties` set to `true` by default. This means that headers can have any key-value pair, but the value must match the schema. #### Specs HTTP headers let the client and the server pass additional information with an HTTP request or response, usually treated as metadata. This field is usually used to enforce some specific header fields, for example, `Authorization`. Headers are provided in the same way as the `body` in `fetch` API. ```typescript fetch('https://elysiajs.com/', { headers: { authorization: 'Bearer 12345' } }) ``` ::: tip Elysia will parse headers as lower-case keys only. Please make sure that you are using lower-case field names when using header validation. ::: ## Cookie Cookie is the data sent through the request's cookie. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/cookie', ({ cookie }) => cookie, { // ^? cookie: t.Cookie({ cookieName: t.String() }) }) ``` Cookies must be provided in the form of `t.Cookie` or `t.Object`. Same as `headers`, cookies have `additionalProperties` set to `true` by default. #### Specs An HTTP cookie is a small piece of data that a server sends to the client. It's data that is sent with every visit to the same web server to let the server remember client information. In simpler terms, it's a stringified state that is sent with every request. This field is usually used to enforce some specific cookie fields. A cookie is a special header field that the Fetch API doesn't accept a custom value for but is managed by the browser. To send a cookie, you must use a `credentials` field instead: ```typescript fetch('https://elysiajs.com/', { credentials: 'include' }) ``` ### t.Cookie `t.Cookie` is a special type that is equivalent to `t.Object` but allows to set cookie-specific options. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/cookie', ({ cookie }) => cookie.name.value, { // ^? cookie: t.Cookie({ name: t.String() }, { secure: true, httpOnly: true }) }) ``` ## Response Response is the data returned from the handler. ```typescript import { Elysia, t } from 'elysia' new Elysia() .get('/response', () => { return { name: 'Jane Doe' } }, { response: t.Object({ name: t.String() }) }) ``` ### Response per status Responses can be set per status code. ```typescript import { Elysia, t } from 'elysia' new Elysia() .get('/response', ({ status }) => { if (Math.random() > 0.5) return status(400, { error: 'Something went wrong' }) return { name: 'Jane Doe' } }, { response: { 200: t.Object({ name: t.String() }), 400: t.Object({ error: t.String() }) } }) ``` This is an Elysia-specific feature, allowing us to make a field optional. ## Error Provider There are two ways to provide a custom error message when the validation fails: 1. Inline `status` property 2. Using [onError](/essential/life-cycle.html#on-error) event ### Error Property Elysia offers an additional **error** property, allowing us to return a custom error message if the field is invalid. ```typescript import { Elysia, t } from 'elysia' new Elysia() .post('/', () => 'Hello World!', { body: t.Object({ x: t.Number({ error: 'x must be a number' }) }) }) .listen(3000) ``` The following is an example of using the error property on various types: ```typescript t.String({ format: 'email', error: 'Invalid email :(' }) ``` ``` Invalid Email :( ``` ```typescript t.Array( t.String(), { error: 'All members must be a string' } ) ``` ``` All members must be a string ``` ```typescript t.Object({ x: t.Number() }, { error: 'Invalid object UwU' }) ``` ``` Invalid object UwU ``` ```typescript t.Object({ x: t.Number({ error({ errors, type, validation, value }) { return 'Expected x to be a number' } }) }) ``` ``` Expected x to be a number ``` ## Custom Error TypeBox offers an additional "**error**" property, allowing us to return a custom error message if the field is invalid. ```typescript t.String({ format: 'email', error: 'Invalid email :(' }) ``` ``` Invalid Email :( ``` ```typescript t.Object({ x: t.Number() }, { error: 'Invalid object UwU' }) ``` ``` Invalid object UwU ``` ### Error message as function In addition to a string, Elysia type's error can also accept a function to programmatically return a custom error for each property. The error function accepts the same arguments as `ValidationError` ```typescript import { Elysia, t } from 'elysia' new Elysia() .post('/', () => 'Hello World!', { body: t.Object({ x: t.Number({ error() { return 'Expected x to be a number' } }) }) }) .listen(3000) ``` ::: tip Hover over the `error` to see the type. ::: ### Error is Called Per Field Please note that the error function will only be called if the field is invalid. Please consider the following table: ```typescript t.Object({ x: t.Number({ error() { return 'Expected x to be a number' } }) }) ``` ```json { x: "hello" } ``` ```typescript t.Object({ x: t.Number({ error() { return 'Expected x to be a number' } }) }) ``` ```json "hello" ``` ```typescript t.Object( { x: t.Number({ error() { return 'Expected x to be a number' } }) }, { error() { return 'Expected value to be an object' } } ) ``` ```json "hello" ``` ### onError We can customize the behavior of validation based on the [onError](/essential/life-cycle.html#on-error) event by narrowing down the error code to "**VALIDATION**". ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .onError(({ code, error }) => { if (code === 'VALIDATION') return error.message }) .listen(3000) ``` The narrowed-down error type will be typed as `ValidationError` imported from **elysia/error**. **ValidationError** exposes a property named **validator**, typed as [TypeCheck](https://github.com/sinclairzx81/typebox#typecheck), allowing us to interact with TypeBox functionality out of the box. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .onError(({ code, error }) => { if (code === 'VALIDATION') return error.validator.Errors(error.value).First().message }) .listen(3000) ``` ### Error List **ValidationError** provides a method `ValidatorError.all`, allowing us to list all of the error causes. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .post('/', ({ body }) => body, { body: t.Object({ name: t.String(), age: t.Number() }), error({ code, error }) { switch (code) { case 'VALIDATION': console.log(error.all) // Find a specific error name (path is OpenAPI Schema compliance) const name = error.all.find( (x) => x.summary && x.path === '/name' ) // If there is a validation error, then log it if(name) console.log(name) } } }) .listen(3000) ``` For more information about TypeBox's validator, see [TypeCheck](https://github.com/sinclairzx81/typebox#typecheck). ## Reference Model Sometimes you might find yourself declaring duplicate models or re-using the same model multiple times. With a reference model, we can name our model and reuse it by referencing the name. Let's start with a simple scenario. Suppose we have a controller that handles sign-in with the same model. ```typescript twoslash import { Elysia, t } from 'elysia' const app = new Elysia() .post('/sign-in', ({ body }) => body, { body: t.Object({ username: t.String(), password: t.String() }), response: t.Object({ username: t.String(), password: t.String() }) }) ``` We can refactor the code by extracting the model as a variable and referencing it. ```typescript twoslash import { Elysia, t } from 'elysia' // Maybe in a different file eg. models.ts const SignDTO = t.Object({ username: t.String(), password: t.String() }) const app = new Elysia() .post('/sign-in', ({ body }) => body, { body: SignDTO, response: SignDTO }) ``` This method of separating concerns is an effective approach, but we might find ourselves reusing multiple models with different controllers as the app gets more complex. We can resolve that by creating a "reference model", allowing us to name the model and use auto-completion to reference it directly in `schema` by registering the models with `model`. ```typescript twoslash import { Elysia, t } from 'elysia' const app = new Elysia() .model({ sign: t.Object({ username: t.String(), password: t.String() }) }) .post('/sign-in', ({ body }) => body, { // with auto-completion for existing model name body: 'sign', response: 'sign' }) ``` When we want to access the model's group, we can separate a `model` into a plugin, which when registered will provide a set of models instead of multiple imports. ```typescript // auth.model.ts import { Elysia, t } from 'elysia' export const authModel = new Elysia() .model({ sign: t.Object({ username: t.String(), password: t.String() }) }) ``` Then in an instance file: ```typescript twoslash // @filename: auth.model.ts import { Elysia, t } from 'elysia' export const authModel = new Elysia() .model({ sign: t.Object({ username: t.String(), password: t.String() }) }) // @filename: index.ts // ---cut--- // index.ts import { Elysia } from 'elysia' import { authModel } from './auth.model' const app = new Elysia() .use(authModel) .post('/sign-in', ({ body }) => body, { // with auto-completion for existing model name body: 'sign', response: 'sign' }) ``` This approach not only allows us to separate concerns but also enables us to reuse the model in multiple places while integrating the model into Swagger documentation. ### Multiple Models `model` accepts an object with the key as a model name and the value as the model definition. Multiple models are supported by default. ```typescript // auth.model.ts import { Elysia, t } from 'elysia' export const authModel = new Elysia() .model({ number: t.Number(), sign: t.Object({ username: t.String(), password: t.String() }) }) ``` ### Naming Convention Duplicate model names will cause Elysia to throw an error. To prevent declaring duplicate model names, we can use the following naming convention. Let's say that we have all models stored at `models/.ts` and declare the prefix of the model as a namespace. ```typescript import { Elysia, t } from 'elysia' // admin.model.ts export const adminModels = new Elysia() .model({ 'admin.auth': t.Object({ username: t.String(), password: t.String() }) }) // user.model.ts export const userModels = new Elysia() .model({ 'user.auth': t.Object({ username: t.String(), password: t.String() }) }) ``` This can prevent naming duplication to some extent, but ultimately, it's best to let your team decide on the naming convention. Elysia provides an opinionated option to help prevent decision fatigue. --- --- url: /patterns/websocket.md --- # WebSocket WebSocket is a realtime protocol for communication between your client and server. Unlike HTTP where our client repeatedly asking the website for information and waiting for a reply each time, WebSocket sets up a direct line where our client and server can send messages back and forth directly, making the conversation quicker and smoother without having to start over each message. SocketIO is a popular library for WebSocket, but it is not the only one. Elysia uses [uWebSocket](https://github.com/uNetworking/uWebSockets) which Bun uses under the hood with the same API. To use websocket, simply call `Elysia.ws()`: ```typescript import { Elysia } from 'elysia' new Elysia() .ws('/ws', { message(ws, message) { ws.send(message) } }) .listen(3000) ``` ## WebSocket message validation: Same as normal route, WebSockets also accepts a **schema** object to strictly type and validate requests. ```typescript import { Elysia, t } from 'elysia' const app = new Elysia() .ws('/ws', { // validate incoming message body: t.Object({ message: t.String() }), query: t.Object({ id: t.String() }), message(ws, { message }) { // Get schema from `ws.data` const { id } = ws.data.query ws.send({ id, message, time: Date.now() }) } }) .listen(3000) ``` WebSocket schema can validate the following: * **message** - An incoming message. * **query** - Query string or URL parameters. * **params** - Path parameters. * **header** - Request's headers. * **cookie** - Request's cookie * **response** - Value returned from handler By default Elysia will parse incoming stringified JSON message as Object for validation. ## Configuration You can set Elysia constructor to set the Web Socket value. ```ts import { Elysia } from 'elysia' new Elysia({ websocket: { idleTimeout: 30 } }) ``` Elysia's WebSocket implementation extends Bun's WebSocket configuration, please refer to [Bun's WebSocket documentation](https://bun.sh/docs/api/websockets) for more information. The following are a brief configuration from [Bun WebSocket](https://bun.sh/docs/api/websockets#create-a-websocket-server) ### perMessageDeflate @default `false` Enable compression for clients that support it. By default, compression is disabled. ### maxPayloadLength The maximum size of a message. ### idleTimeout @default `120` After a connection has not received a message for this many seconds, it will be closed. ### backpressureLimit @default `16777216` (16MB) The maximum number of bytes that can be buffered for a single connection. ### closeOnBackpressureLimit @default `false` Close the connection if the backpressure limit is reached. ## Methods Below are the new methods that are available to the WebSocket route ## ws Create a websocket handler Example: ```typescript import { Elysia } from 'elysia' const app = new Elysia() .ws('/ws', { message(ws, message) { ws.send(message) } }) .listen(3000) ``` Type: ```typescript .ws(endpoint: path, options: Partial>): this ``` * **endpoint** - A path to exposed as websocket handler * **options** - Customize WebSocket handler behavior ## WebSocketHandler WebSocketHandler extends config from [config](#configuration). Below is a config which is accepted by `ws`. ## open Callback function for new websocket connection. Type: ```typescript open(ws: ServerWebSocket<{ // uid for each connection id: string data: Context }>): this ``` ## message Callback function for incoming websocket message. Type: ```typescript message( ws: ServerWebSocket<{ // uid for each connection id: string data: Context }>, message: Message ): this ``` `Message` type based on `schema.message`. Default is `string`. ## close Callback function for closing websocket connection. Type: ```typescript close(ws: ServerWebSocket<{ // uid for each connection id: string data: Context }>): this ``` ## drain Callback function for the server is ready to accept more data. Type: ```typescript drain( ws: ServerWebSocket<{ // uid for each connection id: string data: Context }>, code: number, reason: string ): this ``` ## parse `Parse` middleware to parse the request before upgrading the HTTP connection to WebSocket. ## beforeHandle `Before Handle` middleware which execute before upgrading the HTTP connection to WebSocket. Ideal place for validation. ## transform `Transform` middleware which execute before validation. ## transformMessage Like `transform`, but execute before validation of WebSocket message ## header Additional headers to add before upgrade connection to WebSocket.