├── README.md └── docs ├── at-glance.md ├── blog.md ├── blog ├── elysia-02.md ├── elysia-03.md ├── elysia-04.md ├── elysia-05.md ├── elysia-06.md ├── elysia-07.md ├── elysia-08.md ├── elysia-10.md ├── elysia-11.md ├── elysia-12.md ├── elysia-supabase.md ├── integrate-trpc-with-elysia.md └── with-prisma.md ├── eden ├── fetch.md ├── installation.md ├── overview.md ├── test.md └── treaty │ ├── config.md │ ├── legacy.md │ ├── overview.md │ ├── parameters.md │ ├── response.md │ ├── unit-test.md │ └── websocket.md ├── essential ├── best-practice.md ├── handler.md ├── life-cycle.md ├── plugin.md ├── route.md ├── structure.md └── validation.md ├── index.md ├── integrations ├── astro.md ├── cheat-sheet.md ├── expo.md ├── nextjs.md └── sveltekit.md ├── key-concept.md ├── midori.md ├── patterns ├── configuration.md ├── cookie.md ├── deployment.md ├── macro.md ├── mount.md ├── trace.md ├── unit-test.md └── websocket.md ├── plugins ├── bearer.md ├── cors.md ├── cron.md ├── graphql-apollo.md ├── graphql-yoga.md ├── html.md ├── jwt.md ├── opentelemetry.md ├── overview.md ├── server-timing.md ├── static.md ├── stream.md ├── swagger.md └── trpc.md ├── quick-start.md ├── recipe ├── better-auth.md ├── drizzle.md ├── openapi.md ├── opentelemetry.md └── react-email.md ├── table-of-content.md └── tutorial.md
1 | ## Elysia documentation 2 | Written by VitePress 3 |
1 | ---
2 | title: At glance - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: At glance - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Designed with ergonomic design, extensive support for TypeScript, modern JavaScript API, optimized for Bun. Offers a unique experience unified type, and end-to-end type safety while maintaining excellent performance.
11 |
12 | - - meta
13 | - property: 'og:description'
14 | content: Designed with ergonomic design, extensive support for TypeScript, modern JavaScript API, optimized for Bun. Offers a unique experience unified type, and end-to-end type safety while maintaining excellent performance.
15 | ---
16 |
17 | <script setup>
18 | import Card from '../components/nearl/card.vue'
19 | import Deck from '../components/nearl/card-deck.vue'
20 | import Playground from '../components/nearl/playground.vue'
21 |
22 | import { Elysia } from 'elysia'
23 |
24 | const demo1 = new Elysia()
25 | .get('/', 'Hello Elysia')
26 | .get('/user/:id', ({ params: { id }}) => id)
27 | .post('/form', ({ body }) => body)
28 |
29 | const demo2 = new Elysia()
30 | .get('/user/:id', ({ params: { id }}) => id)
31 | .get('/user/abc', () => 'abc')
32 | </script>
33 |
34 | # At glance
35 | Elysia is an ergonomic web framework for building backend servers with Bun.
36 |
37 | Designed with simplicity and type-safety in mind, Elysia has a familiar API with extensive support for TypeScript, optimized for Bun.
38 |
39 | Here's a simple hello world in Elysia.
40 |
41 | typescript twoslash 42 | import { Elysia } from 'elysia' 43 | 44 | new Elysia() 45 | .get('/', 'Hello Elysia') 46 | .get('/user/:id', ({ params: { id }}) => id) 47 | .post('/form', ({ body }) => body) 48 | .listen(3000) 49 |
50 |
51 | Navigate to localhost:3000 and it should show 'Hello Elysia' as a result.
52 |
53 | <Playground
54 | :elysia="demo1"
55 | :alias="{
56 | '/user/:id': '/user/1'
57 | }"
58 | :mock="{
59 | '/user/:id': {
60 | GET: '1'
61 | },
62 | '/form': {
63 | POST: JSON.stringify({
64 | hello: 'Elysia'
65 | })
66 | }
67 | }"
68 | />
69 |
70 | ::: tip
71 | Hover over the code snippet to see the type definition.
72 |
73 | In the mock browser, click on the path highlighted in blue to change paths and preview the response.
74 |
75 | Elysia can run in the browser, and the results you see are actually run using Elysia.
76 | :::
77 |
78 | ## Performance
79 |
80 | Building on Bun and extensive optimization like Static Code Analysis allows Elysia to generate optimized code on the fly.
81 |
82 | Elysia can outperform most of the web frameworks available today[1], and even match the performance of Golang and Rust frameworks[2].
83 |
84 | | Framework | Runtime | Average | Plain Text | Dynamic Parameters | JSON Body |
85 | | ------------- | ------- | ----------- | ---------- | ------------------ | ---------- |
86 | | bun | bun | 262,660.433 | 326,375.76 | 237,083.18 | 224,522.36 |
87 | | elysia | bun | 255,574.717 | 313,073.64 | 241,891.57 | 211,758.94 |
88 | | hyper-express | node | 234,395.837 | 311,775.43 | 249,675 | 141,737.08 |
89 | | hono | bun | 203,937.883 | 239,229.82 | 201,663.43 | 170,920.4 |
90 | | h3 | node | 96,515.027 | 114,971.87 | 87,935.94 | 86,637.27 |
91 | | oak | deno | 46,569.853 | 55,174.24 | 48,260.36 | 36,274.96 |
92 | | fastify | bun | 65,897.043 | 92,856.71 | 81,604.66 | 23,229.76 |
93 | | fastify | node | 60,322.413 | 71,150.57 | 62,060.26 | 47,756.41 |
94 | | koa | node | 39,594.14 | 46,219.64 | 40,961.72 | 31,601.06 |
95 | | express | bun | 29,715.537 | 39,455.46 | 34,700.85 | 14,990.3 |
96 | | express | node | 15,913.153 | 17,736.92 | 17,128.7 | 12,873.84 |
97 |
98 | ## TypeScript
99 |
100 | Elysia is designed to help you write less TypeScript.
101 |
102 | 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.
103 |
104 | Take a look at this example:
105 |
106 | typescript twoslash 107 | import { Elysia } from 'elysia' 108 | 109 | new Elysia() 110 | .get('/user/:id', ({ params: { id } }) => id) 111 | // ^? 112 | .listen(3000) 113 |
114 |
115 |
116 |
117 | 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.
118 |
119 | <Playground
120 | :elysia="demo2"
121 | :alias="{
122 | '/user/:id': '/user/123'
123 | }"
124 | :mock="{
125 | '/user/:id': {
126 | GET: '123'
127 | },
128 | }"
129 | />
130 |
131 | 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.
132 |
133 | TypeScript is not needed to use Elysia, but it's recommended to use Elysia with TypeScript.
134 |
135 | ## Type Integrity
136 |
137 | 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.
138 |
139 | Let's modify the previous code to accept only a numeric value instead of a string.
140 |
141 | typescript twoslash 142 | import { Elysia, t } from 'elysia' 143 | 144 | new Elysia() 145 | .get('/user/:id', ({ params: { id } }) => id, { 146 | // ^? 147 | params: t.Object({ 148 | id: t.Numeric() 149 | }) 150 | }) 151 | .listen(3000) 152 |
153 |
154 | This code ensures that our path parameter id will always be a numeric string and then transforms it into a number automatically at both runtime and compile-time (type-level).
155 |
156 | ::: tip
157 | Hover over "id" in the above code snippet to see a type definition.
158 | :::
159 |
160 | With Elysia's schema builder, we can ensure type safety like a strongly-typed language with a single source of truth.
161 |
162 | ## Standard
163 |
164 | 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.
165 |
166 | For instance, because Elysia adopts OpenAPI by default, generating documentation with Swagger is as easy as adding a one-liner:
167 |
168 | typescript twoslash 169 | import { Elysia, t } from 'elysia' 170 | import { swagger } from '@elysiajs/swagger' 171 | 172 | new Elysia() 173 | .use(swagger()) 174 | .get('/user/:id', ({ params: { id } }) => id, { 175 | params: t.Object({ 176 | id: t.Number() 177 | }) 178 | }) 179 | .listen(3000) 180 |
181 |
182 | With the Swagger plugin, you can seamlessly generate a Swagger page without additional code or specific config and share it with your team effortlessly.
183 |
184 | ## End-to-end Type Safety
185 |
186 | With Elysia, type safety is not limited to server-side only.
187 |
188 | With Elysia, you can synchronize your types with your frontend team automatically like tRPC, with Elysia's client library, "Eden".
189 |
190 | typescript twoslash 191 | import { Elysia, t } from 'elysia' 192 | import { swagger } from '@elysiajs/swagger' 193 | 194 | const app = new Elysia() 195 | .use(swagger()) 196 | .get('/user/:id', ({ params: { id } }) => id, { 197 | params: t.Object({ 198 | id: t.Number() 199 | }) 200 | }) 201 | .listen(3000) 202 | 203 | export type App = typeof app 204 |
205 |
206 | And on your client-side:
207 |
208 | typescript twoslash 209 | // @filename: server.ts 210 | import { Elysia, t } from 'elysia' 211 | 212 | const app = new Elysia() 213 | .get('/user/:id', ({ params: { id } }) => id, { 214 | params: t.Object({ 215 | id: t.Number() 216 | }) 217 | }) 218 | .listen(3000) 219 | 220 | export type App = typeof app 221 | 222 | // @filename: client.ts 223 | // ---cut--- 224 | // client.ts 225 | import { treaty } from '@elysiajs/eden' 226 | import type { App } from './server' 227 | 228 | const app = treaty<App>('localhost:3000') 229 | 230 | // Get data from /user/617 231 | const { data } = await app.user({ id: 617 }).get() 232 | // ^? 233 | 234 | console.log(data) 235 |
236 |
237 | 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.
238 |
239 | Elysia is not only about helping you create a confident backend but for all that is beautiful in this world.
240 |
241 | ## Platform Agnostic
242 |
243 | Elysia was designed for Bun, but is not limited to Bun. Being WinterCG compliant allows you to deploy Elysia servers on Cloudflare Workers, Vercel Edge Functions, and most other runtimes that support Web Standard Requests.
244 |
245 | ## Our Community
246 |
247 | If you have questions or get stuck regarding Elysia, feel free to ask our community on GitHub Discussions, Discord, and Twitter.
248 |
249 |
250 |
251 | Official ElysiaJS discord community server
252 |
253 |
254 | Track update and status of Elysia
255 |
256 |
257 | Source code and development
258 |
259 |
260 |
261 | ---
262 |
263 | 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.
264 |
265 | 2. Based on TechEmpower Benchmark round 22.
266 |
1 | --- 2 | title: Elysia Blog 3 | layout: page 4 | sidebar: false 5 | editLink: false 6 | search: false 7 | head: 8 | - - meta 9 | - property: 'og:title' 10 | content: Blog - ElysiaJS 11 | 12 | - - meta 13 | - name: 'description' 14 | content: Update of ElysiaJS, from core maintainers 15 | 16 | - - meta 17 | - property: 'og:description' 18 | content: Update of ElysiaJS from core maintainers 19 | --- 20 | 21 | <script setup> 22 | import Blogs from '../components/blog/Landing.vue' 23 | </script> 24 | 25 | <Blogs 26 | :blogs="[ 27 | { 28 | title: 'Elysia 1.2 - You and Me', 29 | href: '/blog/elysia-12', 30 | detail: 'Introducing Adapter for universal runtime suppport, Object macro with resolve, Parser with custom name, WebSocket with lifecycle, TypeBox 0.34 with recursive type, and Eden validation inference.' 31 | }, 32 | { 33 | title: 'Elysia 1.1 - Grown-up's Paradise', 34 | href: '/blog/elysia-11', 35 | detail: 'Introducing OpenTelemetry, and Trace v2. Data coercion and normalization. Guard plugin and bulk cast. Optional path parameter. Decorator and Response status reconcilation. Generator response stream.' 36 | }, 37 | { 38 | title: 'Elysia 1.0 - Lament of the Fallen', 39 | href: '/blog/elysia-10', 40 | detail: 'Introducing Sucrose, a better static code analysis engine, improved starts up time up to 14x, remove 40 routes/instance limitation, faster type inference up to ~3.8x, Eden Treaty 2, Hook type (breaking change), and inline error for strict type check.' 41 | }, 42 | { 43 | title: 'Introducing Elysia 0.8 - Gate of Steiner', 44 | href: '/blog/elysia-08', 45 | detail: 'Introducing Macro API, a new way to interact with Elysia. New Lifecycle, resolve, and mapResponse to interact with Elysia even more. Static Content to compile static resource ahead of time. Default Property, Default Header and several improvement.' 46 | }, 47 | { 48 | title: 'Introducing Elysia 0.7 - Stellar Stellar', 49 | href: '/blog/elysia-07', 50 | detail: 'Introducing up to 13x faster type inference. Declarative telemetry with trace. Reactive cookie model, and cookie validation. TypeBox 0.31 and custom decoder support. Rewritten Web Socket. Definitions remapping and custom affix. Leading more solid foundation for Elysia for a brighter future.' 51 | }, 52 | { 53 | title: 'Introducing Elysia 0.6 - This Game', 54 | href: '/blog/elysia-06', 55 | detail: 'Introducing re-imagined plugin model, dynamic mode, better developer experience with declarative custom error, customizable loose and strict path mapping, TypeBox 0.30 and WinterCG framework interlop. Pushing the boundary of what is possible once again' 56 | }, 57 | { 58 | title: 'Accelerate your next Prisma server with Elysia', 59 | href: '/blog/with-prisma', 60 | detail: 'With the support of Prisma with Bun and Elysia, we are entering a new era of a new level of developer experience. For Prisma we can accelerate our interaction with database, Elysia accelerate our creation of backend web server in term of both developer experience and performance.' 61 | }, 62 | { 63 | title: 'Introducing Elysia 0.5 - Radiant', 64 | href: '/blog/elysia-05', 65 | detail: 'Introducing Static Code Analysis, New router Memoirist, TypeBox 0.28, Numeric type, inline schema, state/decorate/model/group rework, and type stability.' 66 | }, 67 | { 68 | title: 'Introducing Elysia 0.4 - 月夜の音楽会 (Moonlit Night Concert)', 69 | href: '/blog/elysia-04', 70 | detail: 'Introducing Ahead of Time Compilation, TypeBox 0.26, Response validation per status, and Separation of Elysia Fn.' 71 | }, 72 | { 73 | title: 'Elysia with Supabase. Your next backend at sonic speed', 74 | href: '/blog/elysia-supabase', 75 | detail: 'Elysia, and Supabase are a great match for rapidly developing prototype in less than a hour, let's take a look of how we can take advantage of both.' 76 | }, 77 | { 78 | title: 'Introducing Elysia 0.3 - 大地の閾を探して [Looking for Edge of Ground]', 79 | href: '/blog/elysia-03', 80 | detail: 'Introducing Elysia Fn, Type Rework for highly scalable TypeScript performance, File Upload support and validation, Reworked Eden Treaty.' 81 | }, 82 | { 83 | title: 'Integrate existing tRPC server to Bun with Elysia', 84 | href: '/blog/integrate-trpc-with-elysia', 85 | detail: 'Learn how to integrate existing tRPC to Elysia and Bun with Elysia tRPC plugin and more about Eden end-to-end type-safety for Elysia.' 86 | }, 87 | { 88 | title: 'Introducing Elysia 0.2 - The Blessing', 89 | href: '/blog/elysia-02', 90 | detail: 'Introducing Elysia 0.2, bringing more improvement, mainly on TypeScript performance, type-inference, and better auto-completion and some new features to reduce boilerplate.' 91 | } 92 | ]" 93 | /> 94 |
1 | ---
2 | title: Elysia 0.2 - The Blessing
3 | sidebar: false
4 | editLink: false
5 | search: false
6 | head:
7 | - - meta
8 | - property: 'og:title'
9 | content: Introducing Elysia 0.2 - The Blessing
10 |
11 | - - meta
12 | - name: 'description'
13 | content: Introducing Elysia 0.2, bringing more improvement, mainly on TypeScript performance, type-inference, and better auto-completion and some new features to reduce boilerplate.
14 |
15 | - - meta
16 | - property: 'og:description'
17 | content: Introducing Elysia 0.2, bringing more improvement, mainly on TypeScript performance, type-inference, and better auto-completion and some new features to reduce boilerplate.
18 |
19 | - - meta
20 | - property: 'og:image'
21 | content: https://elysiajs.com/blog/elysia-02/blessing.webp
22 |
23 | - - meta
24 | - property: 'twitter:image'
25 | content: https://elysiajs.com/blog/elysia-02/blessing.webp
26 | ---
27 |
28 | <script setup>
29 | import Blog from '../../components/blog/Layout.vue'
30 | </script>
31 |
32 | <Blog
33 | title="Elysia 0.2 - The Blessing"
34 | src="/blog/elysia-02/blessing.webp"
35 | alt="blue to purple aurora in the night sky above of snow mountain"
36 | author="saltyaom"
37 | date="29 Jan 2023"
38 | >
39 |
40 | 「Blessing」brings more improvement, mainly on TypeScript performance, type-inference, and better auto-completion and some new features to reduce boilerplate.
41 |
42 | Named after YOASOBI's song「祝福」, an opening for Witch from "Mobile Suit Gundam: The Witch from Mercury".
43 |
44 | ## Defers / Lazy Loading Module
45 | With Elysia 0.2 now add support for the lazy loading module and async plugin.
46 |
47 | This made it possible to defer plugin registration and incrementally apply after the Elysia server is started to achieve the fastest possible start-up time in Serverless/Edge environments.
48 |
49 | To create defers module, simply mark the plugin as async:
50 | typescript 51 | const plugin = async (app: Elysia) => { 52 | const stuff = await doSomeHeavyWork() 53 | 54 | return app.get('/heavy', stuff) 55 | } 56 | 57 | app.use(plugin) 58 |
59 |
60 | ### Lazy Loading
61 | Some modules might be heavy and importing before starting the server might not be a good idea.
62 |
63 | We can tell Elysia to skip the module then register the module later, and register the module when finish loading by using import
statement in use
:
64 | typescript 65 | app.use(import('./some-heavy-module')) 66 |
67 |
68 | This will register the module after the import is finished making the module lazy-load.
69 |
70 | Defers Plugin and lazy loading module will have all type-inference available right out of the box.
71 |
72 |
73 | ## Reference Model
74 | Now Elysia can memorize schema and reference the schema directly in Schema fields, without creating an import file via Elysia.setModel
75 |
76 | This list of schema available, brings auto-completion, complete type-inference, and validation as you expected from inline schema.
77 |
78 | To use a reference model, first, register the model with setModel
, then write a model name to reference a model in schema
79 | typescript 80 | const app = new Elysia() 81 | .setModel({ 82 | sign: t.Object({ 83 | username: t.String(), 84 | password: t.String() 85 | }) 86 | }) 87 | .post('/sign', ({ body }) => body, { 88 | schema: { 89 | body: 'sign', 90 | response: 'sign' 91 | } 92 | }) 93 |
94 |
95 | This will bring auto-completion of known models.
96 |
97 |
98 | And type reference stopping you from accidentally returning invalid type.
99 |
100 |
101 | Using
@elysiajs/swagger
will also create a separate Model
section for listing available models.
102 |
103 |
104 | Reference also handles validation as you expected.
105 |
106 | In short, it's as same as inline schema but now you only need to type the name of the schema to handle validation and typing instead of a long list of imports.
107 |
108 | ## OpenAPI Detail field
109 | Introducing new field
schema.detail
for customizing detail for the route following the standard of OpenAPI Schema V2 with auto-completion.
110 |
111 |
112 |
113 | This allows you to write better documentation and fully editable Swagger as you want:
114 |
115 |
116 | ## Union Type
117 | The previous version of Elysia sometime has a problem with distinct Union types, as Elysia tries to catch the response to create a full type reference for Eden.
118 |
119 | Results in invalidation of possible types,
120 |
121 | ## Union Response
122 | Made possible by Union Type, now returning multiple response status for
schema
now available using schema.response[statusCode]
123 |
124 | typescript 125 | app 126 | .post( 127 | '/json/:id', 128 | ({ body, params: { id } }) => ({ 129 | ...body, 130 | id 131 | }), 132 | { 133 | schema: { 134 | body: 'sign', 135 | response: { 136 | 200: t.Object({ 137 | username: t.String(), 138 | password: t.String(), 139 | id: t.String() 140 | }), 141 | 400: t.Object({ 142 | error: t.String() 143 | }) 144 | } 145 | } 146 | } 147 | ) 148 |
149 |
150 | Elysia will try to validate all schema in response
allowing one of the types to be returned.
151 |
152 | Return types are also supported report in Swagger's response.
153 |
154 | ## Faster Type Inference
155 | As Elysia 0.1 explore the possibility of using type inference for improving better Developer Experience, we found that sometimes it takes a long time to update type inference because of heavy type inference and in-efficient custom generic.
156 |
157 | With Elysia 0.2 now optimized for faster type-inference, preventing duplication of heavy type unwrap, results in better performance for updating type and inference.
158 |
159 | ## Ecosystem
160 | With Elysia 0.2 enabling async plugin and deferred module many new plugins that isn't possible before became reality.
161 |
162 | Like:
163 | - Elysia Static plugin with the non-blocking capability
164 | - Eden with union-type inference for multiple responses
165 | - New Elysia Apollo Plugin for Elysia
166 |
167 | ### Notable Improvement:
168 | - onRequest
and onParse
now can access PreContext
169 | - Support application/x-www-form-urlencoded
by default
170 | - body parser now parse content-type
with extra attribute eg. application/json;charset=utf-8
171 | - Decode URI parameter path parameter
172 | - Eden now reports an error if Elysia is not installed
173 | - Skip declaration of existing model and decorators
174 |
175 | ### Breaking Changes:
176 | - onParse
now accepts (context: PreContext, contentType: string)
instead of (request: Request, contentType: string)
177 | - To migrate, add .request
to context to access Request
178 |
179 | ### Afterward
180 | Thank you for supporting Elysia and being interested in this project.
181 |
182 | This release brings better DX and hopefully all you need to write great software with Bun.
183 |
184 | Now we have Discord server where you can ask any questions about Elysia or just hang out and chill around is also welcome.
185 |
186 | With the wonderful tools, we are happy to see what wonderful software you will build.
187 |
188 | > Not to be part of those images someone paints
189 | >
190 | > Not advancing in that show chosen by someone else
191 | >
192 | > You and I, alive to write our story
193 | >
194 | > Will never let you be lone and be gone from your side
195 | >
196 |
1 | ---
2 | title: Elysia 0.3 - 大地の閾を探して [Looking for Edge of Ground]
3 | sidebar: false
4 | editLink: false
5 | search: false
6 | head:
7 | - - meta
8 | - property: 'og:title'
9 | content: Introducing Elysia 0.3 - 大地の閾を探して [Looking for Edge of Ground]
10 |
11 | - - meta
12 | - name: 'description'
13 | content: Introducing Elysia Fn, Type Rework for highly scalable TypeScript performance, File Upload support and validation, Reworked Eden Treaty.
14 |
15 | - - meta
16 | - property: 'og:description'
17 | content: Introducing Elysia Fn, Type Rework for highly scalable TypeScript performance, File Upload support and validation, Reworked Eden Treaty.
18 |
19 | - - meta
20 | - property: 'og:image'
21 | content: https://elysiajs.com/blog/elysia-03/edge-of-ground.webp
22 |
23 | - - meta
24 | - property: 'twitter:image'
25 | content: https://elysiajs.com/blog/elysia-03/edge-of-ground.webp
26 | ---
27 |
28 | <script setup>
29 | import Blog from '../../components/blog/Layout.vue'
30 | </script>
31 |
32 | <Blog
33 | title="Elysia 0.3 - 大地の閾を探して [Looking for Edge of Ground]"
34 | src="/blog/elysia-03/edge-of-ground.webp"
35 | alt="shattered glass pieces floating in the abyss"
36 | author="saltyaom"
37 | date="17 Mar 2023"
38 | >
39 |
40 | Named after Camellia's song「大地の閾を探して [Looking for Edge of Ground]」ft. Hatsune Miku, is the last track of my most favorite's Camellia album,「U.U.F.O」. This song has a high impact on me personally, so I'm not taking the name lightly.
41 |
42 | This is the most challenging update, bringing the biggest release of Elysia yet, with rethinking and redesigning of Elysia architecture to be highly scalable while making less breaking change as possible.
43 |
44 | I'm pleased to announce the release candidate of Elysia 0.3 with exciting new features coming right up.
45 |
46 | ## Elysia Fn
47 | Introducing Elysia Fn, run any backend function on the frontend with full auto-completion and full type support.
48 |
49 |
52 |
53 | For rapid development, Elysia Fn allows you to "expose" backend code to call from the frontend with full type-safety, autocompletion, original code comment, and "click-to-definition", allowing you to speed up the development.
54 |
55 | You can use Elysia Fn with Eden for full-type safety via Eden Fn.
56 |
57 | ### Permission
58 | You can limit allow or deny scopes of the function, check for authorization header and other headers' fields, validate parameters, or limit keys access programmatically.
59 |
60 | Keys checking supports type-safety and auto-completion of all possible functions, so you're not missing out on some function or accidentally typing down the wrong name.
61 |
62 |
63 | And narrowing the scope of property programmatically also narrow down the type of parameters, or in other words, full type-safety.
64 |
65 |
66 | ### Technical detail
67 | In technical detail, Elysia Fn uses JavaScript's Proxy to capture object property, and parameters to create batched requests to the server to handle and returns the value across the network.
68 | Elysia Fn extends superjson, allowing native type in JavaScript like Error, Map, Set, and undefined to parse across JSON data.
69 |
70 | Elysia Fn supports multiple use-cases, for example accessing Prisma on the client-side Nextjs app.
71 | Theoretically, it's possible to use Redis, Sequelize, RabbitMQ, and more.
72 | As Elysia is running on Bun, Elysia Fn can run over 1.2 million operation/second concurrently (tested on M1 Max).
73 |
74 | Learn more about Elysia Fn at Eden Fn.
75 |
76 | ## Type Rework
77 | Over 6.5-9x faster for type checking, and uncountable type's LoC reduction.
78 |
79 | Elysia 0.3, over 80% of Elysia, and Eden types have been rewritten to focus on performance, type-inference, and fast auto-completion.
80 |
81 | Testing for over 350 routes with complex types, Elysia uses only 0.22
82 | seconds to generate a type declaration to use with Eden.
83 |
84 | As the Elysia route now compile directly to literal object instead of Typebox reference, Elysia type declaration is much smaller than it used to be on 0.2 and is easier to be consumed by Eden. And by much smaller, it means 50-99% smaller.
85 |
86 | Not only Elysia integration with TypeScript is significantly faster, but Elysia is better at understanding TypeScript and your code better.
87 |
88 | For example, with 0.3, Elysia will be less strict with plugin registration, allowing you to register the plugin without full type-completion of Elysia Instance.
89 | Inlining
use
function now infers the parent type, and the nested guard can reference types of models from the parent more accurately.
90 |
91 | Type Declaration is now also can be built, and exported.
92 |
93 | With the rewrite of type, Elysia understands TypeScript far better than it used to, type-completion will be significantly faster than it was, and we encourage you to try it out to see how fast it is.
94 | For more detail, see this thread on Twitter
95 |
96 | ## File Upload
97 | Thanks to Bun 0.5.7, Form Data is implemented and enabled by default in Elysia 0.3 with multipart/formdata
.
98 |
99 | To define type completion and validation for uploading a file, Elysia.t
now extends TypeBox with File
and Files
for file validation.
100 |
101 | The validation includes checking for file type with auto-completion of standard file size, the minimum and maximum size of the file, and the total of files per field.
102 |
103 | Elysia 0.3 also features schema.contentType
to explicitly validate incoming request type to strictly check headers before validating the data.
104 |
105 | ## OpenAPI Schema 3.0.x
106 | With Elysia 0.3, Elysia now uses OpenAPI schema 3.0.x by default for better stating API definitions, and better support for multiple types based on content-type.
107 |
108 | schema.details
are now updated to OpenAPI 3.0.x, and Elysia also updates the Swagger plugin to match the OpenAPI 3.0.x to take advantage of new features in OpenAPI 3 and Swagger, especially with file uploading.
109 |
110 | ## Eden Rework
111 | To support more demand for Elysia, supporting Elysia Fn, Rest all together, Eden has been reworked to scale with the new architecture.
112 |
113 | Eden now exports 3 types of function.
114 | - Eden Treaty eden/treaty
: Original Eden syntax you know and love
115 | - Eden Fn eden/fn
: Access to Eden Fn
116 | - Eden Fetch eden/fetch
: Fetch-like syntax, for highly complex Elysia type (> 1,000 route / Elysia instance)
117 |
118 | With the rework of type definitions and support for Elysia Eden, Eden is now much faster and better at inference type from the server.
119 |
120 | Auto-completion and faster and use fewer resources than it used to, in fact, Eden's type declaration has been almost 100% reworked to reduce the size and inference time, making it support over 350 routes of auto-completion in a blink of an eye (~0.26 seconds).
121 |
122 | To make Elysia Eden, fully type-safe, with Elysia's better understanding of TypeScript, Eden can now narrow down the type based on response status, allowing you to capture the type correctly in any matter of condition.
123 |
124 |
125 | ### Notable Improvement:
126 | - Add string format: 'email', 'uuid', 'date', 'date-time'
127 | - Update @sinclair/typebox to 0.25.24
128 | - Update Raikiri to 0.2.0-beta.0 (ei)
129 | - Add file upload test thanks to #21 (@amirrezamahyari)
130 | - Pre compile lowercase method for Eden
131 | - Reduce complex instruction for most Elysia types
132 | - Compile
ElysiaRoute
type to literal
133 | - Optimize type compliation, type inference and auto-completion
134 | - Improve type compilation speed
135 | - Improve TypeScript inference between plugin registration
136 | - Optimize TypeScript inference size
137 | - Context creation optimization
138 | - Use Raikiri router by default
139 | - Remove unused function
140 | - Refactor registerSchemaPath
to support OpenAPI 3.0.3
141 | - Add error
inference for Eden
142 | - Mark @sinclair/typebox
as optional peerDenpendencies
143 |
144 | Fix:
145 | - Raikiri 0.2 thrown error on not found
146 | - Union response with t.File
is not working
147 | - Definitions isn't defined on Swagger
148 | - details are missing on group plugin
149 | - group plugin, isn't unable to compile schema
150 | - group is not exportable because EXPOSED is a private property
151 | - Multiple cookies doesn't set content-type
to application/json
152 | - EXPOSED
is not export when using fn.permission
153 | - Missing merged return type for .ws
154 | - Missing nanoid
155 | - context side-effects
156 | - t.Files
in swagger is referring to single file
157 | - Eden response type is unknown
158 | - Unable to type setModel
inference definition via Eden
159 | - Handle error thrown in non permission function
160 | - Exported variable has or is using name 'SCHEMA' from external module
161 | - Exported variable has or is using name 'DEFS' from external module
162 | - Possible errors for building Elysia app with declaration: true
in tsconfig.json
163 |
164 | Breaking Change:
165 | - Rename inject
to derive
166 | - Depreacate ElysiaRoute
, changed to inline
167 | - Remove derive
168 | - Update from OpenAPI 2.x to OpenAPI 3.0.3
169 | - Move context.store[SYMBOL] to meta[SYMBOL]
170 |
171 |
172 | ## Afterward
173 | With the introduction of Elysia Fn, I'm personally excited to see how it will be adopted in frontend development, removing the line between frontend and backend. And Type Rework of Elysia, making type-checking and auto-completion much faster.
174 |
175 | I'm excited to see how you will use Elysia to create the wonderful things you are going to build.
176 |
177 | We have Discord server dedicated to Elysia. Feel free to say hi or just chill and hang out.
178 |
179 | Thank you for supporting Elysia.
180 |
181 | > Under a celestial map that never have ends
182 | >
183 | > On a cliff that never have name
184 | >
185 | > I just holwed
186 | >
187 | > Hoping the neverending reverberation will reach you
188 | >
189 | > And I believe someday, I will stand on edge of the ground
190 | >
191 | > (Until the day I can be back to you to tell it)
192 | >
193 |
1 | ---
2 | title: Elysia 0.4 - 月夜の音楽会 (Moonlit Night Concert)
3 | sidebar: false
4 | editLink: false
5 | search: false
6 | head:
7 | - - meta
8 | - property: 'og:title'
9 | content: Introducing Elysia 0.4 - 月夜の音楽会 (Moonlit Night Concert)
10 |
11 | - - meta
12 | - name: 'description'
13 | content: Introducing Ahead of Time Compilation, TypeBox 0.26, Response validation per status, and Separation of Elysia Fn.
14 |
15 | - - meta
16 | - property: 'og:description'
17 | content: Introducing Ahead of Time Compilation, TypeBox 0.26, Response validation per status, and Separation of Elysia Fn
18 |
19 | - - meta
20 | - property: 'og:image'
21 | content: https://elysiajs.com/blog/elysia-04/moonlit-night-concert.webp
22 |
23 | - - meta
24 | - property: 'twitter:image'
25 | content: https://elysiajs.com/blog/elysia-04/moonlit-night-concert.webp
26 | ---
27 |
28 | <script setup>
29 | import Blog from '../../components/blog/Layout.vue'
30 | </script>
31 |
32 | <Blog
33 | title="Elysia 0.4 - 月夜の音楽会 (Moonlit Night Concert)"
34 | src="/blog/elysia-04/moonlit-night-concert.webp"
35 | alt="shattered glass pieces floating in the abyss"
36 | author="saltyaom"
37 | date="30 Mar 2023"
38 | >
39 |
40 | Named after the opening music of "The Liar Princess and the Blind Prince" trailer, 「月夜の音楽会」(Moonlit Night Concert) composed and sang by Akiko Shikata.
41 |
42 | This version doesn't introduce an exciting new feature, later but a foundation for more solid ground, and internal improvement for the future of Elysia.
43 |
44 | ## Ahead of Time Complie
45 | By default, Elysia has to deal with conditional checking in various situations, for example, checking if the life-cycle of the route existed before performing, or unwrapping validation schema if provided.
46 |
47 | This introduces a minimal overhead to Elysia and overall because even if the route doesn't have a life-cycle event attached to it, it needs to be runtime checked.
48 |
49 | Since every function is checked on compile time, it's not possible to have a conditional async, for example, a simple route that returns a file should be synced, but since it's compile-time checking, some routes might be async thus making the same simple route async too.
50 |
51 | An async function introduces an additional tick to the function, making it a bit slower. But since Elysia is a foundation for web servers, we want to optimize every part to make sure that you don't run into performance problems.
52 |
53 | We fix this small overhead by introducing Ahead Time Compilation.
54 |
55 | As the name suggests, instead of checking dynamic life-cycle and validation on the runtime, Elysia checks life-cycle, validation, and the possibility of an async function and generates a compact function, removing an unnecessary part like an un-used life-cycle and validation.
56 |
57 | Making conditional async function possible, since instead of using a centralized function for handling, we compose a new function especially created for each route instead. Elysia then checks all life-cycle functions and handlers to see if there's an async, and if not, the function will be synced to reduce additional overhead.
58 |
59 | ## TypeBox 0.26
60 | TypeBox is a library that powered Elysia's validation and type provider to create a type-level single source of truth, re-exported as Elysia.t.
61 |
62 | In this update, we update TypeBox from 0.25.4 to 0.26.
63 |
64 | This brings a lot of improvement and new features, for example, a Not
type and Convert
for coercion
value which we might support in some next version of Elysia.
65 |
66 | But the one benefit for Elysia would be, Error.First()
which allows us to get the first error of type instead of using Iterator, this reduces overhead in creating a new Error to send back to the client.
67 |
68 | There are some changes to TypeBox and Elysia.t that normally wouldn't have much effect on your end, but you can see what's a new feature in TypeBox release here.
69 |
70 | ## Validate response per status
71 | Previously, Elysia's response validate multiple status responses using union type.
72 |
73 | This might have unexpected results for highly dynamic apps with a strict response for status.
74 | For example if you have a route like:
75 | ts 76 | app.post('/strict-status', process, { 77 | schema: { 78 | response: { 79 | 200: t.String(), 80 | 400: t.Number() 81 | } 82 | } 83 | }) 84 |
85 |
86 | It's expected that if 200 response is not a string, then it should throw a validation error, but in reality, it wouldn't throw an error because response validation is using union. This might leave an unexpected value to the end user and a type error for Eden Treaty.
87 |
88 | With this release, a response is validated per status instead of union, which means that it will strictly validate based on response status instead of unioned type.
89 |
90 | ## Separation of Elysia Fn
91 | Elysia Fn is a great addition to Elysia, with Eden, it breaks the boundary between client and server allowing you to use any server-side function in your client, fully type-safe and even with primitive types like Error, Set, and Map.
92 |
93 | But with the primitive type support, Elysia Fn depends on "superjson" which is around 38% of Elysia's dependency size.
94 |
95 | In this release, to use Elysia Fn, you're required to explicitly install @elysiajs/fn
to use Elysia Fn. This approach is like installing an additional feature same as cargo add --feature
.
96 |
97 | This makes Elysia lighter for servers that don't use Elysia Fn, Following our philosophy, To ensure that you have what you actually need
98 |
99 | However, if you forgot to install Elysia Fn and accidentally use Elysia Fn, there will be a type warning reminding you to install Elysia Fn before usage, and a runtime error telling the same thing.
100 |
101 | For migration, besides a breaking change of installing @elysiajs/fn
explicitly, there's no migration need.
102 |
103 | ## Conditional Route
104 | This release introduces .if
method for registering a conditional route or plugin.
105 |
106 | This allows you to declaratively for a specific conditional, for example excluding Swagger documentation from the production environment.
107 | ts 108 | const isProduction = process.env.NODE_ENV === 'production' 109 | 110 | const app = new Elysia().if(!isProduction, (app) => 111 | app.use(swagger()) 112 | ) 113 |
114 |
115 | Eden Treaty will be able to recognize the route as if it's a normal route/plugin.
116 |
117 | ## Custom Validation Error
118 | Big thanks to amirrezamahyari on #31 which allows Elysia to access TypeBox's error property, for a better programmatically error response.
119 |
120 | ts 121 | new Elysia() 122 | .onError(({ code, error, set }) => { 123 | if (code === 'NOT_FOUND') { 124 | set.status = 404 125 | 126 | return 'Not Found :(' 127 | } 128 | 129 | if (code === 'VALIDATION') { 130 | set.status = 400 131 | 132 | return { 133 | fields: error.all() 134 | } 135 | } 136 | }) 137 | .post('/sign-in', () => 'hi', { 138 | schema: { 139 | body: t.Object({ 140 | username: t.String(), 141 | password: t.String() 142 | }) 143 | } 144 | }) 145 | .listen(3000) 146 |
147 |
148 | Now you can create a validation error for your API not limited to only the first error.
149 |
150 | ---
151 |
152 | ### Notable Improvement:
153 | - Update TypeBox to 0.26.8
154 | - Inline a declaration for response type
155 | - Refactor some type for faster response
156 | - Use Typebox Error().First()
instead of iteration
157 | - Add innerHandle
for returning an actual response (for benchmark)
158 |
159 | ### Breaking Change:
160 | - Separate .fn
to @elysiajs/fn
161 |
162 | ## Afterward
163 | This release might not be a big release with a new exciting feature, but this improve a solid foundation, and Proof of Concept for planned I have for Elysia in the future, and making Elysia even faster and more versatile than it was.
164 |
165 | I'm really excited for what will be unfold in the future.
166 |
167 | Thank you for your continuous support for Elysia~
168 |
169 | > the moonlit night concert, our secret
170 | >
171 | > let’s start again without letting go of this hand
172 |
173 | > the moonlit night concert, our bonds
174 | >
175 | > I want to tell you, “you are not a liar”
176 |
177 | > the memories in my heart is like flower that keeps blooming
178 | >
179 | > no matter what you look like, you are my songstress
180 | >
181 | > be by my side tonight
182 |
183 |
1 | ---
2 | title: Eden Fetch - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Eden Fetch - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: A fetch-like alternative to Eden Treaty with faster type inference. With Eden Fetch, you can make requests to an Elysia server with end-to-end type-safety without the need of code generation.
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: A fetch-like alternative to Eden Treaty with faster type inference. With Eden Fetch, you can make requests to an Elysia server with end-to-end type-safety without the need of code generation.
15 | ---
16 |
17 | # Eden Fetch
18 | A fetch-like alternative to Eden Treaty .
19 |
20 | With Eden Fetch can interact with Elysia server in a type-safe manner using Fetch API.
21 |
22 | ---
23 |
24 | First export your existing Elysia server type:
25 | typescript 26 | // server.ts 27 | import { Elysia, t } from 'elysia' 28 | 29 | const app = new Elysia() 30 | .get('/hi', () => 'Hi Elysia') 31 | .get('/id/:id', ({ params: { id } }) => id) 32 | .post('/mirror', ({ body }) => body, { 33 | body: t.Object({ 34 | id: t.Number(), 35 | name: t.String() 36 | }) 37 | }) 38 | .listen(3000) 39 | 40 | export type App = typeof app 41 |
42 |
43 | Then import the server type, and consume the Elysia API on client:
44 | typescript 45 | import { edenFetch } from '@elysiajs/eden' 46 | import type { App } from './server' 47 | 48 | const fetch = edenFetch<App>('http://localhost:3000') 49 | 50 | // response type: 'Hi Elysia' 51 | const pong = await fetch('/hi', {}) 52 | 53 | // response type: 1895 54 | const id = await fetch('/id/:id', { 55 | params: { 56 | id: '1895' 57 | } 58 | }) 59 | 60 | // response type: { id: 1895, name: 'Skadi' } 61 | const nendoroid = await fetch('/mirror', { 62 | method: 'POST', 63 | body: { 64 | id: 1895, 65 | name: 'Skadi' 66 | } 67 | }) 68 |
69 |
70 | ## Error Handling
71 | You can handle errors the same way as Eden Treaty:
72 | typescript 73 | import { edenFetch } from '@elysiajs/eden' 74 | import type { App } from './server' 75 | 76 | const fetch = edenFetch<App>('http://localhost:3000') 77 | 78 | // response type: { id: 1895, name: 'Skadi' } 79 | const { data: nendoroid, error } = await fetch('/mirror', { 80 | method: 'POST', 81 | body: { 82 | id: 1895, 83 | name: 'Skadi' 84 | } 85 | }) 86 | 87 | if(error) { 88 | switch(error.status) { 89 | case 400: 90 | case 401: 91 | throw error.value 92 | break 93 | 94 | case 500: 95 | case 502: 96 | throw error.value 97 | break 98 | 99 | default: 100 | throw error.value 101 | break 102 | } 103 | } 104 | 105 | const { id, name } = nendoroid 106 |
107 |
108 | ## When should I use Eden Fetch over Eden Treaty
109 | Unlike Elysia < 1.0, Eden Fetch is not faster than Eden Treaty anymore.
110 |
111 | The preference is base on you and your team agreement, however we recommend to use Eden Treaty instead.
112 |
113 | For Elysia < 1.0:
114 |
115 | 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.
116 |
117 | 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.
118 |
119 | 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.
120 |
121 | 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.
122 |
1 | ---
2 | title: Eden Installation - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Eden Installation - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Start by installing Eden on your frontend with "bun add elysia @elysiajs/eden", then expose your Elysia server type and then start using Eden Treaty or Eden Fetch.
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Start by installing Eden on your frontend with "bun add elysia @elysiajs/eden", then expose your Elysia server type and then start using Eden Treaty or Eden Fetch.
15 | ---
16 |
17 | # Eden Installation
18 | Start by installing Eden on your frontend:
19 | bash 20 | bun add @elysiajs/eden 21 | bun add -d elysia 22 | 23 | # If you use Bun specific feature, eg. `Bun.file` 24 | bun add -d @types/bun 25 |
26 |
27 | ::: tip
28 | Eden needs Elysia to infer utilities type.
29 |
30 | Make sure to install Elysia with the version matching on the server.
31 | :::
32 |
33 | First, export your existing Elysia server type:
34 | typescript 35 | // server.ts 36 | import { Elysia, t } from 'elysia' 37 | 38 | const app = new Elysia() 39 | .get('/', () => 'Hi Elysia') 40 | .get('/id/:id', ({ params: { id } }) => id) 41 | .post('/mirror', ({ body }) => body, { 42 | body: t.Object({ 43 | id: t.Number(), 44 | name: t.String() 45 | }) 46 | }) 47 | .listen(3000) 48 | 49 | export type App = typeof app // [!code ++] 50 |
51 |
52 | Then consume the Elysia API on client side:
53 | typescript twoslash 54 | // @filename: server.ts 55 | import { Elysia, t } from 'elysia' 56 | 57 | const app = new Elysia() 58 | .get('/', 'Hi Elysia') 59 | .get('/id/:id', ({ params: { id } }) => id) 60 | .post('/mirror', ({ body }) => body, { 61 | body: t.Object({ 62 | id: t.Number(), 63 | name: t.String() 64 | }) 65 | }) 66 | .listen(3000) 67 | 68 | export type App = typeof app // [!code ++] 69 | 70 | // @filename: index.ts 71 | // ---cut--- 72 | // client.ts 73 | import { treaty } from '@elysiajs/eden' 74 | import type { App } from './server' // [!code ++] 75 | 76 | const client = treaty<App>('localhost:3000') // [!code ++] 77 | 78 | // response: Hi Elysia 79 | const { data: index } = await client.index.get() 80 | 81 | // response: 1895 82 | const { data: id } = await client.id({ id: 1895 }).get() 83 | 84 | // response: { id: 1895, name: 'Skadi' } 85 | const { data: nendoroid } = await client.mirror.post({ 86 | id: 1895, 87 | name: 'Skadi' 88 | }) 89 | 90 | // @noErrors 91 | client. 92 | // ^| 93 |
94 |
95 | ## Gotcha
96 | Sometimes Eden may not infer type from Elysia correctly, the following are the most common workaround to fix Eden type inference.
97 |
98 | ### Type Strict
99 | Make sure to enable strict mode in tsconfig.json
100 | json 101 | { 102 | "compilerOptions": { 103 | "strict": true // [!code ++] 104 | } 105 | } 106 |
107 |
108 | ### Unmatch Elysia version
109 | Eden depends Elysia class to import Elysia instance and infers type correctly.
110 |
111 | Make sure that both client and server have a matching Elysia version.
112 |
113 | You can check it with npm why
command:
114 |
115 | bash 116 | npm why elysia 117 |
118 |
119 | And output should contain only one elysia version on top-level:
120 |
121 | 122 | [email protected] 123 | node_modules/elysia 124 | elysia@"1.1.25" from the root project 125 | peer elysia@">= 1.1.0" from @elysiajs/[email protected] 126 | node_modules/@elysiajs/html 127 | dev @elysiajs/html@"1.1.1" from the root project 128 | peer elysia@">= 1.1.0" from @elysiajs/[email protected] 129 | node_modules/@elysiajs/opentelemetry 130 | dev @elysiajs/opentelemetry@"1.1.7" from the root project 131 | peer elysia@">= 1.1.0" from @elysiajs/[email protected] 132 | node_modules/@elysiajs/swagger 133 | dev @elysiajs/swagger@"1.1.6" from the root project 134 | peer elysia@">= 1.1.0" from @elysiajs/[email protected] 135 | node_modules/@elysiajs/eden 136 | dev @elysiajs/eden@"1.1.3" from the root project 137 |
138 |
139 |
140 | ### TypeScript version
141 | Elysia uses newer features and syntax of TypeScript to infer types in a the most performant way. Features like Const Generic and Template Literal are heavily used.
142 |
143 | Make sure your client has a minimum TypeScript version if >= 5.0
144 |
145 | ### Method Chaining
146 | To make Eden works, Elysia must be using method chaining
147 |
148 | Elysia's type system is complex, methods usually introduce a new type to the instance.
149 |
150 | Using method chaining will help save that new type reference.
151 |
152 | For example:
153 | typescript twoslash 154 | import { Elysia } from 'elysia' 155 | 156 | new Elysia() 157 | .state('build', 1) 158 | // Store is strictly typed // [!code ++] 159 | .get('/', ({ store: { build } }) => build) 160 | .listen(3000) 161 |
162 | Using this, state now returns a new ElysiaInstance type, introducing build into store and replace the current one.
163 |
164 | Without using method chaining, Elysia doesn't save the new type when introduced, leading to no type inference.
165 | typescript twoslash 166 | // @errors: 2339 167 | import { Elysia } from 'elysia' 168 | 169 | const app = new Elysia() 170 | 171 | app.state('build', 1) 172 | 173 | app.get('/', ({ store: { build } }) => build) 174 | 175 | app.listen(3000) 176 |
177 |
178 | ### Type Definitions
179 | Sometimes, if you are using a Bun specific feature like Bun.file
or similar API, you may need to install Bun type definitions to the client as well.
180 |
181 | bash 182 | bun add -d @types/bun 183 |
184 |
185 | ### Path alias (monorepo)
186 | If you are using path alias in your monorepo, make sure that frontend are able to resolve the path as same as backend.
187 |
188 | For example, if you have the following path alias for your backend in tsconfig.json:
189 | json 190 | { 191 | "compilerOptions": { 192 | "baseUrl": ".", 193 | "paths": { 194 | "@/*": ["./src/*"] 195 | } 196 | } 197 | } 198 |
199 |
200 | And your backend code is like this:
201 | typescript 202 | import { Elysia } from 'elysia' 203 | import { a, b } from '@/controllers' 204 | 205 | const app = new Elysia() 206 | .use(a) 207 | .use(b) 208 | .listen(3000) 209 | 210 | export type app = typeof app 211 |
212 |
213 | You must make sure that your frontend code is able to resolve the same path alias otherwise type inference will be resolved as any.
214 |
215 | typescript 216 | import { treaty } from '@elysiajs/eden' 217 | import type { app } from '@/index' 218 | 219 | const client = treaty<app>('localhost:3000') 220 | 221 | // This should be able to resolve the same module both frontend and backend, and not `any` 222 | import { a, b } from '@/controllers' 223 |
224 |
225 | To fix this, you must make sure that path alias is resolved to the same file in both frontend and backend.
226 |
227 | So you must change the path alias in tsconfig.json to:
228 | json 229 | { 230 | "compilerOptions": { 231 | "baseUrl": ".", 232 | "paths": { 233 | "@/*": ["../apps/backend/src/*"] 234 | } 235 | } 236 | } 237 |
238 |
239 | If configured correctly, you should be able to resolve the same module in both frontend and backend.
240 | typescript 241 | // This should be able to resolve the same module both frontend and backend, and not `any` 242 | import { a, b } from '@/controllers' 243 |
244 |
245 | #### Scope
246 | We recommended to add a scope prefix for each modules in your monorepo to avoid any confusion and conflict that may happen.
247 |
248 | json 249 | { 250 | "compilerOptions": { 251 | "baseUrl": ".", 252 | "paths": { 253 | "@frontend/*": ["./apps/frontend/src/*"], 254 | "@backend/*": ["./apps/backend/src/*"] 255 | } 256 | } 257 | } 258 |
259 |
260 | Then you can import the module like this:
261 | typescript 262 | // Should work in both frontend and backend and not return `any` 263 | import { a, b } from '@backend/controllers' 264 |
265 |
266 | We recommended creating a single tsconfig.json that define a baseUrl
as the root of your repo, provide a path according to the module location, and create a tsconfig.json for each module that inherits the root tsconfig.json which has the path alias.
267 |
268 | You may find a working example of in this path alias example repo.
269 |
1 | ---
2 | title: End-to-End Type Safety - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: End-to-End Type Safety - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Elysia supports end-to-end type safety with Elysia Eden since start. End-to-end type-safety refers to a system in which every component of the system is checked for type consistency, meaning that data is passed between components only if the types of the data are compatible.
11 |
12 | - - meta
13 | - property: 'og:description'
14 | content: Elysia supports end-to-end type safety with Elysia Eden since start. End-to-end type-safety refers to a system in which every component of the system is checked for type consistency, meaning that data is passed between components only if the types of the data are compatible.
15 | ---
16 |
17 | # End-to-End Type Safety
18 | Imagine you have a toy train set.
19 |
20 | Each piece of the train track has to fit perfectly with the next one, like puzzle pieces.
21 |
22 | 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.
23 |
24 | For a framework to have end-to-end type safety means you can connect client and server in a type-safe manner.
25 |
26 | Elysia provide end-to-end type safety without code generation out of the box with RPC-like connector, Eden
27 |
28 |
32 |
33 | Others framework that support e2e type safety:
34 | - tRPC
35 | - Remix
36 | - SvelteKit
37 | - Nuxt
38 | - TS-Rest
39 |
40 |
51 |
52 | 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.
53 |
54 | ## Eden
55 | Eden is a RPC-like client to connect Elysia end-to-end type safety using only TypeScript's type inference instead of code generation.
56 |
57 | Allowing you to sync client and server types effortlessly, weighing less than 2KB.
58 |
59 | Eden is consists of 2 modules:
60 | 1. Eden Treaty (recommended): an improved version RFC version of Eden Treaty
61 | 2. Eden Fetch: Fetch-like client with type safety.
62 |
63 | Below is an overview, use-case and comparison for each module.
64 |
65 | ## Eden Treaty (Recommended)
66 | Eden Treaty is an object-like representation of an Elysia server providing end-to-end type safety and a significantly improved developer experience.
67 |
68 | 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.
69 |
70 | Example usage of Eden Treaty:
71 | typescript twoslash 72 | // @filename: server.ts 73 | import { Elysia, t } from 'elysia' 74 | 75 | const app = new Elysia() 76 | .get('/', 'hi') 77 | .get('/users', () => 'Skadi') 78 | .put('/nendoroid/:id', ({ body }) => body, { 79 | body: t.Object({ 80 | name: t.String(), 81 | from: t.String() 82 | }) 83 | }) 84 | .get('/nendoroid/:id/name', () => 'Skadi') 85 | .listen(3000) 86 | 87 | export type App = typeof app 88 | 89 | // @filename: index.ts 90 | // ---cut--- 91 | import { treaty } from '@elysiajs/eden' 92 | import type { App } from './server' 93 | 94 | const app = treaty<App>('localhost:3000') 95 | 96 | // @noErrors 97 | app. 98 | // ^| 99 | 100 | 101 | 102 | 103 | // Call [GET] at '/' 104 | const { data } = await app.index.get() 105 | 106 | // Call [POST] at '/nendoroid/:id' 107 | const { data: nendoroid, error } = await app.nendoroid({ id: 1895 }).put({ 108 | name: 'Skadi', 109 | from: 'Arknights' 110 | }) 111 |
112 |
113 | ## Eden Fetch
114 | A fetch-like alternative to Eden Treaty for developers that prefers fetch syntax.
115 | typescript 116 | import { edenFetch } from '@elysiajs/eden' 117 | import type { App } from './server' 118 | 119 | const fetch = edenFetch<App>('http://localhost:3000') 120 | 121 | const { data } = await fetch('/name/:name', { 122 | method: 'POST', 123 | params: { 124 | name: 'Saori' 125 | }, 126 | body: { 127 | branch: 'Arius', 128 | type: 'Striker' 129 | } 130 | }) 131 |
132 |
133 | ::: tip NOTE
134 | Unlike Eden Treaty, Eden Fetch doesn't provide Web Socket implementation for Elysia server
135 | :::
136 |
1 | ---
2 | title: Eden Test - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Eden Unit Test - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Using Eden, we can perform unit-test to provide end-to-end type safety, and auto-completion, tracking type safety from migration
11 |
12 | - - meta
13 | - property: 'og:description'
14 | content: Using Eden, we can perform unit-test to provide end-to-end type safety, and auto-completion, tracking type safety from migration
15 | ---
16 |
17 | # Eden Test
18 | Using Eden, we can create an integration test with end-to-end type safety and auto-completion.
19 |
20 |
24 |
25 | > Using Eden Treaty to create tests by irvilerodrigues on Twitter
26 |
27 | ## Setup
28 | We can use Bun test to create tests.
29 |
30 | Create test/index.test.ts in the root of project directory with the following:
31 |
32 | typescript 33 | // test/index.test.ts 34 | import { describe, expect, it } from 'bun:test' 35 | 36 | import { edenTreaty } from '@elysiajs/eden' 37 | 38 | const app = new Elysia() 39 | .get('/', () => 'hi') 40 | .listen(3000) 41 | 42 | const api = edenTreaty<typeof app>('http://localhost:3000') 43 | 44 | describe('Elysia', () => { 45 | it('return a response', async () => { 46 | const { data } = await api.get() 47 | 48 | expect(data).toBe('hi') 49 | }) 50 | }) 51 |
52 |
53 | Then we can perform tests by running bun test
54 |
55 | bash 56 | bun test 57 |
58 |
59 | This allows us to perform integration tests programmatically instead of manual fetch while supporting type checking automatically.
60 |
1 | ---
2 | title: Eden Treaty Config - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Eden Treaty Config - ElysiaJS
7 |
8 | - - meta
9 | - name: 'og:description'
10 | content: Eden Treaty is an object-like representation of an Elysia server, providing an end-to-end type safety, and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Eden Treaty is an object-like representation of an Elysia server, providing an end-to-end type safety, and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
15 | ---
16 |
17 | # Config
18 | Eden Treaty accepts 2 parameters:
19 | - urlOrInstance - URL endpoint or Elysia instance
20 | - options (optional) - Customize fetch behavior
21 |
22 | ## urlOrInstance
23 | Accept either URL endpoint as string or a literal Elysia instance.
24 |
25 | Eden will change the behavior based on type as follows:
26 |
27 | ### URL Endpoint (string)
28 | If URL endpoint is passed, Eden Treaty will use fetch
or config.fetcher
to create a network request to an Elysia instance.
29 |
30 | typescript 31 | import { treaty } from '@elysiajs/eden' 32 | import type { App } from './server' 33 | 34 | const api = treaty<App>('localhost:3000') 35 |
36 |
37 | You may or may not specified a protocol for URL endpoint.
38 |
39 | Elysia will appends the endpoints automatically as follows:
40 | 1. If protocol is specified, use the URL directly
41 | 2. If the URL is localhost and ENV is not production, use http
42 | 3. Otherwise use https
43 |
44 | This also apply to Web Socket as well for determining between ws:// or wss://.
45 |
46 | ---
47 |
48 | ### Elysia Instance
49 | If Elysia instance is passed, Eden Treaty will create a Request
class and pass to Elysia.handle
directly without creating a network request.
50 |
51 | This allows us to interact with Elysia server directly without request overhead, or the need start a server.
52 |
53 | typescript 54 | import { Elysia } from 'elysia' 55 | import { treaty } from '@elysiajs/eden' 56 | 57 | const app = new Elysia() 58 | .get('/hi', 'Hi Elysia') 59 | .listen(3000) 60 | 61 | const api = treaty(app) 62 |
63 |
64 | If an instance is passed, generic is not needed to be pass as Eden Treaty can infers the type from a parameter directly.
65 |
66 | This patterns is recommended for performing unit tests, or creating a type-safe reverse proxy server or micro-services.
67 |
68 | ## Options
69 | 2nd optional parameters for Eden Treaty to customize fetch behavior, accepting parameters as follows:
70 | - fetch - add default parameters to fetch intialization (RequestInit)
71 | - headers - define default headers
72 | - fetcher - custom fetch function eg. Axios, unfetch
73 | - onRequest - Intercept and modify fetch request before firing
74 | - onResponse - Intercept and modify fetch's response
75 |
76 | ## Fetch
77 | Default parameters append to 2nd parameters of fetch extends type of Fetch.RequestInit.
78 |
79 | typescript 80 | export type App = typeof app // [!code ++] 81 | import { treaty } from '@elysiajs/eden' 82 | // ---cut--- 83 | treaty<App>('localhost:3000', { 84 | fetch: { 85 | credentials: 'include' 86 | } 87 | }) 88 |
89 |
90 | All parameters that passed to fetch, will be passed to fetcher, which is an equivalent to:
91 | typescript 92 | fetch('http://localhost:3000', { 93 | credentials: 'include' 94 | }) 95 |
96 |
97 | ## Headers
98 | Provide an additional default headers to fetch, a shorthand of options.fetch.headers
.
99 |
100 | typescript 101 | treaty<App>('localhost:3000', { 102 | headers: { 103 | 'X-Custom': 'Griseo' 104 | } 105 | }) 106 |
107 |
108 | All parameters that passed to fetch, will be passed to fetcher, which is an equivalent to:
109 | typescript twoslash 110 | fetch('localhost:3000', { 111 | headers: { 112 | 'X-Custom': 'Griseo' 113 | } 114 | }) 115 |
116 |
117 | headers may accepts the following as parameters:
118 | - Object
119 | - Function
120 |
121 | ### Headers Object
122 | If object is passed, then it will be passed to fetch directly
123 |
124 | typescript 125 | treaty<App>('localhost:3000', { 126 | headers: { 127 | 'X-Custom': 'Griseo' 128 | } 129 | }) 130 |
131 |
132 | ### Function
133 | You may specify a headers as a function to return custom headers based on condition
134 |
135 | typescript 136 | treaty<App>('localhost:3000', { 137 | headers(path, options) { 138 | if(path.startsWith('user')) 139 | return { 140 | authorization: 'Bearer 12345' 141 | } 142 | } 143 | }) 144 |
145 |
146 | You may return object to append its value to fetch headers.
147 |
148 | headers function accepts 2 parameters:
149 | - path string
- path which will be sent to parameter
150 | - note: hostname will be exclude eg. (/user/griseo)
151 | - options RequestInit
: Parameters that passed through 2nd parameter of fetch
152 |
153 | ### Array
154 | You may define a headers function as an array if multiple conditions are need.
155 |
156 | typescript 157 | treaty<App>('localhost:3000', { 158 | headers: [ 159 | (path, options) => { 160 | if(path.startsWith('user')) 161 | return { 162 | authorization: 'Bearer 12345' 163 | } 164 | } 165 | ] 166 | }) 167 |
168 |
169 | Eden Treaty will run all functions even if the value is already returned.
170 |
171 | ## Headers Priority
172 | Eden Treaty will prioritize the order headers if duplicated as follows:
173 | 1. Inline method - Passed in method function directly
174 | 2. headers - Passed in config.headers
175 | - If config.headers
is array, parameters that come after will be prioritize
176 | 3. fetch - Passed in config.fetch.headers
177 |
178 | For example, for the following example:
179 | typescript 180 | const api = treaty<App>('localhost:3000', { 181 | headers: { 182 | authorization: 'Bearer Aponia' 183 | } 184 | }) 185 | 186 | api.profile.get({ 187 | headers: { 188 | authorization: 'Bearer Griseo' 189 | } 190 | }) 191 |
192 |
193 | This will be results in:
194 | typescript 195 | fetch('http://localhost:3000', { 196 | headers: { 197 | authorization: 'Bearer Griseo' 198 | } 199 | }) 200 |
201 |
202 | If inline function doesn't specified headers, then the result will be "Bearer Aponia" instead.
203 |
204 | ## Fetcher
205 | Provide a custom fetcher function instead of using an environment's default fetch.
206 |
207 | typescript 208 | treaty<App>('localhost:3000', { 209 | fetcher(url, options) { 210 | return fetch(url, options) 211 | } 212 | }) 213 |
214 |
215 | It's recommended to replace fetch if you want to use other client other than fetch, eg. Axios, unfetch.
216 |
217 | ## OnRequest
218 | Intercept and modify fetch request before firing.
219 |
220 | You may return object to append the value to RequestInit.
221 |
222 | typescript 223 | treaty<App>('localhost:3000', { 224 | onRequest(path, options) { 225 | if(path.startsWith('user')) 226 | return { 227 | headers: { 228 | authorization: 'Bearer 12345' 229 | } 230 | } 231 | } 232 | }) 233 |
234 |
235 | If value is returned, Eden Treaty will perform a shallow merge for returned value and value.headers
.
236 |
237 | onRequest accepts 2 parameters:
238 | - path string
- path which will be sent to parameter
239 | - note: hostname will be exclude eg. (/user/griseo)
240 | - options RequestInit
: Parameters that passed through 2nd parameter of fetch
241 |
242 | ### Array
243 | You may define an onRequest function as an array if multiples conditions are need.
244 |
245 | typescript 246 | treaty<App>('localhost:3000', { 247 | onRequest: [ 248 | (path, options) => { 249 | if(path.startsWith('user')) 250 | return { 251 | headers: { 252 | authorization: 'Bearer 12345' 253 | } 254 | } 255 | } 256 | ] 257 | }) 258 |
259 |
260 | Eden Treaty will run all functions even if the value is already returned.
261 |
262 | ## onResponse
263 | Intercept and modify fetch's response or return a new value.
264 |
265 | typescript 266 | treaty<App>('localhost:3000', { 267 | onResponse(response) { 268 | if(response.ok) 269 | return response.json() 270 | } 271 | }) 272 |
273 |
274 | onRequest accepts 1 parameter:
275 | - response Response
- Web Standard Response normally returned from fetch
276 |
277 | ### Array
278 | You may define an onResponse function as an array if multiple conditions are need.
279 |
280 | typescript 281 | treaty<App>('localhost:3000', { 282 | onResponse: [ 283 | (response) => { 284 | if(response.ok) 285 | return response.json() 286 | } 287 | ] 288 | }) 289 |
290 | Unlike headers and 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.
291 |
1 | ---
2 | title: Eden Treaty Legacy - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Eden Treaty Legacy - ElysiaJS
7 |
8 | - - meta
9 | - name: 'og:description'
10 | content: Eden Treaty is an object-like representation of an Elysia server, providing an end-to-end type safety, and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Eden Treaty is an object-like representation of an Elysia server, providing an end-to-end type safety, and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
15 | ---
16 |
17 | # Eden Treaty Legacy
18 |
19 | ::: tip NOTE
20 | This is a documentation for Eden Treaty 1 or (edenTreaty)
21 |
22 | For a new project, we recommended starting with Eden Treaty 2 (treaty) instead.
23 | :::
24 |
25 | Eden Treaty is an object-like representation of an Elysia server.
26 |
27 | Providing accessor like a normal object with type directly from the server, helping us to move faster, and make sure that nothing break
28 |
29 | ---
30 |
31 | To use Eden Treaty, first export your existing Elysia server type:
32 | typescript 33 | // server.ts 34 | import { Elysia, t } from 'elysia' 35 | 36 | const app = new Elysia() 37 | .get('/', () => 'Hi Elysia') 38 | .get('/id/:id', ({ params: { id } }) => id) 39 | .post('/mirror', ({ body }) => body, { 40 | body: t.Object({ 41 | id: t.Number(), 42 | name: t.String() 43 | }) 44 | }) 45 | .listen(3000) 46 | 47 | export type App = typeof app // [!code ++] 48 |
49 |
50 | Then import the server type, and consume the Elysia API on client:
51 | typescript 52 | // client.ts 53 | import { edenTreaty } from '@elysiajs/eden' 54 | import type { App } from './server' // [!code ++] 55 | 56 | const app = edenTreaty<App>('http://localhost:') 57 | 58 | // response type: 'Hi Elysia' 59 | const { data: pong, error } = app.get() 60 | 61 | // response type: 1895 62 | const { data: id, error } = app.id['1895'].get() 63 | 64 | // response type: { id: 1895, name: 'Skadi' } 65 | const { data: nendoroid, error } = app.mirror.post({ 66 | id: 1895, 67 | name: 'Skadi' 68 | }) 69 |
70 |
71 | ::: tip
72 | Eden Treaty is fully type-safe with auto-completion support.
73 | :::
74 |
75 | ## Anatomy
76 | Eden Treaty will transform all existing paths to object-like representation, that can be described as:
77 | typescript 78 | EdenTreaty.<1>.<2>.<n>.<method>({ 79 | ...body, 80 | $query?: {}, 81 | $fetch?: RequestInit 82 | }) 83 |
84 |
85 | ### Path
86 | Eden will transform /
into .
which can be called with a registered method
, for example:
87 | - /path -> .path
88 | - /nested/path -> .nested.path
89 |
90 | ### Path parameters
91 | Path parameters will be mapped automatically by their name in the URL.
92 |
93 | - /id/:id -> .id.<anyThing>
94 | - eg: .id.hi
95 | - eg: .id['123']
96 |
97 | ::: tip
98 | If a path doesn't support path parameters, TypeScript will show an error.
99 | :::
100 |
101 | ### Query
102 | You can append queries to path with $query
:
103 | typescript 104 | app.get({ 105 | $query: { 106 | name: 'Eden', 107 | code: 'Gold' 108 | } 109 | }) 110 |
111 |
112 | ### Fetch
113 | Eden Treaty is a fetch wrapper, you can add any valid Fetch parameters to Eden by passing it to $fetch
:
114 | typescript 115 | app.post({ 116 | $fetch: { 117 | headers: { 118 | 'x-organization': 'MANTIS' 119 | } 120 | } 121 | }) 122 |
123 |
124 | ## Error Handling
125 | Eden Treaty will return a value of data
and error
as a result, both fully typed.
126 | typescript 127 | // response type: { id: 1895, name: 'Skadi' } 128 | const { data: nendoroid, error } = app.mirror.post({ 129 | id: 1895, 130 | name: 'Skadi' 131 | }) 132 | 133 | if(error) { 134 | switch(error.status) { 135 | case 400: 136 | case 401: 137 | warnUser(error.value) 138 | break 139 | 140 | case 500: 141 | case 502: 142 | emergencyCallDev(error.value) 143 | break 144 | 145 | default: 146 | reportError(error.value) 147 | break 148 | } 149 | 150 | throw error 151 | } 152 | 153 | const { id, name } = nendoroid 154 |
155 |
156 | Both data, and error will be typed as nullable until you can confirm their statuses with a type guard.
157 |
158 | To put it simply, if fetch is successful, data will have a value and error will be null, and vice-versa.
159 |
160 | ::: tip
161 | Error is wrapped with an Error
with its value return from the server can be retrieve from Error.value
162 | :::
163 |
164 | ### Error type based on status
165 | 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.
166 |
167 | typescript 168 | // server.ts 169 | import { Elysia, t } from 'elysia' 170 | 171 | const app = new Elysia() 172 | .model({ 173 | nendoroid: t.Object({ 174 | id: t.Number(), 175 | name: t.String() 176 | }), 177 | error: t.Object({ 178 | message: t.String() 179 | }) 180 | }) 181 | .get('/', () => 'Hi Elysia') 182 | .get('/id/:id', ({ params: { id } }) => id) 183 | .post('/mirror', ({ body }) => body, { 184 | body: 'nendoroid', 185 | response: { 186 | 200: 'nendoroid', // [!code ++] 187 | 400: 'error', // [!code ++] 188 | 401: 'error' // [!code ++] 189 | } 190 | }) 191 | .listen(3000) 192 | 193 | export type App = typeof app 194 |
195 |
196 | An on the client side:
197 | typescript 198 | const { data: nendoroid, error } = app.mirror.post({ 199 | id: 1895, 200 | name: 'Skadi' 201 | }) 202 | 203 | if(error) { 204 | switch(error.status) { 205 | case 400: 206 | case 401: 207 | // narrow down to type 'error' described in the server 208 | warnUser(error.value) 209 | break 210 | 211 | default: 212 | // typed as unknown 213 | reportError(error.value) 214 | break 215 | } 216 | 217 | throw error 218 | } 219 |
220 |
221 | ## WebSocket
222 | Eden supports WebSocket using the same API as a normal route.
223 | typescript 224 | // Server 225 | import { Elysia, t } from 'elysia' 226 | 227 | const app = new Elysia() 228 | .ws('/chat', { 229 | message(ws, message) { 230 | ws.send(message) 231 | }, 232 | body: t.String(), 233 | response: t.String() 234 | }) 235 | .listen(3000) 236 | 237 | type App = typeof app 238 |
239 |
240 | To start listening to real-time data, call the .subscribe
method:
241 | typescript 242 | // Client 243 | import { edenTreaty } from '@elysiajs/eden' 244 | const app = edenTreaty<App>('http://localhost:') 245 | 246 | const chat = app.chat.subscribe() 247 | 248 | chat.subscribe((message) => { 249 | console.log('got', message) 250 | }) 251 | 252 | chat.send('hello from client') 253 |
254 |
255 | We can use schema to enforce type-safety on WebSockets, just like a normal route.
256 |
257 | ---
258 |
259 | Eden.subscribe returns EdenWebSocket which extends the WebSocket class with type-safety. The syntax is identical with the WebSocket
260 |
261 | If more control is need, EdenWebSocket.raw can be accessed to interact with the native WebSocket API.
262 |
263 | ## File Upload
264 | You may either pass one of the following to the field to attach file:
265 | - File
266 | - FileList
267 | - Blob
268 |
269 | Attaching a file will results content-type to be multipart/form-data
270 |
271 | Suppose we have the server as the following:
272 | typescript 273 | // server.ts 274 | import { Elysia } from 'elysia' 275 | 276 | const app = new Elysia() 277 | .post('/image', ({ body: { image, title } }) => title, { 278 | body: t.Object({ 279 | title: t.String(), 280 | image: t.Files(), 281 | }) 282 | }) 283 | .listen(3000) 284 | 285 | export type App = typeof app 286 |
287 |
288 | We may use the client as follows:
289 | typescript 290 | // client.ts 291 | import { edenTreaty } from '@elysia/eden' 292 | import type { Server } from './server' 293 | 294 | export const client = edenTreaty<Server>('http://localhost:3000') 295 | 296 | const id = <T extends HTMLElement = HTMLElement>(id: string) => 297 | document.getElementById(id)! as T 298 | 299 | const { data } = await client.image.post({ 300 | title: "Misono Mika", 301 | image: id<HTMLInputElement>('picture').files!, 302 | }) 303 |
304 |
1 | ---
2 | title: Overview - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Eden Treaty Overview - ElysiaJS
7 |
8 | - - meta
9 | - name: 'og:description'
10 | content: 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, we can fetch an API from Elysia server fully type-safe without code generation.
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: 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, we can fetch an API from Elysia server fully type-safe without code generation.
15 | ---
16 |
17 | # Eden Treaty
18 |
19 | Eden Treaty is an object representation to interact with a server and features type safety, auto-completion, and error handling.
20 |
21 | To use Eden Treaty, first export your existing Elysia server type:
22 |
23 | typescript 24 | // server.ts 25 | import { Elysia, t } from 'elysia' 26 | 27 | const app = new Elysia() 28 | .get('/hi', () => 'Hi Elysia') 29 | .get('/id/:id', ({ params: { id } }) => id) 30 | .post('/mirror', ({ body }) => body, { 31 | body: t.Object({ 32 | id: t.Number(), 33 | name: t.String() 34 | }) 35 | }) 36 | .listen(3000) 37 | 38 | export type App = typeof app // [!code ++] 39 |
40 |
41 | Then import the server type and consume the Elysia API on the client:
42 |
43 | typescript twoslash 44 | // @filename: server.ts 45 | import { Elysia, t } from 'elysia' 46 | 47 | const app = new Elysia() 48 | .get('/hi', () => 'Hi Elysia') 49 | .get('/id/:id', ({ params: { id } }) => id) 50 | .post('/mirror', ({ body }) => body, { 51 | body: t.Object({ 52 | id: t.Number(), 53 | name: t.String() 54 | }) 55 | }) 56 | .listen(3000) 57 | 58 | export type App = typeof app // [!code ++] 59 | 60 | // @filename: client.ts 61 | // ---cut--- 62 | // client.ts 63 | import { treaty } from '@elysiajs/eden' 64 | import type { App } from './server' // [!code ++] 65 | 66 | const app = treaty<App>('localhost:3000') 67 | 68 | // response type: 'Hi Elysia' 69 | const { data, error } = await app.hi.get() 70 | // ^? 71 |
72 |
73 | ## Tree like syntax
74 |
75 | HTTP Path is a resource indicator for a file system tree.
76 |
77 | File system consists of multiple levels of folders, for example:
78 |
79 | - /documents/elysia
80 | - /documents/kalpas
81 | - /documents/kelvin
82 |
83 | Each level is separated by / (slash) and a name.
84 |
85 | However in JavaScript, instead of using "/" (slash) we use "." (dot) to access deeper resources.
86 |
87 | Eden Treaty turns an Elysia server into a tree-like file system that can be accessed in the JavaScript frontend instead.
88 |
89 | | Path | Treaty |
90 | | ------------ | ------------ |
91 | | / | .index |
92 | | /hi | .hi |
93 | | /deep/nested | .deep.nested |
94 |
95 | Combined with the HTTP method, we can interact with the Elysia server.
96 |
97 | | Path | Method | Treaty |
98 | | ------------ | ------ | ------------------- |
99 | | / | GET | .index.get() |
100 | | /hi | GET | .hi.get() |
101 | | /deep/nested | GET | .deep.nested.get() |
102 | | /deep/nested | POST | .deep.nested.post() |
103 |
104 | ## Dynamic path
105 |
106 | 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.
107 |
108 | typescript 109 | // ❌ Unclear what the value is supposed to represent? 110 | treaty.item['skadi'].get() 111 |
112 |
113 | To handle this, we can specify a dynamic path using a function to provide a key value instead.
114 |
115 | typescript 116 | // ✅ Clear that value is dynamic path is 'name' 117 | treaty.item({ name: 'Skadi' }).get() 118 |
119 |
120 | | Path | Treaty |
121 | | --------------- | -------------------------------- |
122 | | /item | .item |
123 | | /item/:name | .item({ name: 'Skadi' }) |
124 | | /item/:name/id | .item({ name: 'Skadi' }).id |
125 |
1 | ---
2 | title: Eden Treaty Parameters - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Eden Treaty Parameters - ElysiaJS
7 |
8 | - - meta
9 | - name: 'og:description'
10 | content: Eden Treaty is an object-like representation of an Elysia server, providing an end-to-end type safety, and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Eden Treaty is an object-like representation of an Elysia server, providing an end-to-end type safety, and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
15 | ---
16 |
17 | # Parameters
18 |
19 | We need to send a payload to server eventually.
20 |
21 | To handle this, Eden Treaty's methods accept 2 parameters to send data to server.
22 |
23 | Both parameters is type safe and will be guided by TypeScript automatically:
24 |
25 | 1. body
26 | 2. additional parameters
27 | - query
28 | - headers
29 | - fetch
30 |
31 | typescript 32 | import { Elysia, t } from 'elysia' 33 | import { treaty } from '@elysiajs/eden' 34 | 35 | const app = new Elysia() 36 | .post('/user', ({ body }) => body, { 37 | body: t.Object({ 38 | name: t.String() 39 | }) 40 | }) 41 | .listen(3000) 42 | 43 | const api = treaty<typeof app>('localhost:3000') 44 | 45 | // ✅ works 46 | api.user.post({ 47 | name: 'Elysia' 48 | }) 49 | 50 | // ✅ also works 51 | api.user.post({ 52 | name: 'Elysia' 53 | }, { 54 | // This is optional as not specified in schema 55 | headers: { 56 | authorization: 'Bearer 12345' 57 | }, 58 | query: { 59 | id: 2 60 | } 61 | }) 62 |
63 |
64 | Unless if the method doesn't accept body, then body will be omitted and left with single parameter only.
65 |
66 | If the method "GET" or "HEAD":
67 |
68 | 1. Additional parameters
69 | - query
70 | - headers
71 | - fetch
72 |
73 | typescript 74 | import { Elysia } from 'elysia' 75 | import { treaty } from '@elysiajs/eden' 76 | 77 | const app = new Elysia() 78 | .get('/hello', () => 'hi') 79 | .listen(3000) 80 | 81 | const api = treaty<typeof app>('localhost:3000') 82 | 83 | // ✅ works 84 | api.hello.get({ 85 | // This is optional as not specified in schema 86 | headers: { 87 | hello: 'world' 88 | } 89 | }) 90 |
91 |
92 | ## Empty body
93 | If body is optional or not need but query or headers is required, you may pass the body as null
or undefined
instead.
94 |
95 | typescript 96 | import { Elysia, t } from 'elysia' 97 | import { treaty } from '@elysiajs/eden' 98 | 99 | const app = new Elysia() 100 | .post('/user', () => 'hi', { 101 | query: t.Object({ 102 | name: t.String() 103 | }) 104 | }) 105 | .listen(3000) 106 | 107 | const api = treaty<typeof app>('localhost:3000') 108 | 109 | api.user.post(null, { 110 | query: { 111 | name: 'Ely' 112 | } 113 | }) 114 |
115 |
116 | ## Fetch parameters
117 |
118 | Eden Treaty is a fetch wrapper, we may add any valid Fetch parameters to Eden by passing it to $fetch
:
119 |
120 | typescript 121 | import { Elysia, t } from 'elysia' 122 | import { treaty } from '@elysiajs/eden' 123 | 124 | const app = new Elysia() 125 | .get('/hello', () => 'hi') 126 | .listen(3000) 127 | 128 | const api = treaty<typeof app>('localhost:3000') 129 | 130 | const controller = new AbortController() 131 | 132 | const cancelRequest = setTimeout(() => { 133 | controller.abort() 134 | }, 5000) 135 | 136 | await api.hello.get({ 137 | fetch: { 138 | signal: controller.signal 139 | } 140 | }) 141 | 142 | clearTimeout(cancelRequest) 143 |
144 |
145 | ## File Upload
146 | We may either pass one of the following to attach file(s):
147 | - File
148 | - File[]
149 | - FileList
150 | - Blob
151 |
152 | Attaching a file will results content-type to be multipart/form-data
153 |
154 | Suppose we have the server as the following:
155 | typescript 156 | import { Elysia, t } from 'elysia' 157 | import { treaty } from '@elysiajs/eden' 158 | 159 | const app = new Elysia() 160 | .post('/image', ({ body: { image, title } }) => title, { 161 | body: t.Object({ 162 | title: t.String(), 163 | image: t.Files() 164 | }) 165 | }) 166 | .listen(3000) 167 | 168 | export const api = treaty<typeof app>('localhost:3000') 169 | 170 | const images = document.getElementById('images') as HTMLInputElement 171 | 172 | const { data } = await api.image.post({ 173 | title: "Misono Mika", 174 | image: images.files!, 175 | }) 176 |
177 |
1 | ---
2 | title: Eden Treaty Response - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Eden Treaty Response - ElysiaJS
7 |
8 | - - meta
9 | - name: 'og:description'
10 | content: Eden Treaty is an object-like representation of an Elysia server, providing an end-to-end type safety, and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Eden Treaty is an object-like representation of an Elysia server, providing an end-to-end type safety, and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
15 | ---
16 |
17 | # Response
18 | Once the fetch method is called, Eden Treaty return a promise containing an object with the following properties:
19 | - data - returned value of the response (2xx)
20 | - error - returned value from the response (>= 3xx)
21 | - response Response
- Web Standard Response class
22 | - status number
- HTTP status code
23 | - headers FetchRequestInit['headers']
- response headers
24 |
25 | 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.
26 |
27 | typescript 28 | import { Elysia, t } from 'elysia' 29 | import { treaty } from '@elysiajs/eden' 30 | 31 | const app = new Elysia() 32 | .post('/user', ({ body: { name }, error }) => { 33 | if(name === 'Otto') 34 | return error(400, 'Bad Request') 35 | 36 | return name 37 | }, { 38 | body: t.Object({ 39 | name: t.String() 40 | }) 41 | }) 42 | .listen(3000) 43 | 44 | const api = treaty<typeof app>('localhost:3000') 45 | 46 | const submit = async (name: string) => { 47 | const { data, error } = await api.user.post({ 48 | name 49 | }) 50 | 51 | // type: string | null 52 | console.log(data) 53 | 54 | if (error) 55 | switch(error.status) { 56 | case 400: 57 | // Error type will be narrow down 58 | throw error.value 59 | 60 | default: 61 | throw error.value 62 | } 63 | 64 | // Once the error is handled, type will be unwrapped 65 | // type: string 66 | return data 67 | } 68 |
69 |
70 | By default, Elysia infers error
and response
type to TypeScript automatically, and Eden will be providing auto-completion and type narrowing for accurate behavior.
71 |
72 | ::: tip
73 | If the server responds with an HTTP status >= 300, then value will be always be null, and error
will have a returned value instead.
74 |
75 | Otherwise, response will be passed to data.
76 | :::
77 |
78 | ## Stream response
79 | Eden will interpret a stream response as AsyncGenerator
allowing us to use for await
loop to consume the stream.
80 |
81 |
82 | typescript twoslash 83 | import { Elysia } from 'elysia' 84 | import { treaty } from '@elysiajs/eden' 85 | 86 | const app = new Elysia() 87 | .get('/ok', function* () { 88 | yield 1 89 | yield 2 90 | yield 3 91 | }) 92 | 93 | const { data, error } = await treaty(app).ok.get() 94 | if (error) throw error 95 | 96 | for await (const chunk of data) 97 | console.log(chunk) 98 | // ^? 99 |
100 |
1 | ---
2 | title: Eden Treaty Unit Test - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Eden Treaty Unit Test - ElysiaJS
7 |
8 | - - meta
9 | - name: 'og:description'
10 | content: Eden Treaty is an object-like representation of an Elysia server, providing an end-to-end type safety, and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Eden Treaty is an object-like representation of an Elysia server, providing an end-to-end type safety, and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
15 | ---
16 |
17 | # Unit Test
18 | According to Eden Treaty config and Unit Test, we may pass an Elysia instance to Eden Treaty directly to interact with Elysia server directly without sending a network request.
19 |
20 | We may use this patterns to create a unit test with end-to-end type safety and type-level test all at once.
21 |
22 | typescript twoslash 23 | // test/index.test.ts 24 | import { describe, expect, it } from 'bun:test' 25 | import { Elysia } from 'elysia' 26 | import { treaty } from '@elysiajs/eden' 27 | 28 | const app = new Elysia().get('/hello', 'hi') 29 | const api = treaty(app) 30 | 31 | describe('Elysia', () => { 32 | it('return a response', async () => { 33 | const { data } = await api.hello.get() 34 | 35 | expect(data).toBe('hi') 36 | // ^? 37 | 38 | }) 39 | }) 40 |
41 |
42 | ## Type safety test
43 | To perform a type safety test, simply run tsc to test folders.
44 |
45 | bash 46 | tsc --noEmit test/**/*.ts 47 |
48 |
49 | This is useful to ensure type integrity for both client and server, especially during migrations.
50 |
1 | ---
2 | title: Eden Treaty Web Socket - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Eden Treaty Web Socket - ElysiaJS
7 |
8 | - - meta
9 | - name: 'og:description'
10 | content: Eden Treaty is an object-like representation of an Elysia server, providing an end-to-end type safety, and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Eden Treaty is an object-like representation of an Elysia server, providing an end-to-end type safety, and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
15 | ---
16 |
17 | # WebSocket
18 | Eden Treaty supports WebSocket using subscribe
method.
19 |
20 | typescript twoslash 21 | import { Elysia, t } from "elysia"; 22 | import { treaty } from "@elysiajs/eden"; 23 | 24 | const app = new Elysia() 25 | .ws("/chat", { 26 | body: t.String(), 27 | response: t.String(), 28 | message(ws, message) { 29 | ws.send(message); 30 | }, 31 | }) 32 | .listen(3000); 33 | 34 | const api = treaty<typeof app>("localhost:3000"); 35 | 36 | const chat = api.chat.subscribe(); 37 | 38 | chat.subscribe((message) => { 39 | console.log("got", message); 40 | }); 41 | 42 | chat.on("open", () => { 43 | chat.send("hello from client"); 44 | }); 45 |
46 |
47 | .subscribe accepts the same parameter as get
and head
.
48 |
49 | ## Response
50 | Eden.subscribe returns EdenWS which extends the WebSocket results in identical syntax.
51 |
52 | If more control is need, EdenWebSocket.raw can be accessed to interact with the native WebSocket API.
53 |
1 | ---
2 | title: Elysia - Ergonomic Framework for Humans
3 | layout: page
4 | sidebar: false
5 | head:
6 | - - meta
7 | - property: 'og:title'
8 | content: Elysia - Ergonomic Framework for Humans
9 |
10 | - - meta
11 | - name: 'description'
12 | content: Elysia is an ergonomic framework for Humans. With end-to-end type safety and great developer experience. Elysia is familiar, fast, and first class TypeScript support with well-thought integration between services whether it's tRPC, Swagger or WebSocket. Elysia got you covered, start building next generation TypeScript web servers today.
13 |
14 | - - meta
15 | - property: 'og:description'
16 | content: Elysia is an ergonomic framework for Humans. With end-to-end type safety and great developer experience. Elysia is familiar, fast, and first class TypeScript support with well-thought integration between services whether it's tRPC, Swagger or WebSocket. Elysia got you covered, start building next generation TypeScript web servers today.
17 | ---
18 |
19 | <script setup>
20 | import Fern from '../components/fern/fern.vue'
21 | </script>
22 |
23 |
24 |
25 |
26 |
27 | typescript twoslash 28 | // @noErrors 29 | import { Elysia } from 'elysia' 30 | 31 | new Elysia() 32 | .get('/id/:id', ({ params, set }) => { 33 | // ^? 34 | 35 | 36 | 37 | 38 | set.headers.a 39 | // ^| 40 | 41 | 42 | return 'Su' 43 | }) 44 | 45 | .get('/optional/:name?', ({ params: { name } }) => { 46 | // ^? 47 | return name ?? 'Pardofelis' 48 | }) 49 | .listen(3000) 50 |
51 |
52 |
53 |
54 |
55 |
56 | typescript twoslash 57 | import { Elysia, t } from 'elysia' 58 | 59 | new Elysia() 60 | .patch('/profile', ({ body }) => body.profile, { 61 | // ^? 62 | 63 | 64 | 65 | 66 | body: t.Object({ 67 | id: t.Number(), 68 | profile: t.File({ type: 'image' }) 69 | }) 70 | }) 71 | .listen(3000) 72 |
73 |
74 |
75 |
76 |
77 |
78 | typescript twoslash 79 | // @errors: 2345 80 | import { Elysia, t } from 'elysia' 81 | 82 | new Elysia() 83 | .get('/profile', ({ error }) => { 84 | if(Math.random() > .5) 85 | return error(418, 'Mika') 86 | 87 | return 'ok' 88 | }, { 89 | response: { 90 | 200: t.Literal('ok'), 91 | 418: t.Literal('Nagisa') 92 | } 93 | }) 94 | .listen(3000) 95 |
96 |
97 |
98 |
99 |
100 |
101 | typescript twoslash 102 | // @noErrors 103 | import { Elysia, t } from 'elysia' 104 | 105 | const role = new Elysia({ name: 'macro' }) 106 | .macro(({ onBeforeHandle }) => ({ 107 | role(type: 'user' | 'staff' | 'admin') { 108 | onBeforeHandle(({ headers, error }) => { 109 | if(headers.authorization !== type) 110 | return error(401) 111 | }) 112 | } 113 | })) 114 | 115 | new Elysia() 116 | .use(role) 117 | .get('/admin/check', 'ok', { 118 | r 119 | // ^| 120 | }) 121 | .listen(3000) 122 |
123 |
124 |
125 |
126 |
127 |
128 | typescript 129 | import { Elysia, file } from 'elysia' 130 | 131 | new Elysia() 132 | .get('/', 'Hello World') 133 | .get('/image', file('mika.webp')) 134 | .get('/stream', function* () { 135 | yield 'Hello' 136 | yield 'World' 137 | }) 138 | .ws('/realtime', { 139 | message(ws, message) { 140 | ws.send('got:' + message) 141 | } 142 | }) 143 | .listen(3000) 144 |
145 |
146 |
147 |
148 |
149 |
150 | typescript 151 | import { Elysia } from 'elysia' 152 | import swagger from '@elysiajs/swagger' 153 | 154 | new Elysia() 155 | .use(swagger()) 156 | .use(character) 157 | .use(auth) 158 | .listen(3000) 159 |
160 |
161 |
162 |
163 |
164 |
165 | typescript twoslash 166 | // @noErrors 167 | // @filename: server.ts 168 | import { Elysia, t } from 'elysia' 169 | 170 | const app = new Elysia() 171 | .patch( 172 | '/profile', 173 | ({ body, error }) => { 174 | if(body.age < 18) 175 | return error(400, "Oh no") 176 | 177 | return body 178 | }, 179 | { 180 | body: t.Object({ 181 | age: t.Number() 182 | }) 183 | } 184 | ) 185 | .listen(80) 186 | 187 | export type App = typeof app 188 | 189 | // @filename: client.ts 190 | // ---cut--- 191 | import { treaty } from '@elysiajs/eden' 192 | import type { App } from './server' 193 | 194 | const api = treaty<App>('api.elysiajs.com') 195 | 196 | const { data } = await api.profile.patch({ 197 | // ^? 198 | age: 21 199 | }) 200 |
201 |
202 |
203 |
204 |
205 |
206 | typescript twoslash 207 | // @errors: 2345 2304 208 | // @filename: index.ts 209 | import { Elysia, t } from 'elysia' 210 | 211 | export const app = new Elysia() 212 | .put( 213 | '/user', 214 | ({ body, error }) => { 215 | if(body.username === 'mika') 216 | return error(400, { 217 | success: false, 218 | message: 'Username already taken' 219 | } as const) 220 | 221 | return { 222 | success: true, 223 | message: 'User created' 224 | } as const 225 | }, 226 | { 227 | body: t.Object({ 228 | username: t.String(), 229 | password: t.String() 230 | }) 231 | } 232 | ) 233 | 234 | // @filename: client.ts 235 | // ---cut--- 236 | import { treaty } from '@elysiajs/eden' 237 | import { app } from './index' 238 | import { test, expect } from 'bun:test' 239 | 240 | const server = treaty(app) 241 | 242 | test('should handle duplicated user', async () => { 243 | const { error } = await server.user.put({ 244 | username: 'mika', 245 | }) 246 | 247 | expect(error?.value).toEqual({ 248 | success: false, 249 | message: 'Username already taken' 250 | }) 251 | }) 252 |
253 |
254 |
255 |
256 |
257 |
258 | bash 259 | $ bun test 260 |
261 |
262 |
263 |
264 |
265 |
1 | ---
2 | title: Integration with Astro - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Integration with Astro - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: You can run Elysia on Astro. Elysia will work normally as expected because of WinterCG compliance.
11 |
12 | - - meta
13 | - property: 'og:description'
14 | content: You can run Elysia on Astro. Elysia will work normally as expected because of WinterCG compliance.
15 | ---
16 |
17 | # Integration with Astro
18 |
19 | With Astro Endpoint, we can run Elysia on Astro directly.
20 |
21 | 1. Set output to server in astro.config.mjs
22 |
23 | javascript 24 | // astro.config.mjs 25 | import { defineConfig } from 'astro/config' 26 | 27 | // https://astro.build/config 28 | export default defineConfig({ 29 | output: 'server' // [!code ++] 30 | }) 31 |
32 |
33 | 2. Create pages/[...slugs].ts
34 | 3. Create or import an existing Elysia server in [...slugs].ts
35 | 4. Export the handler with the name of method you want to expose
36 |
37 | typescript 38 | // pages/[...slugs].ts 39 | import { Elysia, t } from 'elysia' 40 | 41 | const app = new Elysia() 42 | .get('/api', () => 'hi') 43 | .post('/api', ({ body }) => body, { 44 | body: t.Object({ 45 | name: t.String() 46 | }) 47 | }) 48 | 49 | const handle = ({ request }: { request: Request }) => app.handle(request) // [!code ++] 50 | 51 | export const GET = handle // [!code ++] 52 | export const POST = handle // [!code ++] 53 |
54 |
55 | Elysia will work normally as expected because of WinterCG compliance.
56 |
57 | We recommended running Astro on Bun as Elysia is designed to be run on Bun
58 |
59 | ::: tip
60 | You can run Elysia server without running Astro on Bun thanks to WinterCG support.
61 |
62 | However some plugins like Elysia Static may not work if you are running Astro on Node.
63 | :::
64 |
65 | 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.
66 |
67 | Please refer to Astro Endpoint for more information.
68 |
69 | ## Prefix
70 |
71 | 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.
72 |
73 | For example, if you place Elysia server in pages/api/[...slugs].ts, you need to annotate prefix as /api to Elysia server.
74 |
75 | typescript 76 | // pages/api/[...slugs].ts 77 | import { Elysia, t } from 'elysia' 78 | 79 | const app = new Elysia({ prefix: '/api' }) // [!code ++] 80 | .get('/', () => 'hi') 81 | .post('/', ({ body }) => body, { 82 | body: t.Object({ 83 | name: t.String() 84 | }) 85 | }) 86 | 87 | const handle = ({ request }: { request: Request }) => app.handle(request) // [!code ++] 88 | 89 | export const GET = handle // [!code ++] 90 | export const POST = handle // [!code ++] 91 |
92 |
93 | This will ensure that Elysia routing will work properly in any location you place it.
94 |
1 | ---
2 | title: Cheat Sheet (Elysia by example) - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Cheat Sheet (Elysia by example) - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Elysia's cheat sheet in summary and how it work with "Elysia by example"
11 |
12 | - - meta
13 | - property: 'og:description'
14 | content: Elysia's cheat sheet in summary and how it work with "Elysia by example"
15 | ---
16 |
17 | # Cheat Sheet
18 | Here are a quick overview for a common Elysia patterns
19 |
20 | ## Hello World
21 | A simple hello world
22 |
23 | typescript 24 | import { Elysia } from 'elysia' 25 | 26 | new Elysia() 27 | .get('/', () => 'Hello World') 28 | .listen(3000) 29 |
30 |
31 | ## Custom HTTP Method
32 | Define route using custom HTTP methods/verbs
33 |
34 | See Route
35 |
36 | typescript 37 | import { Elysia } from 'elysia' 38 | 39 | new Elysia() 40 | .get('/hi', () => 'Hi') 41 | .post('/hi', () => 'From Post') 42 | .put('/hi', () => 'From Put') 43 | .route('M-SEARCH', '/hi', () => 'Custom Method') 44 | .listen(3000) 45 |
46 |
47 | ## Path Parameter
48 | Using dynamic path parameter
49 |
50 | See Path
51 |
52 | typescript 53 | import { Elysia } from 'elysia' 54 | 55 | new Elysia() 56 | .get('/id/:id', ({ params: { id } }) => id) 57 | .get('/rest/*', () => 'Rest') 58 | .listen(3000) 59 |
60 |
61 | ## Return JSON
62 | Elysia converts response to JSON automatically
63 |
64 | See Handler
65 |
66 | typescript 67 | import { Elysia } from 'elysia' 68 | 69 | new Elysia() 70 | .get('/json', () => { 71 | return { 72 | hello: 'Elysia' 73 | } 74 | }) 75 | .listen(3000) 76 |
77 |
78 | ## Return a file
79 | A file can be return in as formdata response
80 |
81 | The response must be a 1-level deep object
82 |
83 | typescript 84 | import { Elysia, file } from 'elysia' 85 | 86 | new Elysia() 87 | .get('/json', () => { 88 | return { 89 | hello: 'Elysia', 90 | image: file('public/cat.jpg') 91 | } 92 | }) 93 | .listen(3000) 94 |
95 |
96 | ## Header and status
97 | Set a custom header and a status code
98 |
99 | See Handler
100 |
101 | typescript 102 | import { Elysia } from 'elysia' 103 | 104 | new Elysia() 105 | .get('/', ({ set, error }) => { 106 | set.headers['x-powered-by'] = 'Elysia' 107 | 108 | return error(418, "I'm a teapot") 109 | }) 110 | .listen(3000) 111 |
112 |
113 | ## Group
114 | Define a prefix once for sub routes
115 |
116 | See Group
117 |
118 | typescript 119 | import { Elysia } from 'elysia' 120 | 121 | new Elysia() 122 | .get("/", () => "Hi") 123 | .group("/auth", app => { 124 | return app 125 | .get("/", () => "Hi") 126 | .post("/sign-in", ({ body }) => body) 127 | .put("/sign-up", ({ body }) => body) 128 | }) 129 | .listen(3000) 130 |
131 |
132 | ## Schema
133 | Enforce a data type of a route
134 |
135 | See Validation
136 |
137 | typescript 138 | import { Elysia, t } from 'elysia' 139 | 140 | new Elysia() 141 | .post('/mirror', ({ body: { username } }) => username, { 142 | body: t.Object({ 143 | username: t.String(), 144 | password: t.String() 145 | }) 146 | }) 147 | .listen(3000) 148 |
149 |
150 | ## Lifecycle Hook
151 | Intercept an Elysia event in order
152 |
153 | See Lifecycle
154 |
155 | typescript 156 | import { Elysia, t } from 'elysia' 157 | 158 | new Elysia() 159 | .onRequest(() => { 160 | console.log('On request') 161 | }) 162 | .on('beforeHandle', () => { 163 | console.log('Before handle') 164 | }) 165 | .post('/mirror', ({ body }) => body, { 166 | body: t.Object({ 167 | username: t.String(), 168 | password: t.String() 169 | }), 170 | afterHandle: () => { 171 | console.log("After handle") 172 | } 173 | }) 174 | .listen(3000) 175 |
176 |
177 | ## Guard
178 | Enforce a data type of sub routes
179 |
180 | See Scope
181 |
182 | typescript twoslash 183 | // @errors: 2345 184 | import { Elysia, t } from 'elysia' 185 | 186 | new Elysia() 187 | .guard({ 188 | response: t.String() 189 | }, (app) => app 190 | .get('/', () => 'Hi') 191 | // Invalid: will throws error, and TypeScript will report error 192 | .get('/invalid', () => 1) 193 | ) 194 | .listen(3000) 195 |
196 |
197 | ## Customize context
198 | Add custom variable to route context
199 |
200 | See Context
201 |
202 | typescript 203 | import { Elysia } from 'elysia' 204 | 205 | new Elysia() 206 | .state('version', 1) 207 | .decorate('getDate', () => Date.now()) 208 | .get('/version', ({ 209 | getDate, 210 | store: { version } 211 | }) => `${version} ${getDate()}`) 212 | .listen(3000) 213 |
214 |
215 | ## Redirect
216 | Redirect a response
217 |
218 | See Handler
219 |
220 | typescript 221 | import { Elysia } from 'elysia' 222 | 223 | new Elysia() 224 | .get('/', () => 'hi') 225 | .get('/redirect', ({ redirect }) => { 226 | return redirect('/') 227 | }) 228 | .listen(3000) 229 |
230 |
231 | ## Plugin
232 | Create a separate instance
233 |
234 | See Plugin
235 |
236 | typescript 237 | import { Elysia } from 'elysia' 238 | 239 | const plugin = new Elysia() 240 | .state('plugin-version', 1) 241 | .get('/hi', () => 'hi') 242 | 243 | new Elysia() 244 | .use(plugin) 245 | .get('/version', ({ store }) => store['plugin-version']) 246 | .listen(3000) 247 |
248 |
249 | ## Web Socket
250 | Create a realtime connection using Web Socket
251 |
252 | See Web Socket
253 |
254 | typescript 255 | import { Elysia } from 'elysia' 256 | 257 | new Elysia() 258 | .ws('/ping', { 259 | message(ws, message) { 260 | ws.send('hello ' + message) 261 | } 262 | }) 263 | .listen(3000) 264 |
265 |
266 | ## OpenAPI documentation
267 | Create interactive documentation using Scalar (or optionally Swagger)
268 |
269 | See swagger
270 |
271 | typescript 272 | import { Elysia } from 'elysia' 273 | import { swagger } from '@elysiajs/swagger' 274 | 275 | const app = new Elysia() 276 | .use(swagger()) 277 | .listen(3000) 278 | 279 | console.log(`View documentation at "${app.server!.url}swagger" in your browser`); 280 |
281 |
282 | ## Unit Test
283 | Write a unit test of your Elysia app
284 |
285 | See Unit Test
286 |
287 | typescript 288 | // test/index.test.ts 289 | import { describe, expect, it } from 'bun:test' 290 | import { Elysia } from 'elysia' 291 | 292 | describe('Elysia', () => { 293 | it('return a response', async () => { 294 | const app = new Elysia().get('/', () => 'hi') 295 | 296 | const response = await app 297 | .handle(new Request('http://localhost/')) 298 | .then((res) => res.text()) 299 | 300 | expect(response).toBe('hi') 301 | }) 302 | }) 303 |
304 |
305 | ## Custom body parser
306 | Create custom logic for parsing body
307 |
308 | See Parse
309 |
310 | typescript 311 | import { Elysia } from 'elysia' 312 | 313 | new Elysia() 314 | .onParse(({ request, contentType }) => { 315 | if (contentType === 'application/custom-type') 316 | return request.text() 317 | }) 318 |
319 |
320 | ## GraphQL
321 | Create a custom GraphQL server using GraphQL Yoga or Apollo
322 |
323 | See GraphQL Yoga
324 |
325 | typescript 326 | import { Elysia } from 'elysia' 327 | import { yoga } from '@elysiajs/graphql-yoga' 328 | 329 | const app = new Elysia() 330 | .use( 331 | yoga({ 332 | typeDefs: /* GraphQL */` 333 | type Query { 334 | hi: String 335 | } 336 | `, 337 | resolvers: { 338 | Query: { 339 | hi: () => 'Hello from Elysia' 340 | } 341 | } 342 | }) 343 | ) 344 | .listen(3000) 345 |
346 |
1 | ---
2 | title: Integration with Expo - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Integration with Expo - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: With Expo App Router, you can run Elysia on Expo route. Elysia will work normally as expected thanks to WinterCG compliance.
11 |
12 | - - meta
13 | - property: 'og:description'
14 | content: With Expo App Router, you can run Elysia on Expo route. Elysia will work normally as expected thanks to WinterCG compliance.
15 | ---
16 |
17 | # Integration with Expo
18 |
19 | Starting from Expo SDK 50, and App Router v3, Expo allows us to create API route directly in an Expo app.
20 |
21 | 1. Create an Expo app if it doesn't exist with:
22 | typescript 23 | bun create expo-app --template tabs 24 |
25 |
26 | 2. Create app/[...slugs]+api.ts
27 | 3. In [...slugs]+api.ts, create or import an existing Elysia server
28 | 4. Export the handler with the name of method you want to expose
29 |
30 | typescript 31 | // app/[...slugs]+api.ts 32 | import { Elysia, t } from 'elysia' 33 | 34 | const app = new Elysia() 35 | .get('/', () => 'hello Next') 36 | .post('/', ({ body }) => body, { 37 | body: t.Object({ 38 | name: t.String() 39 | }) 40 | }) 41 | 42 | export const GET = app.handle // [!code ++] 43 | export const POST = app.handle // [!code ++] 44 |
45 |
46 | 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.
47 |
48 | You can treat the Elysia server as if normal Expo API route.
49 |
50 | 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 with both client-side and server action.
51 |
52 | Please refer to API route for more information.
53 |
54 | ## Prefix
55 | 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.
56 |
57 | For example, if you place Elysia server in app/api/[...slugs]+api.ts, you need to annotate prefix as /api to Elysia server.
58 |
59 | typescript 60 | // app/api/[...slugs]+api.ts 61 | import { Elysia, t } from 'elysia' 62 | 63 | const app = new Elysia({ prefix: '/api' }) 64 | .get('/', () => 'hi') 65 | .post('/', ({ body }) => body, { 66 | body: t.Object({ 67 | name: t.String() 68 | }) 69 | }) 70 | 71 | export const GET = app.handle 72 | export const POST = app.handle 73 |
74 |
75 | This will ensure that Elysia routing will work properly in any location you place in.
76 |
77 | ## Deployment
78 | You can either directly use API route using Elysia and deploy as normal Elysia app normally if need or using experimental Expo server runtime.
79 |
80 | 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
81 |
82 | ::: tip
83 | 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.
84 | :::
85 |
86 | You may use the Expo function adapter provided by Expo to deploy your Edge Function.
87 |
88 | Currently Expo support the following adapter:
89 | - Express
90 | - Netlify
91 | - Vercel
92 |
1 | ---
2 | title: Integration with Nextjs - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Integration with Nextjs - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: With Nextjs App Router, you can run Elysia on Nextjs route. Elysia will work normally as expected because of WinterCG compliance.
11 |
12 | - - meta
13 | - property: 'og:description'
14 | content: With Nextjs App Router, you can run Elysia on Nextjs route. Elysia will work normally as expected because of WinterCG compliance.
15 | ---
16 |
17 | # Integration with Nextjs
18 |
19 | With Nextjs App Router, we can run Elysia on Nextjs route.
20 |
21 | 1. Create api/[[...slugs]]/route.ts inside app router
22 | 2. In route.ts, create or import an existing Elysia server
23 | 3. Export the handler with the name of method you want to expose
24 |
25 | typescript 26 | // app/api/[[...slugs]]/route.ts 27 | import { Elysia, t } from 'elysia' 28 | 29 | const app = new Elysia({ prefix: '/api' }) 30 | .get('/', () => 'hello Next') 31 | .post('/', ({ body }) => body, { 32 | body: t.Object({ 33 | name: t.String() 34 | }) 35 | }) 36 | 37 | export const GET = app.handle // [!code ++] 38 | export const POST = app.handle // [!code ++] 39 |
40 |
41 | 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.
42 |
43 | You can treat the Elysia server as a normal Nextjs API route.
44 |
45 | 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 with both client-side and server action
46 |
47 | Please refer to Nextjs Route Handlers for more information.
48 |
49 | ## Prefix
50 |
51 | Because our Elysia server is not in the root directory of the app router, you need to annotate the prefix to the Elysia server.
52 |
53 | For example, if you place Elysia server in app/user/[[...slugs]]/route.ts, you need to annotate prefix as /user to Elysia server.
54 |
55 | typescript 56 | // app/api/[[...slugs]]/route.ts 57 | import { Elysia, t } from 'elysia' 58 | 59 | const app = new Elysia({ prefix: '/user' }) // [!code ++] 60 | .get('/', () => 'hi') 61 | .post('/', ({ body }) => body, { 62 | body: t.Object({ 63 | name: t.String() 64 | }) 65 | }) 66 | 67 | export const GET = app.handle 68 | export const POST = app.handle 69 |
70 |
71 | This will ensure that Elysia routing will work properly in any location you place it.
72 |
1 | ---
2 | title: Integration with SvelteKit - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Integration with SvelteKit - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: With SvelteKit, you can run Elysia on server routes.
11 |
12 | - - meta
13 | - property: 'og:description'
14 | content: With SvelteKit, you can run Elysia on server routes.
15 | ---
16 |
17 | # Integration with SvelteKit
18 |
19 | With SvelteKit, you can run Elysia on server routes.
20 |
21 | 1. Create src/routes/[...slugs]/+server.ts.
22 | 2. In +server.ts, create or import an existing Elysia server
23 | 3. Export the handler with the name of method you want to expose
24 |
25 | typescript 26 | // src/routes/[...slugs]/+server.ts 27 | import { Elysia, t } from 'elysia'; 28 | 29 | const app = new Elysia() 30 | .get('/', () => 'hello SvelteKit') 31 | .post('/', ({ body }) => body, { 32 | body: t.Object({ 33 | name: t.String() 34 | }) 35 | }) 36 | 37 | type RequestHandler = (v: { request: Request }) => Response | Promise<Response> 38 | 39 | export const GET: RequestHandler = ({ request }) => app.handle(request) 40 | export const POST: RequestHandler = ({ request }) => app.handle(request) 41 |
42 |
43 | You can treat the Elysia server as a normal SvelteKit server route.
44 |
45 | 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 with both client-side and server action
46 |
47 | Please refer to SvelteKit Routing for more information.
48 |
49 | ## Prefix
50 | 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.
51 |
52 | For example, if you place Elysia server in src/routes/api/[...slugs]/+server.ts, you need to annotate prefix as /api to Elysia server.
53 |
54 | typescript twoslash 55 | // src/routes/api/[...slugs]/+server.ts 56 | import { Elysia, t } from 'elysia'; 57 | 58 | const app = new Elysia({ prefix: '/api' }) // [!code ++] 59 | .get('/', () => 'hi') 60 | .post('/', ({ body }) => body, { 61 | body: t.Object({ 62 | name: t.String() 63 | }) 64 | }) 65 | 66 | type RequestHandler = (v: { request: Request }) => Response | Promise<Response> 67 | 68 | export const GET: RequestHandler = ({ request }) => app.handle(request) 69 | export const POST: RequestHandler = ({ request }) => app.handle(request) 70 |
71 |
72 | This will ensure that Elysia routing will work properly in any location you place it.
73 |
1 | ---
2 | title: Key Concept - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Key Concept - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Although Elysia is a simple library, it has some key concepts that you need to understand to use it effectively. This page will guide you through the key concepts of ElysiaJS.
11 |
12 | - - meta
13 | - property: 'og:description'
14 | content: Although Elysia is a simple library, it has some key concepts that you need to understand to use it effectively. This page will guide you through the key concepts of ElysiaJS.
15 | ---
16 |
17 | # Key Concept
18 |
19 | ### We highly recommended you to read this page before start using Elysia.
20 |
21 | Although Elysia is a simple library, it has some key concepts that you need to understand to use it effectively.
22 |
23 | This page covers most important concepts of Elysia that you should to know.
24 |
25 | ## Everything is a component
26 |
27 | Every Elysia instance is a component.
28 |
29 | A component is a plugin that could plug into other instances.
30 |
31 | It could be a router, a store, a service, or anything else.
32 |
33 | ts twoslash 34 | import { Elysia } from 'elysia' 35 | 36 | const store = new Elysia() 37 | .state({ visitor: 0 }) 38 | 39 | const router = new Elysia() 40 | .use(store) 41 | .get('/increase', ({ store }) => store.visitor++) 42 | 43 | const app = new Elysia() 44 | .use(router) 45 | .get('/', ({ store }) => store) 46 | .listen(3000) 47 |
48 |
49 | This force you to break down your application into small pieces, making it to add or remove features easily.
50 |
51 | Learn more about this in plugin.
52 |
53 | ## Scope
54 | By default, event/life-cycle in each instance is isolated from each other.
55 |
56 | ts twoslash 57 | // @errors: 2339 58 | import { Elysia } from 'elysia' 59 | 60 | const ip = new Elysia() 61 | .derive(({ server, request }) => ({ 62 | ip: server?.requestIP(request) 63 | })) 64 | .get('/ip', ({ ip }) => ip) 65 | 66 | const server = new Elysia() 67 | .use(ip) 68 | .get('/ip', ({ ip }) => ip) 69 | .listen(3000) 70 |
71 |
72 | In this example, the ip
property is only share in it's own instance but not in the server
instance.
73 |
74 | To share the lifecycle, in our case, an ip
property with server
instance, we need to explicitly says it could be shared.
75 |
76 | ts twoslash 77 | import { Elysia } from 'elysia' 78 | 79 | const ip = new Elysia() 80 | .derive( 81 | { as: 'global' }, // [!code ++] 82 | ({ server, request }) => ({ 83 | ip: server?.requestIP(request) 84 | }) 85 | ) 86 | .get('/ip', ({ ip }) => ip) 87 | 88 | const server = new Elysia() 89 | .use(ip) 90 | .get('/ip', ({ ip }) => ip) 91 | .listen(3000) 92 |
93 |
94 | In this example, ip
property is shared between ip
and server
instance because we define it as global
.
95 |
96 | This force you to think about the scope of each property preventing you from accidentally sharing the property between instances.
97 |
98 | Learn more about this in scope.
99 |
100 | ## Method Chaining
101 | Elysia code should always use method chaining.
102 |
103 | As Elysia type system is complex, every methods in Elysia returns a new type reference.
104 |
105 | This is important to ensure type integrity and inference.
106 |
107 | typescript twoslash 108 | import { Elysia } from 'elysia' 109 | 110 | new Elysia() 111 | .state('build', 1) 112 | // Store is strictly typed // [!code ++] 113 | .get('/', ({ store: { build } }) => build) 114 | // ^? 115 | .listen(3000) 116 |
117 |
118 | In the code above state returns a new ElysiaInstance type, adding a build
type.
119 |
120 | ### ❌ Don't: Use Elysia without method chaining
121 | Without using method chaining, Elysia doesn't save these new types, leading to no type inference.
122 |
123 | typescript twoslash 124 | // @errors: 2339 125 | import { Elysia } from 'elysia' 126 | 127 | const app = new Elysia() 128 | 129 | app.state('build', 1) 130 | 131 | app.get('/', ({ store: { build } }) => build) 132 | 133 | app.listen(3000) 134 |
135 |
136 | We recommend to always use method chaining to provide an accurate type inference.
137 |
138 | ## Dependency
139 | By default, each instance will be re-execute everytime it's applied to another instance.
140 |
141 | This can cause a duplication of the same method being applied multiple times but some methods should be called once like lifecycle or routes.
142 |
143 | To prevent lifecycle methods from being duplicated, we can add an unique identifier to the instance.
144 |
145 | ts twoslash 146 | import { Elysia } from 'elysia' 147 | 148 | const ip = new Elysia({ name: 'ip' }) 149 | .derive( 150 | { as: 'global' }, 151 | ({ server, request }) => ({ 152 | ip: server?.requestIP(request) 153 | }) 154 | ) 155 | .get('/ip', ({ ip }) => ip) 156 | 157 | const router1 = new Elysia() 158 | .use(ip) 159 | .get('/ip-1', ({ ip }) => ip) 160 | 161 | const router2 = new Elysia() 162 | .use(ip) 163 | .get('/ip-2', ({ ip }) => ip) 164 | 165 | const server = new Elysia() 166 | .use(router1) 167 | .use(router2) 168 |
169 |
170 | This will prevent the ip
property from being call multiple time by applying deduplication using an unique name.
171 |
172 | Once name
is provided, the instance will become a singleton. Allowing Elysia to apply plugin deduplication.
173 |
174 | Allowing us to reuse the same instance multiple time without performance penalty.
175 |
176 | This force you to think about the dependency of each instance, allowing for easily applying migration or refactoring.
177 |
178 | Learn more about this in plugin deduplication.
179 |
180 | ## Type Inference
181 | Elysia has a complex type system that allows you to infer types from the instance.
182 |
183 | ts twoslash 184 | import { Elysia, t } from 'elysia' 185 | 186 | const app = new Elysia() 187 | .post('/', ({ body }) => body, { 188 | // ^? 189 | 190 | 191 | 192 | 193 | body: t.Object({ 194 | name: t.String() 195 | }) 196 | }) 197 |
198 |
199 | If possible, always use an inline function to provide an accurate type inference.
200 |
201 | 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.
202 |
203 | ts twoslash 204 | import { Elysia, t } from 'elysia' 205 | 206 | abstract class Controller { 207 | static greet({ name }: { name: string }) { 208 | return 'hello ' + name 209 | } 210 | } 211 | 212 | const app = new Elysia() 213 | .post('/', ({ body }) => Controller.greet(body), { 214 | body: t.Object({ 215 | name: t.String() 216 | }) 217 | }) 218 |
219 |
220 | Learn more about this in Best practice: MVC Controller.
221 |
1 | ---
2 | title: Elysia - Ergonomic Framework for Humans
3 | layout: page
4 | sidebar: false
5 | head:
6 | - - meta
7 | - property: 'og:title'
8 | content: Elysia - Ergonomic Framework for Humans
9 |
10 | - - meta
11 | - name: 'description'
12 | content: Elysia is an ergonomic framework for Humans. With end-to-end type safety and great developer experience. Elysia is familiar, fast, and first class TypeScript support with well-thought integration between services whether it's tRPC, Swagger or WebSocket. Elysia got you covered, start building next generation TypeScript web servers today.
13 |
14 | - - meta
15 | - property: 'og:description'
16 | content: Elysia is an ergonomic framework for Humans. With end-to-end type safety and great developer experience. Elysia is familiar, fast, and first class TypeScript support with well-thought integration between services whether it's tRPC, Swagger or WebSocket. Elysia got you covered, start building next generation TypeScript web servers today.
17 | ---
18 |
19 | <script setup>
20 | import Landing from '../components/midori/index.vue'
21 | </script>
22 |
23 |
24 |
25 |
26 | typescript twoslash 27 | import { Elysia } from 'elysia' 28 | 29 | new Elysia() 30 | .get('/', 'Hello World') 31 | .get('/json', { 32 | hello: 'world' 33 | }) 34 | .get('/id/:id', ({ params: { id } }) => id) 35 | .listen(3000) 36 | 37 |
38 |
39 |
40 |
41 |
42 |
43 | typescript twoslash 44 | import { Elysia, t } from 'elysia' 45 | 46 | new Elysia() 47 | .post( 48 | '/profile', 49 | // ↓ hover me ↓ 50 | ({ body }) => body, 51 | { 52 | body: t.Object({ 53 | username: t.String() 54 | }) 55 | } 56 | ) 57 | .listen(3000) 58 | 59 |
60 |
61 |
62 |
63 |
64 | ts twoslash 65 | // @filename: controllers.ts 66 | import { Elysia } from 'elysia' 67 | 68 | export const users = new Elysia() 69 | .get('/users', 'Dreamy Euphony') 70 | 71 | export const feed = new Elysia() 72 | .get('/feed', ['Hoshino', 'Griseo', 'Astro']) 73 | 74 | // @filename: server.ts 75 | // ---cut--- 76 | import { Elysia, t } from 'elysia' 77 | import { swagger } from '@elysiajs/swagger' 78 | import { users, feed } from './controllers' 79 | 80 | new Elysia() 81 | .use(swagger()) 82 | .use(users) 83 | .use(feed) 84 | .listen(3000) 85 |
86 |
87 |
88 |
89 |
90 | typescript twoslash 91 | // @filename: server.ts 92 | // ---cut--- 93 | // server.ts 94 | import { Elysia, t } from 'elysia' 95 | 96 | const app = new Elysia() 97 | .patch( 98 | '/user/profile', 99 | ({ body, error }) => { 100 | if(body.age < 18) 101 | return error(400, "Oh no") 102 | 103 | if(body.name === 'Nagisa') 104 | return error(418) 105 | 106 | return body 107 | }, 108 | { 109 | body: t.Object({ 110 | name: t.String(), 111 | age: t.Number() 112 | }) 113 | } 114 | ) 115 | .listen(80) 116 | 117 | export type App = typeof app 118 |
119 |
120 |
121 |
122 |
123 | typescript twoslash 124 | // @errors: 2322 1003 125 | // @filename: server.ts 126 | import { Elysia, t } from 'elysia' 127 | 128 | const app = new Elysia() 129 | .patch( 130 | '/user/profile', 131 | ({ body, error }) => { 132 | if(body.age < 18) 133 | return error(400, "Oh no") 134 | 135 | if(body.name === 'Nagisa') 136 | return error(418) 137 | 138 | return body 139 | }, 140 | { 141 | body: t.Object({ 142 | name: t.String(), 143 | age: t.Number() 144 | }) 145 | } 146 | ) 147 | .listen(80) 148 | 149 | export type App = typeof app 150 | 151 | // @filename: client.ts 152 | // ---cut--- 153 | // client.ts 154 | import { treaty } from '@elysiajs/eden' 155 | import type { App } from './server' 156 | 157 | const api = treaty<App>('localhost') 158 | 159 | const { data, error } = await api.user.profile.patch({ 160 | name: 'saltyaom', 161 | age: '21' 162 | }) 163 | 164 | if(error) 165 | switch(error.status) { 166 | case 400: 167 | throw error.value 168 | // ^? 169 | 170 | case 418: 171 | throw error.value 172 | // ^? 173 | } 174 | 175 | data 176 | // ^? 177 |
178 |
179 |
180 |
181 |
182 |
1 | ---
2 | title: Configuration - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Configuration - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Elysia can be configured by passing an object to the constructor. We can configure Elysia behavior by passing an object to the constructor.
11 |
12 | - - meta
13 | - property: 'og:description'
14 | content: Elysia can be configured by passing an object to the constructor. We can configure Elysia behavior by passing an object to the constructor.
15 | ---
16 |
17 | # Configuration
18 |
19 | We can configure Elysia behavior by passing an object to the constructor.
20 |
21 | ts 22 | import { Elysia } from 'elysia' 23 | 24 | new Elysia({ 25 | prefix: '/group' 26 | }) 27 |
28 |
29 | Above is an example of configuring Elysia to use /group
as the path prefix.
30 |
31 | ## Max body size
32 | We can set the maximum body size by setting serve.maxRequestBodySize
in the serve
configuration.
33 |
34 | ts 35 | import { Elysia } from 'elysia' 36 | 37 | new Elysia({ 38 | serve: { 39 | maxRequestBodySize: 1024 * 1024 * 256 // 256MB 40 | } 41 | }) 42 |
43 |
44 | By default the maximum request body size is 128MB (1024 * 1024 * 128).
45 |
46 | ## TLS
47 | We can enable TLS (known as successor of SSL) by passing in a value for key and cert; both are required to enable TLS.
48 |
49 | ts 50 | import { Elysia, file } from 'elysia' 51 | 52 | new Elysia({ 53 | serve: { 54 | tls: { 55 | cert: file('cert.pem'), 56 | key: file('key.pem') 57 | } 58 | } 59 | }) 60 |
61 |
62 | Elysia extends Bun configuration which supports TLS out of the box, powered by BoringSSL.
63 |
64 | See TLS Options for available configuration.
65 |
66 | ## Config
67 | Below is a config accepted by Elysia:
68 |
69 | ### prefix
70 | @default ""
71 |
72 | Path prefix of the instance
73 |
74 | ### name
75 | Name of the instance for debugging, and plugin deduplication purpose
76 |
77 | ### seed
78 | Seed for generating checksum for plugin deduplication
79 |
80 | ### detail
81 | OpenAPI documentation for documentation generation
82 |
83 | This configuration extends Swagger Specification.
84 |
85 | @see Swagger Specification
86 |
87 | ### tags
88 | OpenAPI tags for documentation generation
89 |
90 | Decorate all instance routes with tags
91 |
92 | This configuration extends Tag Object
93 |
94 | @see Tag Object
95 |
96 | ### precompile
97 | @default false
98 |
99 | Warm up Elysia before starting the server
100 |
101 | This will perform Ahead of Time compilation and generate code for route handlers
102 |
103 | If set to false, Elysia will perform Just in Time compilation
104 |
105 | Only required for root instance (instance which use listen) to effect
106 |
107 | ### aot
108 | @default false
109 |
110 | Ahead of Time compliation
111 |
112 | Reduced performance but faster startup time
113 |
114 | ## strictPath
115 | @default false
116 |
117 | Whether should Elysia tolerate suffix /
or vice-versa
118 |
119 | #### Example
120 | If strictPath
is set to true
, Elysia will match /id
and not /id/
121 |
122 | ts 123 | import { Elysia } from 'elysia' 124 | 125 | new Elysia({ 126 | strictPath: true 127 | }) 128 | .get('/id', 'work') 129 |
130 |
131 | Normally, both /id
and /id/
will match the route handler (default is false
)
132 |
133 | ## cookie
134 | Set default cookie options
135 |
136 | ## normalize
137 | @default true
138 |
139 | If enabled, the handlers will run a clean on incoming and outgoing bodies instead of failing directly.
140 |
141 | This allows for sending unknown or disallowed properties in the bodies. These will simply be filtered out instead of failing the request.
142 |
143 | This has no effect when the schemas allow additional properties.
144 |
145 | Since this uses dynamic schema it may have an impact on performance.
146 |
147 | ## nativeStaticResponse
148 | @default true
149 | @since 1.1.11
150 |
151 | Enable Bun static response.
152 |
153 | Whether Elysia should use Bun's static response.
154 |
155 | This allows Elysia to improve static content performance and reduce memory significantly.
156 |
157 | #### Example
158 | Elysia will use Bun's static response for static content
159 | ts 160 | import { Elysia } from 'elysia' 161 | 162 | new Elysia() 163 | .get('/static', 'work') 164 |
165 |
166 | Above is an equivalent to:
167 | ts 168 | Bun.serve({ 169 | static: { 170 | 'static': 'work', 171 | '/static': '/work' 172 | } 173 | }) 174 |
175 |
176 | ::: tip
177 | This configuration will only work if using Bun > 1.1.27 as the server
178 | :::
179 |
180 | ## websocket
181 | Override websocket configuration
182 |
183 | Recommended to leave this as default as Elysia will generate suitable configuration for handling WebSocket automatically
184 |
185 | This configuration extends Bun WebSocket API
186 |
187 | ## serve
188 | Bun serve configuration.
189 |
190 | ts 191 | import { Elysia } from 'elysia' 192 | 193 | new Elysia({ 194 | serve: { 195 | hostname: 'elysiajs.com', 196 | tls: { 197 | cert: Bun.file('cert.pem'), 198 | key: Bun.file('key.pem') 199 | } 200 | }, 201 | }) 202 |
203 |
204 | This configuration extends Bun Serve API and Bun TLS
205 |
206 | The following is ported from Bun JSDoc and Bun documentation:
207 |
208 | ### port
209 | @default 3000
210 |
211 | Port to listen on
212 |
213 | ### unix
214 | If set, the HTTP server will listen on a unix socket instead of a port.
215 |
216 | (Cannot be used with hostname+port)
217 |
218 | ### hostname
219 | @default 0.0.0.0
220 |
221 | Set the hostname which the server listens on
222 |
223 | ### maxRequestBodySize
224 | @default 1024 * 1024 * 128
(128MB)
225 |
226 | Set the maximum size of a request body (in bytes)
227 |
228 | ### reusePort
229 | @default true
230 |
231 | If the SO_REUSEPORT
flag should be set
232 |
233 | This allows multiple processes to bind to the same port, which is useful for load balancing
234 |
235 | This configuration is override and turns on by default by Elysia
236 |
237 | ### id
238 | Uniquely identify a server instance with an ID
239 |
240 | 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
.
241 |
242 | ### rejectUnauthorized
243 | @default NODE_TLS_REJECT_UNAUTHORIZED
environment variable
244 |
245 | If set to false
, any certificate is accepted.
246 |
247 | ## TLS options
248 | This configuration extends Bun TLS API
249 |
250 | ts 251 | import { Elysia } from 'elysia' 252 | 253 | new Elysia({ 254 | tls: { 255 | cert: Bun.file('cert.pem'), 256 | key: Bun.file('key.pem') 257 | } 258 | }) 259 |
260 |
261 | ### cert
262 | Cert chains in PEM format. One cert chain should be provided per private key.
263 |
264 | 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
265 | including the root CA (the root CA must be pre-known to the peer, see ca).
266 |
267 | When providing multiple cert chains, they do not have to be in the same order as their private keys in key.
268 |
269 | If the intermediate certificates are not provided, the peer will not be
270 | able to validate the certificate, and the handshake will fail.
271 |
272 | ### key
273 | Private keys in PEM format. PEM allows the option of private keys being encrypted. Encrypted keys will be decrypted with options.passphrase.
274 |
275 | 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 .
276 |
277 | The object form can only occur in an array.
278 |
279 | object.passphrase is optional. Encrypted keys will be decrypted with
280 |
281 | object.passphrase if provided, or options.passphrase if it is not.
282 |
283 | ### ca
284 | Optionally override the trusted CA certificates. Default is to trust the well-known CAs curated by Mozilla.
285 |
286 | Mozilla's CAs are completely replaced when CAs are explicitly specified using this option.
287 |
288 | ### passphrase
289 | Shared passphrase for a single private key and/or a PFX.
290 |
291 | ### dhParamsFile
292 | File path to a .pem file custom Diffie Helman parameters
293 |
294 | ### requestCert
295 | @default false
296 |
297 | If set to true
, the server will request a client certificate.
298 |
299 | ### secureOptions
300 | Optionally affect the OpenSSL protocol behavior, which is not usually necessary.
301 |
302 | This should be used carefully if at all!
303 |
304 | Value is a numeric bitmask of the SSL_OP_* options from OpenSSL Options
305 |
306 | ### serverName
307 | Explicitly set a server name
308 |
309 | ### lowMemoryMode
310 | @default false
311 |
312 | This sets OPENSSL_RELEASE_BUFFERS
to 1.
313 |
314 | It reduces overall performance but saves some memory.
315 |
1 | ---
2 | title: Production Deployment - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Production Deployment - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: This page
11 |
12 | - - meta
13 | - property: 'og:description'
14 | content: Elysia can be configured by passing an object to the constructor. We can configure Elysia behavior by passing an object to the constructor.
15 | ---
16 |
17 | # Deploy to production
18 | This page is a guide on how to deploy Elysia to production.
19 |
20 | ## Compile to binary
21 | We recommended running a build command before deploying to produciton as it could potentially reduce memory usage and file size significantly.
22 |
23 | We recommended compile Elysia into a single binary using the command as follows:
24 | bash 25 | bun build \ 26 | --compile \ 27 | --minify-whitespace \ 28 | --minify-syntax \ 29 | --target bun \ 30 | --outfile server \ 31 | ./src/index.ts 32 |
33 |
34 | This will generate a portable binary server
which we can run to start our server.
35 |
36 | Compiling server to binary usually significantly reduce memory usage by 2-3x compared to development environment.
37 |
38 | This command is a bit long, so let's break it down:
39 | 1. --compile
- Compile TypeScript to binary
40 | 2. --minify-whitespace
- Remove unnecessary whitespace
41 | 3. --minify-syntax
- Minify JavaScript syntax to reduce file size
42 | 4. --target bun
- Target the bun
platform, this can optimize the binary for the target platform
43 | 5. --outfile server
- Output the binary as server
44 | 6. ./src/index.ts
- The entry file of our server (codebase)
45 |
46 | To start our server, simly run the binary.
47 | bash 48 | ./server 49 |
50 |
51 | Once binary is compiled, you don't need Bun
installed on the machine to run the server.
52 |
53 | This is great as the deployment server doesn't need to install an extra runtime to run making binary portable.
54 |
55 | ### Why not --minify
56 | Bun does have --minify
flag that will minify the binary.
57 |
58 | However if we are using OpenTelemetry, it's going to reduce a function name to a single character.
59 |
60 | This make tracing harder than it should as OpenTelemetry rely on a function name.
61 |
62 | However, if you're not using OpenTelemetry, you may opt in for --minify
instead
63 | bash 64 | bun build \ 65 | --compile \ 66 | --minify \ 67 | --target bun \ 68 | --outfile server \ 69 | ./src/index.ts 70 |
71 |
72 | ### Permission
73 | Some Linux distro might not be able to run the binary, we suggest enable executable permission to a binary if you're on Linux:
74 | bash 75 | chmod +x ./server 76 | 77 | ./server 78 |
79 |
80 | ### Unknown random Chinese error
81 | If you're trying to deploy a binary to your server but unable to run with random chinese character error.
82 |
83 | It means that the machine you're running on doesn't support AVX2.
84 |
85 | Unfortunately, Bun require machine that has an AVX2
hardware support.
86 |
87 | There's no workaround as far as we know.
88 |
89 | ## Compile to JavaScript
90 | If you are unable to compile to binary or you are deploying on a Window server.
91 |
92 | You may bundle your server to a JavaScript file instead.
93 |
94 | bash 95 | bun build \ 96 | --compile \ // [!code --] 97 | --minify-whitespace \ 98 | --minify-syntax \ 99 | --target bun \ 100 | --outfile ./dist/index.js \ 101 | ./src/index.ts 102 |
103 |
104 | This will generate a single portable JavaScript file that you can deploy on your server.
105 | bash 106 | NODE_ENV=production bun ./dist/index.js 107 |
108 |
109 | ## Docker
110 | On Docker, we recommended to always compile to binary to reduce base image overhead.
111 |
112 | Here's an example image using Distroless image using binary.
113 | dockerfile [Dockerfile] 114 | FROM oven/bun AS build 115 | 116 | WORKDIR /app 117 | 118 | # Cache packages installation 119 | COPY package.json package.json 120 | COPY bun.lockb bun.lockb 121 | 122 | RUN bun install 123 | 124 | COPY ./src ./src 125 | 126 | ENV NODE_ENV=production 127 | 128 | RUN bun build \ 129 | --compile \ 130 | --minify-whitespace \ 131 | --minify-syntax \ 132 | --target bun \ 133 | --outfile server \ 134 | ./src/index.ts 135 | 136 | FROM gcr.io/distroless/base 137 | 138 | WORKDIR /app 139 | 140 | COPY --from=build /app/server server 141 | 142 | ENV NODE_ENV=production 143 | 144 | CMD ["./server"] 145 | 146 | EXPOSE 3000 147 |
148 |
149 | ### Monorepo
150 | If you are using Elysia with Monorepo, you may need to include dependent packages
.
151 |
152 | 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.
153 |
154 | Assume that our monorepo are using Turborepo with structure as follows:
155 | - apps
156 | - server
157 | - Dockerfile (place a Dockerfile here)
158 | - packages
159 | - config
160 |
161 | Then we can build our Dockerfile on monorepo root (not app root):
162 | bash 163 | docker build -t elysia-mono . 164 |
165 |
166 | With Dockerfile as follows:
167 | dockerfile [apps/server/Dockerfile] 168 | FROM oven/bun:1 AS build 169 | 170 | WORKDIR /app 171 | 172 | # Cache packages 173 | COPY package.json package.json 174 | COPY bun.lockb bun.lockb 175 | 176 | COPY /apps/server/package.json ./apps/server/package.json 177 | COPY /packages/config/package.json ./packages/config/package.json 178 | 179 | RUN bun install 180 | 181 | COPY /apps/server ./apps/server 182 | COPY /packages/config ./packages/config 183 | 184 | ENV NODE_ENV=production 185 | 186 | RUN bun build \ 187 | --compile \ 188 | --minify-whitespace \ 189 | --minify-syntax \ 190 | --target bun \ 191 | --outfile server \ 192 | ./src/index.ts 193 | 194 | FROM gcr.io/distroless/base 195 | 196 | WORKDIR /app 197 | 198 | COPY --from=build /app/server server 199 | 200 | ENV NODE_ENV=production 201 | 202 | CMD ["./server"] 203 | 204 | EXPOSE 3000 205 |
206 |
207 | ## Railway
208 | Railway is one of the popular deployment platform.
209 |
210 | Railway assign random port to expose for each deployment that can be access via PORT
environment variable.
211 |
212 | We need to modify our Elysia server to accept PORT
environment to comply with Railway port.
213 |
214 | Instead of a fixed port, we may use process.env.PORT
and provide a fallback on development instead.
215 | ts 216 | new Elysia() 217 | .listen(3000) // [!code --] 218 | .listen(process.env.PORT ?? 3000) // [!code ++] 219 |
220 |
221 | This should allows Elysia to intercept port provided by Railway.
222 |
223 | ::: tip
224 | Elysia assign hostname to 0.0.0.0
automatically, which works with Railway
225 | :::
226 |
1 | ---
2 | title: Macro - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Macro - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Macro allows us to define a custom field to the hook.
11 |
12 | - - meta
13 | - property: 'og:description'
14 | content: Macro allows us to define a custom field to the hook.
15 | ---
16 |
17 | # Macro
18 |
19 | <script setup>
20 | import Tab from '../../components/fern/tab.vue'
21 | </script>
22 |
23 | Macro allows us to define a custom field to the hook.
24 |
25 | <Tab
26 | id="macro"
27 | :names="['Macro v2', 'Macro v1']"
28 | :tabs="['macro2', 'macro1']"
29 | >
30 |
31 |
32 |
33 | Macro v1 use functional callback with event listener function.
34 |
35 | Elysia.macro allows us to compose custom heavy logic into a simple configuration available in hook, and guard with full type safety.
36 |
37 | typescript twoslash 38 | import { Elysia } from 'elysia' 39 | 40 | const plugin = new Elysia({ name: 'plugin' }) 41 | .macro(({ onBeforeHandle }) => ({ 42 | hi(word: string) { 43 | onBeforeHandle(() => { 44 | console.log(word) 45 | }) 46 | } 47 | })) 48 | 49 | const app = new Elysia() 50 | .use(plugin) 51 | .get('/', () => 'hi', { 52 | hi: 'Elysia' 53 | }) 54 |
55 |
56 | Accessing the path should log "Elysia" as the results.
57 |
58 | ### API
59 |
60 | 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.
61 |
62 | In previous example, we create hi accepting a string.
63 |
64 | 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.
65 |
66 | Which is an equivalent of pushing function to beforeHandle as the following:
67 |
68 | typescript 69 | import { Elysia } from 'elysia' 70 | 71 | const app = new Elysia() 72 | .get('/', () => 'hi', { 73 | beforeHandle() { 74 | console.log('Elysia') 75 | } 76 | }) 77 |
78 |
79 | macro shine when a logic is more complex than accepting a new function, for example creating an authorization layer for each route.
80 |
81 | typescript twoslash 82 | // @filename: auth.ts 83 | import { Elysia } from 'elysia' 84 | 85 | export const auth = new Elysia() 86 | .macro(() => { 87 | return { 88 | isAuth(isAuth: boolean) {}, 89 | role(role: 'user' | 'admin') {}, 90 | } 91 | }) 92 | 93 | // @filename: index.ts 94 | // ---cut--- 95 | import { Elysia } from 'elysia' 96 | import { auth } from './auth' 97 | 98 | const app = new Elysia() 99 | .use(auth) 100 | .get('/', () => 'hi', { 101 | isAuth: true, 102 | role: 'admin' 103 | }) 104 |
105 |
106 | The field can accept anything ranging from string to function, allowing us to create a custom life cycle event.
107 |
108 | 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.
109 |
110 | ### Parameters
111 |
112 | Elysia.macro parameters to interact with the life cycle event as the following:
113 |
114 | - onParse
115 | - onTransform
116 | - onBeforeHandle
117 | - onAfterHandle
118 | - onError
119 | - onResponse
120 | - events - Life cycle store
121 | - global: Life cycle of a global stack
122 | - local: Life cycle of an inline hook (route)
123 |
124 | Parameters start with on is a function to appends function into a life cycle stack.
125 |
126 | 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.
127 |
128 | ### Options
129 |
130 | The life cycle function of an extension API accepts additional options to ensure control over life cycle events.
131 |
132 | - options (optional) - determine which stack
133 | - function - function to execute on the event
134 |
135 | typescript 136 | import { Elysia } from 'elysia' 137 | 138 | const plugin = new Elysia({ name: 'plugin' }) 139 | .macro(({ onBeforeHandle }) => { 140 | return { 141 | hi(word: string) { 142 | onBeforeHandle( 143 | { insert: 'before' }, // [!code ++] 144 | () => { 145 | console.log(word) 146 | } 147 | ) 148 | } 149 | } 150 | }) 151 |
152 |
153 | Options may accept the following parameter:
154 |
155 | - insert
156 | - Where should the function be added
157 | - value: 'before' | 'after'
158 | - @default: 'after'
159 | - stack
160 | - Determine which type of stack should be added
161 | - value: 'global' | 'local'
162 | - @default: 'local'
163 |
164 |
165 |
166 |
167 |
168 | Macro v2 use an object syntax with return lifecycle like inline hook.
169 |
170 | Elysia.macro allows us to compose custom heavy logic into a simple configuration available in hook, and guard with full type safety.
171 |
172 | typescript twoslash 173 | import { Elysia } from 'elysia' 174 | 175 | const plugin = new Elysia({ name: 'plugin' }) 176 | .macro({ 177 | hi(word: string) { 178 | return { 179 | beforeHandle() { 180 | console.log(word) 181 | } 182 | } 183 | } 184 | }) 185 | 186 | const app = new Elysia() 187 | .use(plugin) 188 | .get('/', () => 'hi', { 189 | hi: 'Elysia' 190 | }) 191 |
192 |
193 | Accessing the path should log "Elysia" as the results.
194 |
195 | ### API
196 |
197 | macro has the same API as hook.
198 |
199 | In previous example, we create a hi macro accepting a string.
200 |
201 | 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.
202 |
203 | Which is an equivalent of pushing function to beforeHandle as the following:
204 |
205 | typescript 206 | import { Elysia } from 'elysia' 207 | 208 | const app = new Elysia() 209 | .get('/', () => 'hi', { 210 | beforeHandle() { 211 | console.log('Elysia') 212 | } 213 | }) 214 |
215 |
216 | macro shine when a logic is more complex than accepting a new function, for example creating an authorization layer for each route.
217 |
218 | typescript twoslash 219 | // @filename: auth.ts 220 | import { Elysia } from 'elysia' 221 | 222 | export const auth = new Elysia() 223 | .macro({ 224 | isAuth(isAuth: boolean) { 225 | return { 226 | resolve() { 227 | return { 228 | user: 'saltyaom' 229 | } 230 | } 231 | } 232 | }, 233 | role(role: 'admin' | 'user') { 234 | return {} 235 | } 236 | }) 237 | 238 | // @filename: index.ts 239 | // ---cut--- 240 | import { Elysia } from 'elysia' 241 | import { auth } from './auth' 242 | 243 | const app = new Elysia() 244 | .use(auth) 245 | .get('/', ({ user }) => user, { 246 | // ^? 247 | isAuth: true, 248 | role: 'admin' 249 | }) 250 |
251 |
252 | Macro v2 can also register a new property to the context, allowing us to access the value directly from the context.
253 |
254 | The field can accept anything ranging from string to function, allowing us to create a custom life cycle event.
255 |
256 | 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.
257 |
258 |
259 |
260 |
261 |
1 | ---
2 | title: Mount - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Mount - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Applying WinterCG interplopable code to run with Elysia or vice-versa.
11 |
12 | - - meta
13 | - property: 'og:description'
14 | content: Applying WinterCG interplopable code to run with Elysia or vice-versa.
15 | ---
16 |
17 | # Mount
18 | 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
.
19 |
20 | Elysia is WinterCG compliant. We are optimized to run on Bun but also openly support other runtimes if possible.
21 |
22 | 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.
23 |
24 | 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.
25 |
26 | ## Mount
27 | To use .mount, simply pass a fetch
function:
28 | ts 29 | import { Elysia } from 'elysia' 30 | 31 | const app = new Elysia() 32 | .get('/', () => 'Hello from Elysia') 33 | .mount('/hono', hono.fetch) 34 |
35 |
36 | A fetch function is a function that accepts a Web Standard Request and returns a Web Standard Response with the definition of:
37 | ts 38 | // Web Standard Request-like object 39 | // Web Standard Response 40 | type fetch = (request: RequestLike) => Response 41 |
42 |
43 | By default, this declaration is used by:
44 | - Bun
45 | - Deno
46 | - Vercel Edge Runtime
47 | - Cloudflare Worker
48 | - Netlify Edge Function
49 | - Remix Function Handler
50 | - etc.
51 |
52 | 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.
53 |
54 | If the framework also supports a .mount function, you can deeply nest a framework that supports it.
55 | ts 56 | import { Elysia } from 'elysia' 57 | import { Hono } from 'hono' 58 | 59 | const elysia = new Elysia() 60 | .get('/', () => 'Hello from Elysia inside Hono inside Elysia') 61 | 62 | const hono = new Hono() 63 | .get('/', (c) => c.text('Hello from Hono!')) 64 | .mount('/elysia', elysia.fetch) 65 | 66 | const main = new Elysia() 67 | .get('/', () => 'Hello from Elysia') 68 | .mount('/hono', hono.fetch) 69 | .listen(3000) 70 |
71 |
72 | ## Reusing Elysia
73 | Moreover, you can re-use multiple existing Elysia projects on your server.
74 |
75 | ts 76 | import { Elysia } from 'elysia' 77 | 78 | import A from 'project-a/elysia' 79 | import B from 'project-b/elysia' 80 | import C from 'project-c/elysia' 81 | 82 | new Elysia() 83 | .mount(A) 84 | .mount(B) 85 | .mount(C) 86 |
87 |
88 | 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.
89 |
90 | This makes the possibility of an interoperable framework and runtime a reality.
91 |
1 | ---
2 | title: Trace - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Trace - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Trace is an API to measure the performance of your server. Allowing us to interact with the duration span of each life-cycle events and measure the performance of each function to identify performance bottlenecks of the server.
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Trace is an API to measure the performance of your server. Allowing us to interact with the duration span of each life-cycle events and measure the performance of each function to identify performance bottlenecks of the server.
15 | ---
16 |
17 | # Trace
18 |
19 | Performance is an important aspect for Elysia.
20 |
21 | We don't want to be fast for benchmarking purposes, we want you to have a real fast server in real-world scenario.
22 |
23 | 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.
24 |
25 | Trace allows us to inject code to before and after of each life-cycle event, block and interact with the execution of the function.
26 |
27 | ## Trace
28 | Trace use a callback listener to ensure that callback function is finished before moving on to the next lifecycle event.
29 |
30 | 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.
31 |
32 | You may listen to each lifecycle by adding on
prefix follows by life-cycle name, for example onHandle
to listen to handle
event.
33 |
34 | ts twoslash 35 | import { Elysia } from 'elysia' 36 | 37 | const app = new Elysia() 38 | .trace(async ({ onHandle }) => { 39 | onHandle(({ begin, onStop }) => { 40 | onStop(({ end }) => { 41 | console.log('handle took', end - begin, 'ms') 42 | }) 43 | }) 44 | }) 45 | .get('/', () => 'Hi') 46 | .listen(3000) 47 |
48 |
49 | Please refer to Life Cycle Events for more information:
50 |
51 |
52 |
53 | ## Children
54 | Every events except
handle
have a children, which is an array of events that are executed inside for each life-cycle event.
55 |
56 | You can use onEvent
to listen to each child event in order
57 |
58 | ts twoslash 59 | import { Elysia } from 'elysia' 60 | 61 | const sleep = (time = 1000) => 62 | new Promise((resolve) => setTimeout(resolve, time)) 63 | 64 | const app = new Elysia() 65 | .trace(async ({ onBeforeHandle }) => { 66 | onBeforeHandle(({ total, onEvent }) => { 67 | console.log('total children:', total) 68 | 69 | onEvent(({ onStop }) => { 70 | onStop(({ elapsed }) => { 71 | console.log('child took', elapsed, 'ms') 72 | }) 73 | }) 74 | }) 75 | }) 76 | .get('/', () => 'Hi', { 77 | beforeHandle: [ 78 | function setup() {}, 79 | async function delay() { 80 | await sleep() 81 | } 82 | ] 83 | }) 84 | .listen(3000) 85 |
86 |
87 | In this example, total children will be 2
because there are 2 children in the beforeHandle
event.
88 |
89 | Then we listen to each child event by using onEvent
and print the duration of each child event.
90 |
91 | ## Trace Parameter
92 | When each lifecycle is called
93 |
94 | ts twoslash 95 | import { Elysia } from 'elysia' 96 | 97 | const app = new Elysia() 98 | // This is trace parameter 99 | // hover to view the type 100 | .trace((parameter) => { 101 | }) 102 | .get('/', () => 'Hi') 103 | .listen(3000) 104 |
105 |
106 | trace
accept the following parameters:
107 |
108 | ### id - number
109 | Randomly generated unique id for each request
110 |
111 | ### context - Context
112 | Elysia's Context, eg. set
, store
, query
, params
113 |
114 | ### set - Context.set
115 | Shortcut for context.set
, to set a headers or status of the context
116 |
117 | ### store - Singleton.store
118 | Shortcut for context.store
, to access a data in the context
119 |
120 | ### time - number
121 | Timestamp of when request is called
122 |
123 | ### on[Event] - TraceListener
124 | An event listener for each life-cycle event.
125 |
126 | You may listen to the following life-cycle:
127 | - onRequest - get notified of every new request
128 | - onParse - array of functions to parse the body
129 | - onTransform - transform request and context before validation
130 | - onBeforeHandle - custom requirement to check before the main handler, can skip the main handler if response returned.
131 | - onHandle - function assigned to the path
132 | - onAfterHandle - interact with the response before sending it back to the client
133 | - onMapResponse - map returned value into a Web Standard Response
134 | - onError - handle error thrown during processing request
135 | - onAfterResponse - cleanup function after response is sent
136 |
137 | ## Trace Listener
138 | A listener for each life-cycle event
139 |
140 | ts twoslash 141 | import { Elysia } from 'elysia' 142 | 143 | const app = new Elysia() 144 | .trace(({ onBeforeHandle }) => { 145 | // This is trace listener 146 | // hover to view the type 147 | onBeforeHandle((parameter) => { 148 | 149 | }) 150 | }) 151 | .get('/', () => 'Hi') 152 | .listen(3000) 153 |
154 |
155 | Each lifecycle listener accept the following
156 |
157 | ### name - string
158 | The name of the function, if the function is anonymous, the name will be anonymous
159 |
160 | ### begin - number
161 | The time when the function is started
162 |
163 | ### end - Promise<number>
164 | The time when the function is ended, will be resolved when the function is ended
165 |
166 | ### error - Promise<Error | null>
167 | Error that was thrown in the lifecycle, will be resolved when the function is ended
168 |
169 | ### onStop - callback?: (detail: TraceEndDetail) => any
170 | A callback that will be executed when the lifecycle is ended
171 |
172 | ts twoslash 173 | import { Elysia } from 'elysia' 174 | 175 | const app = new Elysia() 176 | .trace(({ onBeforeHandle, set }) => { 177 | onBeforeHandle(({ onStop }) => { 178 | onStop(({ elapsed }) => { 179 | set.headers['X-Elapsed'] = elapsed.toString() 180 | }) 181 | }) 182 | }) 183 | .get('/', () => 'Hi') 184 | .listen(3000) 185 |
186 |
187 | 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
188 |
189 | ## TraceEndDetail
190 | A parameter that passed to onStop
callback
191 |
192 | ### end - number
193 | The time when the function is ended
194 |
195 | ### error - Error | null
196 | Error that was thrown in the lifecycle
197 |
198 | ### elapsed - number
199 | Elapsed time of the lifecycle or end - begin
200 |
1 | ---
2 | title: Testing - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Testing - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: You can use bun:test
to create a unit test with Elysia. Elysia instance has a handle
method that accepts Request
and will return a Response
, the same as creating an HTTP request.
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: You can use bun:test
to create a unit test with Elysia. Elysia instance has a handle
method that accepts Request
and will return a Response
, the same as creating an HTTP request.
15 | ---
16 |
17 | # Unit Test
18 |
19 | Being WinterCG compliant, we can use Request / Response classes to test an Elysia server.
20 |
21 | Elysia provides the Elysia.handle method, which accepts a Web Standard Request and returns Response, simulating an HTTP Request.
22 |
23 | Bun includes a built-in test runner that offers a Jest-like API through the bun:test
module, facilitating the creation of unit tests.
24 |
25 | Create test/index.test.ts in the root of project directory with the following:
26 |
27 | typescript 28 | // test/index.test.ts 29 | import { describe, expect, it } from 'bun:test' 30 | import { Elysia } from 'elysia' 31 | 32 | describe('Elysia', () => { 33 | it('return a response', async () => { 34 | const app = new Elysia().get('/', () => 'hi') 35 | 36 | const response = await app 37 | .handle(new Request('http://localhost/')) 38 | .then((res) => res.text()) 39 | 40 | expect(response).toBe('hi') 41 | }) 42 | }) 43 |
44 |
45 | Then we can perform tests by running bun test
46 |
47 | bash 48 | bun test 49 |
50 |
51 | New requests to an Elysia server must be a fully valid URL, NOT a part of a URL.
52 |
53 | The request must provide URL as the following:
54 |
55 | | URL | Valid |
56 | | --------------------- | ----- |
57 | | http://localhost/user | ✅ |
58 | | /user | ❌ |
59 |
60 | We can also use other testing libraries like Jest to create Elysia unit tests.
61 |
62 | ## Eden Treaty test
63 |
64 | We may use Eden Treaty to create an end-to-end type safety test for Elysia server as follows:
65 |
66 | typescript twoslash 67 | // test/index.test.ts 68 | import { describe, expect, it } from 'bun:test' 69 | import { Elysia } from 'elysia' 70 | import { treaty } from '@elysiajs/eden' 71 | 72 | const app = new Elysia().get('/hello', 'hi') 73 | 74 | const api = treaty(app) 75 | 76 | describe('Elysia', () => { 77 | it('return a response', async () => { 78 | const { data, error } = await api.hello.get() 79 | 80 | expect(data).toBe('hi') 81 | // ^? 82 | }) 83 | }) 84 |
85 |
86 | See Eden Treaty Unit Test for setup and more information.
87 |
1 | ---
2 | title: WebSocket - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'title'
6 | content: WebSocket - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Elysia's WebSocket implementation. Start by declaring WebSocket route with "ws". WebSocket is a realtime protocol for communication between your client and server.
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Elysia's WebSocket implementation. Start by declaring WebSocket route with "ws". WebSocket is a realtime protocol for communication between your client and server.
15 | ---
16 |
17 | # WebSocket
18 |
19 | WebSocket is a realtime protocol for communication between your client and server.
20 |
21 | 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.
22 |
23 | SocketIO is a popular library for WebSocket, but it is not the only one. Elysia uses uWebSocket which Bun uses under the hood with the same API.
24 |
25 | To use websocket, simply call Elysia.ws()
:
26 |
27 | typescript 28 | import { Elysia } from 'elysia' 29 | 30 | new Elysia() 31 | .ws('/ws', { 32 | message(ws, message) { 33 | ws.send(message) 34 | } 35 | }) 36 | .listen(3000) 37 |
38 |
39 | ## WebSocket message validation:
40 |
41 | Same as normal route, WebSockets also accepts a schema object to strictly type and validate requests.
42 |
43 | typescript 44 | import { Elysia, t } from 'elysia' 45 | 46 | const app = new Elysia() 47 | .ws('/ws', { 48 | // validate incoming message 49 | body: t.Object({ 50 | message: t.String() 51 | }), 52 | query: t.Object({ 53 | id: t.String() 54 | }), 55 | message(ws, { message }) { 56 | // Get schema from `ws.data` 57 | const { id } = ws.data.query 58 | ws.send({ 59 | id, 60 | message, 61 | time: Date.now() 62 | }) 63 | } 64 | }) 65 | .listen(3000) 66 |
67 |
68 | WebSocket schema can validate the following:
69 |
70 | - message - An incoming message.
71 | - query - Query string or URL parameters.
72 | - params - Path parameters.
73 | - header - Request's headers.
74 | - cookie - Request's cookie
75 | - response - Value returned from handler
76 |
77 | By default Elysia will parse incoming stringified JSON message as Object for validation.
78 |
79 | ## Configuration
80 |
81 | You can set Elysia constructor to set the Web Socket value.
82 |
83 | ts 84 | import { Elysia } from 'elysia' 85 | 86 | new Elysia({ 87 | websocket: { 88 | idleTimeout: 30 89 | } 90 | }) 91 |
92 |
93 | Elysia's WebSocket implementation extends Bun's WebSocket configuration, please refer to Bun's WebSocket documentation for more information.
94 |
95 | The following are a brief configuration from Bun WebSocket
96 |
97 | ### perMessageDeflate
98 |
99 | @default false
100 |
101 | Enable compression for clients that support it.
102 |
103 | By default, compression is disabled.
104 |
105 | ### maxPayloadLength
106 |
107 | The maximum size of a message.
108 |
109 | ### idleTimeout
110 |
111 | @default 120
112 |
113 | After a connection has not received a message for this many seconds, it will be closed.
114 |
115 | ### backpressureLimit
116 |
117 | @default 16777216
(16MB)
118 |
119 | The maximum number of bytes that can be buffered for a single connection.
120 |
121 | ### closeOnBackpressureLimit
122 |
123 | @default false
124 |
125 | Close the connection if the backpressure limit is reached.
126 |
127 | ## Methods
128 |
129 | Below are the new methods that are available to the WebSocket route
130 |
131 | ## ws
132 |
133 | Create a websocket handler
134 |
135 | Example:
136 |
137 | typescript 138 | import { Elysia } from 'elysia' 139 | 140 | const app = new Elysia() 141 | .ws('/ws', { 142 | message(ws, message) { 143 | ws.send(message) 144 | } 145 | }) 146 | .listen(3000) 147 |
148 |
149 | Type:
150 |
151 | typescript 152 | .ws(endpoint: path, options: Partial<WebSocketHandler<Context>>): this 153 |
154 |
155 | * endpoint - A path to exposed as websocket handler
156 | * options - Customize WebSocket handler behavior
157 |
158 | ## WebSocketHandler
159 |
160 | WebSocketHandler extends config from config.
161 |
162 | Below is a config which is accepted by ws
.
163 |
164 | ## open
165 |
166 | Callback function for new websocket connection.
167 |
168 | Type:
169 |
170 | typescript 171 | open(ws: ServerWebSocket<{ 172 | // uid for each connection 173 | id: string 174 | data: Context 175 | }>): this 176 |
177 |
178 | ## message
179 |
180 | Callback function for incoming websocket message.
181 |
182 | Type:
183 |
184 | typescript 185 | message( 186 | ws: ServerWebSocket<{ 187 | // uid for each connection 188 | id: string 189 | data: Context 190 | }>, 191 | message: Message 192 | ): this 193 |
194 |
195 | Message
type based on schema.message
. Default is string
.
196 |
197 | ## close
198 |
199 | Callback function for closing websocket connection.
200 |
201 | Type:
202 |
203 | typescript 204 | close(ws: ServerWebSocket<{ 205 | // uid for each connection 206 | id: string 207 | data: Context 208 | }>): this 209 |
210 |
211 | ## drain
212 |
213 | Callback function for the server is ready to accept more data.
214 |
215 | Type:
216 |
217 | typescript 218 | drain( 219 | ws: ServerWebSocket<{ 220 | // uid for each connection 221 | id: string 222 | data: Context 223 | }>, 224 | code: number, 225 | reason: string 226 | ): this 227 |
228 |
229 | ## parse
230 |
231 | Parse
middleware to parse the request before upgrading the HTTP connection to WebSocket.
232 |
233 | ## beforeHandle
234 |
235 | Before Handle
middleware which execute before upgrading the HTTP connection to WebSocket.
236 |
237 | Ideal place for validation.
238 |
239 | ## transform
240 |
241 | Transform
middleware which execute before validation.
242 |
243 | ## transformMessage
244 |
245 | Like transform
, but execute before validation of WebSocket message
246 |
247 | ## header
248 |
249 | Additional headers to add before upgrade connection to WebSocket.
250 |
1 | ---
2 | title: Bearer Plugin - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Bearer Plugin - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Plugin for Elysia for retrieving Bearer token as specified in RFC6750. Start by installing the plugin with "bun add @elysiajs/bearer".
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Plugin for Elysia for retrieving Bearer token as specified in RFC6750. Start by installing the plugin with "bun add @elysiajs/bearer".
15 | ---
16 |
17 | # Bearer Plugin
18 | Plugin for elysia for retrieving the Bearer token.
19 |
20 | Install with:
21 | bash 22 | bun add @elysiajs/bearer 23 |
24 |
25 | Then use it:
26 | typescript 27 | import { Elysia } from 'elysia' 28 | import { bearer } from '@elysiajs/bearer' 29 | 30 | const app = new Elysia() 31 | .use(bearer()) 32 | .get('/sign', ({ bearer }) => bearer, { 33 | beforeHandle({ bearer, set }) { 34 | if (!bearer) { 35 | set.status = 400 36 | set.headers[ 37 | 'WWW-Authenticate' 38 | ] = `Bearer realm='sign', error="invalid_request"` 39 | 40 | return 'Unauthorized' 41 | } 42 | } 43 | }) 44 | .listen(3000) 45 |
46 |
47 | This plugin is for retrieving a Bearer token specified in RFC6750.
48 |
49 | 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.
50 |
1 | ---
2 | title: CORS Plugin - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: CORS Plugin - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Plugin for Elysia that adds support for customizing Cross-Origin Resource Sharing behavior. Start by installing the plugin with "bun add @elysiajs/cors".
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Plugin for Elysia that adds support for customizing Cross-Origin Resource Sharing behavior. Start by installing the plugin with "bun add @elysiajs/cors".
15 | ---
16 |
17 | # CORS Plugin
18 | This plugin adds support for customizing Cross-Origin Resource Sharing behavior.
19 |
20 | Install with:
21 | bash 22 | bun add @elysiajs/cors 23 |
24 |
25 | Then use it:
26 | typescript 27 | import { Elysia } from 'elysia' 28 | import { cors } from '@elysiajs/cors' 29 | 30 | new Elysia() 31 | .use(cors()) 32 | .listen(3000) 33 |
34 |
35 | This will set Elysia to accept requests from any origin.
36 |
37 | ## Config
38 | Below is a config which is accepted by the plugin
39 |
40 | ### origin
41 | @default true
42 |
43 | Indicates whether the response can be shared with the requesting code from the given origins.
44 |
45 | Value can be one of the following:
46 | - string - Name of origin which will directly assign to Access-Control-Allow-Origin header.
47 | - boolean - If set to true, Access-Control-Allow-Origin will be set to *
(any origins)
48 | - RegExp - Pattern to match request's URL, allowed if matched.
49 | - Function - Custom logic to allow resource sharing, allow if true
is returned.
50 | - Expected to have the type of:
51 | typescript 52 | cors(context: Context) => boolean | void 53 |
54 | - Array<string | RegExp | Function> - iterate through all cases above in order, allowed if any of the values are true
.
55 |
56 | ---
57 | ### methods
58 | @default *
59 |
60 | Allowed methods for cross-origin requests.
61 |
62 | Assign Access-Control-Allow-Methods header.
63 |
64 | Value can be one of the following:
65 | - undefined | null | '' - Ignore all methods.
66 | - * - Allows all methods.
67 | - string - Expects either a single method or a comma-delimited string
68 | - (eg: 'GET, PUT, POST'
)
69 | - string[] - Allow multiple HTTP methods.
70 | - eg: ['GET', 'PUT', 'POST']
71 |
72 | ---
73 | ### allowedHeaders
74 | @default *
75 |
76 | Allowed headers for an incoming request.
77 |
78 | Assign Access-Control-Allow-Headers header.
79 |
80 | Value can be one of the following:
81 | - string - Expects either a single header or a comma-delimited string
82 | - eg: 'Content-Type, Authorization'
.
83 | - string[] - Allow multiple HTTP headers.
84 | - eg: ['Content-Type', 'Authorization']
85 |
86 | ---
87 | ### exposeHeaders
88 | @default *
89 |
90 | Response CORS with specified headers.
91 |
92 | Assign Access-Control-Expose-Headers header.
93 |
94 | Value can be one of the following:
95 | - string - Expects either a single header or a comma-delimited string.
96 | - eg: 'Content-Type, X-Powered-By'
.
97 | - string[] - Allow multiple HTTP headers.
98 | - eg: ['Content-Type', 'X-Powered-By']
99 |
100 | ---
101 | ### credentials
102 | @default true
103 |
104 | 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 is include
.
105 |
106 | When a request's credentials mode Request.credentials is include
, browsers will only expose the response to the frontend JavaScript code if the Access-Control-Allow-Credentials value is true.
107 |
108 | Credentials are cookies, authorization headers, or TLS client certificates.
109 |
110 | Assign Access-Control-Allow-Credentials header.
111 |
112 | ---
113 | ### maxAge
114 | @default 5
115 |
116 | Indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached.
117 |
118 | Assign Access-Control-Max-Age header.
119 |
120 | ---
121 | ### preflight
122 | 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.
123 |
124 | Response with OPTIONS request with 3 HTTP request headers:
125 | - Access-Control-Request-Method
126 | - Access-Control-Request-Headers
127 | - Origin
128 |
129 | This config indicates if the server should respond to preflight requests.
130 |
131 | ## Pattern
132 | Below you can find the common patterns to use the plugin.
133 |
134 | ## Allow CORS by top-level domain
135 |
136 | typescript 137 | import { Elysia } from 'elysia' 138 | import { cors } from '@elysiajs/cors' 139 | 140 | const app = new Elysia() 141 | .use(cors({ 142 | origin: /.*\.saltyaom\.com$/ 143 | })) 144 | .get('/', () => 'Hi') 145 | .listen(3000) 146 |
147 |
148 | This will allow requests from top-level domains with saltyaom.com
149 |
1 | ---
2 | title: Cron Plugin - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Cron Plugin - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Plugin for Elysia that adds support for running cronjob in Elysia server. Start by installing the plugin with "bun add @elysiajs/cron".
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Plugin for Elysia that adds support for customizing Cross-Origin Resource Sharing behavior. Start by installing the plugin with "bun add @elysiajs/cors".
15 | ---
16 |
17 | # Cron Plugin
18 |
19 | This plugin adds support for running cronjob in the Elysia server.
20 |
21 | Install with:
22 |
23 | bash 24 | bun add @elysiajs/cron 25 |
26 |
27 | Then use it:
28 |
29 | typescript 30 | import { Elysia } from 'elysia' 31 | import { cron } from '@elysiajs/cron' 32 | 33 | new Elysia() 34 | .use( 35 | cron({ 36 | name: 'heartbeat', 37 | pattern: '*/10 * * * * *', 38 | run() { 39 | console.log('Heartbeat') 40 | } 41 | }) 42 | ) 43 | .listen(3000) 44 |
45 |
46 | The above code will log heartbeat
every 10 seconds.
47 |
48 | ## cron
49 |
50 | Create a cronjob for the Elysia server.
51 |
52 | type:
53 |
54 | 55 | cron(config: CronConfig, callback: (Instance['store']) => void): this 56 |
57 |
58 | CronConfig
accepts the parameters specified below:
59 |
60 | ### name
61 |
62 | Job name to register to store
.
63 |
64 | 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.
65 |
66 | ### pattern
67 |
68 | Time to run the job as specified by cron syntax specified as below:
69 |
70 | 71 | ┌────────────── second (optional) 72 | │ ┌──────────── minute 73 | │ │ ┌────────── hour 74 | │ │ │ ┌──────── day of the month 75 | │ │ │ │ ┌────── month 76 | │ │ │ │ │ ┌──── day of week 77 | │ │ │ │ │ │ 78 | * * * * * * 79 |
80 |
81 | This can be generated by tools like Crontab Guru
82 |
83 | ---
84 |
85 | This plugin extends the cron method to Elysia using cronner.
86 |
87 | Below are the configs accepted by cronner.
88 |
89 | ### timezone
90 |
91 | Time zone in Europe/Stockholm format
92 |
93 | ### startAt
94 |
95 | Schedule start time for the job
96 |
97 | ### stopAt
98 |
99 | Schedule stop time for the job
100 |
101 | ### maxRuns
102 |
103 | Maximum number of executions
104 |
105 | ### catch
106 |
107 | Continue execution even if an unhandled error is thrown by a triggered function.
108 |
109 | ### interval
110 |
111 | The minimum interval between executions, in seconds.
112 |
113 | ## Pattern
114 |
115 | Below you can find the common patterns to use the plugin.
116 |
117 | ## Stop cronjob
118 |
119 | You can stop cronjob manually by accessing the cronjob name registered to store
.
120 |
121 | typescript 122 | import { Elysia } from 'elysia' 123 | import { cron } from '@elysiajs/cron' 124 | 125 | const app = new Elysia() 126 | .use( 127 | cron({ 128 | name: 'heartbeat', 129 | pattern: '*/1 * * * * *', 130 | run() { 131 | console.log("Heartbeat") 132 | } 133 | }) 134 | ) 135 | .get('/stop', ({ store: { cron: { heartbeat } } }) => { 136 | heartbeat.stop() 137 | 138 | return 'Stop heartbeat' 139 | }) 140 | .listen(3000) 141 |
142 |
143 | ## Predefined patterns
144 |
145 | You can use predefined patterns from @elysiajs/cron/schedule
146 | typescript 147 | import { Elysia } from 'elysia' 148 | import { cron, Patterns } from '@elysiajs/cron' 149 | 150 | const app = new Elysia() 151 | .use( 152 | cron({ 153 | name: 'heartbeat', 154 | pattern: Patterns.everySecond(), 155 | run() { 156 | console.log("Heartbeat") 157 | } 158 | }) 159 | ) 160 | .get('/stop', ({ store: { cron: { heartbeat } } }) => { 161 | heartbeat.stop() 162 | 163 | return 'Stop heartbeat' 164 | }) 165 | .listen(3000) 166 |
167 |
168 |
169 | ### Functions
170 |
171 | Function | Description
172 | ------------- | -------------
173 | .everySeconds(2)
| Run the task every 2 seconds
174 | .everyMinutes(5)
| Run the task every 5 minutes
175 | .everyHours(3)
| Run the task every 3 hours
176 | .everyHoursAt(3, 15)
| Run the task every 3 hours at 15 minutes
177 | .everyDayAt('04:19')
| Run the task every day at 04:19
178 | .everyWeekOn(Patterns.MONDAY, '19:30')
| Run the task every Monday at 19:30
179 | .everyWeekdayAt('17:00')
| Run the task every day from Monday to Friday at 17:00
180 | .everyWeekendAt('11:00')
| Run the task on Saturday and Sunday at 11:00
181 |
182 | ### Function aliases to constants
183 |
184 | Function | Constant
185 | ------------- | -------------
186 | .everySecond()
| EVERY_SECOND
187 | .everyMinute()
| EVERY_MINUTE
188 | .hourly()
| EVERY_HOUR
189 | .daily()
| EVERY_DAY_AT_MIDNIGHT
190 | .everyWeekday()
| EVERY_WEEKDAY
191 | .everyWeekend()
| EVERY_WEEKEND
192 | .weekly()
| EVERY_WEEK
193 | .monthly()
| EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT
194 | .everyQuarter()
| EVERY_QUARTER
195 | .yearly()
| EVERY_YEAR
196 |
197 | ### Constants
198 |
199 | Constant | Pattern
200 | ------------- | -------------
201 | .EVERY_SECOND
| * * * * * *
202 | .EVERY_5_SECONDS
| */5 * * * * *
203 | .EVERY_10_SECONDS
| */10 * * * * *
204 | .EVERY_30_SECONDS
| */30 * * * * *
205 | .EVERY_MINUTE
| */1 * * * *
206 | .EVERY_5_MINUTES
| 0 */5 * * * *
207 | .EVERY_10_MINUTES
| 0 */10 * * * *
208 | .EVERY_30_MINUTES
| 0 */30 * * * *
209 | .EVERY_HOUR
| 0 0-23/1 * * *
210 | .EVERY_2_HOURS
| 0 0-23/2 * * *
211 | .EVERY_3_HOURS
| 0 0-23/3 * * *
212 | .EVERY_4_HOURS
| 0 0-23/4 * * *
213 | .EVERY_5_HOURS
| 0 0-23/5 * * *
214 | .EVERY_6_HOURS
| 0 0-23/6 * * *
215 | .EVERY_7_HOURS
| 0 0-23/7 * * *
216 | .EVERY_8_HOURS
| 0 0-23/8 * * *
217 | .EVERY_9_HOURS
| 0 0-23/9 * * *
218 | .EVERY_10_HOURS
| 0 0-23/10 * * *
219 | .EVERY_11_HOURS
| 0 0-23/11 * * *
220 | .EVERY_12_HOURS
| 0 0-23/12 * * *
221 | .EVERY_DAY_AT_1AM
| 0 01 * * *
222 | .EVERY_DAY_AT_2AM
| 0 02 * * *
223 | .EVERY_DAY_AT_3AM
| 0 03 * * *
224 | .EVERY_DAY_AT_4AM
| 0 04 * * *
225 | .EVERY_DAY_AT_5AM
| 0 05 * * *
226 | .EVERY_DAY_AT_6AM
| 0 06 * * *
227 | .EVERY_DAY_AT_7AM
| 0 07 * * *
228 | .EVERY_DAY_AT_8AM
| 0 08 * * *
229 | .EVERY_DAY_AT_9AM
| 0 09 * * *
230 | .EVERY_DAY_AT_10AM
| 0 10 * * *
231 | .EVERY_DAY_AT_11AM
| 0 11 * * *
232 | .EVERY_DAY_AT_NOON
| 0 12 * * *
233 | .EVERY_DAY_AT_1PM
| 0 13 * * *
234 | .EVERY_DAY_AT_2PM
| 0 14 * * *
235 | .EVERY_DAY_AT_3PM
| 0 15 * * *
236 | .EVERY_DAY_AT_4PM
| 0 16 * * *
237 | .EVERY_DAY_AT_5PM
| 0 17 * * *
238 | .EVERY_DAY_AT_6PM
| 0 18 * * *
239 | .EVERY_DAY_AT_7PM
| 0 19 * * *
240 | .EVERY_DAY_AT_8PM
| 0 20 * * *
241 | .EVERY_DAY_AT_9PM
| 0 21 * * *
242 | .EVERY_DAY_AT_10PM
| 0 22 * * *
243 | .EVERY_DAY_AT_11PM
| 0 23 * * *
244 | .EVERY_DAY_AT_MIDNIGHT
| 0 0 * * *
245 | .EVERY_WEEK
| 0 0 * * 0
246 | .EVERY_WEEKDAY
| 0 0 * * 1-5
247 | .EVERY_WEEKEND
| 0 0 * * 6,0
248 | .EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT
| 0 0 1 * *
249 | .EVERY_1ST_DAY_OF_MONTH_AT_NOON
| 0 12 1 * *
250 | .EVERY_2ND_HOUR
| 0 */2 * * *
251 | .EVERY_2ND_HOUR_FROM_1AM_THROUGH_11PM
| 0 1-23/2 * * *
252 | .EVERY_2ND_MONTH
| 0 0 1 */2 *
253 | .EVERY_QUARTER
| 0 0 1 */3 *
254 | .EVERY_6_MONTHS
| 0 0 1 */6 *
255 | .EVERY_YEAR
| 0 0 1 1 *
256 | .EVERY_30_MINUTES_BETWEEN_9AM_AND_5PM
| 0 */30 9-17 * * *
257 | .EVERY_30_MINUTES_BETWEEN_9AM_AND_6PM
| 0 */30 9-18 * * *
258 | .EVERY_30_MINUTES_BETWEEN_10AM_AND_7PM
| 0 */30 10-19 * * *
259 |
1 | ---
2 | title: Apollo GraphQL Plugin - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Apollo GraphQL Plugin - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Plugin for Elysia that adds support for using GraphQL Apollo on the Elysia server. Start by installing the plugin with "bun add graphql @elysiajs/apollo @apollo/server".
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Plugin for Elysia that adds support for using GraphQL Apollo on the Elysia server. Start by installing the plugin with "bun add graphql @elysiajs/apollo @apollo/server".
15 | ---
16 |
17 | # GraphQL Apollo Plugin
18 | Plugin for elysia for using GraphQL Apollo.
19 |
20 | Install with:
21 | bash 22 | bun add graphql @elysiajs/apollo @apollo/server 23 |
24 |
25 | Then use it:
26 | typescript 27 | import { Elysia } from 'elysia' 28 | import { apollo, gql } from '@elysiajs/apollo' 29 | 30 | const app = new Elysia() 31 | .use( 32 | apollo({ 33 | typeDefs: gql` 34 | type Book { 35 | title: String 36 | author: String 37 | } 38 | 39 | type Query { 40 | books: [Book] 41 | } 42 | `, 43 | resolvers: { 44 | Query: { 45 | books: () => { 46 | return [ 47 | { 48 | title: 'Elysia', 49 | author: 'saltyAom' 50 | } 51 | ] 52 | } 53 | } 54 | } 55 | }) 56 | ) 57 | .listen(3000) 58 |
59 |
60 | Accessing /graphql
should show Apollo GraphQL playground work with.
61 |
62 | ## Context
63 | 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.
64 |
65 | Because of this, Elysia replaces both with context
like route parameters.
66 | typescript 67 | const app = new Elysia() 68 | .use( 69 | apollo({ 70 | typeDefs, 71 | resolvers, 72 | context: async ({ request }) => { 73 | const authorization = request.headers.get('Authorization') 74 | 75 | return { 76 | authorization 77 | } 78 | } 79 | }) 80 | ) 81 | .listen(3000) 82 |
83 |
84 |
85 | ## Config
86 | This plugin extends Apollo's ServerRegistration (which is ApolloServer
's' constructor parameter).
87 |
88 | Below are the extended parameters for configuring Apollo Server with Elysia.
89 | ### path
90 | @default "/graphql"
91 |
92 | Path to expose Apollo Server.
93 |
94 | ### enablePlayground
95 | @default process.env.ENV !== 'production'
96 |
97 | Determine whether should Apollo should provide Apollo Playground.
98 |
1 | ---
2 | title: GraphQL Yoga Plugin - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: GraphQL Yoga Plugin - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Plugin for Elysia that adds support for using GraphQL Yoga on the Elysia server. Start by installing the plugin with "bun add graphql graphql-yoga @elysiajs/graphql-yoga".
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Plugin for Elysia that adds support for using GraphQL Yoga on the Elysia server. Start by installing the plugin with "bun add graphql graphql-yoga @elysiajs/graphql-yoga".
15 | ---
16 |
17 | # GraphQL Yoga Plugin
18 | This plugin integrates GraphQL yoga with Elysia
19 |
20 | Install with:
21 | bash 22 | bun add @elysiajs/graphql-yoga 23 |
24 |
25 | Then use it:
26 | typescript 27 | import { Elysia } from 'elysia' 28 | import { yoga } from '@elysiajs/graphql-yoga' 29 | 30 | const app = new Elysia() 31 | .use( 32 | yoga({ 33 | typeDefs: /* GraphQL */` 34 | type Query { 35 | hi: String 36 | } 37 | `, 38 | resolvers: { 39 | Query: { 40 | hi: () => 'Hello from Elysia' 41 | } 42 | } 43 | }) 44 | ) 45 | .listen(3000) 46 |
47 |
48 | Accessing /graphql
in the browser (GET request) would show you a GraphiQL instance for the GraphQL-enabled Elysia server.
49 |
50 | optional: you can install a custom version of optional peer dependencies as well:
51 | bash 52 | bun add graphql graphql-yoga 53 |
54 |
55 | ## Resolver
56 | Elysia uses Mobius to infer type from typeDefs field automatically, allowing you to get full type-safety and auto-complete when typing resolver types.
57 |
58 | ## Context
59 | You can add custom context to the resolver function by adding context
60 | ts 61 | import { Elysia } from 'elysia' 62 | import { yoga } from '@elysiajs/graphql-yoga' 63 | 64 | const app = new Elysia() 65 | .use( 66 | yoga({ 67 | typeDefs: /* GraphQL */` 68 | type Query { 69 | hi: String 70 | } 71 | `, 72 | context: { 73 | name: 'Mobius' 74 | }, 75 | // If context is a function on this doesn't present 76 | // for some reason it won't infer context type 77 | useContext(_) {}, 78 | resolvers: { 79 | Query: { 80 | hi: async (parent, args, context) => context.name 81 | } 82 | } 83 | }) 84 | ) 85 | .listen(3000) 86 |
87 |
88 | ## Config
89 | This plugin extends GraphQL Yoga's createYoga options, please refer to the GraphQL Yoga documentation with inlining schema
config to root.
90 |
91 | Below is a config which is accepted by the plugin
92 |
93 | ### path
94 | @default /graphql
95 |
96 | Endpoint to expose GraphQL handler
97 |
1 | ---
2 | title: HTML Plugin - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: HTML Plugin - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Plugin for Elysia that adds shortcut support for returning HTML in the Elysia server. Start by installing the plugin with "bun add @elysiajs/html".
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Plugin for Elysia that adds shortcut support for returning HTML in the Elysia server. Start by installing the plugin with "bun add @elysiajs/html".
15 | ---
16 |
17 | # HTML Plugin
18 |
19 | Allows you to use JSX and HTML with proper headers and support.
20 |
21 | Install with:
22 |
23 | bash 24 | bun add @elysiajs/html 25 |
26 |
27 | Then use it:
28 |
29 | tsx 30 | import { Elysia } from 'elysia' 31 | import { html, Html } from '@elysiajs/html' 32 | 33 | new Elysia() 34 | .use(html()) 35 | .get( 36 | '/html', 37 | () => ` 38 | <html lang='en'> 39 | <head> 40 | <title>Hello World</title> 41 | </head> 42 | <body> 43 | <h1>Hello World</h1> 44 | </body> 45 | </html>` 46 | ) 47 | .get('/jsx', () => ( 48 | <html lang='en'> 49 | <head> 50 | <title>Hello World</title> 51 | </head> 52 | <body> 53 | <h1>Hello World</h1> 54 | </body> 55 | </html> 56 | )) 57 | .listen(3000) 58 |
59 |
60 | This plugin will automatically add Content-Type: text/html; charset=utf8
header to the response, add <!doctype html>
, and convert it into a Response object.
61 |
62 | ## JSX
63 | Elysia HTML is based on @kitajs/html allowing us to define JSX to string in compile time to achieve high performance.
64 |
65 | Name your file that needs to use JSX to end with affix "x":
66 | - .js -> .jsx
67 | - .ts -> .tsx
68 |
69 | To register the TypeScript type, please append the following to tsconfig.json:
70 | jsonc 71 | // tsconfig.json 72 | { 73 | "compilerOptions": { 74 | "jsx": "react", 75 | "jsxFactory": "Html.createElement", 76 | "jsxFragmentFactory": "Html.Fragment" 77 | } 78 | } 79 |
80 |
81 | That's it, now you can use JSX as your template engine:
82 | tsx 83 | import { Elysia } from 'elysia' 84 | import { html, Html } from '@elysiajs/html' // [!code ++] 85 | 86 | new Elysia() 87 | .use(html()) // [!code ++] 88 | .get('/', () => ( 89 | <html lang="en"> 90 | <head> 91 | <title>Hello World</title> 92 | </head> 93 | <body> 94 | <h1>Hello World</h1> 95 | </body> 96 | </html> 97 | )) 98 | .listen(3000) 99 |
100 |
101 | If the error Cannot find name 'Html'. Did you mean 'html'?
occurs, this import must be added to the JSX template:
102 | tsx 103 | import { Html } from '@elysiajs/html' 104 |
105 |
106 | It is important that it is written in uppercase.
107 |
108 | ## XSS
109 | Elysia HTML is based use of the Kita HTML plugin to detect possible XSS attacks in compile time.
110 |
111 | You can use a dedicated safe
attribute to sanitize user value to prevent XSS vulnerability.
112 | tsx 113 | import { Elysia, t } from 'elysia' 114 | import { html, Html } from '@elysiajs/html' 115 | 116 | new Elysia() 117 | .use(html()) 118 | .post('/', ({ body }) => ( 119 | <html lang="en"> 120 | <head> 121 | <title>Hello World</title> 122 | </head> 123 | <body> 124 | <h1 safe>{body}</h1> 125 | </body> 126 | </html> 127 | ), { 128 | body: t.String() 129 | }) 130 | .listen(3000) 131 |
132 |
133 | However, when are building a large-scale app, it's best to have a type reminder to detect possible XSS vulnerabilities in your codebase.
134 |
135 | To add a type-safe reminder, please install:
136 | sh 137 | bun add @kitajs/ts-html-plugin 138 |
139 |
140 | Then appends the following tsconfig.json
141 | jsonc 142 | // tsconfig.json 143 | { 144 | "compilerOptions": { 145 | "jsx": "react", 146 | "jsxFactory": "Html.createElement", 147 | "jsxFragmentFactory": "Html.Fragment", 148 | "plugins": [{ "name": "@kitajs/ts-html-plugin" }] 149 | } 150 | } 151 |
152 |
153 | ## Options
154 |
155 | ### contentType
156 |
157 | - Type: string
158 | - Default: 'text/html; charset=utf8'
159 |
160 | The content-type of the response.
161 |
162 | ### autoDetect
163 |
164 | - Type: boolean
165 | - Default: true
166 |
167 | Whether to automatically detect HTML content and set the content-type.
168 |
169 | ### autoDoctype
170 |
171 | - Type: boolean | 'full'
172 | - Default: true
173 |
174 | Whether to automatically add <!doctype html>
to a response starting with <html>
, if not found.
175 |
176 | Use full
to also automatically add doctypes on responses returned without this plugin
177 |
178 | ts 179 | // without the plugin 180 | app.get('/', () => '<html></html>') 181 | 182 | // With the plugin 183 | app.get('/', ({ html }) => html('<html></html>')) 184 |
185 |
186 | ### isHtml
187 |
188 | - Type: (value: string) => boolean
189 | - Default: isHtml
(exported function)
190 |
191 | 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 >
.
192 |
193 | Keep in mind there's no real way to validate HTML, so the default implementation is a best guess.
194 |
1 | ---
2 | title: JWT Plugin - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: JWT Plugin - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Plugin for Elysia that adds support for using JWT (JSON Web Token) in Elysia server. Start by installing the plugin with "bun add @elysiajs/jwt".
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Plugin for Elysia that adds support for using JWT (JSON Web Token) in Elysia server. Start by installing the plugin with "bun add @elysiajs/jwt".
15 | ---
16 |
17 | # JWT Plugin
18 | This plugin adds support for using JWT in Elysia handler
19 |
20 | Install with:
21 | bash 22 | bun add @elysiajs/jwt 23 |
24 |
25 | Then use it:
26 | typescript 27 | import { Elysia } from 'elysia' 28 | import { jwt } from '@elysiajs/jwt' 29 | 30 | const app = new Elysia() 31 | .use( 32 | jwt({ 33 | name: 'jwt', 34 | secret: 'Fischl von Luftschloss Narfidort' 35 | }) 36 | ) 37 | .get('/sign/:name', async ({ jwt, cookie: { auth }, params }) => { 38 | auth.set({ 39 | value: await jwt.sign(params), 40 | httpOnly: true, 41 | maxAge: 7 * 86400, 42 | path: '/profile', 43 | }) 44 | 45 | return `Sign in as ${auth.value}` 46 | }) 47 | .get('/profile', async ({ jwt, set, cookie: { auth } }) => { 48 | const profile = await jwt.verify(auth.value) 49 | 50 | if (!profile) { 51 | set.status = 401 52 | return 'Unauthorized' 53 | } 54 | 55 | return `Hello ${profile.name}` 56 | }) 57 | .listen(3000) 58 |
59 |
60 | ## Config
61 | This plugin extends config from jose.
62 |
63 | Below is a config that is accepted by the plugin.
64 |
65 | ### name
66 | Name to register jwt
function as.
67 |
68 | For example, jwt
function will be registered with a custom name.
69 | typescript 70 | app 71 | .use( 72 | jwt({ 73 | name: 'myJWTNamespace', 74 | secret: process.env.JWT_SECRETS! 75 | }) 76 | ) 77 | .get('/sign/:name', ({ myJWTNamespace, params }) => { 78 | return myJWTNamespace.sign(params) 79 | }) 80 |
81 |
82 | 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.
83 |
84 | ### secret
85 | The private key to sign JWT payload with.
86 |
87 | ### schema
88 | Type strict validation for JWT payload.
89 |
90 | ---
91 | Below is a config that extends from cookie
92 |
93 | ### alg
94 | @default HS256
95 |
96 | Signing Algorithm to sign JWT payload with.
97 |
98 | Possible properties for jose are:
99 | HS256
100 | HS384
101 | HS512
102 | PS256
103 | PS384
104 | PS512
105 | RS256
106 | RS384
107 | RS512
108 | ES256
109 | ES256K
110 | ES384
111 | ES512
112 | EdDSA
113 |
114 | ### iss
115 | The issuer claim identifies the principal that issued the JWT as per RFC7519
116 |
117 | TLDR; is usually (the domain) name of the signer.
118 |
119 | ### sub
120 | The subject claim identifies the principal that is the subject of the JWT.
121 |
122 | The claims in a JWT are normally statements about the subject as per RFC7519
123 |
124 | ### aud
125 | The audience claim identifies the recipients that the JWT is intended for.
126 |
127 | Each principal intended to process the JWT MUST identify itself with a value in the audience claim as per RFC7519
128 |
129 | ### jti
130 | JWT ID claim provides a unique identifier for the JWT as per RFC7519
131 |
132 | ### nbf
133 | The "not before" claim identifies the time before which the JWT must not be accepted for processing as per RFC7519
134 |
135 | ### exp
136 | The expiration time claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing as per RFC7519
137 |
138 | ### iat
139 | The "issued at" claim identifies the time at which the JWT was issued.
140 |
141 | This claim can be used to determine the age of the JWT as per RFC7519
142 |
143 | ### b64
144 | This JWS Extension Header Parameter modifies the JWS Payload representation and the JWS Signing input computation as per RFC7797.
145 |
146 | ### kid
147 | A hint indicating which key was used to secure the JWS.
148 |
149 | This parameter allows originators to explicitly signal a change of key to recipients as per RFC7515
150 |
151 | ### x5t
152 | (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 corresponding to the key used to digitally sign the JWS as per RFC7515
153 |
154 | ### x5c
155 | (X.509 certificate chain) header parameter contains the X.509 public key certificate or certificate chain RFC5280 corresponding to the key used to digitally sign the JWS as per RFC7515
156 |
157 | ### x5u
158 | (X.509 URL) header parameter is a URI 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
159 |
160 | ### jwk
161 | 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.
162 |
163 | The keys MUST be encoded as a JWK Set [JWK] as per RFC7515
164 |
165 | ### typ
166 | The typ
(type) Header Parameter is used by JWS applications to declare the media type [IANA.MediaTypes] of this complete JWS.
167 |
168 | 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
169 |
170 | ### ctr
171 | Content-Type parameter is used by JWS applications to declare the media type [IANA.MediaTypes] of the secured content (the payload).
172 |
173 | 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
174 |
175 | ## Handler
176 | Below are the value added to the handler.
177 |
178 | ### jwt.sign
179 | A dynamic object of collection related to use with JWT registered by the JWT plugin.
180 |
181 | Type:
182 | typescript 183 | sign: (payload: JWTPayloadSpec): Promise<string> 184 |
185 |
186 | JWTPayloadSpec
accepts the same value as JWT config
187 |
188 | ### jwt.verify
189 | Verify payload with the provided JWT config
190 |
191 | Type:
192 | typescript 193 | verify(payload: string) => Promise<JWTPayloadSpec | false> 194 |
195 |
196 | JWTPayloadSpec
accepts the same value as JWT config
197 |
198 | ## Pattern
199 | Below you can find the common patterns to use the plugin.
200 |
201 | ## Set JWT expiration date
202 | By default, the config is passed to setCookie
and inherits its value.
203 |
204 | typescript 205 | const app = new Elysia() 206 | .use( 207 | jwt({ 208 | name: 'jwt', 209 | secret: 'kunikuzushi', 210 | exp: '7d' 211 | }) 212 | ) 213 | .get('/sign/:name', async ({ jwt, params }) => jwt.sign(params)) 214 |
215 |
216 | This will sign JWT with an expiration date of the next 7 days.
217 |
1 | ---
2 | title: OpenTelemetry Plugin - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: OpenTelemetry Plugin - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Plugin for Elysia that adds support for OpenTelemetry. Start by installing the plugin with "bun add @elysiajs/opentelemetry".
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Plugin for Elysia that adds support for OpenTelemetry. Start by installing the plugin with "bun add @elysiajs/opentelemetry".
15 | ---
16 |
17 | # OpenTelemetry
18 |
19 | To start using OpenTelemetry, install @elysiajs/opentelemetry
and apply plugin to any instance.
20 |
21 | typescript 22 | import { Elysia } from 'elysia' 23 | import { opentelemetry } from '@elysiajs/opentelemetry' 24 | 25 | import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node' 26 | import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' 27 | 28 | new Elysia() 29 | .use( 30 | opentelemetry({ 31 | spanProcessors: [ 32 | new BatchSpanProcessor( 33 | new OTLPTraceExporter() 34 | ) 35 | ] 36 | }) 37 | ) 38 |
39 |
40 |
41 |
42 | Elysia OpenTelemetry is will collect span of any library compatible OpenTelemetry standard, and will apply parent and child span automatically.
43 |
44 | ## Usage
45 | See opentelemetry for usage and utilities
46 |
47 | ## Config
48 | This plugin extends OpenTelemetry SDK parameters options.
49 |
50 | Below is a config which is accepted by the plugin
51 |
52 | ### autoDetectResources - boolean
53 | Detect resources automatically from the environment using the default resource detectors.
54 |
55 | default:
true
56 |
57 | ### contextManager - ContextManager
58 | Use a custom context manager.
59 |
60 | default: AsyncHooksContextManager
61 |
62 | ### textMapPropagator - TextMapPropagator
63 | Use a custom propagator.
64 |
65 | default: CompositePropagator
using W3C Trace Context and Baggage
66 |
67 | ### metricReader - MetricReader
68 | Add a MetricReader that will be passed to the MeterProvider.
69 |
70 | ### views - View[]
71 | A list of views to be passed to the MeterProvider.
72 |
73 | Accepts an array of View-instances. This parameter can be used to configure explicit bucket sizes of histogram metrics.
74 |
75 | ### instrumentations - (Instrumentation | Instrumentation[])[]
76 | Configure instrumentations.
77 |
78 | By default getNodeAutoInstrumentations
is enabled, if you want to enable them you can use either metapackage or configure each instrumentation individually.
79 |
80 | default: getNodeAutoInstrumentations()
81 |
82 | ### resource - IResource
83 | Configure a resource.
84 |
85 | Resources may also be detected by using the autoDetectResources method of the SDK.
86 |
87 | ### resourceDetectors - Array<Detector | DetectorSync>
88 | 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.
89 |
90 | 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:
91 |
92 | - env
93 | - host
94 | - os
95 | - process
96 | - serviceinstance (experimental)
97 | - all - enable all resource detectors above
98 | - none - disable resource detection
99 |
100 | For example, to enable only the env, host detectors:
101 |
102 | bash 103 | export OTEL_NODE_RESOURCE_DETECTORS="env,host" 104 |
105 |
106 | ### sampler - Sampler
107 | Configure a custom sampler. By default, all traces will be sampled.
108 |
109 | ### serviceName - string
110 | Namespace to be identify as.
111 |
112 | ### spanProcessors - SpanProcessor[]
113 | An array of span processors to register to the tracer provider.
114 |
115 | ### traceExporter - SpanExporter
116 | Configure a trace exporter. If an exporter is configured, it will be used with a BatchSpanProcessor
.
117 |
118 | 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.
119 |
120 | ### spanLimits - SpanLimits
121 | Configure tracing parameters. These are the same trace parameters used to configure a tracer.
122 |
1 | ---
2 | title: Server Timing Plugin - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Server Timing Plugin - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Plugin for Elysia for performance audit via Server Timing API. Start by installing the plugin with "bun add @elysiajs/server-timing".
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Plugin for Elysia for performance audit via Server Timing API. Start by installing the plugin with "bun add @elysiajs/server-timing".
15 | ---
16 |
17 | # Server Timing Plugin
18 | This plugin adds support for auditing performance bottlenecks with Server Timing API
19 |
20 | Install with:
21 | bash 22 | bun add @elysiajs/server-timing 23 |
24 |
25 | Then use it:
26 | typescript 27 | import { Elysia } from 'elysia' 28 | import { serverTiming } from '@elysiajs/server-timing' 29 | 30 | new Elysia() 31 | .use(serverTiming()) 32 | .get('/', () => 'hello') 33 | .listen(3000) 34 |
35 |
36 | Server Timing then will append header 'Server-Timing' with log duration, function name, and detail for each life-cycle function.
37 |
38 | To inspect, open browser developer tools > Network > [Request made through Elysia server] > Timing.
39 |
40 |
41 |
42 | Now you can effortlessly audit the performance bottleneck of your server.
43 |
44 | ## Config
45 | Below is a config which is accepted by the plugin
46 |
47 | ### enabled
48 | @default
NODE_ENV !== 'production'
49 |
50 | Determine whether or not Server Timing should be enabled
51 |
52 | ### allow
53 | @default undefined
54 |
55 | A condition whether server timing should be log
56 |
57 | ### trace
58 | @default undefined
59 |
60 | Allow Server Timing to log specified life-cycle events:
61 |
62 | Trace accepts objects of the following:
63 | - request: capture duration from request
64 | - parse: capture duration from parse
65 | - transform: capture duration from transform
66 | - beforeHandle: capture duration from beforeHandle
67 | - handle: capture duration from the handle
68 | - afterHandle: capture duration from afterHandle
69 | - total: capture total duration from start to finish
70 |
71 | ## Pattern
72 | Below you can find the common patterns to use the plugin.
73 |
74 | - Allow Condition
75 |
76 | ## Allow Condition
77 | You may disable Server Timing on specific routes via allow
property
78 |
79 | ts 80 | import { Elysia } from 'elysia' 81 | import { serverTiming } from '@elysiajs/server-timing' 82 | 83 | new Elysia() 84 | .use( 85 | serverTiming({ 86 | allow: ({ request }) => { 87 | return new URL(request.url).pathname !== '/no-trace' 88 | } 89 | }) 90 | ) 91 |
92 |
1 | ---
2 | title: Static Plugin - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Static Plugin - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Plugin for Elysia that adds support for serving static files/folders for Elysia Server. Start by installing the plugin with "bun add @elysiajs/static".
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Plugin for Elysia that adds support for serving static files/folders for Elysia Server. Start by installing the plugin with "bun add @elysiajs/static".
15 | ---
16 |
17 | # Static Plugin
18 | This plugin can serve static files/folders for Elysia Server
19 |
20 | Install with:
21 | bash 22 | bun add @elysiajs/static 23 |
24 |
25 | Then use it:
26 | typescript 27 | import { Elysia } from 'elysia' 28 | import { staticPlugin } from '@elysiajs/static' 29 | 30 | new Elysia() 31 | .use(staticPlugin()) 32 | .listen(3000) 33 |
34 |
35 | By default, the static plugin default folder is public
, and registered with /public
prefix.
36 |
37 | Suppose your project structure is:
38 | 39 | | - src 40 | | - index.ts 41 | | - public 42 | | - takodachi.png 43 | | - nested 44 | | - takodachi.png 45 |
46 |
47 | The available path will become:
48 | - /public/takodachi.png
49 | - /public/nested/takodachi.png
50 |
51 | ## Config
52 | Below is a config which is accepted by the plugin
53 |
54 | ### assets
55 | @default "public"
56 |
57 | Path to the folder to expose as static
58 |
59 | ### prefix
60 | @default "/public"
61 |
62 | Path prefix to register public files
63 |
64 | ### ignorePatterns
65 | @default []
66 |
67 | List of files to ignore from serving as static files
68 |
69 | ### staticLimit
70 | @default 1024
71 |
72 | 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.
73 | Tradeoff memory with performance.
74 |
75 | ### alwaysStatic
76 | @default false
77 |
78 | If set to true, static files path will be registered to Router skipping the staticLimits
.
79 |
80 | ### headers
81 | @default {}
82 |
83 | Set response headers of files
84 |
85 | ### indexHTML
86 | @default false
87 |
88 | 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.
89 |
90 | ## Pattern
91 | Below you can find the common patterns to use the plugin.
92 |
93 | - Single File
94 |
95 | ## Single file
96 | Suppose you want to return just a single file, you can use file
instead of using the static plugin
97 | typescript 98 | import { Elysia, file } from 'elysia' 99 | 100 | new Elysia() 101 | .get('/file', () => file('public/takodachi.png')) 102 |
103 |
1 | ---
2 | title: Stream Plugin - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Stream Plugin - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Plugin for Elysia that adds support for streaming response and Server-Sent Events, eg. OpenAI integration. Start by installing the plugin with "bun add @elysiajs/stream".
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Plugin for Elysia that adds support for streaming response and Server-Sent Events, eg. OpenAI integration. Start by installing the plugin with "bun add @elysiajs/stream".
15 | ---
16 |
17 | # Stream Plugin
18 |
19 | ::: warning
20 | This plugin is in maintenance mode and will not receive new features. We recommend using the Generator Stream instead
21 | :::
22 |
23 | This plugin adds support for streaming response or sending Server-Sent Event back to the client.
24 |
25 | Install with:
26 | bash 27 | bun add @elysiajs/stream 28 |
29 |
30 | Then use it:
31 | typescript 32 | import { Elysia } from 'elysia' 33 | import { Stream } from '@elysiajs/stream' 34 | 35 | new Elysia() 36 | .get('/', () => new Stream(async (stream) => { 37 | stream.send('hello') 38 | 39 | await stream.wait(1000) 40 | stream.send('world') 41 | 42 | stream.close() 43 | })) 44 | .listen(3000) 45 |
46 |
47 | By default, Stream
will return Response
with content-type
of text/event-stream; charset=utf8
.
48 |
49 | ## Constructor
50 | Below is the constructor parameter accepted by Stream
:
51 | 1. Stream:
52 | - Automatic: Automatically stream response from a provided value
53 | - Iterable
54 | - AsyncIterable
55 | - ReadableStream
56 | - Response
57 | - Manual: Callback of (stream: this) => unknown
or undefined
58 | 2. Options: StreamOptions
59 | - event: A string identifying the type of event described
60 | - retry: The reconnection time in milliseconds
61 |
62 | ## Method
63 | Below is the method provided by Stream
:
64 |
65 | ### send
66 | Enqueue data to stream to send back to the client
67 |
68 | ### close
69 | Close the stream
70 |
71 | ### wait
72 | Return a promise that resolves in the provided value in ms
73 |
74 | ### value
75 | Inner value of the ReadableStream
76 |
77 | ## Pattern
78 | Below you can find the common patterns to use the plugin.
79 | - OpenAI
80 | - Fetch Stream
81 | - Server Sent Event
82 |
83 | ## OpenAI
84 | Automatic mode is triggered when the parameter is either Iterable
or AsyncIterable
streaming the response back to the client automatically.
85 |
86 | Below is an example of integrating ChatGPT into Elysia.
87 |
88 | ts 89 | new Elysia() 90 | .get( 91 | '/ai', 92 | ({ query: { prompt } }) => 93 | new Stream( 94 | openai.chat.completions.create({ 95 | model: 'gpt-3.5-turbo', 96 | stream: true, 97 | messages: [{ 98 | role: 'user', 99 | content: prompt 100 | }] 101 | }) 102 | ) 103 | ) 104 |
105 |
106 | By default openai chatGPT completion returns AsyncIterable
so you should be able to wrap the OpenAI in Stream
.
107 |
108 | ## Fetch Stream
109 | You can pass a fetch from an endpoint that returns the stream to proxy a stream.
110 |
111 | This is useful for those endpoints that use AI text generation since you can proxy it directly, eg. Cloudflare AI.
112 | ts 113 | const model = '@cf/meta/llama-2-7b-chat-int8' 114 | const endpoint = `https://api.cloudflare.com/client/v4/accounts/${process.env.ACCOUNT_ID}/ai/run/${model}` 115 | 116 | new Elysia() 117 | .get('/ai', ({ query: { prompt } }) => 118 | fetch(endpoint, { 119 | method: 'POST', 120 | headers: { 121 | authorization: `Bearer ${API_TOKEN}`, 122 | 'content-type': 'application/json' 123 | }, 124 | body: JSON.stringify({ 125 | messages: [ 126 | { role: 'system', content: 'You are a friendly assistant' }, 127 | { role: 'user', content: prompt } 128 | ] 129 | }) 130 | }) 131 | ) 132 |
133 |
134 | ## Server Sent Event
135 | Manual mode is triggered when the parameter is either callback
or undefined
, allowing you to control the stream.
136 |
137 | ### callback-based
138 | Below is an example of creating a Server-Sent Event endpoint using a constructor callback
139 |
140 | ts 141 | new Elysia() 142 | .get('/source', () => 143 | new Stream((stream) => { 144 | const interval = setInterval(() => { 145 | stream.send('hello world') 146 | }, 500) 147 | 148 | setTimeout(() => { 149 | clearInterval(interval) 150 | stream.close() 151 | }, 3000) 152 | }) 153 | ) 154 |
155 |
156 | ### value-based
157 | Below is an example of creating a Server-Sent Event endpoint using a value-based
158 |
159 | ts 160 | new Elysia() 161 | .get('/source', () => { 162 | const stream = new Stream() 163 | 164 | const interval = setInterval(() => { 165 | stream.send('hello world') 166 | }, 500) 167 | 168 | setTimeout(() => { 169 | clearInterval(interval) 170 | stream.close() 171 | }, 3000) 172 | 173 | return stream 174 | }) 175 |
176 |
177 | Both callback-based and value-based streams work in the same way but with different syntax for your preference.
178 |
1 | ---
2 | title: Swagger Plugin - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Swagger Plugin - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Plugin for Elysia that adds support for generating Swagger API documentation for Elysia Server. Start by installing the plugin with "bun add @elysiajs/swagger".
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Plugin for Elysia that adds support for generating Swagger API documentation for Elysia Server. Start by installing the plugin with "bun add @elysiajs/swagger".
15 | ---
16 |
17 | # Swagger Plugin
18 |
19 | This plugin generates a Swagger endpoint for an Elysia server
20 |
21 | Install with:
22 |
23 | bash 24 | bun add @elysiajs/swagger 25 |
26 |
27 | Then use it:
28 |
29 | typescript 30 | import { Elysia } from 'elysia' 31 | import { swagger } from '@elysiajs/swagger' 32 | 33 | new Elysia() 34 | .use(swagger()) 35 | .get('/', () => 'hi') 36 | .post('/hello', () => 'world') 37 | .listen(3000) 38 |
39 |
40 | 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
.
41 |
42 | ## Config
43 |
44 | Below is a config which is accepted by the plugin
45 |
46 | ### provider
47 |
48 | @default scalar
49 |
50 | UI Provider for documentation. Default to Scalar.
51 |
52 | ### scalar
53 |
54 | Configuration for customizing Scalar.
55 |
56 | Please refer to the Scalar config
57 |
58 | ### swagger
59 |
60 | Configuration for customizing Swagger.
61 |
62 | Please refer to the Swagger specification.
63 |
64 | ### excludeStaticFile
65 |
66 | @default true
67 |
68 | Determine if Swagger should exclude static files.
69 |
70 | ### path
71 |
72 | @default /swagger
73 |
74 | Endpoint to expose Swagger
75 |
76 | ### exclude
77 |
78 | Paths to exclude from Swagger documentation.
79 |
80 | Value can be one of the following:
81 |
82 | - string
83 | - RegExp
84 | - Array<string | RegExp>
85 |
86 | ## Pattern
87 |
88 | Below you can find the common patterns to use the plugin.
89 |
90 | ## Change Swagger Endpoint
91 |
92 | You can change the swagger endpoint by setting path in the plugin config.
93 |
94 | typescript 95 | import { Elysia } from 'elysia' 96 | import { swagger } from '@elysiajs/swagger' 97 | 98 | new Elysia() 99 | .use( 100 | swagger({ 101 | path: '/v2/swagger' 102 | }) 103 | ) 104 | .listen(3000) 105 |
106 |
107 | ## Customize Swagger info
108 |
109 | typescript 110 | import { Elysia } from 'elysia' 111 | import { swagger } from '@elysiajs/swagger' 112 | 113 | new Elysia() 114 | .use( 115 | swagger({ 116 | documentation: { 117 | info: { 118 | title: 'Elysia Documentation', 119 | version: '1.0.0' 120 | } 121 | } 122 | }) 123 | ) 124 | .listen(3000) 125 |
126 |
127 | ## Using Tags
128 |
129 | Elysia can separate the endpoints into groups by using the Swaggers tag system
130 |
131 | Firstly define the available tags in the swagger config object
132 |
133 | typescript 134 | app.use( 135 | swagger({ 136 | documentation: { 137 | tags: [ 138 | { name: 'App', description: 'General endpoints' }, 139 | { name: 'Auth', description: 'Authentication endpoints' } 140 | ] 141 | } 142 | }) 143 | ) 144 |
145 |
146 | Then use the details property of the endpoint configuration section to assign that endpoint to the group
147 |
148 | typescript 149 | app.get('/', () => 'Hello Elysia', { 150 | detail: { 151 | tags: ['App'] 152 | } 153 | }) 154 | 155 | app.group('/auth', (app) => 156 | app.post( 157 | '/sign-up', 158 | async ({ body }) => 159 | db.user.create({ 160 | data: body, 161 | select: { 162 | id: true, 163 | username: true 164 | } 165 | }), 166 | { 167 | detail: { 168 | tags: ['Auth'] 169 | } 170 | } 171 | ) 172 | ) 173 |
174 |
175 | Which will produce a swagger page like the following
176 |
177 |
178 | ## Security Configuration
179 |
180 | 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:
181 |
182 |
typescript 183 | app.use( 184 | swagger({ 185 | documentation: { 186 | components: { 187 | securitySchemes: { 188 | bearerAuth: { 189 | type: 'http', 190 | scheme: 'bearer', 191 | bearerFormat: 'JWT' 192 | } 193 | } 194 | } 195 | } 196 | }) 197 | ) 198 | 199 | export const addressController = new Elysia({ 200 | prefix: '/address', 201 | detail: { 202 | tags: ['Address'], 203 | security: [ 204 | { 205 | bearerAuth: [] 206 | } 207 | ] 208 | } 209 | }) 210 |
211 |
212 | This configuration ensures that all endpoints under the /address
prefix require a valid JWT token for access.
213 |
1 | ---
2 | title: tRPC Plugin - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: tRPC Plugin - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Plugin for Elysia that adds support for using tRPC on Bun with Elysia Server. Start by installing the plugin with "bun add @elysiajs/trpc".
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Plugin for Elysia that adds support for using tRPC on Bun with Elysia Server. Start by installing the plugin with "bun add @elysiajs/trpc".
15 | ---
16 |
17 | # tRPC Plugin
18 | This plugin adds support for using tRPC
19 |
20 | Install with:
21 | bash 22 | bun add @elysiajs/trpc @trpc/server @elysiajs/websocket 23 |
24 |
25 | Then use it:
26 | typescript 27 | import { compile as c, trpc } from "@elysiajs/trpc"; 28 | import { initTRPC } from "@trpc/server"; 29 | import { Elysia, t as T } from "elysia"; 30 | 31 | const t = initTRPC.create(); 32 | const p = t.procedure; 33 | 34 | const router = t.router({ 35 | greet: p 36 | 37 | // 💡 Using Zod 38 | //.input(z.string()) 39 | // 💡 Using Elysia's T 40 | .input(c(T.String())) 41 | .query(({ input }) => input), 42 | }); 43 | 44 | export type Router = typeof router; 45 | 46 | const app = new Elysia().use(trpc(router)).listen(3000); 47 |
48 |
49 | ## trpc
50 | Accept the tRPC router and register to Elysia's handler.
51 |
52 | type:
53 | 54 | trpc(router: Router, option?: { 55 | endpoint?: string 56 | }): this 57 |
58 |
59 | Router
is the TRPC Router instance.
60 |
61 | ### endpoint
62 | The path to the exposed TRPC endpoint.
63 |
1 | ---
2 | title: Quick Start - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Quick Start - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Elysia is a library built for Bun and the only prerequisite. To start, bootstrap a new project with "bun create elysia hi-elysia" and start the development server with "bun dev". This is all it needs to do a quick start or get started with ElysiaJS.
11 |
12 | - - meta
13 | - property: 'og:description'
14 | content: Elysia is a library built for Bun and the only prerequisite. To start, bootstrap a new project with "bun create elysia hi-elysia" and start the development server with "bun dev". This is all it needs to do a quick start or get started with ElysiaJS.
15 | ---
16 |
17 | <script setup>
18 | import Card from '../components/nearl/card.vue'
19 | import Deck from '../components/nearl/card-deck.vue'
20 | import Tab from '../components/fern/tab.vue'
21 | </script>
22 |
23 | # Quick Start
24 |
25 | Elysia is a TypeScript backend framework with multiple runtime support but optimized for Bun.
26 |
27 | However, you can use Elysia with other runtimes like Node.js.
28 |
29 | <Tab
30 | id="quickstart"
31 | :names="['Bun', 'Node.js', 'Web Standard']"
32 | :tabs="['bun', 'node', 'web-standard']"
33 | >
34 |
35 |
36 |
37 | Elysia is optimized for Bun which is a JavaScript runtime that aims to be a drop-in replacement for Node.js.
38 |
39 | You can install Bun with the command below:
40 |
41 | ::: code-group
42 |
43 | bash [MacOS/Linux] 44 | curl -fsSL https://bun.sh/install | bash 45 |
46 |
47 | bash [Windows] 48 | powershell -c "irm bun.sh/install.ps1 | iex" 49 |
50 |
51 | :::
52 |
53 | <Tab
54 | id="quickstart"
55 | :names="['Auto Installation', 'Manual Installation']"
56 | :tabs="['auto', 'manual']"
57 | >
58 |
59 |
60 |
61 | We recommend starting a new Elysia server using bun create elysia
, which sets up everything automatically.
62 |
63 | bash 64 | bun create elysia app 65 |
66 |
67 | Once done, you should see the folder name app
in your directory.
68 |
69 | bash 70 | cd app 71 |
72 |
73 | Start a development server by:
74 |
75 | bash 76 | bun dev 77 |
78 |
79 | Navigate to localhost:3000 should greet you with "Hello Elysia".
80 |
81 | ::: tip
82 | Elysia ships you with dev
command to automatically reload your server on file change.
83 | :::
84 |
85 |
86 |
87 |
88 |
89 | To manually create a new Elysia app, install Elysia as a package:
90 |
91 | typescript 92 | bun add elysia 93 | bun add -d @types/bun 94 |
95 |
96 | This will install Elysia and Bun type definitions.
97 |
98 | Create a new file src/index.ts
and add the following code:
99 |
100 | typescript 101 | import { Elysia } from 'elysia' 102 | 103 | const app = new Elysia() 104 | .get('/', () => 'Hello Elysia') 105 | .listen(3000) 106 | 107 | console.log( 108 | `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` 109 | ) 110 |
111 |
112 | Open your package.json
file and add the following scripts:
113 |
114 | json 115 | { 116 | "scripts": { 117 | "dev": "bun --watch src/index.ts", 118 | "build": "bun build src/index.ts --target bun --outdir ./dist", 119 | "start": "NODE_ENV=production bun dist/index.js", 120 | "test": "bun test" 121 | } 122 | } 123 |
124 |
125 | These scripts refer to the different stages of developing an application:
126 |
127 | - dev - Start Elysia in development mode with auto-reload on code change.
128 | - build - Build the application for production usage.
129 | - start - Start an Elysia production server.
130 |
131 | If you are using TypeScript, make sure to create, and update tsconfig.json
to include compilerOptions.strict
to true
:
132 |
133 | json 134 | { 135 | "compilerOptions": { 136 | "strict": true 137 | } 138 | } 139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 | Node.js is a JavaScript runtime for server-side applications, the most popular runtime for JavaScript which Elysia supports.
149 |
150 | You can install Bun with the command below:
151 |
152 | ::: code-group
153 |
154 | bash [MacOS] 155 | brew install node 156 |
157 |
158 | bash [Windows] 159 | choco install nodejs 160 |
161 |
162 | bash [apt (Linux)] 163 | sudo apt install nodejs 164 |
165 |
166 | bash [pacman (Arch)] 167 | pacman -S nodejs npm 168 |
169 |
170 | :::
171 |
172 | ## Setup
173 |
174 | We recommended using TypeScript for your Node.js project.
175 |
176 | <Tab
177 | id="language"
178 | :names="['TypeScript', 'JavaScript']"
179 | :tabs="['ts', 'js']"
180 | >
181 |
182 |
183 |
184 | To create a new Elysia app with TypeScript, we recommended install Elysia with tsx
:
185 |
186 | ::: code-group
187 |
188 | bash [pnpm] 189 | pnpm add elysia @elysiajs/node && \ 190 | pnpm add -d tsx @types/node typescript 191 |
192 |
193 | bash [npm] 194 | npm install elysia @elysiajs/node && \ 195 | npm install --save-dev tsx @types/node typescript 196 |
197 |
198 | bash [yarn] 199 | yarn add elysia && \ 200 | yarn add -d tsx @types/node typescript 201 |
202 |
203 | :::
204 |
205 | This will install Elysia, TypeScript, and tsx
.
206 |
207 | tsx
is a CLI that transpiles TypeScript to JavaScript with hot-reload and several more feature you expected from a modern development environment.
208 |
209 | Create a new file src/index.ts
and add the following code:
210 |
211 | typescript 212 | import { Elysia } from 'elysia' 213 | import { node } from '@elysiajs/node' 214 | 215 | const app = new Elysia({ adapter: node() }) 216 | .get('/', () => 'Hello Elysia') 217 | .listen(3000, ({ hostname, port }) => { 218 | console.log( 219 | `🦊 Elysia is running at ${hostname}:${port}` 220 | ) 221 | }) 222 |
223 |
224 | Open your package.json
file and add the following scripts:
225 |
226 | json 227 | { 228 | "scripts": { 229 | "dev": "tsx watch src/index.ts", 230 | "build": "tsc src/index.ts --outDir dist", 231 | "start": "NODE_ENV=production node dist/index.js" 232 | } 233 | } 234 |
235 |
236 | These scripts refer to the different stages of developing an application:
237 |
238 | - dev - Start Elysia in development mode with auto-reload on code change.
239 | - build - Build the application for production usage.
240 | - start - Start an Elysia production server.
241 |
242 | Make sure to create tsconfig.json
243 |
244 | bash 245 | npx tsc init 246 |
247 |
248 | Don't forget to update tsconfig.json
to include compilerOptions.strict
to true
:
249 | json 250 | { 251 | "compilerOptions": { 252 | "strict": true 253 | } 254 | } 255 |
256 |
257 |
258 |
259 |
260 |
261 | ::: warning
262 | Using Elysia with TypeScript will miss out on some features auto-completion, advanced type checking and end-to-end type safety which is the core feature of Elysia.
263 | :::
264 |
265 | To create a new Elysia app with JavaScript, starts by installing Elysia:
266 |
267 | ::: code-group
268 |
269 | bash [pnpm] 270 | pnpm add elysia @elysiajs/node 271 |
272 |
273 | bash [npm] 274 | npm install elysia @elysiajs/node 275 |
276 |
277 | bash [yarn] 278 | yarn add elysia @elysiajs/node 279 |
280 |
281 | :::
282 |
283 | This will install Elysia, TypeScript, and tsx
.
284 |
285 | tsx
is a CLI that transpiles TypeScript to JavaScript with hot-reload and several more feature you expected from a modern development environment.
286 |
287 | Create a new file src/index.ts
and add the following code:
288 |
289 | javascript 290 | import { Elysia } from 'elysia' 291 | import { node } from '@elysiajs/node' 292 | 293 | const app = new Elysia({ adapter: node() }) 294 | .get('/', () => 'Hello Elysia') 295 | .listen(3000, ({ hostname, port }) => { 296 | console.log( 297 | `🦊 Elysia is running at ${hostname}:${port}` 298 | ) 299 | }) 300 |
301 |
302 | Open your package.json
file and add the following scripts:
303 |
304 | json 305 | { 306 | "type", "module", 307 | "scripts": { 308 | "dev": "node src/index.ts", 309 | "start": "NODE_ENV=production node src/index.js" 310 | } 311 | } 312 |
313 |
314 | These scripts refer to the different stages of developing an application:
315 |
316 | - dev - Start Elysia in development mode with auto-reload on code change.
317 | - start - Start an Elysia production server.
318 |
319 | Make sure to create tsconfig.json
320 |
321 | bash 322 | npx tsc init 323 |
324 |
325 | Don't forget to update tsconfig.json
to include compilerOptions.strict
to true
:
326 | json 327 | { 328 | "compilerOptions": { 329 | "strict": true 330 | } 331 | } 332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 | Elysia is a WinterCG compliance library, which means if a framework or runtime supports Web Standard Request/Response, it can run Elysia.
343 |
344 | First, install Elysia with the command below:
345 |
346 | ::: code-group
347 |
348 | bash [bun] 349 | bun install elysia 350 |
351 |
352 | bash [pnpm] 353 | pnpm install elysia 354 |
355 |
356 | bash [npm] 357 | npm install elysia 358 |
359 |
360 | bash [yarn] 361 | yarn add elysia 362 |
363 |
364 | :::
365 |
366 | Next, select a runtime that supports Web Standard Request/Response.
367 |
368 | We have a few recommendations:
369 |
370 |
371 |
372 | Elysia as Next.js API routes.
373 |
374 |
375 | Elysia as Expo App Router API.
376 |
377 |
378 | Elysia as Astro API routes.
379 |
380 |
381 | Elysia as SvelteKit API routes.
382 |
383 |
384 |
385 | ### Not in the list?
386 | If you are using a custom runtime, you may access app.fetch
to handle the request and response manually.
387 |
388 | typescript 389 | import { Elysia } from 'elysia' 390 | 391 | const app = new Elysia() 392 | .get('/', () => 'Hello Elysia') 393 | .listen(3000) 394 | 395 | export default app.fetch 396 | 397 | console.log( 398 | `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` 399 | ) 400 |
401 |
402 |
403 |
404 |
405 |
406 | ## Next Steps
407 |
408 | We recommend checking out the either one of the following:
409 |
410 |
411 |
412 | The core concept of Elysia and how to use it.
413 |
414 |
415 | A step-by-step guide walkthrough Elysia's features.
416 |
417 |
418 |
419 | If you have any questions, feel free to ask in our Discord community.
420 |
1 | ---
2 | title: Better Auth - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: Better Auth - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: We may use @better-auth/cli to generate auth schema and migrate our database as well.
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: We may use @better-auth/cli to generate auth schema and migrate our database as well.
15 | ---
16 |
17 | # Better Auth
18 | 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.
19 |
20 | Better Auth has a cli tool to generate auth schema and migrate our database as well. It currently has 3 database adapters:
21 |
22 | - Prisma
23 | - Drizzle
24 | - Mongoose
25 |
26 | ## Better Auth CLI
27 | Better Auth has a cli tool to generate auth schema with the following core tables in our database: user
, session
, account
, and verification
. More information about the core schema can be found in Better Auth Core Schema.
28 |
29 | To read more on configuring your database, please refer to Better Auth Database.
30 |
31 | To read more on how to use the cli, please refer to Better Auth CLI.
32 |
33 | ## Installation
34 | To install Better Auth, run the following command:
35 |
36 | bash 37 | bun add better-auth 38 |
39 |
40 | Make sure to set your environment variables for better auth secret BETTER_AUTH_SECRET=
and other enviroment variables such as Github and Google client id and secret.
41 |
42 | In your project inside the src
folder, create a libs/auth
or utils/auth
folder, and create a auth.ts
file inside it and copy the following code:
43 |
44 | ## Better Auth Instance
45 |
46 | ts 47 | import { betterAuth } from "better-auth"; 48 | import { drizzleAdapter } from "better-auth/adapters/drizzle"; 49 | import db from "../../database"; 50 | import { account, session, user, verification } from "../../database/schema"; 51 | export const auth = betterAuth({ 52 | database: drizzleAdapter(db, { // We're using Drizzle as our database 53 | provider: "pg", 54 | /* 55 | * Map your schema into a better-auth schema 56 | */ 57 | schema: { 58 | user, 59 | session, 60 | verification, 61 | account, 62 | }, 63 | }), 64 | socialProviders: { 65 | /* 66 | * We're using Google and Github as our social provider, 67 | * make sure you have set your environment variables 68 | */ 69 | emailAndPassword: { 70 | enabled: true // If you want to use email and password auth 71 | }, 72 | github: { 73 | clientId: process.env.GITHUB_CLIENT_ID!, 74 | clientSecret: process.env.GITHUB_CLIENT_SECRET!, 75 | }, 76 | google: { 77 | clientId: process.env.GOOGLE_CLIENT_ID!, 78 | clientSecret: process.env.GOOGLE_CLIENT_SECRET!, 79 | }, 80 | }, 81 | }); 82 | 83 |
84 |
85 | Now just run to generate an auth schema with the necessary tables.
86 | bash 87 | bunx @better-auth/cli generate --config ./src/libs/auth/auth.ts 88 |
89 | Additionally you can use the --output
option to specify the output directory for the generated files. We can then use the drizzle migrate command to migrate our database drizzle-kit migrate
.
90 |
91 | ## Better Auth View
92 |
93 | We need to setup a view to handle contexts for better auth. Create a file inside src/utils/auth-view.ts
or src/libs/auth/auth-view.ts
and copy the following code:
94 |
95 | ts 96 | import { Context } from "elysia"; 97 | import { auth } from "./auth"; 98 | 99 | const betterAuthView = (context: Context) => { 100 | const BETTER_AUTH_ACCEPT_METHODS = ["POST", "GET"] 101 | if(BETTER_AUTH_ACCEPT_METHODS.includes(context.request.method)) { 102 | console.log(context.request) 103 | return auth.handler(context.request); 104 | } 105 | else { 106 | context.error(405) 107 | } 108 | } 109 | 110 | export default betterAuthView; 111 |
112 |
113 | ## Better Auth Middleware
114 |
115 | We can setup a simple middleware to handle better auth. Create a file inside src/middlewares/auth-middleware.ts
and copy the following code:
116 |
117 | ts 118 | import { Session, User } from "better-auth/types"; 119 | import { auth } from "../../utils/auth/auth"; 120 | import { Context } from "elysia"; 121 | 122 | export const userMiddleware = async (c: Context) => { 123 | const session = await auth.api.getSession({ headers: c.request.headers }); 124 | 125 | if (!session) { 126 | c.set.status = 401; 127 | return { success: 'error', message: "Unauthorized Access: Token is missing" }; 128 | } 129 | 130 | return { 131 | user: session.user, 132 | session: session.session 133 | } 134 | } 135 | 136 | export const userInfo = (user: User | null, session: Session | null) => { 137 | return { 138 | user: user, 139 | session: session 140 | } 141 | } 142 |
143 |
144 | ## Attaching Better Auth Into Our Elysia App
145 |
146 | Inside our index.ts file, we can attach the auth view so that it listens to our auth routes and add the following code:
147 |
148 | ts 149 | const app = new Elysia() 150 | .use(cors()).use(swagger()).all("/api/auth/*", betterAuthView); 151 | 152 | app.listen(process.env.BACKEND_PORT || 8000); 153 | 154 | console.log( 155 | `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` 156 | ); 157 |
158 |
159 | Our Auth Should now be working as expected! We can then just access our auth routes from our frontend as such:
160 |
161 | ts 162 | import { createAuthClient } from "better-auth/client" 163 | export const authClient = createAuthClient({ 164 | baseURL: process.env.BETTER_AUTH_URL! 165 | }) 166 | 167 | export const signinGoogle = async () => { 168 | const data = await authClient.signIn.social({ 169 | provider: "google", 170 | }); 171 | 172 | return data; 173 | }; 174 |
175 |
176 | For a detailed client side guide do check out Better Auth Frontend
1 | ---
2 | title: OpenAPI - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: OpenAPI - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Elysia has first-class support and follows OpenAPI schema by default. Allowing any Elysia server to generate a Swagger page and serve as documentation automatically by using just 1 line of the Elysia Swagger plugin.
11 |
12 | - - meta
13 | - property: 'og:description'
14 | content: Elysia has first-class support and follows OpenAPI schema by default. Allowing any Elysia server to generate a Swagger page and serve as documentation automatically by using just 1 line of the Elysia Swagger plugin.
15 | ---
16 |
17 | # OpenAPI
18 | Elysia has first-class support and follows OpenAPI schema by default.
19 |
20 | Elysia can automatically generate an API documentation page by providing a Swagger plugin.
21 |
22 | To generate the Swagger page, install the plugin:
23 | bash 24 | bun add @elysiajs/swagger 25 |
26 |
27 | And register the plugin to the server:
28 | typescript 29 | import { Elysia } from 'elysia' 30 | import { swagger } from '@elysiajs/swagger' 31 | 32 | const app = new Elysia() 33 | .use(swagger()) 34 |
35 |
36 | By default, Elysia use OpenAPI V3 schema and Scalar UI by default
37 |
38 | For Swagger plugin configuration, see the Swagger plugin page.
39 |
40 | ## Route definitions
41 | We add route information by providing a schema type.
42 |
43 | 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.
44 |
45 | typescript 46 | import { Elysia, t } from 'elysia' 47 | import { swagger } from '@elysiajs/swagger' 48 | 49 | new Elysia() 50 | .use(swagger()) 51 | .post('/sign-in', ({ body }) => body, { 52 | body: t.Object( 53 | { 54 | username: t.String(), 55 | password: t.String({ 56 | minLength: 8, 57 | description: 'User password (at least 8 characters)' // [!code ++] 58 | }) 59 | }, 60 | { // [!code ++] 61 | description: 'Expected an username and password' // [!code ++] 62 | } // [!code ++] 63 | ), 64 | detail: { // [!code ++] 65 | summary: 'Sign in the user', // [!code ++] 66 | tags: ['authentication'] // [!code ++] 67 | } // [!code ++] 68 | }) 69 |
70 |
71 | The detail fields follows an OpenAPI V3 definition with auto-completion and type-safety by default.
72 |
73 | Detail is then passed to Swagger to put the description to Swagger route.
74 |
75 | ### detail
76 | detail
extends the OpenAPI Operation Object
77 |
78 | The detail field is an object that can be use to describe information about the route for API documentation.
79 |
80 | Which may contains the following fields:
81 |
82 | ### tags
83 | An array of tags for the operation. Tags can be used for logical grouping of operations by resources or any other qualifier.
84 |
85 | ### summary
86 | A short summary of what the operation does.
87 |
88 | ### description
89 | A verbose explanation of the operation behavior.
90 |
91 | ### externalDocs
92 | Additional external documentation for this operation.
93 |
94 | ### operationId
95 | 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.
96 |
97 | ### deprecated
98 | Declares this operation to be deprecated. Consumers SHOULD refrain from usage of the declared operation. Default value is false
.
99 |
100 | ### security
101 | 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.
102 |
103 | ## Hide
104 | You can hide the route from the Swagger page by setting detail.hide
to true
105 |
106 | typescript 107 | import { Elysia, t } from 'elysia' 108 | import { swagger } from '@elysiajs/swagger' 109 | 110 | new Elysia() 111 | .use(swagger()) 112 | .post('/sign-in', ({ body }) => body, { 113 | body: t.Object( 114 | { 115 | username: t.String(), 116 | password: t.String() 117 | }, 118 | { 119 | description: 'Expected an username and password' 120 | } 121 | ), 122 | detail: { // [!code ++] 123 | hide: true // [!code ++] 124 | } // [!code ++] 125 | }) 126 |
127 |
128 | ## Tags group
129 | Elysia may accept tags to add an entire instance or group of routes to a specific tag.
130 |
131 | typescript 132 | import { Elysia, t } from 'elysia' 133 | 134 | new Elysia({ 135 | tags: ['user'] 136 | }) 137 | .get('/user', 'user') 138 | .get('/admin', 'admin') 139 |
140 |
141 | ## Guard
142 | Alternatively, Elysia may accept guards to add an entire instance or group of routes to a specific guard.
143 |
144 | typescript 145 | import { Elysia, t } from 'elysia' 146 | 147 | new Elysia() 148 | .guard({ 149 | detail: { 150 | description: 'Require user to be logged in' 151 | } 152 | }) 153 | .get('/user', 'user') 154 | .get('/admin', 'admin') 155 |
156 |
1 | ---
2 | title: OpenTelemetry Plugin - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: OpenTelemetry Plugin - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: Plugin for Elysia that adds support for OpenTelemetry. Start by installing the plugin with "bun add @elysiajs/opentelemetry".
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: Plugin for Elysia that adds support for OpenTelemetry. Start by installing the plugin with "bun add @elysiajs/opentelemetry".
15 | ---
16 |
17 | # OpenTelemetry
18 |
19 | To start using OpenTelemetry, install @elysiajs/opentelemetry
and apply plugin to any instance.
20 |
21 | typescript 22 | import { Elysia } from 'elysia' 23 | import { opentelemetry } from '@elysiajs/opentelemetry' 24 | 25 | import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node' 26 | import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' 27 | 28 | new Elysia() 29 | .use( 30 | opentelemetry({ 31 | spanProcessors: [ 32 | new BatchSpanProcessor( 33 | new OTLPTraceExporter() 34 | ) 35 | ] 36 | }) 37 | ) 38 |
39 |
40 |
41 |
42 | Elysia OpenTelemetry is will collect span of any library compatible OpenTelemetry standard, and will apply parent and child span automatically.
43 |
44 | In the code above, we apply
Prisma
to trace how long each query took.
45 |
46 | By applying OpenTelemetry, Elysia will then:
47 | - collect telemetry data
48 | - Grouping relevant lifecycle together
49 | - Measure how long each function took
50 | - Instrument HTTP request and response
51 | - Collect error and exception
52 |
53 | You may export telemetry data to Jaeger, Zipkin, New Relic, Axiom or any other OpenTelemetry compatible backend.
54 |
55 |
56 |
57 | Here's an example of exporting telemetry to Axiom
58 |
typescript 59 | import { Elysia } from 'elysia' 60 | import { opentelemetry } from '@elysiajs/opentelemetry' 61 | 62 | import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node' 63 | import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' 64 | 65 | new Elysia() 66 | .use( 67 | opentelemetry({ 68 | spanProcessors: [ 69 | new BatchSpanProcessor( 70 | new OTLPTraceExporter({ 71 | url: 'https://api.axiom.co/v1/traces', // [!code ++] 72 | headers: { // [!code ++] 73 | Authorization: `Bearer ${Bun.env.AXIOM_TOKEN}`, // [!code ++] 74 | 'X-Axiom-Dataset': Bun.env.AXIOM_DATASET // [!code ++] 75 | } // [!code ++] 76 | }) 77 | ) 78 | ] 79 | }) 80 | ) 81 |
82 |
83 | ## OpenTelemetry SDK
84 | Elysia OpenTelemetry is for applying OpenTelemetry to Elysia server only.
85 |
86 | You may use OpenTelemetry SDK normally, and the span is run under Elysia's request span, it will be automatically appear in Elysia trace.
87 |
88 | However, we also provide a getTracer
, and record
utility to collect span from any part of your application.
89 |
90 | typescript 91 | import { Elysia } from 'elysia' 92 | import { record } from '@elysiajs/opentelemetry' 93 | 94 | export const plugin = new Elysia() 95 | .get('', () => { 96 | return record('database.query', () => { 97 | return db.query('SELECT * FROM users') 98 | }) 99 | }) 100 |
101 |
102 | ## Record utility
103 | record
is an equivalent to OpenTelemetry's startActiveSpan
but it will handle auto-closing and capture exception automatically.
104 |
105 | You may think of record
as a label for your code that will be shown in trace.
106 |
107 | ### Prepare your codebase for observability
108 | Elysia OpenTelemetry will group lifecycle and read the function name of each hook as the name of the span.
109 |
110 | It's a good time to name your function.
111 |
112 | 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
.
113 |
114 | typescript 115 | const bad = new Elysia() 116 | // ⚠️ span name will be anonymous 117 | .derive(async ({ cookie: { session } }) => { 118 | return { 119 | user: await getProfile(session) 120 | } 121 | }) 122 | 123 | const good = new Elysia() 124 | // ✅ span name will be getProfile 125 | .derive(async function getProfile({ cookie: { session } }) { 126 | return { 127 | user: await getProfile(session) 128 | } 129 | }) 130 |
131 |
132 | ## getCurrentSpan
133 | getCurrentSpan
is a utility to get the current span of the current request when you are outside of the handler.
134 |
135 | typescript 136 | import { getCurrentSpan } from '@elysiajs/opentelemetry' 137 | 138 | function utility() { 139 | const span = getCurrentSpan() 140 | span.setAttributes({ 141 | 'custom.attribute': 'value' 142 | }) 143 | } 144 |
145 |
146 | This works outside of the handler by retriving current span from AsyncLocalStorage
147 |
148 | ## setAttribute
149 | setAttribute
is a utility to set attribute to the current span.
150 |
151 | typescript 152 | import { setAttribute } from '@elysiajs/opentelemetry' 153 | 154 | function utility() { 155 | setAttribute('custom.attribute', 'value') 156 | } 157 |
158 |
159 | This is a syntax sugar for getCurrentSpan().setAttributes
160 |
161 | ## Configuration
162 | See opentelemetry plugin for configuration option and definition.
163 |
1 | ---
2 | title: React Email - ElysiaJS
3 | head:
4 | - - meta
5 | - property: 'og:title'
6 | content: React Email - ElysiaJS
7 |
8 | - - meta
9 | - name: 'description'
10 | content: As Elysia is using Bun as runtime environment, we may directly use React Email and import JSX directly to our base to send emails.
11 |
12 | - - meta
13 | - name: 'og:description'
14 | content: As Elysia is using Bun as runtime environment, we may directly use React Email and import JSX directly to our code to send emails.
15 | ---
16 |
17 | # React Email
18 | React Email is a library that allows you to use React components to create emails.
19 |
20 | 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.
21 |
22 | ## Installation
23 | To install React Email, run the following command:
24 |
25 | bash 26 | bun add -d react-email 27 | bun add @react-email/components react react-dom 28 |
29 |
30 | Then add this script to package.json
:
31 | json 32 | { 33 | "scripts": { 34 | "email": "email dev --dir src/emails" 35 | } 36 | } 37 |
38 |
39 | We recommend adding email templates into the src/emails
directory as we can directly import the JSX files.
40 |
41 | ### TypeScript
42 | If you are using TypeScript, you may need to add the following to your tsconfig.json
:
43 |
44 | json 45 | { 46 | "compilerOptions": { 47 | "jsx": "react" 48 | } 49 | } 50 |
51 |
52 | ## Your first email
53 | Create file src/emails/otp.tsx
with the following code:
54 |
55 | tsx 56 | import * as React from 'react' 57 | import { Tailwind, Section, Text } from '@react-email/components' 58 | 59 | export default function OTPEmail({ otp }: { otp: number }) { 60 | return ( 61 | <Tailwind> 62 | <Section className="flex justify-center items-center w-full min-h-screen font-sans"> 63 | <Section className="flex flex-col items-center w-76 rounded-2xl px-6 py-1 bg-gray-50"> 64 | <Text className="text-xs font-medium text-violet-500"> 65 | Verify your Email Address 66 | </Text> 67 | <Text className="text-gray-500 my-0"> 68 | Use the following code to verify your email address 69 | </Text> 70 | <Text className="text-5xl font-bold pt-2">{otp}</Text> 71 | <Text className="text-gray-400 font-light text-xs pb-4"> 72 | This code is valid for 10 minutes 73 | </Text> 74 | <Text className="text-gray-600 text-xs"> 75 | Thank you joining us 76 | </Text> 77 | </Section> 78 | </Section> 79 | </Tailwind> 80 | ) 81 | } 82 | 83 | OTPEmail.PreviewProps = { 84 | otp: 123456 85 | } 86 |
87 |
88 | You may notice that we are using @react-email/components
to create the email template.
89 |
90 | This library provides a set of components including styling with Tailwind that are compatible with email clients like Gmail, Outlook, etc.
91 |
92 | We also added a PreviewProps
to the OTPEmail
function. This is only apply when previewing the email on our playground.
93 |
94 | ## Preview your email
95 | To preview your email, run the following command:
96 |
97 | bash 98 | bun email 99 |
100 |
101 | This will open a browser window with the preview of your email.
102 |
103 |
104 |
105 | ## Sending email
106 | To send an email, we can use
react-dom/server
to render the the email then submit using a preferred provider:
107 |
108 | ::: code-group
109 |
110 | tsx [Nodemailer] 111 | import { Elysia, t } from 'elysia' 112 | 113 | import * as React from 'react' 114 | import { renderToStaticMarkup } from 'react-dom/server' 115 | 116 | import OTPEmail from './emails/otp' 117 | 118 | import nodemailer from 'nodemailer' // [!code ++] 119 | 120 | const transporter = nodemailer.createTransport({ // [!code ++] 121 | host: 'smtp.gehenna.sh', // [!code ++] 122 | port: 465, // [!code ++] 123 | auth: { // [!code ++] 124 | user: 'makoto', // [!code ++] 125 | pass: '12345678' // [!code ++] 126 | } // [!code ++] 127 | }) // [!code ++] 128 | 129 | new Elysia() 130 | .get('/otp', ({ body }) => { 131 | // Random between 100,000 and 999,999 132 | const otp = ~~(Math.random() * (900_000 - 1)) + 100_000 133 | 134 | const html = renderToStaticMarkup(<OTPEmail otp={otp} />) 135 | 136 | await transporter.sendMail({ // [!code ++] 137 | from: '[email protected]', // [!code ++] 138 | to: body, // [!code ++] 139 | subject: 'Verify your email address', // [!code ++] 140 | html, // [!code ++] 141 | }) // [!code ++] 142 | 143 | return { success: true } 144 | }, { 145 | body: t.String({ format: 'email' }) 146 | }) 147 | .listen(3000) 148 |
149 |
150 | tsx [Resend] 151 | import { Elysia, t } from 'elysia' 152 | 153 | import OTPEmail from './emails/otp' 154 | 155 | import Resend from 'resend' // [!code ++] 156 | 157 | const resend = new Resend('re_123456789') // [!code ++] 158 | 159 | new Elysia() 160 | .get('/otp', ({ body }) => { 161 | // Random between 100,000 and 999,999 162 | const otp = ~~(Math.random() * (900_000 - 1)) + 100_000 163 | 164 | await resend.emails.send({ // [!code ++] 165 | from: '[email protected]', // [!code ++] 166 | to: body, // [!code ++] 167 | subject: 'Verify your email address', // [!code ++] 168 | html: <OTPEmail otp={otp} />, // [!code ++] 169 | }) // [!code ++] 170 | 171 | return { success: true } 172 | }, { 173 | body: t.String({ format: 'email' }) 174 | }) 175 | .listen(3000) 176 |
177 |
178 | tsx [AWS SES] 179 | import { Elysia, t } from 'elysia' 180 | 181 | import * as React from 'react' 182 | import { renderToStaticMarkup } from 'react-dom/server' 183 | 184 | import OTPEmail from './emails/otp' 185 | 186 | import { type SendEmailCommandInput, SES } from '@aws-sdk/client-ses' // [!code ++] 187 | import { fromEnv } from '@aws-sdk/credential-providers' // [!code ++] 188 | 189 | const ses = new SES({ // [!code ++] 190 | credentials: // [!code ++] 191 | process.env.NODE_ENV === 'production' ? fromEnv() : undefined // [!code ++] 192 | }) // [!code ++] 193 | 194 | new Elysia() 195 | .get('/otp', ({ body }) => { 196 | // Random between 100,000 and 999,999 197 | const otp = ~~(Math.random() * (900_000 - 1)) + 100_000 198 | 199 | const html = renderToStaticMarkup(<OTPEmail otp={otp} />) 200 | 201 | await ses.sendEmail({ // [!code ++] 202 | Source: '[email protected]', // [!code ++] 203 | Destination: { // [!code ++] 204 | ToAddresses: [body] // [!code ++] 205 | }, // [!code ++] 206 | Message: { // [!code ++] 207 | Body: { // [!code ++] 208 | Html: { // [!code ++] 209 | Charset: 'UTF-8', // [!code ++] 210 | Data: html // [!code ++] 211 | } // [!code ++] 212 | }, // [!code ++] 213 | Subject: { // [!code ++] 214 | Charset: 'UTF-8', // [!code ++] 215 | Data: 'Verify your email address' // [!code ++] 216 | } // [!code ++] 217 | } // [!code ++] 218 | } satisfies SendEmailCommandInput) // [!code ++] 219 | 220 | return { success: true } 221 | }, { 222 | body: t.String({ format: 'email' }) 223 | }) 224 | .listen(3000) 225 |
226 |
227 | tsx [Sendgrid] 228 | import { Elysia, t } from 'elysia' 229 | 230 | import OTPEmail from './emails/otp' 231 | 232 | import sendgrid from "@sendgrid/mail" // [!code ++] 233 | 234 | sendgrid.setApiKey(process.env.SENDGRID_API_KEY) // [!code ++] 235 | 236 | new Elysia() 237 | .get('/otp', ({ body }) => { 238 | // Random between 100,000 and 999,999 239 | const otp = ~~(Math.random() * (900_000 - 1)) + 100_000 240 | 241 | const html = renderToStaticMarkup(<OTPEmail otp={otp} />) 242 | 243 | await sendgrid.send({ // [!code ++] 244 | from: '[email protected]', // [!code ++] 245 | to: body, // [!code ++] 246 | subject: 'Verify your email address', // [!code ++] 247 | html // [!code ++] 248 | }) // [!code ++] 249 | 250 | return { success: true } 251 | }, { 252 | body: t.String({ format: 'email' }) 253 | }) 254 | .listen(3000) 255 |
256 |
257 | :::
258 |
259 | ::: tip
260 | Notice that we can directly import the email component out of the box thanks to Bun
261 | :::
262 |
263 | You may see all of the available integration with React Email in the React Email Integration, and learn more about React Email in React Email documentation
264 |
1 | --- 2 | title: Table of Content - ElysiaJS 3 | head: 4 | - - meta 5 | - property: 'og:title' 6 | content: Table of Content - ElysiaJS 7 | 8 | - - meta 9 | - name: 'description' 10 | content: There's no correct or organized way to learn Elysia, however, we recommended completing the essential chapter first as the chapter briefly covers most of Elysia's features and foundation before jumping to other topics that interest you. Once you've completed the essential chapter, you may jump to any topic that interests you. However, we recommended following the order of the chapter as it may reference to previous chapter. 11 | 12 | - - meta 13 | - property: 'og:description' 14 | content: There's no correct or organized way to learn Elysia, however, we recommended completing the essential chapter first as the chapter briefly covers most of Elysia's features and foundation before jumping to other topics that interest you. Once you've completed the essential chapter, you may jump to any topic that interests you. However, we recommended following the order of the chapter as it may reference to previous chapter. 15 | --- 16 | 17 | <script setup> 18 | import Card from '../components/nearl/card.vue' 19 | import Deck from '../components/nearl/card-deck.vue' 20 | </script> 21 | 22 | # Table of Content 23 | There's no correct way to learn Elysia, but we recommended completing the essential chapter first as the chapter briefly covers most of Elysia's features and foundation before jumping to other topics that interest you. 24 | 25 | 26 | 27 | A quick overview of Elysia 28 | 29 | 30 | Understand how to structure your code 31 | 32 | 33 | Enforce data type and create a unified type 34 | 35 | 36 | Intercept events and customize behaviors 37 | 38 | 39 | Understand how to separate Elysia, and scope 40 | 41 | 42 | End-to-end type safety client for Elysia 43 | 44 | 45 | 46 | --- 47 | 48 | Once you've completed the essential chapter, you may jump to any topic that interests you. We have organized a recommended chapter in order as it may reference to previous chapter. 49 | 50 | ### Prerequisite Knowledge 51 | Although Elysia's documentation is designed to be beginner-friendly, we need to establish a baseline so that the docs can stay focused on Elysia's functionality. We will provide links to relevant documentation whenever we introduce a new concept. 52 | 53 | To get the most out of our documentation, it's recommended that you have a basic understanding of Node.js and basic HTTP. 54 |