From 007d7882f3a1b28d528fb3db05e2cb2211c25af3 Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 14:52:56 -0500 Subject: [PATCH 01/25] Enables translations --- docs/.vitepress/config.ts | 11 + docs/{ => en}/api/constructor.md | 52 +-- docs/{ => en}/at-glance.md | 0 docs/{ => en}/blog.md | 0 docs/{ => en}/blog/elysia-02.md | 0 docs/{ => en}/blog/elysia-03.md | 154 +++++---- docs/{ => en}/blog/elysia-04.md | 0 docs/{ => en}/blog/elysia-05.md | 167 +++++----- docs/{ => en}/blog/elysia-06.md | 137 ++++---- docs/{ => en}/blog/elysia-07.md | 235 +++++++------ docs/{ => en}/blog/elysia-08.md | 201 +++++++----- docs/{ => en}/blog/elysia-supabase.md | 310 ++++++++++-------- .../blog/integrate-trpc-with-elysia.md | 159 ++++++--- docs/{ => en}/blog/with-prisma.md | 125 ++++--- docs/{ => en}/eden/fetch.md | 0 docs/{ => en}/eden/installation.md | 0 docs/{ => en}/eden/overview.md | 0 docs/{ => en}/eden/test.md | 0 docs/{ => en}/eden/treaty.md | 77 +++-- docs/{ => en}/essential/context.md | 116 +++---- docs/{ => en}/essential/handler.md | 0 docs/{ => en}/essential/life-cycle.md | 0 docs/{ => en}/essential/path.md | 14 +- docs/{ => en}/essential/plugin.md | 58 ++-- docs/{ => en}/essential/route.md | 2 +- docs/{ => en}/essential/schema.md | 0 docs/{ => en}/essential/scope.md | 0 docs/{ => en}/index.md | 0 docs/{ => en}/integrations/astro.md | 0 docs/{ => en}/integrations/cheat-sheet.md | 0 docs/{ => en}/integrations/docker.md | 0 docs/{ => en}/integrations/nextjs.md | 0 docs/{ => en}/introduction.md | 0 docs/{ => en}/life-cycle/after-handle.md | 0 docs/{ => en}/life-cycle/before-handle.md | 6 +- docs/{ => en}/life-cycle/map-response.md | 0 docs/{ => en}/life-cycle/on-error.md | 69 ++-- docs/{ => en}/life-cycle/on-response.md | 0 docs/{ => en}/life-cycle/overview.md | 4 +- docs/{ => en}/life-cycle/parse.md | 0 docs/{ => en}/life-cycle/request.md | 0 docs/{ => en}/life-cycle/trace.md | 130 ++++---- docs/{ => en}/life-cycle/transform.md | 0 docs/{ => en}/patterns/cookie-signature.md | 0 docs/{ => en}/patterns/cookie.md | 97 +++--- docs/{ => en}/patterns/documentation.md | 56 ++-- docs/{ => en}/patterns/group.md | 2 +- docs/{ => en}/patterns/lazy-loading-module.md | 0 docs/{ => en}/patterns/macro.md | 0 docs/{ => en}/patterns/mount.md | 0 docs/{ => en}/patterns/unit-test.md | 9 +- docs/{ => en}/patterns/websocket.md | 0 docs/{ => en}/plugins/bearer.md | 0 docs/{ => en}/plugins/cors.md | 0 docs/{ => en}/plugins/cron.md | 0 docs/{ => en}/plugins/graphql-apollo.md | 0 docs/{ => en}/plugins/graphql-yoga.md | 0 docs/{ => en}/plugins/html.md | 0 docs/{ => en}/plugins/jwt.md | 0 docs/en/plugins/overview.md | 78 +++++ docs/{ => en}/plugins/server-timing.md | 43 ++- docs/{ => en}/plugins/static.md | 0 docs/{ => en}/plugins/stream.md | 0 docs/{ => en}/plugins/swagger.md | 0 docs/{ => en}/plugins/trpc.md | 0 docs/{ => en}/quick-start.md | 0 docs/{ => en}/table-of-content.md | 0 docs/{ => en}/validation/elysia-type.md | 0 docs/{ => en}/validation/error-provider.md | 98 +++--- docs/{ => en}/validation/overview.md | 0 docs/{ => en}/validation/primitive-type.md | 77 ++--- docs/{ => en}/validation/reference-model.md | 0 docs/{ => en}/validation/schema-type.md | 7 +- docs/plugins/overview.md | 74 ----- docs/public/essential/url-object-dark.svg | 2 +- docs/public/essential/url-object.svg | 2 +- docs/vite.config.ts | 1 - package.json | 80 ++--- 78 files changed, 1488 insertions(+), 1165 deletions(-) rename docs/{ => en}/api/constructor.md (52%) rename docs/{ => en}/at-glance.md (100%) rename docs/{ => en}/blog.md (100%) rename docs/{ => en}/blog/elysia-02.md (100%) rename docs/{ => en}/blog/elysia-03.md (64%) rename docs/{ => en}/blog/elysia-04.md (100%) rename docs/{ => en}/blog/elysia-05.md (85%) rename docs/{ => en}/blog/elysia-06.md (86%) rename docs/{ => en}/blog/elysia-07.md (81%) rename docs/{ => en}/blog/elysia-08.md (80%) rename docs/{ => en}/blog/elysia-supabase.md (80%) rename docs/{ => en}/blog/integrate-trpc-with-elysia.md (77%) rename docs/{ => en}/blog/with-prisma.md (88%) rename docs/{ => en}/eden/fetch.md (100%) rename docs/{ => en}/eden/installation.md (100%) rename docs/{ => en}/eden/overview.md (100%) rename docs/{ => en}/eden/test.md (100%) rename docs/{ => en}/eden/treaty.md (83%) rename docs/{ => en}/essential/context.md (73%) rename docs/{ => en}/essential/handler.md (100%) rename docs/{ => en}/essential/life-cycle.md (100%) rename docs/{ => en}/essential/path.md (93%) rename docs/{ => en}/essential/plugin.md (85%) rename docs/{ => en}/essential/route.md (98%) rename docs/{ => en}/essential/schema.md (100%) rename docs/{ => en}/essential/scope.md (100%) rename docs/{ => en}/index.md (100%) rename docs/{ => en}/integrations/astro.md (100%) rename docs/{ => en}/integrations/cheat-sheet.md (100%) rename docs/{ => en}/integrations/docker.md (100%) rename docs/{ => en}/integrations/nextjs.md (100%) rename docs/{ => en}/introduction.md (100%) rename docs/{ => en}/life-cycle/after-handle.md (100%) rename docs/{ => en}/life-cycle/before-handle.md (93%) rename docs/{ => en}/life-cycle/map-response.md (100%) rename docs/{ => en}/life-cycle/on-error.md (66%) rename docs/{ => en}/life-cycle/on-response.md (100%) rename docs/{ => en}/life-cycle/overview.md (95%) rename docs/{ => en}/life-cycle/parse.md (100%) rename docs/{ => en}/life-cycle/request.md (100%) rename docs/{ => en}/life-cycle/trace.md (53%) rename docs/{ => en}/life-cycle/transform.md (100%) rename docs/{ => en}/patterns/cookie-signature.md (100%) rename docs/{ => en}/patterns/cookie.md (65%) rename docs/{ => en}/patterns/documentation.md (51%) rename docs/{ => en}/patterns/group.md (98%) rename docs/{ => en}/patterns/lazy-loading-module.md (100%) rename docs/{ => en}/patterns/macro.md (100%) rename docs/{ => en}/patterns/mount.md (100%) rename docs/{ => en}/patterns/unit-test.md (94%) rename docs/{ => en}/patterns/websocket.md (100%) rename docs/{ => en}/plugins/bearer.md (100%) rename docs/{ => en}/plugins/cors.md (100%) rename docs/{ => en}/plugins/cron.md (100%) rename docs/{ => en}/plugins/graphql-apollo.md (100%) rename docs/{ => en}/plugins/graphql-yoga.md (100%) rename docs/{ => en}/plugins/html.md (100%) rename docs/{ => en}/plugins/jwt.md (100%) create mode 100644 docs/en/plugins/overview.md rename docs/{ => en}/plugins/server-timing.md (78%) rename docs/{ => en}/plugins/static.md (100%) rename docs/{ => en}/plugins/stream.md (100%) rename docs/{ => en}/plugins/swagger.md (100%) rename docs/{ => en}/plugins/trpc.md (100%) rename docs/{ => en}/quick-start.md (100%) rename docs/{ => en}/table-of-content.md (100%) rename docs/{ => en}/validation/elysia-type.md (100%) rename docs/{ => en}/validation/error-provider.md (68%) rename docs/{ => en}/validation/overview.md (100%) rename docs/{ => en}/validation/primitive-type.md (91%) rename docs/{ => en}/validation/reference-model.md (100%) rename docs/{ => en}/validation/schema-type.md (97%) delete mode 100644 docs/plugins/overview.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 8f465e02..e5d7b2cb 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -9,6 +9,17 @@ export default defineConfig({ // description, ignoreDeadLinks: true, lastUpdated: true, + locales: { + root: { + label: 'English', + lang: 'en' + }, + fr: { + label: 'French', + lang: 'fr' // optional, will be added as `lang` attribute on `html` tag + // link: '/fr/guide' // default /fr/ -- shows on navbar translations menu, can be external + } + }, markdown: { theme: { light: 'github-light', diff --git a/docs/api/constructor.md b/docs/en/api/constructor.md similarity index 52% rename from docs/api/constructor.md rename to docs/en/api/constructor.md index 622fb6d3..ecba9720 100644 --- a/docs/api/constructor.md +++ b/docs/en/api/constructor.md @@ -1,26 +1,28 @@ --- title: Constructor - ElysiaJS head: - - - meta - - property: 'og:title' - content: Constructor - ElysiaJS + - - meta + - property: 'og:title' + content: Constructor - ElysiaJS - - - meta - - name: 'description' - content: You can customize Elysia behavior with "constructor" or "listen", for example setting hostname, max body size or Web Socket config. + - - meta + - name: 'description' + content: You can customize Elysia behavior with "constructor" or "listen", for example setting hostname, max body size or Web Socket config. - - - - meta - - property: 'og:description' - content: You can customize Elysia behavior with "constructor" or "listen", for example setting hostname, max body size or Web Socket config. + - - meta + - property: 'og:description' + content: You can customize Elysia behavior with "constructor" or "listen", for example setting hostname, max body size or Web Socket config. --- # Constructor + You can customize Elysia behavior by: -1. using constructor + +1. using constructor 2. using `listen` ## Constructor + Constructor will change some behavior of Elysia. ```typescript @@ -32,6 +34,7 @@ new Elysia({ ``` ## Listen + `.listen` will config any value for starting server. By default `listen` will either accept `number` or `Object`. @@ -40,38 +43,37 @@ For Object, `listen` accept the same value as `Bun.serve`, you can provide any c ```typescript // ✅ This is fine -new Elysia() - .listen(8080) +new Elysia().listen(8080) // ✅ This is fine -new Elysia() - .listen({ - port: 8080, - hostname: '0.0.0.0' - }) +new Elysia().listen({ + port: 8080, + hostname: '0.0.0.0' +}) ``` ::: tip -For providing WebSocket, please use [`WebSocket`](/patterns/websocket) +For providing WebSocket, please use [`WebSocket`](/en/patterns/websocket) ::: ## Custom Port + You can provide a custom port from ENV by using `process.env` + ```typescript -new Elysia() - .listen(process.env.PORT ?? 8080) +new Elysia().listen(process.env.PORT ?? 8080) ``` ## Retrieve Port + You can get underlying `Server` instance from either using: `.server` property. Using callback in `.listen` ```typescript -const app = new Elysia() - .listen(8080, ({ hostname, port }) => { - console.log(`Running at http://${hostname}:${port}`) - }) +const app = new Elysia().listen(8080, ({ hostname, port }) => { + console.log(`Running at http://${hostname}:${port}`) +}) // `server` will be null if listen isn't called console.log(`Running at http://${app.server!.hostname}:${app.server!.port}`) diff --git a/docs/at-glance.md b/docs/en/at-glance.md similarity index 100% rename from docs/at-glance.md rename to docs/en/at-glance.md diff --git a/docs/blog.md b/docs/en/blog.md similarity index 100% rename from docs/blog.md rename to docs/en/blog.md diff --git a/docs/blog/elysia-02.md b/docs/en/blog/elysia-02.md similarity index 100% rename from docs/blog/elysia-02.md rename to docs/en/blog/elysia-02.md diff --git a/docs/blog/elysia-03.md b/docs/en/blog/elysia-03.md similarity index 64% rename from docs/blog/elysia-03.md rename to docs/en/blog/elysia-03.md index 9c1fe866..b37c2c53 100644 --- a/docs/blog/elysia-03.md +++ b/docs/en/blog/elysia-03.md @@ -4,25 +4,25 @@ sidebar: false editLink: false search: false head: - - - meta - - property: 'og:title' - content: Introducing Elysia 0.3 - 大地の閾を探して [Looking for Edge of Ground] + - - meta + - property: 'og:title' + content: Introducing Elysia 0.3 - 大地の閾を探して [Looking for Edge of Ground] - - - meta - - name: 'description' - content: Introducing Elysia Fn, Type Rework for highly scalable TypeScript performance, File Upload support and validation, Reworked Eden Treaty. + - - meta + - name: 'description' + content: Introducing Elysia Fn, Type Rework for highly scalable TypeScript performance, File Upload support and validation, Reworked Eden Treaty. - - - meta - - property: 'og:description' - content: Introducing Elysia Fn, Type Rework for highly scalable TypeScript performance, File Upload support and validation, Reworked Eden Treaty. + - - meta + - property: 'og:description' + content: Introducing Elysia Fn, Type Rework for highly scalable TypeScript performance, File Upload support and validation, Reworked Eden Treaty. - - - meta - - property: 'og:image' - content: https://elysiajs.com/blog/elysia-03/edge-of-ground.webp + - - meta + - property: 'og:image' + content: https://elysiajs.com/blog/elysia-03/edge-of-ground.webp - - - meta - - property: 'twitter:image' - content: https://elysiajs.com/blog/elysia-03/edge-of-ground.webp + - - meta + - property: 'twitter:image' + content: https://elysiajs.com/blog/elysia-03/edge-of-ground.webp --- Named after Camellia's song[「大地の閾を探して [Looking for Edge of Ground]」](https://youtu.be/oyJf72je2U0)ft. Hatsune Miku, is the last track of my most favorite's Camellia album,「U.U.F.O」. This song has a high impact on me personally, so I'm not taking the name lightly. @@ -44,6 +45,7 @@ This is the most challenging update, bringing the biggest release of Elysia yet, I'm pleased to announce the release candidate of Elysia 0.3 with exciting new features coming right up. ## Elysia Fn + Introducing Elysia Fn, run any backend function on the frontend with full auto-completion and full type support. \ No newline at end of file +> diff --git a/docs/blog/elysia-04.md b/docs/en/blog/elysia-04.md similarity index 100% rename from docs/blog/elysia-04.md rename to docs/en/blog/elysia-04.md diff --git a/docs/blog/elysia-05.md b/docs/en/blog/elysia-05.md similarity index 85% rename from docs/blog/elysia-05.md rename to docs/en/blog/elysia-05.md index 0688717d..ae393eee 100644 --- a/docs/blog/elysia-05.md +++ b/docs/en/blog/elysia-05.md @@ -30,11 +30,12 @@ head: Named after Arknights' original music, 「[Radiant](https://youtu.be/QhUjD--UUV4)」composed by Monster Sirent Records. @@ -42,6 +43,7 @@ Named after Arknights' original music, 「[Radiant](https://youtu.be/QhUjD--UUV4 Radiant push the boundary of performance with more stability improvement especially types, and dynamic routes. ## Static Code Analysis + With Elysia 0.4 introducing Ahead of Time compliation, allowing Elysia to optimize function calls, and eliminate many over head we previously had. Today we are expanding Ahead of Time compliation to be even faster wtih Static Code Analysis, to be the fastest Bun web framework. @@ -84,11 +86,12 @@ app.post('/id/:id', ({ params: { id } }) => id, { With Static Code Analysis, and Ahead of Time compilation, you can rest assure that Elysia is very good at reading your code and adjust itself to maximize the performance automatically. Static Code Analysis allows us to improve Elysia performance beyond we have imagined, here's a notable mention: -- overall improvement by ~15% -- static router fast ~33% -- empty query parsing ~50% -- strict type body parsing faster by ~100% -- empty body parsing faster by ~150% + +- overall improvement by ~15% +- static router fast ~33% +- empty query parsing ~50% +- strict type body parsing faster by ~100% +- empty body parsing faster by ~150% With this improvement, we are able to surpass **Stricjs** in term of performance, compared using Elysia 0.5.0-beta.0 and Stricjs 2.0.4 @@ -118,7 +121,7 @@ TypeBox is a core library that powered Elysia's strict type system known as **El In this update, we update TypeBox from 0.26 to 0.28 to make even more fine-grained Type System near strictly typed language. -We update Typebox to improve Elysia typing system to match new TypeBox feature with newer version of TypeScript like **Constant Generic** +We update Typebox to improve Elysia typing system to match new TypeBox feature with newer version of TypeScript like **Constant Generic** ```ts new Elysia() @@ -127,10 +130,7 @@ new Elysia() 'name', Type.TemplateLiteral([ Type.Literal('Elysia '), - Type.Union([ - Type.Literal('The Blessing'), - Type.Literal('Radiant') - ]) + Type.Union([Type.Literal('The Blessing'), Type.Literal('Radiant')]) ]) ) // Strictly check for template literal @@ -148,12 +148,14 @@ That's why we introduced a new Type, **URLEncoded**. As we previously mentioned before, Elysia now can take an advantage of schema and optimize itself Ahead of Time, body parsing is one of more expensive area in Elysia, that's why we introduce a dedicated type for parsing body like URLEncoded. By default, Elysia will parse body based on body's schema type as the following: -- t.URLEncoded -> `application/x-www-form-urlencoded` -- t.Object -> `application/json` -- t.File -> `multipart/form-data` -- the rest -> `text/plain` + +- t.URLEncoded -> `application/x-www-form-urlencoded` +- t.Object -> `application/json` +- t.File -> `multipart/form-data` +- the rest -> `text/plain` However, you can explictly tells Elysia to parse body with the specific method using `type` as the following: + ```ts app.post('/', ({ body }) => body, { type: 'json' @@ -161,6 +163,7 @@ app.post('/', ({ body }) => body, { ``` `type` may be one of the following: + ```ts type ContentType = | // Shorthand for 'text/plain' @@ -177,14 +180,16 @@ type ContentType = | | 'application/x-www-form-urlencoded' ``` -You can find more detail at the [explicit body](/concept/explicit-body) page in concept. +You can find more detail at the [explicit body](/en/concept/explicit-body) page in concept. ### Numeric Type + We found that one of the redundant task our developers found using Elysia is to parse numeric string. That's we introduce a new **Numeric** Type. Previously on Elysia 0.4, to parse numeric string, we need to use `transform` to manually parse the string ourself. + ```ts app.get('/id/:id', ({ params: { id } }) => id, { schema: { @@ -195,15 +200,14 @@ app.get('/id/:id', ({ params: { id } }) => id, { transform({ params }) { const id = +params.id - if(!Number.isNaN(id)) - params.id = id + if (!Number.isNaN(id)) params.id = id } }) ``` We found that this step is redundant, and full of boiler-plate, we want to tap into this problem and solve it in a declarative way. -Thanks to Static Code Analysis, Numeric type allow you to defined a numeric string and parse it to number automatically. +Thanks to Static Code Analysis, Numeric type allow you to defined a numeric string and parse it to number automatically. Once validated, a numeric type will be parsed as number automatically both on runtime and type level to fits our need. @@ -216,19 +220,21 @@ app.get('/id/:id', ({ params: { id } }) => id, { ``` You can use numeric type on any property that support schema typing, including: -- params -- query -- headers -- body -- response + +- params +- query +- headers +- body +- response We hope that you will find this new Numeric type useful in your server. -You can find more detail at [numeric type](/concept/numeric) page in concept. +You can find more detail at [numeric type](/en/concept/numeric) page in concept. With TypeBox 0.28, we are making Elysia type system we more complete, and we excited to see how it play out on your end. ## Inline Schema + You might have notice already that our example are not using a `schema.type` to create a type anymore, because we are making a **breaking change** to move schema and inline it to hook statement instead. ```ts @@ -238,7 +244,7 @@ app.get('/id/:id', ({ params: { id } }) => id, { params: t.Object({ id: t.Number() }) - }, + } }) // ? To @@ -256,6 +262,7 @@ Based on a lot of tinkering and real-world usage, we try to suggest this new cha But we also listen the the rest of our community, and try to get around with the argument against this decision: ### Clear separation + With the old syntax, you have to explicitly tells Elysia that the part you are creating are a schema using `Elysia.t`. Creating a clear separation between life-cycle and schema are more clear and has a better readability. @@ -263,6 +270,7 @@ Creating a clear separation between life-cycle and schema are more clear and has But from our intense test, we found that most people don't have any problem struggling reading a new syntax, separating life-cycle hook from schema type, we found that it still has clear separation with `t.Type` and function, and a different syntax highlight when reviewing the code, although not as good as clear as explicit schema, but people can get used to the new syntax very quickly especially if they are familiar the Elysia. ### Auto completion + One of the other area that people are concerned about are reading auto-completion. Merging schema and life-cycle hook caused the auto-completion to have around 10 properties for auto-complete to suggest, and based on many proven general User Experience research, it can be frastating for user to that many options to choose from, and can cause a steeper learning curve. @@ -274,6 +282,7 @@ For example, if you want to access a headers, you can acceess `headers` in Conte With this, Elysia might have a little more learning curve, however it's a trade-off that we are willing to take for better developer experience. ## "headers" fields + Previously, you can get headers field by accessing `request.headers.get`, and you might wonder why we don't ship headers by default. ```ts @@ -295,6 +304,7 @@ app.post('/headers', ({ headers }) => headers['content-type']) Parsed headers will be available as plain object with a lower-case key of the header name. ## State, Decorate, Model rework + One of the main feature of Elysia is able to customize Elysia to your need. We revisits `state`, `decorate`, and `setModel`, and we saw that api is not consistant, and can be improved. @@ -307,23 +317,23 @@ So we renamed `setModel` to `model`, and add support for setting single and mult import { Elysia, t } from 'elysia' const app = new Elysia() - // ? set model using label - .model('string', t.String()) - .model({ - number: t.Number() - }) - .state('visitor', 1) - // ? set model using object - .state({ - multiple: 'value', - are: 'now supported!' - }) - .decorate('visitor', 1) - // ? set model using object - .decorate({ - name: 'world', - number: 2 - }) + // ? set model using label + .model('string', t.String()) + .model({ + number: t.Number() + }) + .state('visitor', 1) + // ? set model using object + .state({ + multiple: 'value', + are: 'now supported!' + }) + .decorate('visitor', 1) + // ? set model using object + .decorate({ + name: 'world', + number: 2 + }) ``` And as we raised minimum support of TypeScript to 5.0 to improve strictly typed with **Constant Generic**. @@ -331,14 +341,15 @@ And as we raised minimum support of TypeScript to 5.0 to improve strictly typed `state`, `decorate`, and `model` now support literal type, and template string to strictly validate type both runtime and type-level. ```ts - // ? state, decorate, now support literal +// ? state, decorate, now support literal app.get('/', ({ body }) => number, { - body: t.Literal(1), - response: t.Literal(2) - }) + body: t.Literal(1), + response: t.Literal(2) +}) ``` ### Group and Guard + We found that many developers often use `group` with `guard`, we found that nesting them can be later redundant and maybe boilerplate full. Starting with Elysia 0.5, we add a guard scope for `.group` as an optional second parameter. @@ -356,19 +367,21 @@ app.group('/v1', (app) => // ✅ new, compatible with old syntax app.group( - '/v1', { + '/v1', + { body: t.Literal('Rikuhachima Aru') - }, - app => app.get('/student', () => 'Rikuhachima Aru') + }, + (app) => app.get('/student', () => 'Rikuhachima Aru') ) // ✅ compatible with function overload -app.group('/v1', app => app.get('/student', () => 'Rikuhachima Aru')) +app.group('/v1', (app) => app.get('/student', () => 'Rikuhachima Aru')) ``` We hope that you will find all these new revisited API more useful and fits more to your use-case. ## Type Stability + Elysia Type System is complex. We can declare variable on type-level, reference type by name, apply multiple Elysia instance, and even have support for clousure-like at type level, which is really complex to make you have the best developer experience especially with Eden. @@ -382,26 +395,30 @@ Which means that you can now rely on us to check for type integrity for every re --- ### Notable Improvement: -- Add CommonJS support for running Elysia with Node adapter -- Remove manual fragment mapping to speed up path extraction -- Inline validator in `composeHandler` to improve performance -- Use one time context assignment -- Add support for lazy context injection via Static Code Analysis -- Ensure response non nullability -- Add unioned body validator check -- Set default object handler to inherits -- Using `constructor.name` mapping instead of `instanceof` to improve speed -- Add dedicated error constructor to improve performance -- Conditional literal fn for checking onRequest iteration -- improve WebSocket type + +- Add CommonJS support for running Elysia with Node adapter +- Remove manual fragment mapping to speed up path extraction +- Inline validator in `composeHandler` to improve performance +- Use one time context assignment +- Add support for lazy context injection via Static Code Analysis +- Ensure response non nullability +- Add unioned body validator check +- Set default object handler to inherits +- Using `constructor.name` mapping instead of `instanceof` to improve speed +- Add dedicated error constructor to improve performance +- Conditional literal fn for checking onRequest iteration +- improve WebSocket type Breaking Change: -- Rename `innerHandle` to `fetch` - - to migrate: rename `.innerHandle` to `fetch` -- Rename `.setModel` to `.model` - - to migrate: rename `setModel` to `model` -- Remove `hook.schema` to `hook` - - to migrate: remove schema and curly brace `schema.type`: + +- Rename `innerHandle` to `fetch` + - to migrate: rename `.innerHandle` to `fetch` +- Rename `.setModel` to `.model` + - to migrate: rename `setModel` to `model` +- Remove `hook.schema` to `hook` + + - to migrate: remove schema and curly brace `schema.type`: + ```ts // from app.post('/', ({ body }) => body, { @@ -419,9 +436,11 @@ Breaking Change: }) }) ``` -- remove `mapPathnameRegex` (internal) + +- remove `mapPathnameRegex` (internal) ## Afterward + Pushing performance boundary of JavaScript with Bun is what we really excited! Even with the new features every release, Elysia keeps getting faster, with an improved reliabilty and stability, we hope that Elysia will become one of the choice for the next generation TypeScript framework. diff --git a/docs/blog/elysia-06.md b/docs/en/blog/elysia-06.md similarity index 86% rename from docs/blog/elysia-06.md rename to docs/en/blog/elysia-06.md index 6d89aecc..7373a66d 100644 --- a/docs/blog/elysia-06.md +++ b/docs/en/blog/elysia-06.md @@ -30,11 +30,12 @@ head: Named after the opening of the legendary anime, **"No Game No Life"**, 「[This Game](https://youtu.be/kJ04dMmimn8)」composed by Konomi Suzuki. @@ -44,43 +45,45 @@ This Game push the boundary of medium-size project to large-scale app with re-im ###### (We are still waiting for No Game No Life season 2) ## New Plugin Model + This Game introduce a new syntax for plugin registration, and come up with a new plugin model internally. Previously you can register plugin by defining a callback function for Elysia instance like this: + ```ts const plugin = (app: Elysia) => app.get('/', () => 'hello') ``` With the new plugin, you can now turns and Elysia instance into a plugin: + ```ts -const plugin = new Elysia() - .get('/', () => 'hello') +const plugin = new Elysia().get('/', () => 'hello') ``` This allows any Elysia instance and even existing one to be used across application, removing any possible addition callback and tab spacing. This improved Developer Experience significantly when working and nested group + ```ts // < 0.6 -const group = (app: Elysia) => app - .group('/v1', (app) => app - .get('/hello', () => 'Hello World') - ) +const group = (app: Elysia) => + app.group('/v1', (app) => app.get('/hello', () => 'Hello World')) // >= 0.6 -const group = new Elysia({ prefix: '/v1' }) - .get('/hello', () => 'Hello World') +const group = new Elysia({ prefix: '/v1' }).get('/hello', () => 'Hello World') ``` We encourage you to use the new model of Elysia plugin instance, as we can take advantage of Plugin Checksum and new possible features in the future. However, we are **NOT deprecating** the callback function method as there's some case function model is useful like: -- Inline function -- Plugins that required an information of main instance (for example accessing OpenAPI schema) + +- Inline function +- Plugins that required an information of main instance (for example accessing OpenAPI schema) With this new plugin model, we hope that you can make your codebase even easier to maintain. ## Plugin Checksum + By default, Elysia plugin use function callback to register plugin. This means that if you register a plugin for type declaration, it will duplicate itself for just providing a type support, leading to duplication of plugin used in production. @@ -88,6 +91,7 @@ This means that if you register a plugin for type declaration, it will duplicate Which is why Plugin Checksum is introduced, to de-duplicated plugin registered for type declaration. To opt-in to Plugin Checksum, you need to use a new plugin model, and provide a `name` property to tell Elysia to prevent the plugin from being deduplicate + ```ts const plugin = new Elysia({ name: 'plugin' @@ -101,10 +105,10 @@ Any duplicated name will be registered only once but type-safety will be provide In case your plugin needs configuration, you can provide the configuration into a **seed** property to generate a checksum for deduplicating the plugin. ```ts -const plugin = (config) = new Elysia({ +const plugin = (config = new Elysia({ name: 'plugin', seed: config -}) +})) ``` Name and seed will be used to generate a checksum to de-duplicated registration, which leads to even better performance improvement. @@ -114,6 +118,7 @@ This update also fixed the deduplication of the plugin's lifecycle accidentally As always, means performance improvement for an app that's larger than "Hello World". ## Mount and WinterCG Compliance + WinterCG is a standard for web-interoperable runtimes supports by Cloudflare, Deno, Vercel Edge Runtime, Netlify Function and various more. WinterCG is a standard to allows web server to runs interoperable across runtime, which use Web Standard definitions like Fetch, Request, and Response. @@ -125,6 +130,7 @@ This allows any framework and code that is WinterCG compliance to be run togethe By this, we implemented the same logic for Elysia by introducing `.mount` method to runs any framework or code that is WinterCG compliant. To use `.mount`, [simply pass a `fetch` function](https://twitter.com/saltyAom/status/1684786233594290176): + ```ts const app = new Elysia() .get('/', () => 'Hello from Elysia') @@ -132,6 +138,7 @@ const app = new Elysia() ``` A **fetch** function is a function that accept Web Standard Request and return Web Standard Response as the definition of: + ```ts // Web Standard Request-like object // Web Standard Response @@ -139,19 +146,20 @@ type fetch = (request: RequestLike) => Response ``` By default, this declaration is used by: -- Bun -- Deno -- Vercel Edge Runtime -- Cloudflare Worker -- Netlify Edge Function -- Remix Function Handler + +- Bun +- Deno +- Vercel Edge Runtime +- Cloudflare Worker +- Netlify Edge Function +- Remix Function Handler Which means you can run all of the above code to interlop with Elysia all in a single server, or re-used and existing function all in one deployment, no need to setting up Reverse Proxy for handling multiple server. If the framework also support a **.mount** function, you can deeply nested a framework that support it infinitely. + ```ts -const elysia = new Elysia() - .get('/Hello from Elysia inside Hono inside Elysia') +const elysia = new Elysia().get('/Hello from Elysia inside Hono inside Elysia') const hono = new Hono() .get('/', (c) => c.text('Hello from Hono!')) @@ -170,10 +178,7 @@ import A from 'project-a/elysia' import B from 'project-b/elysia' import C from 'project-c/elysia' -new Elysia() - .mount(A) - .mount(B) - .mount(C) +new Elysia().mount(A).mount(B).mount(C) ``` If an instance passed to mount is an Elysia instance, it will resolve to `use` automatically, providing type-safety and support for Eden by default. @@ -181,6 +186,7 @@ If an instance passed to mount is an Elysia instance, it will resolve to `use` a This made the possibility of interlopable framework and runtime to a reality. ## Improved starts up time + Starts up time is an important metric in a serverless environment which Elysia excels it incredibly, but we have taken it even further. By default, Elysia generates OpenAPI schema for every route automatically and stored it internally when if not used. @@ -190,6 +196,7 @@ In this version, Elysia defers the compilation and moved to `@elysiajs/swagger` And with various micro-optimization, and made possible by new Plugin model, starts up time is now up to 35% faster. ## Dynamic Mode + Elysia introduces Static Code Analysis and Ahead of Time compilation to push the boundary of performance. Static Code Analysis allow Elysia to read your code then produce the most optimized version code, allowing Elysia to push the performance to its limit. @@ -199,6 +206,7 @@ Even if Elysia is WinterCG compliance, environment like Cloudflare worker doesn' This means that Ahead of Time Compliation isn't possible, leading us to create a dynamic mode which use JIT compilation instead of AoT, allowing Elysia to run in Cloudflare Worker as well. To enable dynamic mode, set `aot` to false. + ```ts new Elysia({ aot: false @@ -218,6 +226,7 @@ Elysia is able to register 10,000 routes in just 78ms which means in an average That being said, we are leaving a choice for you to decided yourself. ## Declarative Custom Error + This update adds support for adding type support for handling custom error. ```ts @@ -232,7 +241,7 @@ new Elysia() MyError: CustomError }) .onError(({ code, error }) => { - switch(code) { + switch (code) { // With auto-completion case 'MyError': // With type narrowing @@ -251,6 +260,7 @@ Elysia Type System is complex, yet we try to refrained our users need to write a It just works, and all the code looks just like JavaScript. ## TypeBox 0.30 + TypeBox is a core library that powers Elysia's strict type system known as **Elysia.t**. In this update, we update TypeBox from 0.28 to 0.30 to make even more fine-grained Type System nearly strictly typed language. @@ -258,37 +268,41 @@ In this update, we update TypeBox from 0.28 to 0.30 to make even more fine-grain These updates introduce new features and many interesting changes, for example **Iterator** type, reducing packages size, TypeScript code generation. And with support for Utility Types like: -- `t.Awaited` -- `t.Uppercase` -- `t.Capitlized` + +- `t.Awaited` +- `t.Uppercase` +- `t.Capitlized` ## Strict Path + We got a lot of requests for handling loose path. By default, Elysia handle path strictly, which means that if you have to support path with or without optional `/` , it will not be resolved and you have to duplicate the pathname twice. ```ts -new Elysia() - .group('/v1', (app) => app +new Elysia().group('/v1', (app) => + app // Handle /v1 .get('', handle) // Handle /v1/ .get('/', handle) - ) +) ``` By this, many have been requesting that `/v1/` should also resolved `/v1` as well. With this update, we add support for loose path matching by default, to opt-in into this feature automatically. + ```ts -new Elysia() - .group('/v1', (app) => app +new Elysia().group('/v1', (app) => + app // Handle /v1 and /v1/ .get('/', handle) - ) +) ``` To disable loosePath mapping, you can set `strictPath` to true to used the previous behavior: + ```ts new Elysia({ strictPath: false @@ -298,12 +312,13 @@ new Elysia({ We hope that this will clear any questions regards to path matching and its expected behavior ## onResponse + This update introduce a new lifecycle hook called `onResponse`. This is a proposal proposed by [elysia#67](https://github.com/elysiajs/elysia/issues/67) Previously Elysia life-cycle works as illustrated in this diagram. -![Elysia life-cycle diagram](/blog/elysia-06/lifecycle-05.webp) +![Elysia life-cycle diagram](/en/blog/elysia-06/lifecycle-05.webp) For any metric, data-collection or logging purpose, you can use `onAfterHandle` to run the function to collect metrics, however, this lifecycle isn't executed when handler runs into an error, whether it's a routing error or a custom error provided. @@ -312,34 +327,39 @@ Which is why we introduced `onResponse` to handle all cases of Response. You can use `onRequest`, and `onResponse` together to measure a metric of performance or any required logging. Quoted + > However, the onAfterHandle function only fires on successful responses. For instance, if the route is not found, or the body is invalid, or an error is thrown, it is not fired. How can I listen to both successful and non-successful requests? This is why I suggested onResponse. > > Based on the drawing, I would suggest the following: -> ![Elysia life-cycle diagram with onResponse hook](/blog/elysia-06/lifecycle-06.webp) +> ![Elysia life-cycle diagram with onResponse hook](/en/blog/elysia-06/lifecycle-06.webp) --- ### Notable Improvement: -- Added an error field to the Elysia type system for adding custom error messages -- Support Cloudflare worker with Dynamic Mode (and ENV) -- AfterHandle now automatically maps the value -- Using bun build to target Bun environment, improving the overall performance by 5-10% -- Deduplicated inline lifecycle when using plugin registration -- Support for setting `prefix` -- Recursive path typing -- Slightly improved type checking speed -- Recursive schema collision causing infinite types + +- Added an error field to the Elysia type system for adding custom error messages +- Support Cloudflare worker with Dynamic Mode (and ENV) +- AfterHandle now automatically maps the value +- Using bun build to target Bun environment, improving the overall performance by 5-10% +- Deduplicated inline lifecycle when using plugin registration +- Support for setting `prefix` +- Recursive path typing +- Slightly improved type checking speed +- Recursive schema collision causing infinite types ### Change: -- Moved **registerSchemaPath** to @elysiajs/swagger -- [Internal] Add qi (queryIndex) to context + +- Moved **registerSchemaPath** to @elysiajs/swagger +- [Internal] Add qi (queryIndex) to context ### Breaking Change: -- [Internal] Removed Elysia Symbol -- [Internal] Refactored `getSchemaValidator`, `getResponseSchemaValidator` to named parameters -- [Internal] Moved `registerSchemaPath` to `@elysiajs/swagger` + +- [Internal] Removed Elysia Symbol +- [Internal] Refactored `getSchemaValidator`, `getResponseSchemaValidator` to named parameters +- [Internal] Moved `registerSchemaPath` to `@elysiajs/swagger` ## Afterward + We have just passed a one year milestone, and really excited how Elysia and Bun have improved over the year! Pushing the performance boundaries of JavaScript with Bun, and developer experience with Elysia, we are thrilled to have kept in touch with you and our community. @@ -347,8 +367,9 @@ Pushing the performance boundaries of JavaScript with Bun, and developer experie Every updates, keeps making Elysia even more stable, and gradually providing a better developer experience without a drop in performance and features. We're thrilled to see our community of open-source developers bringing Elysia to life with their projects like. -- [Elysia Vite Plugin SSR](https://github.com/timnghg/elysia-vite-plugin-ssr) allowing us to use Vite Server Side Rendering using Elysia as the server. -- [Elysia Connect](https://github.com/timnghg/elysia-connect) which made Connect's plugin compatible with Elysia + +- [Elysia Vite Plugin SSR](https://github.com/timnghg/elysia-vite-plugin-ssr) allowing us to use Vite Server Side Rendering using Elysia as the server. +- [Elysia Connect](https://github.com/timnghg/elysia-connect) which made Connect's plugin compatible with Elysia And much more developers that choose Elysia for their next big project. @@ -371,7 +392,7 @@ We incredibly thankful for your overwhelming continous support for Elysia, and w > We are maverick > > We won't give in, until we win this game -> +> > Though I don't know what tomorrow holds > > I'll make a bet any play my cards to win this game diff --git a/docs/blog/elysia-07.md b/docs/en/blog/elysia-07.md similarity index 81% rename from docs/blog/elysia-07.md rename to docs/en/blog/elysia-07.md index 50906b06..03d71ac6 100644 --- a/docs/blog/elysia-07.md +++ b/docs/en/blog/elysia-07.md @@ -30,11 +30,12 @@ head: Name after our never giving up spirit, our beloved Virtual YouTuber, ~~Suicopath~~ Hoshimachi Suisei, and her brilliance voice: 「[Stellar Stellar](https://youtu.be/AAsRtnbDs-0)」from her first album:「Still Still Stellar」 @@ -42,13 +43,14 @@ Name after our never giving up spirit, our beloved Virtual YouTuber, ~~Suicopath For once being forgotten, she really is a star that truly shine in the dark. **Stellar Stellar** brings many exciting new update to help Elysia solid the foundation, and handle complexity with ease, featuring: -- Entirely rewrite type, up to 13x faster type inference. -- "Trace" for declarative telemetry and better performance audit. -- Reactive Cookie model and cookie valiation to simplify cookie handling. -- TypeBox 0.31 with a custom decoder support. -- Rewritten Web Socket for even better support. -- Definitions remapping, and declarative affix for preventing name collision. -- Text based status + +- Entirely rewrite type, up to 13x faster type inference. +- "Trace" for declarative telemetry and better performance audit. +- Reactive Cookie model and cookie valiation to simplify cookie handling. +- TypeBox 0.31 with a custom decoder support. +- Rewritten Web Socket for even better support. +- Definitions remapping, and declarative affix for preventing name collision. +- Text based status ## Rewritten Type @@ -75,7 +77,7 @@ With our new found experience, and newer TypeScript feature like const generic, Allowing us to refine our type system to be even faster, and even more stable. -![Comparison between Elysia 0.6 and 0.7 on complex project with our 300 routes, and 3,500 lines of type declaration](/blog/elysia-07/inference-comparison.webp) +![Comparison between Elysia 0.6 and 0.7 on complex project with our 300 routes, and 3,500 lines of type declaration](/en/blog/elysia-07/inference-comparison.webp) Using Perfetto and TypeScript CLI to generate trace on a large-scale and complex app, we measure up to 13x inference speed. @@ -93,7 +95,7 @@ There are many factor that can slow down your app, and it's hard to identifying **Trace** allow us to take tap into a life-cycle event and identifying performance bottleneck for our app. -![Example of usage of Trace](/blog/elysia-07/trace.webp) +![Example of usage of Trace](/en/blog/elysia-07/trace.webp) This example code allow us tap into all **beforeHandle** event, and extract the execution time one-by-one before setting the Server-Timing API to inspect the performance bottleneck. @@ -104,24 +106,26 @@ This API allows us to effortlessly auditing performance bottleneck of your Elysi By default, Trace use AoT compilation and Dynamic Code injection to conditionally report and even that you actually use automatically, which means there's no performance impact at all. ## Reactive Cookie + We merged our cookie plugin into Elysia core. Same as Trace, Reactive Cookie use AoT compilation and Dynamic Code injection to conditionally inject the cookie usage code, leading to no performance impact if you don't use one. Reactive Cookie take a more modern approach like signal to handle cookie with an ergonomic API. -![Example of usage of Reactive Cookie](/blog/elysia-07/cookie.webp) +![Example of usage of Reactive Cookie](/en/blog/elysia-07/cookie.webp) There's no `getCookie`, `setCookie`, everything is just a cookie object. When you want to use cookie, you just extract the name get/set its value like: + ```typescript app.get('/', ({ cookie: { name } }) => { // Get name.value // Set - name.value = "New Value" + name.value = 'New Value' }) ``` @@ -130,30 +134,36 @@ Then cookie will be automatically sync the value with headers, and the cookie ja The Cookie Jar is reactive, which means that if you don't set the new value for the cookie, the `Set-Cookie` header will not be send to keep the same cookie value and reduce performance bottleneck. ### Cookie Schema + With the merge of cookie into the core of Elysia, we introduce a new **Cookie Schema** for validating cookie value. This is useful when you have to strictly validate cookie session or want to have a strict type or type inference for handling cookie. ```typescript -app.get('/', ({ cookie: { name } }) => { - // Set - name.value = { - id: 617, - name: 'Summoning 101' - } -}, { - cookie: t.Cookie({ - value: t.Object({ - id: t.Numeric(), - name: t.String() +app.get( + '/', + ({ cookie: { name } }) => { + // Set + name.value = { + id: 617, + name: 'Summoning 101' + } + }, + { + cookie: t.Cookie({ + value: t.Object({ + id: t.Numeric(), + name: t.String() + }) }) - }) -}) + } +) ``` Elysia encode and decode cookie value for you automatically, so if you want to store JSON in a cookie like decoded JWT value, or just want to make sure if the value is a numeric string, you can do that effortlessly. ### Cookie Signature + And lastly, with an introduction of Cookie Schema, and `t.Cookie` type. We are able to create a unified type for handling sign/verify cookie signature automatically. Cookie signature is a cryptographic hash appended to a cookie's value, generated using a secret key and the content of the cookie to enhance security by adding a signature to the cookie. @@ -161,27 +171,34 @@ Cookie signature is a cryptographic hash appended to a cookie's value, generated This make sure that the cookie value is not modified by malicious actor, helps in verifying the authenticity and integrity of the cookie data. To handle cookie signature in Elysia, it's a simple as providing a `secert` and `sign` property: + ```typescript new Elysia({ cookie: { secret: 'Fischl von Luftschloss Narfidort' } -}) - .get('/', ({ cookie: { profile } }) => { +}).get( + '/', + ({ cookie: { profile } }) => { profile.value = { id: 617, name: 'Summoning 101' } - }, { - cookie: t.Cookie({ - profile: t.Object({ - id: t.Numeric(), - name: t.String() - }) - }, { - sign: ['profile'] - }) - }) + }, + { + cookie: t.Cookie( + { + profile: t.Object({ + id: t.Numeric(), + name: t.String() + }) + }, + { + sign: ['profile'] + } + ) + } +) ``` By provide a cookie secret, and `sign` property to indicate which cookie should have a signature verification. @@ -189,6 +206,7 @@ By provide a cookie secret, and `sign` property to indicate which cookie should Elysia then sign and unsign cookie value automatically, eliminate the need of **sign** / **unsign** function manually. Elysia handle Cookie's secret rotation automatically, so if you have to migrate to a new cookie secret, you can just append the secret, and Elysia will use the first value to sign a new cookie, while trying to unsign cookie with the rest of the secret if match. + ```typescript new Elysia({ cookie: { @@ -200,6 +218,7 @@ new Elysia({ The Reactive Cookie API is declarative and straigth forward, and there's some magical thing about the ergonomic it provide, and we really looking forward for you to try it. ## TypeBox 0.31 + With the release of 0.7, we are updating to TypeBox 0.31 to brings even more feature to Elysia. This brings new exciting feature like support for TypeBox's `Decode` in Elysia natively. @@ -207,6 +226,7 @@ This brings new exciting feature like support for TypeBox's `Decode` in Elysia n Previously, a custom type like `Numeric` require a dynamic code injection to convert numeric string to number, but with the use of TypeBox's decode, we are allow to define a custom function to encode and decode the value of a type automatically. Allowing us to simplify type to: + ```typescript Numeric: (property?: NumericOptions) => Type.Transform(Type.Union([Type.String(), Type.Number(property)])) @@ -228,31 +248,42 @@ Not only limited to that, with `t.Transform` you can now also define a custom ty We can't wait to see what you will brings with the introduction of `t.Transform`. ### New Type + With an introduction **Transform**, we have add a new type like `t.ObjectString` to automatically decode a value of Object in request. This is useful when you have to use **multipart/formdata** for handling file uploading but doesn't support object. You can now just use `t.ObjectString()` to tells Elysia that the field is a stringified JSON, so Elysia can decode it automatically. + ```typescript new Elysia({ cookie: { secret: 'Fischl von Luftschloss Narfidort' } -}) - .post('/', ({ body: { data: { name } } }) => name, { +}).post( + '/', + ({ + body: { + data: { name } + } + }) => name, + { body: t.Object({ image: t.File(), data: t.ObjectString({ name: t.String() }) }) - }) + } +) ``` We hope that this will simplify the need for JSON with **multipart**. ## Rewritten Web Socket + Aside from entirely rewritten type, we also entirely rewritten Web Socket as well. Previously, we found that Web Socket has 3 major problem: + 1. Schema is not strictly validated 2. Slow type inference 3. The need for `.use(ws())` in every plugin @@ -271,56 +302,55 @@ Bringing the performance to near Bun native Web Socket performance. Thanks to [Bogeychan](https://github.com/bogeychan) for providing the test case for Elysia Web Socket, helping us to rewrite Web Socket with confidence. ## Definitions Remap + Proposed on [#83](https://github.com/elysiajs/elysia/issues/83) by [Bogeychan](https://github.com/bogeychan) To summarize, Elysia allows us to decorate and request and store with any value we desire, however some plugin might a duplicate name with the value we have, and sometime plugin has a name collision but we can't rename the property at all. ### Remapping + As the name suggest, this allow us to remap existing `state`, `decorate`, `model`, `derive` to anything we like to prevent name collision, or just wanting to rename a property. By providing a function as a first parameters, the callback will accept current value, allowing us to remap the value to anything we like. + ```typescript new Elysia() .state({ - a: "a", - b: "b" + a: 'a', + b: 'b' }) // Exclude b state .state(({ b, ...rest }) => rest) ``` This is useful when you have to deal with a plugin that has some duplicate name, allowing you to remap the name of the plugin: + ```typescript -new Elysia() - .use( - plugin - .decorate(({ logger, ...rest }) => ({ - pluginLogger: logger, - ...rest - })) - ) +new Elysia().use( + plugin.decorate(({ logger, ...rest }) => ({ + pluginLogger: logger, + ...rest + })) +) ``` Remap function can be use with `state`, `decorate`, `model`, `derive` to helps you define a correct property name and preventing name collision. ### Affix + To provide a smoother experience, some plugins might have a lot of property value which can be overwhelming to remap one-by-one. The **Affix** function, which consists of a **prefix** and **suffix**, allows us to remap all properties of an instance, preventing the name collision of the plugin. ```typescript -const setup = new Elysia({ name: 'setup' }) - .decorate({ - argon: 'a', - boron: 'b', - carbon: 'c' - }) +const setup = new Elysia({ name: 'setup' }).decorate({ + argon: 'a', + boron: 'b', + carbon: 'c' +}) const app = new Elysia() - .use( - setup - .prefix('decorator', 'setup') - ) + .use(setup.prefix('decorator', 'setup')) .get('/', ({ setupCarbon }) => setupCarbon) ``` @@ -329,18 +359,17 @@ Allowing us to bulk remap a property of the plugin effortlessly, preventing the By default, **affix** will handle both runtime, type-level code automatically, remapping the property to camelCase as naming convention. In some condition, you can also remap `all` property of the plugin: + ```typescript const app = new Elysia() - .use( - setup - .prefix('all', 'setup') - ) + .use(setup.prefix('all', 'setup')) .get('/', ({ setupCarbon }) => setupCarbon) ``` We hope that remapping and affix will provide a powerful API for you to handle multiple complex plugin with ease. ## True Encapsulation Scope + With the introduction of Elysia 0.7, Elysia can now truly encapsulate an instance by treating a scoped instance as another instance. The new scope model can even prevent event like `onRequest` to be resolve on a main instance which is not possible. @@ -363,54 +392,61 @@ Further more, scoped is now truly scoped down both in runtime, and type level wh This is exciting from maintainer side because previously, it's almost impossible to truly encapsulate the scope the an instance, but using `mount` and WinterCG compilance, we are finally able to truly encapsulate the instance of the plugin while providing a soft link with main instance property like `state`, `decorate`. ## Text based status + There are over 64 standard HTTP status codes to remember, and I admit that sometime we also forget the status we want to use. This is why we ship 64 HTTP Status codes in text-based form with autocompletion for you. -![Example of using text-base status code](/blog/elysia-07/teapot.webp) +![Example of using text-base status code](/en/blog/elysia-07/teapot.webp) Text will then resolved to status code automatically as expected. As you type, there should be auto-completion for text popup automatically for your IDE, whether it's NeoVim or VSCode, as it's a built-in TypeScript feature. -![Text-base status code showing auto-completion](/blog/elysia-07/teapot-autocompletion.webp) +![Text-base status code showing auto-completion](/en/blog/elysia-07/teapot-autocompletion.webp) This is a small ergonomic feature to helps you develop your server without switching between IDE and MDN to search for a correct status code. ## Notable Improvement + Improvement: -- `onRequest` can now be async -- add `Context` to `onError` -- lifecycle hook now accept array function -- static Code Analysis now support rest parameter -- breakdown dynamic router into single pipeline instead of inlining to static router to reduce memory usage -- set `t.File` and `t.Files` to `File` instead of `Blob` -- skip class instance merging -- handle `UnknownContextPassToFunction` -- [#157](https://github.com/elysiajs/elysia/pull/179) WebSocket - added unit tests and fixed example & api by @bogeychan -- [#179](https://github.com/elysiajs/elysia/pull/179) add github action to run bun test by @arthurfiorette + +- `onRequest` can now be async +- add `Context` to `onError` +- lifecycle hook now accept array function +- static Code Analysis now support rest parameter +- breakdown dynamic router into single pipeline instead of inlining to static router to reduce memory usage +- set `t.File` and `t.Files` to `File` instead of `Blob` +- skip class instance merging +- handle `UnknownContextPassToFunction` +- [#157](https://github.com/elysiajs/elysia/pull/179) WebSocket - added unit tests and fixed example & api by @bogeychan +- [#179](https://github.com/elysiajs/elysia/pull/179) add github action to run bun test by @arthurfiorette Breaking Change: -- remove `ws` plugin, migrate to core -- rename `addError` to `error` + +- remove `ws` plugin, migrate to core +- rename `addError` to `error` Change: -- using single findDynamicRoute instead of inlining to static map -- remove `mergician` -- remove array routes due to problem with TypeScript -- rewrite Type.ElysiaMeta to use TypeBox.Transform + +- using single findDynamicRoute instead of inlining to static map +- remove `mergician` +- remove array routes due to problem with TypeScript +- rewrite Type.ElysiaMeta to use TypeBox.Transform Bug fix: -- strictly validate response by default -- `t.Numeric` not working on headers / query / params -- `t.Optional(t.Object({ [name]: t.Numeric }))` causing error -- add null check before converting `Numeric` -- inherits store to instance plugin -- handle class overlapping -- [#187](https://github.com/elysiajs/elysia/pull/187) InternalServerError message fixed to "INTERNAL_SERVER_ERROR" instead of "NOT_FOUND" by @bogeychan -- [#167](https://github.com/elysiajs/elysia/pull/167) mapEarlyResponse with aot on after handle + +- strictly validate response by default +- `t.Numeric` not working on headers / query / params +- `t.Optional(t.Object({ [name]: t.Numeric }))` causing error +- add null check before converting `Numeric` +- inherits store to instance plugin +- handle class overlapping +- [#187](https://github.com/elysiajs/elysia/pull/187) InternalServerError message fixed to "INTERNAL_SERVER_ERROR" instead of "NOT_FOUND" by @bogeychan +- [#167](https://github.com/elysiajs/elysia/pull/167) mapEarlyResponse with aot on after handle ## Afterward + Since the latest release, we have gained over 2,000 stars on GitHub! Taking a look back, we have progressed more than we have ever imagined back then. @@ -423,10 +459,11 @@ A future where we can freely create anything we want with an astonishing develop We feels truly thanksful to be loved by you and lovely community of TypeScript and Bun. -It's exciting to see Elysia is bring to live with amazing developer like: -- [Ethan Niser with his amazing BETH Stack](https://youtu.be/aDYYn9R-JyE?si=hgvGgbywu_-jsmhR) -- Being mentioned by [Fireship](https://youtu.be/dWqNgzZwVJQ?si=AeCmcMsTZtNwmhm2) -- Having official integration for [Lucia Auth](https://github.com/pilcrowOnPaper/lucia) +It's exciting to see Elysia is bring to live with amazing developer like: + +- [Ethan Niser with his amazing BETH Stack](https://youtu.be/aDYYn9R-JyE?si=hgvGgbywu_-jsmhR) +- Being mentioned by [Fireship](https://youtu.be/dWqNgzZwVJQ?si=AeCmcMsTZtNwmhm2) +- Having official integration for [Lucia Auth](https://github.com/pilcrowOnPaper/lucia) And much more developers that choose Elysia for their next project. @@ -453,7 +490,7 @@ Thanks you and your love and overwhelming support for Elysia, we hope we can pai > Not Cinderella, forever waiting > > But the prince that came to for her -> +> > Cause I'm a star, that's why > > Stellar Stellar diff --git a/docs/blog/elysia-08.md b/docs/en/blog/elysia-08.md similarity index 80% rename from docs/blog/elysia-08.md rename to docs/en/blog/elysia-08.md index b05a588b..7b67fcf7 100644 --- a/docs/blog/elysia-08.md +++ b/docs/en/blog/elysia-08.md @@ -30,11 +30,12 @@ head: Named after the ending song of Steins;Gate Zero, [**"Gate of Steiner"**](https://youtu.be/S5fnglcM5VI). @@ -42,15 +43,17 @@ Named after the ending song of Steins;Gate Zero, [**"Gate of Steiner"**](https:/ Gate of Steiner isn't focused on new exciting APIs and features but on API stability and a solid foundation to make sure that the API will be stable once Elysia 1.0 is released. However, we do bring improvement and new features including: -- [Macro API](#macro-api) -- [New Lifecycle: resolve, mapResponse](#new-life-cycle) -- [Error Function](#error-function) -- [Static Content](#static-content) -- [Default Property](#default-property) -- [Default Header](#default-header) -- [Performance and Notable Improvement](#performance-and-notable-improvement) + +- [Macro API](#macro-api) +- [New Lifecycle: resolve, mapResponse](#new-life-cycle) +- [Error Function](#error-function) +- [Static Content](#static-content) +- [Default Property](#default-property) +- [Default Header](#default-header) +- [Performance and Notable Improvement](#performance-and-notable-improvement) ## Macro API + Macro allows us to define a custom field to hook and guard by exposing full control of the life cycle event stack. Allowing us to compose custom logic into a simple configuration with full type safety. @@ -61,24 +64,23 @@ Suppose we have an authentication plugin to restrict access based on role, we ca import { Elysia } from 'elysia' import { auth } from '@services/auth' -const app = new Elysia() - .use(auth) - .get('/', ({ user }) => user.profile, { - role: 'admin' - }) +const app = new Elysia().use(auth).get('/', ({ user }) => user.profile, { + role: 'admin' +}) ``` Macro has full access to the life cycle stack, allowing us to add, modify, or delete existing events directly for each route. + ```typescript const plugin = new Elysia({ name: 'plugin' }).macro(({ beforeHandle }) => { return { role(type: 'admin' | 'user') { beforeHandle( - { insert: 'before' }, + { insert: 'before' }, async ({ cookie: { session } }) => { - const user = await validateSession(session.value) - await validateRole('admin', user) -} + const user = await validateSession(session.value) + await validateRole('admin', user) + } ) } } @@ -87,21 +89,23 @@ const plugin = new Elysia({ name: 'plugin' }).macro(({ beforeHandle }) => { We hope that with this macro API, plugin maintainers will be able to customize Elysia to their heart's content opening a new way to interact better with Elysia, and Elysia users will be able to enjoy even more ergonomic API Elysia could provide. -The documentation of [Macro API](/patterns/macro) is now available in **pattern** section. +The documentation of [Macro API](/en/patterns/macro) is now available in **pattern** section. The next generation of customizability is now only a reach away from your keyboard and imagination. ## New Life Cycle + Elysia introduced a new life cycle to to fix an existing problem and highly requested API including **Resolve** and **MapResponse**: resolve: a safe version of **derive**. Execute in the same queue as **beforeHandle** mapResponse: Execute just after **afterResponse** for providing transform function from primitive value to Web Standard Response ### Resolve -A "safe" version of [derive](/essential/context.html#derive). + +A "safe" version of [derive](/en/essential/context.html#derive). Designed to append new value to context after validation process storing in the same stack as **beforeHandle**. -Resolve syntax is identical to [derive](/life-cycle/before-handle#derive), below is an example of retrieving a bearer header from Authorization plugin. +Resolve syntax is identical to [derive](/en/life-cycle/before-handle#derive), below is an example of retrieving a bearer header from Authorization plugin. ```typescript import { Elysia } from 'elysia' @@ -126,6 +130,7 @@ new Elysia() ``` ### MapResponse + Executed just after **"afterHandle"**, designed to provide custom response mapping from primitive value into a Web Standard Response. Below is an example of using mapResponse to provide Response compression. @@ -149,15 +154,16 @@ new Elysia() Why not use **afterHandle** but introduce a new API? -Because **afterHandle** is designed to read and modify a primitive value. Storing plugins like HTML, and Compression which depends on creating Web Standard Response. +Because **afterHandle** is designed to read and modify a primitive value. Storing plugins like HTML, and Compression which depends on creating Web Standard Response. This means that plugins registered after this type of plugin will be unable to read a value or modify the value making the plugin behavior incorrect. This is why we introduce a new life-cycle run after **afterHandle** dedicated to providing a custom response mapping instead of mixing the response mapping and primitive value mutation in the same queue. - ## Error Function + We can set the status code by using either **set.status** or returning a new Response. + ```typescript import { Elysia } from 'elysia' @@ -172,7 +178,7 @@ new Elysia() This aligns with our goal, to just the literal value to the client instead of worrying about how the server should behave. -However, this is proven to have a challenging integration with Eden. Since we return a literal value, we can't infer the status code from the response making Eden unable to differentiate the response from the status code. +However, this is proven to have a challenging integration with Eden. Since we return a literal value, we can't infer the status code from the response making Eden unable to differentiate the response from the status code. This results in Eden not being able to use its full potential, especially in error handling as it cannot infer type without declaring explicit response type for each status. @@ -189,6 +195,7 @@ new Elysia() ``` Which is an equivalent to: + ```typescript import { Elysia } from 'elysia' @@ -209,20 +216,27 @@ This means that by using **error**, we don't have to include the explicit respon import { Elysia, error, t } from 'elysia' new Elysia() - .get('/', ({ set }) => { - set.status = 418 - return "I'm a teapot" - }, { // [!code --] - response: { // [!code --] - 418: t.String() // [!code --] - } // [!code --] - }) // [!code --] + .get( + '/', + ({ set }) => { + set.status = 418 + return "I'm a teapot" + }, + { + // [!code --] + response: { + // [!code --] + 418: t.String() // [!code --] + } // [!code --] + } + ) // [!code --] .listen(3000) ``` We recommended using `error` function to return a response with the status code for the correct type inference, however, we do not intend to remove the usage of **set.status** from Elysia to keep existing servers working. ## Static Content + Static Content refers to a response that almost always returns the same value regardless of the incoming request. This type of resource on the server is usually something like a public **File**, **video** or hardcode value that is rarely changed unless the server is updated. @@ -243,6 +257,7 @@ Notice that the handler now isn't a function but is an inline value instead. This will improve the performance by around 20-25% by compiling the response ahead of time. ## Default Property + Elysia 0.8 updates to [TypeBox to 0.32](https://github.com/sinclairzx81/typebox/blob/index/changelog/0.32.0.md) which introduces many new features including dedicated RegEx, Deref but most importantly the most requested feature in Elysia, **default** field support. Now defining a default field in Type Builder, Elysia will provide a default value if the value is not provided, supporting schema types from type to body. @@ -255,7 +270,7 @@ new Elysia() query: t.Object({ name: t.String({ default: 'Elysia' - }) + }) }) }) .listen(3000) @@ -264,6 +279,7 @@ new Elysia() This allows us to provide a default value from schema directly, especially useful when using reference schema. ## Default Header + We can set headers using **set.headers**, which Elysia always creates a default empty object for every request. Previously we could use **onRequest** to append desired values into set.headers, but this will always have some overhead because a function is called. @@ -271,27 +287,31 @@ Previously we could use **onRequest** to append desired values into set.headers, Stacking functions to mutate an object can be a little slower than having the desired value set in the first hand if the value is always the same for every request like CORS or cache header. This is why we now support setting default headers out of the box instead of creating an empty object for every new request. + ```typescript -new Elysia() - .headers({ - 'X-Powered-By': 'Elysia' - }) +new Elysia().headers({ + 'X-Powered-By': 'Elysia' +}) ``` Elysia CORS plugin also has an update to use this new API to improve this performance. ## Performance and notable improvement + As usual, we found a way to optimize Elysia even more to make sure you have the best performance out of the box. ### Removable of bind + We found that **.bind** is slowing down the path lookup by around ~5%, with the removal of bind from our codebase we can speed up that process a little bit. ### Static Query Analysis + Elysia Static Code Analysis is now able to infer a query if the query name is referenced in the code. This usually results in a speed-up of 15-20% by default. ### Video Stream + Elysia now adds **content-range** header to File and Blob by default to fix problems with large files like videos that require to be sent by chunk. To fix this, Elysia now adds **content-range** header to by default. @@ -303,56 +323,61 @@ It's recommended to use [ETag plugin](https://github.com/bogeychan/elysia-etag) This is an initial support for **content-range** header, we have created a discussion to implement more accurate behavior based on [RPC-7233](https://datatracker.ietf.org/doc/html/rfc7233#section-4.2) in the future. Feels free to join the discussion to propose a new behavior for Elysia with **content-range** and **etag generation** at [Discussion 371](https://github.com/elysiajs/elysia/discussions/371). ### Runtime Memory improvement + Elysia now reuses the return value of the life cycle event instead of declaring a new dedicated value. This reduces the memory usage of Elysia by a little bit better for peak concurrent requests a little better. ### Runtime Memory improvement + Elysia now reuses the return value of the life cycle event instead of declaring a new dedicated value. This reduces the memory usage of Elysia by a little bit better for peak concurrent requests a little better. ### Plugins + Most official plugins now take advantage of newer **Elysia.headers**, Static Content, **MapResponse** ,and revised code to comply with static code analysis even more to improve the overall performance. Plugins that are improved by this are the following: Static, HTML, and CORS. ### Validation Error + Elysia now returns validation error as JSON instead of text. Showing current errors and all errors and expected values instead, to help you identify an error easier. Example: + ```json { - "type": "query", - "at": "password", - "message": "Required property", - "expected": { - "email": "eden@elysiajs.com", - "password": "" - }, - "found": { - "email": "eden@elysiajs.com" - }, - "errors": [ - { - "type": 45, - "schema": { - "type": "string" - }, - "path": "/password", - "message": "Required property" + "type": "query", + "at": "password", + "message": "Required property", + "expected": { + "email": "eden@elysiajs.com", + "password": "" }, - { - "type": 54, - "schema": { - "type": "string" - }, - "path": "/password", - "message": "Expected string" - } - ] + "found": { + "email": "eden@elysiajs.com" + }, + "errors": [ + { + "type": 45, + "schema": { + "type": "string" + }, + "path": "/password", + "message": "Required property" + }, + { + "type": 54, + "schema": { + "type": "string" + }, + "path": "/password", + "message": "Expected string" + } + ] } ``` @@ -361,25 +386,30 @@ Example: ## Notable Improvement **Improvement** -- lazy query reference -- add content-range header to `Blob` by default -- update TypeBox to 0.32 -- override lifecycle response of `be` and `af` + +- lazy query reference +- add content-range header to `Blob` by default +- update TypeBox to 0.32 +- override lifecycle response of `be` and `af` **Breaking Change** -- `afterHandle` no longer early return + +- `afterHandle` no longer early return **Change** -- change validation response to JSON -- differentiate derive from `decorator['request']` as `decorator['derive']` -- `derive` now don't show infer type in onRequest + +- change validation response to JSON +- differentiate derive from `decorator['request']` as `decorator['derive']` +- `derive` now don't show infer type in onRequest **Bug fix** -- remove `headers`, `path` from `PreContext` -- remove `derive` from `PreContext` -- Elysia type doesn't output custom `error` + +- remove `headers`, `path` from `PreContext` +- remove `derive` from `PreContext` +- Elysia type doesn't output custom `error` ## Afterward + It has been a great journey since the first release. Elysia evolved from a generic REST API framework to an ergonomic framework to support End-to-end type safety, OpenAPI documentation generation, we we would like to keep introduce more exciting features in the future. @@ -388,24 +418,25 @@ Elysia evolved from a generic REST API framework to an ergonomic framework to su We are happy to have you, and the developers to build amazing stuff with Elysia and your overwhelming continuous support for Elysia encourages us to keep going. It's exciting to see Elysia grow more as a community: -- [Scalar's Elysia theme](https://x.com/saltyAom/status/1737468941696421908?s=20) for new documentation instead of Swagger UI. -- [pkgx](https://pkgx.dev/) supports Elysia out of the box. -- Community submitted Elysia to [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r22&hw=ph&test=composite) ranking at 32 out of all frameworks in composite score, even surpassing Go's Gin and Echo. -We are now trying to provide more support for each runtime, plugin, and integration to return the kindness you have given us, starting with the rewrite of the documentation with more detailed and beginner-friendliness, [Integration with Nextjs](/integrations/nextj), [Astro](/integrations/astro) and more to come in the future. +- [Scalar's Elysia theme](https://x.com/saltyAom/status/1737468941696421908?s=20) for new documentation instead of Swagger UI. +- [pkgx](https://pkgx.dev/) supports Elysia out of the box. +- Community submitted Elysia to [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r22&hw=ph&test=composite) ranking at 32 out of all frameworks in composite score, even surpassing Go's Gin and Echo. + +We are now trying to provide more support for each runtime, plugin, and integration to return the kindness you have given us, starting with the rewrite of the documentation with more detailed and beginner-friendliness, [Integration with Nextjs](/en/integrations/nextj), [Astro](/en/integrations/astro) and more to come in the future. And since the release of 0.7, we have seen fewer issues compared to the previous releases. Now **we are preparing for the first stable release of Elysia**, Elysia 1.0 aiming to release **in Q1 2024** to repay your kindness. -Elysia will now enter soft API lockdown mode, to prevent an API change and make sure that there will be no or less breaking change once the stable release arrives. +Elysia will now enter soft API lockdown mode, to prevent an API change and make sure that there will be no or less breaking change once the stable release arrives. So you can expect your Elysia app to work starting from 0.7 with no or minimal change to support the stable release of Elysia. We again thank your continuous support for Elysia, and we hope to see you again on the stable release day. -*Keep fighting for all that is beautiful in this world*. +_Keep fighting for all that is beautiful in this world_. -Until then, *El Psy Congroo*. +Until then, _El Psy Congroo_. > A drop in the darkness 小さな命 > diff --git a/docs/blog/elysia-supabase.md b/docs/en/blog/elysia-supabase.md similarity index 80% rename from docs/blog/elysia-supabase.md rename to docs/en/blog/elysia-supabase.md index 1b8413c8..0c4ac76d 100644 --- a/docs/blog/elysia-supabase.md +++ b/docs/en/blog/elysia-supabase.md @@ -30,11 +30,12 @@ head: Supabase, an Open Source alternative to Firebase, has become one of the developers' favorite toolkits known for rapid development. @@ -59,7 +60,7 @@ Things that take many hours to redo in every project are now a matter of a minut If you haven't heard, Elysia is a Bun-first web framework built with speed and Developer Experience in mind. -Elysia outperforms Express by nearly ~20x faster, while having almost the same syntax as Express and Fastify. +Elysia outperforms Express by nearly ~20x faster, while having almost the same syntax as Express and Fastify. ###### (Performance may vary per machine, we recommended you run [the benchmark](https://github.com/SaltyAom/bun-http-framework-benchmark) on your machine before deciding the performance) @@ -156,26 +157,27 @@ And now, let's apply Supabase to authenticate our user. ```ts // src/modules/authen.ts import { Elysia } from 'elysia' -import { supabase } from '../../libs' // [!code ++] +import { supabase } from '../../libs' // [!code ++] const authen = (app: Elysia) => app.group('/auth', (app) => app .post('/sign-up', async ({ body }) => { const { data, error } = await supabase.auth.signUp(body) // [!code ++] - // [!code ++] + // [!code ++] if (error) return error // [!code ++] return data.user // [!code ++] return 'This route is expected to sign up a user' // [!code --] }) .post('/sign-in', async ({ body }) => { - const { data, error } = await supabase.auth.signInWithPassword( // [!code ++] + const { data, error } = await supabase.auth.signInWithPassword( + // [!code ++] body // [!code ++] ) // [!code ++] - // [!code ++] + // [!code ++] if (error) return error // [!code ++] - // [!code ++] + // [!code ++] return data.user // [!code ++] return 'This route is expected to sign in a user' // [!code --] }) @@ -205,13 +207,18 @@ const authen = (app: Elysia) => return data.user }, - { // [!code ++] - schema: { // [!code ++] - body: t.Object({ // [!code ++] - email: t.String({ // [!code ++] + { + // [!code ++] + schema: { + // [!code ++] + body: t.Object({ + // [!code ++] + email: t.String({ + // [!code ++] format: 'email' // [!code ++] }), // [!code ++] - password: t.String({ // [!code ++] + password: t.String({ + // [!code ++] minLength: 8 // [!code ++] }) // [!code ++] }) // [!code ++] @@ -228,13 +235,18 @@ const authen = (app: Elysia) => return data.user }, - { // [!code ++] - schema: { // [!code ++] - body: t.Object({ // [!code ++] - email: t.String({ // [!code ++] + { + // [!code ++] + schema: { + // [!code ++] + body: t.Object({ + // [!code ++] + email: t.String({ + // [!code ++] format: 'email' // [!code ++] }), // [!code ++] - password: t.String({ // [!code ++] + password: t.String({ + // [!code ++] minLength: 8 // [!code ++] }) // [!code ++] }) // [!code ++] @@ -264,12 +276,16 @@ import { supabase } from '../../libs' const authen = (app: Elysia) => app.group('/auth', (app) => app - .setModel({ // [!code ++] - sign: t.Object({ // [!code ++] - email: t.String({ // [!code ++] + .setModel({ + // [!code ++] + sign: t.Object({ + // [!code ++] + email: t.String({ + // [!code ++] format: 'email' // [!code ++] }), // [!code ++] - password: t.String({ // [!code ++] + password: t.String({ + // [!code ++] minLength: 8 // [!code ++] }) // [!code ++] }) // [!code ++] @@ -285,11 +301,14 @@ const authen = (app: Elysia) => { schema: { body: 'sign', // [!code ++] - body: t.Object({ // [!code --] - email: t.String({ // [!code --] + body: t.Object({ + // [!code --] + email: t.String({ + // [!code --] format: 'email' // [!code --] }), // [!code --] - password: t.String({ // [!code --] + password: t.String({ + // [!code --] minLength: 8 // [!code --] }) // [!code --] }) // [!code --] @@ -309,11 +328,14 @@ const authen = (app: Elysia) => { schema: { body: 'sign', // [!code ++] - body: t.Object({ // [!code --] - email: t.String({ // [!code --] + body: t.Object({ + // [!code --] + email: t.String({ + // [!code --] format: 'email' // [!code --] }), // [!code --] - password: t.String({ // [!code --] + password: t.String({ + // [!code --] minLength: 8 // [!code --] }) // [!code --] }) // [!code --] @@ -352,33 +374,37 @@ import { cookie } from '@elysiajs/cookie' // [!code ++] import { supabase } from '../../libs' const authen = (app: Elysia) => - app.group('/auth', (app) => - app - .use( // [!code ++] - cookie({ // [!code ++] - httpOnly: true, // [!code ++] - // If you need cookie to deliver via https only // [!code ++] - // secure: true, // [!code ++] - // // [!code ++] - // If you need a cookie to be available for same-site only // [!code ++] - // sameSite: "strict", // [!code ++] - // // [!code ++] - // If you want to encrypt a cookie // [!code ++] - // signed: true, // [!code ++] - // secret: process.env.COOKIE_SECRET, // [!code ++] - }) // [!code ++] - ) // [!code ++] - .setModel({ - sign: t.Object({ - email: t.String({ - format: 'email' - }), - password: t.String({ - minLength: 8 + app.group( + '/auth', + (app) => + app + .use( + // [!code ++] + cookie({ + // [!code ++] + httpOnly: true // [!code ++] + // If you need cookie to deliver via https only // [!code ++] + // secure: true, // [!code ++] + // // [!code ++] + // If you need a cookie to be available for same-site only // [!code ++] + // sameSite: "strict", // [!code ++] + // // [!code ++] + // If you want to encrypt a cookie // [!code ++] + // signed: true, // [!code ++] + // secret: process.env.COOKIE_SECRET, // [!code ++] + }) // [!code ++] + ) // [!code ++] + .setModel({ + sign: t.Object({ + email: t.String({ + format: 'email' + }), + password: t.String({ + minLength: 8 + }) }) }) - }) - // rest of the code + // rest of the code ) ``` @@ -398,66 +424,73 @@ import { Elysia, t } from 'elysia' import { supabase } from '../../libs' const authen = (app: Elysia) => - app.group('/auth', (app) => - app - .setModel({ - sign: t.Object({ - email: t.String({ - format: 'email' - }), - password: t.String({ - minLength: 8 + app.group( + '/auth', + (app) => + app + .setModel({ + sign: t.Object({ + email: t.String({ + format: 'email' + }), + password: t.String({ + minLength: 8 + }) }) }) - }) - .post( - '/sign-up', - async ({ body }) => { - const { data, error } = await supabase.auth.signUp(body) - - if (error) return error - return data.user - }, - { - schema: { - body: 'sign' + .post( + '/sign-up', + async ({ body }) => { + const { data, error } = await supabase.auth.signUp(body) + + if (error) return error + return data.user + }, + { + schema: { + body: 'sign' + } } - } - ) - .post( - '/sign-in', - async ({ body }) => { - const { data, error } = - await supabase.auth.signInWithPassword(body) - - if (error) return error - - return data.user - }, - { - schema: { - body: 'sign' + ) + .post( + '/sign-in', + async ({ body }) => { + const { data, error } = + await supabase.auth.signInWithPassword(body) + + if (error) return error + + return data.user + }, + { + schema: { + body: 'sign' + } } - } - ) - .get( // [!code ++] - '/refresh', // [!code ++] - async ({ setCookie, cookie: { refresh_token } }) => { // [!code ++] - const { data, error } = await supabase.auth.refreshSession({ // [!code ++] - refresh_token // [!code ++] - }) // [!code ++] - // [!code ++] - if (error) return error // [!code ++] - // [!code ++] - setCookie('refresh_token', data.session!.refresh_token) // [!code ++] - // [!code ++] - return data.user // [!code ++] - } // [!code ++] - ) // [!code ++] + ) + .get( + // [!code ++] + '/refresh', // [!code ++] + async ({ setCookie, cookie: { refresh_token } }) => { + // [!code ++] + const { data, error } = + await supabase.auth.refreshSession({ + // [!code ++] + refresh_token // [!code ++] + }) // [!code ++] + // [!code ++] + if (error) return error // [!code ++] + // [!code ++] + setCookie('refresh_token', data.session!.refresh_token) // [!code ++] + // [!code ++] + return data.user // [!code ++] + } // [!code ++] + ) // [!code ++] ) ``` Finally, add routes to the main server. + ```ts import { Elysia, t } from 'elysia' @@ -542,18 +575,21 @@ export const post = (app: Elysia) => '/create', async ({ body }) => { let userId: string // [!code ++] - // [!code ++] - const { data, error } = await supabase.auth.getUser( // [!code ++] + // [!code ++] + const { data, error } = await supabase.auth.getUser( + // [!code ++] access_token // [!code ++] ) // [!code ++] - // [!code ++] - if(error) { // [!code ++] - const { data, error } = await supabase.auth.refreshSession({ // [!code ++] + // [!code ++] + if (error) { + // [!code ++] + const { data, error } = await supabase.auth.refreshSession({ + // [!code ++] refresh_token // [!code ++] }) // [!code ++] - // [!code ++] + // [!code ++] if (error) throw error // [!code ++] - // [!code ++] + // [!code ++] userId = data.user!.id // [!code ++] } // [!code ++] @@ -584,6 +620,7 @@ export const post = (app: Elysia) => Great! Now we can extract `user_id` from our cookie using **supabase.auth.getUser** ## Derive + Our code work fine for now, but let's paint a little picture. Let's say you have so many routes that require authorization like this, requiring you to extract the `userId`, it means that you will have a lot of duplicated code here, right? @@ -665,20 +702,25 @@ export const post = (app: Elysia) => .use(authen) // [!code ++] .put( '/create', - async ({ body, userId }) => { // [!code ++] + async ({ body, userId }) => { + // [!code ++] let userId: string // [!code --] - // [!code --] - const { data, error } = await supabase.auth.getUser( // [!code --] + // [!code --] + const { data, error } = await supabase.auth.getUser( + // [!code --] access_token // [!code --] ) // [!code --] - // [!code --] - if(error) { // [!code --] - const { data, error } = await supabase.auth.refreshSession({ // [!code --] - refresh_token // [!code --] - }) // [!code --] - // [!code --] + // [!code --] + if (error) { + // [!code --] + const { data, error } = + await supabase.auth.refreshSession({ + // [!code --] + refresh_token // [!code --] + }) // [!code --] + // [!code --] if (error) throw error // [!code --] - // [!code --] + // [!code --] userId = data.user!.id // [!code --] } // [!code --] @@ -703,16 +745,16 @@ export const post = (app: Elysia) => } ) ) - ``` -Great right? We don't even see that we handled the authorization by looking at the code like magic. +Great right? We don't even see that we handled the authorization by looking at the code like magic. Putting our focus back on our core business logic instead. Using Rest Client to create post ## Non-authorized scope + Now let's create one more route to fetch the post from the database. ```ts @@ -723,15 +765,17 @@ import { authen, supabase } from '../../libs' export const post = (app: Elysia) => app.group('/post', (app) => app - .get('/:id', async ({ params: { id } }) => { // [!code ++] + .get('/:id', async ({ params: { id } }) => { + // [!code ++] const { data, error } = await supabase // [!code ++] .from('post') // [!code ++] .select() // [!code ++] .eq('id', id) // [!code ++] - // [!code ++] + // [!code ++] if (error) return error // [!code ++] - // [!code ++] - return { // [!code ++] + // [!code ++] + return { + // [!code ++] success: !!data[0], // [!code ++] data: data[0] ?? null // [!code ++] } // [!code ++] @@ -773,6 +817,7 @@ If not, we are going to return `success: false` and `data: null` instead. As we mentioned before, the `.use(authen)` is applied to the scoped **but** with only the one declared after itself, which means that anything before isn't affected, and what came after is now authorized only route. And one last thing, don't forget to add routes to the main server. + ```ts import { Elysia, t } from 'elysia' @@ -788,7 +833,6 @@ console.log( ) ``` - ## Bonus: Documentation As a bonus, after all of what we create, instead of telling exactly route by route, we can create documentation for our frontend devs in 1 line. @@ -839,5 +883,5 @@ Elysia is on a journey for creating a Bun-first web framework with new technolog If you're interested in Elysia, feel free to check out our [Discord server](https://discord.gg/eaFJ2KDJck) or see [Elysia on GitHub](https://github.com/elysiajs/elysia) -Also, you might want to checkout out [Elysia Eden](/plugins/eden/overview), a fully type-safe, no-code-gen fetch client like tRPC for Elysia server. +Also, you might want to checkout out [Elysia Eden](/en/plugins/eden/overview), a fully type-safe, no-code-gen fetch client like tRPC for Elysia server. diff --git a/docs/blog/integrate-trpc-with-elysia.md b/docs/en/blog/integrate-trpc-with-elysia.md similarity index 77% rename from docs/blog/integrate-trpc-with-elysia.md rename to docs/en/blog/integrate-trpc-with-elysia.md index 237c5652..696bfd9e 100644 --- a/docs/blog/integrate-trpc-with-elysia.md +++ b/docs/en/blog/integrate-trpc-with-elysia.md @@ -4,25 +4,25 @@ sidebar: false editLink: false search: false head: - - - meta - - property: 'og:title' - content: Integrate tRPC server to Bun with Elysia + - - meta + - property: 'og:title' + content: Integrate tRPC server to Bun with Elysia - - - meta - - name: 'description' - content: 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. + - - meta + - name: 'description' + content: 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. - - - meta - - property: 'og:description' - content: 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. + - - meta + - property: 'og:description' + content: 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. - - - meta - - property: 'og:image' - content: https://elysiajs.com/blog/integrate-trpc-with-elysia/elysia-trpc.webp + - - meta + - property: 'og:image' + content: https://elysiajs.com/blog/integrate-trpc-with-elysia/elysia-trpc.webp - - - meta - - property: 'twitter:image' - content: https://elysiajs.com/blog/integrate-trpc-with-elysia/elysia-trpc.webp + - - meta + - property: 'twitter:image' + content: https://elysiajs.com/blog/integrate-trpc-with-elysia/elysia-trpc.webp --- tRPC has been a popular choice for web development recently, thanks to its end-to-end type-safety approach to accelerate development by blurring the line between front and backend, and inferring types from the backend automatically. @@ -44,6 +45,7 @@ Helping developers develop faster and safer code, knowing instantly when things But we can when extending tRPC more. ## Elysia + Elysia is a web framework optimized for Bun, inspired by many frameworks including tRPC. Elysia supports end-to-end type safety by default, but unlike tRPC, Elysia uses Express-like syntax that many already know, removing the learning curve of tRPC. With Bun being the runtime for Elysia, the speed and throughput for Elysia server are fast and even outperforming [Express up to 21x and Fastify up to 12x on mirroring JSON body (see benchmark)](https://github.com/SaltyAom/bun-http-framework-benchmark/tree/655fe7f87f0f4f73f2121433f4741a9d6cf00de4). @@ -51,13 +53,16 @@ With Bun being the runtime for Elysia, the speed and throughput for Elysia serve The ability to combine the existing tRPC server into Elysia has been one of the very first objectives of Elysia since its start. The reason why you might want to switch from tRPC to Bun: -- Significantly faster, even outperform many popular web frameworks running in Nodejs without changing a single piece of code. -- Extend tRPC with RESTful or GraphQL, both co-existing in the same server. -- Elysia has end-to-end type-safety like tRPC but with almost no-learning curve for most developer. -- Using Elysia is the great first start experimenting/investing in Bun runtime. + +- Significantly faster, even outperform many popular web frameworks running in Nodejs without changing a single piece of code. +- Extend tRPC with RESTful or GraphQL, both co-existing in the same server. +- Elysia has end-to-end type-safety like tRPC but with almost no-learning curve for most developer. +- Using Elysia is the great first start experimenting/investing in Bun runtime. ## Creating Elysia Server + To get started, let's create a new Elysia server, make sure you have [Bun](https://bun.sh) installed first, then run this command to scaffold Elysia project. + ``` bun create elysia elysia-trpc && cd elysia-trpc && bun add elysia ``` @@ -69,6 +74,7 @@ Sometimes Bun doesn't resolve the latest field correctly, so we are using `bun a This should create a folder name **"elysia-trpc"** with Elysia pre-configured. Let's start a development server by running the dev command: + ``` bun run dev ``` @@ -76,12 +82,15 @@ bun run dev This command should start a development server on port :3000 ## Elysia tRPC plugin + Building on top of the tRPC Web Standard adapter, Elysia has a plugin for integrating the existing tRPC server into Elysia. + ```bash bun add @trpc/server zod @elysiajs/trpc @elysiajs/cors ``` Suppose that this is an existing tRPC server: + ```typescript import { initTRPC } from '@trpc/server' import { observable } from '@trpc/server/observable' @@ -91,7 +100,7 @@ import { z } from 'zod' const t = initTRPC.create() export const router = t.router({ - mirror: t.procedure.input(z.string()).query(({ input }) => input), + mirror: t.procedure.input(z.string()).query(({ input }) => input) }) export type Router = typeof router @@ -100,6 +109,7 @@ export type Router = typeof router Normally all we need to use tRPC is to export the type of router, but to integrate tRPC with Elysia, we need to export the instance of router too. Then in the Elysia server, we import the router and register tRPC router with `.use(trpc)` + ```typescript import { Elysia } from 'elysia' import { cors } from '@elysiajs/cors' // [!code ++] @@ -110,22 +120,27 @@ import { router } from './trpc' // [!code ++] const app = new Elysia() .use(cors()) // [!code ++] .get('/', () => 'Hello Elysia') - .use( // [!code ++] + .use( + // [!code ++] trpc(router) // [!code ++] ) // [!code ++] .listen(3000) -console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`) +console.log( + `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` +) ``` -And that's it! 🎉 +And that's it! 🎉 That's all it takes to integrate tRPC with Elysia, making tRPC run on Bun. ## tRPC config and Context + To create context, `trpc` can accept 2nd parameters that can configure tRPC as same as `createHTTPServer`. For example, adding `createContext` into tRPC server: + ```typescript // trpc.ts import { initTRPC } from '@trpc/server' @@ -133,22 +148,25 @@ import { observable } from '@trpc/server/observable' import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch' // [!code ++] import { z } from 'zod' -export const createContext = async (opts: FetchCreateContextFnOptions) => { // [!code ++] - return { // [!code ++] +export const createContext = async (opts: FetchCreateContextFnOptions) => { + // [!code ++] + return { + // [!code ++] name: 'elysia' // [!code ++] } // [!code ++] } // [!code ++] const t = initTRPC.context>>().create() // [!code ++] -export const router = t.router({ - mirror: t.procedure.input(z.string()).query(({ input }) => input), +export const router = t.router({ + mirror: t.procedure.input(z.string()).query(({ input }) => input) }) export type Router = typeof router ``` And in the Elysia server + ```typescript import { Elysia } from 'elysia' import { cors } from '@elysiajs/cors' @@ -160,16 +178,20 @@ const app = new Elysia() .use(cors()) .get('/', () => 'Hello Elysia') .use( - trpc(router, { // [!code ++] + trpc(router, { + // [!code ++] createContext // [!code ++] }) // [!code ++] ) .listen(3000) -console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`) +console.log( + `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` +) ``` And we can specify a custom endpoint of tRPC by using `endpoint`: + ```typescript import { Elysia } from 'elysia' import { cors } from '@elysiajs/cors' @@ -188,20 +210,25 @@ const app = new Elysia() ) .listen(3000) -console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`) +console.log( + `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` +) ``` ## Subscription + By default, tRPC uses WebSocketServer to support `subscription`, but unfortunately as Bun 0.5.4 doesn't support WebSocketServer yet, we can't directly use WebSocket Server. However, Bun does support Web Socket using `Bun.serve`, and with Elysia tRPC plugin has wired all the usage of tRPC's Web Socket into `Bun.serve`, you can directly use tRPC's `subscription` with Elysia Web Socket plugin directly: Start by installing the Web Socket plugin: + ```bash bun add @elysiajs/websocket ``` Then inside tRPC server: + ```typescript import { initTRPC } from '@trpc/server' import { observable } from '@trpc/server/observable' // [!code ++] @@ -226,12 +253,16 @@ export const router = t.router({ return input }), - listen: t.procedure.subscription(() => // [!code ++] - observable((emit) => { // [!code ++] - ee.on('listen', (input) => { // [!code ++] - emit.next(input) // [!code ++] + listen: t.procedure.subscription( + () => + // [!code ++] + observable((emit) => { + // [!code ++] + ee.on('listen', (input) => { + // [!code ++] + emit.next(input) // [!code ++] + }) // [!code ++] }) // [!code ++] - }) // [!code ++] ) // [!code ++] }) @@ -239,6 +270,7 @@ export type Router = typeof router ``` And then we register: + ```typescript import { Elysia, ws } from 'elysia' import { cors } from '@elysiajs/cors' @@ -255,7 +287,9 @@ const app = new Elysia() }) .listen(3000) -console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`) +console.log( + `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` +) ``` And that's all it takes to integrate the existing fully functional tRPC server to Elysia Server thus making tRPC run on Bun 🥳. @@ -263,6 +297,7 @@ And that's all it takes to integrate the existing fully functional tRPC server t Elysia is excellent when you need both tRPC and REST API, as they can co-exist together in one server. ## Bonus: Type-Safe Elysia with Eden + As Elysia is inspired by tRPC, Elysia also supports end-to-end type-safety like tRPC by default using **"Eden"**. This means that you can use Express-like syntax to create RESTful API with full-type support on a client like tRPC. @@ -292,15 +327,19 @@ const app = new Elysia() export type App = typeof app // [!code ++] -console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`) +console.log( + `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` +) ``` And on the client side: + ```bash bun add @elysia/eden && bun add -d elysia ``` And in the code: + ```typescript import { edenTreaty } from '@elysiajs/eden' import type { App } from '../server' @@ -315,9 +354,11 @@ const data = await app.index.get() Elysia is a good start when you want end-to-end type-safety like tRPC but need to support more standard patterns like REST, and still have to support tRPC or need to migrate from one. ## Bonus: Extra tip for Elysia + An additional thing you can do with Elysia is not only that it has support for tRPC and end-to-end type-safety, but also has a variety of support for many essential plugins configured especially for Bun. -For example, you can generate documentation with Swagger only in 1 line using [Swagger plugin](/plugins/swagger). +For example, you can generate documentation with Swagger only in 1 line using [Swagger plugin](/en/plugins/swagger). + ```typescript import { Elysia, t } from 'elysia' import { swagger } from '@elysiajs/swagger' // [!code ++] @@ -341,17 +382,22 @@ const app = new Elysia() export type App = typeof app -console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`) +console.log( + `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` +) ``` -Or when you want to use [GraphQL Apollo](/plugins/graphql-apollo) on Bun. +Or when you want to use [GraphQL Apollo](/en/plugins/graphql-apollo) on Bun. + ```typescript import { Elysia } from 'elysia' import { apollo, gql } from '@elysiajs/apollo' // [!code ++] const app = new Elysia() - .use( // [!code ++] - apollo({ // [!code ++] + .use( + // [!code ++] + apollo({ + // [!code ++] typeDefs: gql` // [!code ++] type Book { // [!code ++] title: String // [!code ++] @@ -362,11 +408,16 @@ const app = new Elysia() books: [Book] // [!code ++] } // [!code ++] `, // [!code ++] - resolvers: { // [!code ++] - Query: { // [!code ++] - books: () => { // [!code ++] - return [ // [!code ++] - { // [!code ++] + resolvers: { + // [!code ++] + Query: { + // [!code ++] + books: () => { + // [!code ++] + return [ + // [!code ++] + { + // [!code ++] title: 'Elysia', // [!code ++] author: 'saltyAom' // [!code ++] } // [!code ++] @@ -381,7 +432,9 @@ const app = new Elysia() export type App = typeof app -console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`) +console.log( + `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` +) ``` Or supporting OAuth 2.0 with a [community OAuth plugin](https://github.com/bogeychan/elysia-oauth2). diff --git a/docs/blog/with-prisma.md b/docs/en/blog/with-prisma.md similarity index 88% rename from docs/blog/with-prisma.md rename to docs/en/blog/with-prisma.md index cc527b08..7b902270 100644 --- a/docs/blog/with-prisma.md +++ b/docs/en/blog/with-prisma.md @@ -12,7 +12,6 @@ head: - name: 'description' content: 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. - - - meta - property: 'og:description' content: 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. @@ -36,20 +35,21 @@ src="/blog/with-prisma/prism.webp" alt="Triangular Prism placing in the center" author="saltyaom" date="4 Jun 2023" -> -Prisma is a renowned TypeScript ORM for its developer experience. + +> Prisma is a renowned TypeScript ORM for its developer experience. With type-safe and intuitive API that allows us to interact with databases using a fluent and natural syntax. Writing a database query is as simple as writing a shape of data with TypeScript auto-completion, then Prisma takes care of the rest by generating efficient SQL queries and handling database connections in the background. One of the standout features of Prisma is its seamless integration with popular databases like: -- PostgreSQL -- MySQL -- SQLite -- SQL Server -- MongoDB -- CockroachDB + +- PostgreSQL +- MySQL +- SQLite +- SQL Server +- MongoDB +- CockroachDB So we have the flexibility to choose the database that best suits our project's needs, without compromising on the power and performance that Prisma brings to the table. @@ -84,11 +84,13 @@ bun create elysia elysia-prisma Where `elysia-prisma` is our project name (folder destination), feels free to change the name to anything you like. Now in our folder, and let's install Prisma CLI as dev dependency. + ```ts bun add -d prisma ``` Then we can setup prisma project with `prisma init` + ```ts bunx prisma init ``` @@ -97,9 +99,10 @@ bunx prisma init Once setup, we can see that Prisma will update `.env` file and generate a folder named **prisma** with **schema.prisma** as a file inside. -**schema.prisma** is an database model defined with Prisma's schema language. +**schema.prisma** is an database model defined with Prisma's schema language. Let's update our **schema.prisma** file like this for a demonstration: + ```ts generator client { provider = "prisma-client-js" @@ -120,28 +123,31 @@ model User { Telling Prisma that we want to create a table name **User** with column as: | Column | Type | Constraint | | --- | --- | --- | -| id | Number | Primary Key with auto increment | +| id | Number | Primary Key with auto increment | | username | String | Unique | | password | String | - | Prisma will then read the schema, and DATABASE_URL from an `.env` file, so before syncing our database we need to define the `DATABASE_URL` first. Since we don't have any database running, we can setup one using docker: + ```bash docker run -p 5432:5432 -e POSTGRES_PASSWORD=12345678 -d postgres ``` Now go into `.env` file at the root of our project then edit: + ``` DATABASE_URL="postgresql://postgres:12345678@localhost:5432/db?schema=public" ``` Then we can run `prisma migrate` to sync our database with Prisma schema: + ```bash bunx prisma migrate dev --name init ``` -Prisma then generate a strongly-typed Prisma Client code based on our schema. +Prisma then generate a strongly-typed Prisma Client code based on our schema. This means we get autocomplete and type checking in our code editor, catching potential errors at compile time rather than runtime. @@ -156,11 +162,14 @@ import { PrismaClient } from '@prisma/client' // [!code ++] const db = new PrismaClient() // [!code ++] const app = new Elysia() - .post( // [!code ++] + .post( + // [!code ++] '/sign-up', // [!code ++] - async ({ body }) => db.user.create({ // [!code ++] - data: body // [!code ++] - }) // [!code ++] + async ({ body }) => + db.user.create({ + // [!code ++] + data: body // [!code ++] + }) // [!code ++] ) // [!code ++] .listen(3000) @@ -180,6 +189,7 @@ As Prisma function doesn't return native Promise, Elysia can not dynamically han Now the problem is that the body could be anything, not limited to our expected defined type. We can improve that by using Elysia's type system. + ```ts import { Elysia, t } from 'elysia' // [!code ++] import { PrismaClient } from '@prisma/client' @@ -188,14 +198,18 @@ const db = new PrismaClient() const app = new Elysia() .post( - '/sign-up', - async ({ body }) => db.user.create({ - data: body - }), - { // [!code ++] - body: t.Object({ // [!code ++] + '/sign-up', + async ({ body }) => + db.user.create({ + data: body + }), + { + // [!code ++] + body: t.Object({ + // [!code ++] username: t.String(), // [!code ++] - password: t.String({ // [!code ++] + password: t.String({ + // [!code ++] minLength: 8 // [!code ++] }) // [!code ++] }) // [!code ++] @@ -209,6 +223,7 @@ console.log( ``` This tells Elysia to validate the body of an incoming request to match the shape, and update TypeScript's type of the `body` inside the callback to match the exact same type: + ```ts // 'body' is now typed as the following: { @@ -222,7 +237,9 @@ This means that if you the shape doesn't interlop with database table, it would Which is effective when you need to edit a table or perform a migration, Elysia can log the error immediately line by line because of a type conflict before reaching the production. ## Error Handling + Since our `username` field is unique, sometime Prisma can throw an error there could be an accidental duplication of `username` when trying to sign up like this: + ```ts Invalid `prisma.user.create()` invocation: @@ -230,6 +247,7 @@ Unique constraint failed on the fields: (`username`) ``` Default Elysia's error handler can handle the case automatically but we can improve that by specifying a custom error using Elysia's local `onError` hook: + ```ts import { Elysia, t } from 'elysia' import { PrismaClient } from '@prisma/client' @@ -239,19 +257,24 @@ const db = new PrismaClient() const app = new Elysia() .post( '/', - async ({ body }) => db.user.create({ - data: body - }), + async ({ body }) => + db.user.create({ + data: body + }), { - error({ code }) { // [!code ++] - switch (code) { // [!code ++] + error({ code }) { + // [!code ++] + switch ( + code // [!code ++] + ) { // Prisma P2002: "Unique constraint failed on the {constraint}" // [!code ++] - case 'P2002': // [!code ++] - return { // [!code ++] - error: 'Username must be unique' // [!code ++] - } // [!code ++] - } // [!code ++] - }, // [!code ++] + case 'P2002': // [!code ++] + return { + // [!code ++] + error: 'Username must be unique' // [!code ++] + } // [!code ++] + } // [!code ++] + }, // [!code ++] body: t.Object({ username: t.String(), password: t.String({ @@ -272,6 +295,7 @@ Using `error` hook, any error thown inside a callback will be populate to `error According to [Prisma documentation](https://www.prisma.io/docs/reference/api-reference/error-reference#p2002), error code 'P2002' means that by performing the query, it will failed a unique constraint. Since this table only a single `username` field that is unique, we can imply that the error is caused because username is not unique, so we return a custom erorr message of: + ```ts { error: 'Username must be unique' @@ -283,6 +307,7 @@ This will return a JSON equivalent of our custom error message when a unique con Allowing us to seemlessly define any custom error from Prisma error. ## Bonus: Reference Schema + When our server grow complex and type becoming more redundant and become a boilerplate, inlining an Elysia type can be improved by using **Reference Schema**. To put it simply, we can named our schema and reference the type by using the name. @@ -294,19 +319,23 @@ import { PrismaClient } from '@prisma/client' const db = new PrismaClient() const app = new Elysia() - .model({ // [!code ++] - 'user.sign': t.Object({ // [!code ++] + .model({ + // [!code ++] + 'user.sign': t.Object({ + // [!code ++] username: t.String(), // [!code ++] - password: t.String({ // [!code ++] + password: t.String({ + // [!code ++] minLength: 8 // [!code ++] }) // [!code ++] }) // [!code ++] }) // [!code ++] .post( '/', - async ({ body }) => db.user.create({ - data: body - }), + async ({ body }) => + db.user.create({ + data: body + }), { error({ code }) { switch (code) { @@ -318,9 +347,11 @@ const app = new Elysia() } }, body: 'user.sign', // [!code ++] - body: t.Object({ // [!code --] + body: t.Object({ + // [!code --] username: t.String(), // [!code --] - password: t.String({ // [!code --] + password: t.String({ + // [!code --] minLength: 8 // [!code --] }) // [!code --] }) // [!code --] @@ -338,6 +369,7 @@ This works as same as using an inline but instead you defined it once and refers TypeScript and validation code will works as expected. ## Bonus: Documentation + As a bonus, Elysia type system is also OpenAPI Schema 3.0 compliance, which means that it can generate documentation with tools that support OpenAPI Schema like Swagger. We can use Elysia Swagger plugin to generate an API documentation in a single line. @@ -362,7 +394,8 @@ const app = new Elysia() async ({ body }) => db.user.create({ data: body, - select: { // [!code ++] + select: { + // [!code ++] id: true, // [!code ++] username: true // [!code ++] } // [!code ++] @@ -383,7 +416,8 @@ const app = new Elysia() minLength: 8 }) }), - response: t.Object({ // [!code ++] + response: t.Object({ + // [!code ++] id: t.Number(), // [!code ++] username: t.String() // [!code ++] }) // [!code ++] @@ -409,6 +443,7 @@ And if anything more, we don't have to worry that we might forget a specificatio We can define our route detail with `detail` that also follows OpenAPI Schema 3.0, so we can properly create documentation effortlessly. ## What's next + With the support of Prisma with Bun and Elysia, we are entering a new era of a new level of developer experience. For Prisma we can accelerate our interaction with database, Elysia accelerate our creation of backend web server in term of both developer experience and performance. @@ -417,7 +452,7 @@ For Prisma we can accelerate our interaction with database, Elysia accelerate ou Elysia is on a journey to create a new standard for a better developer experience with Bun for high performance TypeScript server that can match the performance of Go and Rust. -If you're looking for a place to start learning about out Bun, consider take a look for what Elysia can offer especially with an [end-to-end type safety](/eden/overview) like tRPC but built on REST standard without any code generation. +If you're looking for a place to start learning about out Bun, consider take a look for what Elysia can offer especially with an [end-to-end type safety](/en/eden/overview) like tRPC but built on REST standard without any code generation. If you're interested in Elysia, feel free to check out our [Discord server](https://discord.gg/eaFJ2KDJck) or see [Elysia on GitHub](https://github.com/elysiajs/elysia) diff --git a/docs/eden/fetch.md b/docs/en/eden/fetch.md similarity index 100% rename from docs/eden/fetch.md rename to docs/en/eden/fetch.md diff --git a/docs/eden/installation.md b/docs/en/eden/installation.md similarity index 100% rename from docs/eden/installation.md rename to docs/en/eden/installation.md diff --git a/docs/eden/overview.md b/docs/en/eden/overview.md similarity index 100% rename from docs/eden/overview.md rename to docs/en/eden/overview.md diff --git a/docs/eden/test.md b/docs/en/eden/test.md similarity index 100% rename from docs/eden/test.md rename to docs/en/eden/test.md diff --git a/docs/eden/treaty.md b/docs/en/eden/treaty.md similarity index 83% rename from docs/eden/treaty.md rename to docs/en/eden/treaty.md index eb2437d2..b2a9d958 100644 --- a/docs/eden/treaty.md +++ b/docs/en/eden/treaty.md @@ -1,20 +1,21 @@ --- title: Eden Treaty - ElysiaJS head: - - - meta - - property: 'og:title' - content: Eden Treaty - ElysiaJS + - - meta + - property: 'og:title' + content: Eden Treaty - ElysiaJS - - - meta - - name: 'og:description' - content: Eden Treaty is a 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. + - - meta + - name: 'og:description' + content: Eden Treaty is a 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. - - - meta - - name: 'og:description' - content: Eden Treaty is a 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. + - - meta + - name: 'og:description' + content: Eden Treaty is a 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. --- # Eden Treaty + Eden Treaty is an object-like representation of an Elysia server. Providing accessor like a normal object with type directly from the server, helping us to move faster, and make sure that nothing break @@ -22,6 +23,7 @@ Providing accessor like a normal object with type directly from the server, help --- To use Eden Treaty, first export your existing Elysia server type: + ```typescript // server.ts import { Elysia, t } from 'elysia' @@ -41,6 +43,7 @@ export type App = typeof app // [!code ++] ``` Then import the server type, and consume the Elysia API on client: + ```typescript // client.ts import { edenTreaty } from '@elysiajs/eden' @@ -62,11 +65,13 @@ const { data: nendoroid, error } = app.mirror.post({ ``` ::: tip -Eden Treaty is fully type-safe with auto-completion support. +Eden Treaty is fully type-safe with auto-completion support. ::: ## Anatomy + Eden Treaty will transform all existing paths to object-like representation, that can be described as: + ```typescript EdenTreaty.<1>.<2>..({ ...body, @@ -76,23 +81,28 @@ EdenTreaty.<1>.<2>..({ ``` ### Path + Eden will transform `/` into `.` which can be called with a registered `method`, for example: -- **/path** -> .path -- **/nested/path** -> .nested.path + +- **/path** -> .path +- **/nested/path** -> .nested.path ### Path parameters + Path parameters will be mapped to automatically by their name in the URL. -- **/id/:id** -> .id.`` -- eg: .id.hi -- eg: .id['123'] +- **/id/:id** -> .id.`` +- eg: .id.hi +- eg: .id['123'] ::: tip If a path doesn't support path parameters, TypeScript will show an error. ::: ### Query + You can append queries to path with `$query`: + ```typescript app.get({ $query: { @@ -103,7 +113,9 @@ app.get({ ``` ### Fetch + Eden Treaty is a fetch wrapper, you can add any valid [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) parameters to Eden by passing it to `$fetch`: + ```typescript app.post({ $fetch: { @@ -115,7 +127,9 @@ app.post({ ``` ## Error Handling + Eden Treaty will return a value of `data` and `error` as a result, both fully typed. + ```typescript // response type: { id: 1895, name: 'Skadi' } const { data: nendoroid, error } = app.mirror.post({ @@ -123,8 +137,8 @@ const { data: nendoroid, error } = app.mirror.post({ name: 'Skadi' }) -if(error) { - switch(error.status) { +if (error) { + switch (error.status) { case 400: case 401: warnUser(error.value) @@ -155,6 +169,7 @@ Error is wrapped with an `Error` with its value return from the server can be re ::: ### Error type based on status + Both Eden Treaty and Eden Fetch can narrow down an error type based on status code if you explictly provided an error type in the Elysia server. ```typescript @@ -187,14 +202,15 @@ export type App = typeof app ``` An on the client side: + ```typescript const { data: nendoroid, error } = app.mirror.post({ id: 1895, name: 'Skadi' }) -if(error) { - switch(error.status) { +if (error) { + switch (error.status) { case 400: case 401: // narrow down to type 'error' described in the server @@ -212,7 +228,9 @@ if(error) { ``` ## WebSocket + Eden supports WebSocket using the same API as same as normal route. + ```typescript // Server import { Elysia, t, ws } from 'elysia' @@ -232,6 +250,7 @@ type App = typeof app ``` To start listening to real-time data, call the `.subscribe` method: + ```typescript // Client import { edenTreaty } from '@elysiajs/eden' @@ -246,7 +265,7 @@ chat.subscribe((message) => { chat.send('hello from client') ``` -We can use [schema](/essential/schema) to enforce type-safety on WebSockets, just like a normal route. +We can use [schema](/en/essential/schema) to enforce type-safety on WebSockets, just like a normal route. --- @@ -255,14 +274,17 @@ We can use [schema](/essential/schema) to enforce type-safety on WebSockets, jus If more control is need, **EdenWebSocket.raw** can be accessed to interact with the native WebSocket API. ## File Upload + You may either pass one of the following to the field to attach file: -- **File** -- **FileList** -- **Blob** + +- **File** +- **FileList** +- **Blob** Attaching a file will results **content-type** to be **multipart/form-data** Suppose we have the server as the following: + ```typescript // server.ts import { Elysia } from 'elysia' @@ -271,7 +293,7 @@ const app = new Elysia() .post('/image', ({ body: { image, title } }) => title, { body: t.Object({ title: t.String(), - image: t.Files(), + image: t.Files() }) }) .listen(3000) @@ -280,6 +302,7 @@ export type App = typeof app ``` We may use the client as follows: + ```typescript // client.ts import { edenTreaty } from '@elysia/eden' @@ -291,7 +314,7 @@ const id = (id: string) => document.getElementById(id)! as T const { data } = await client.image.post({ - title: "Misono Mika", - image: id('picture').files!, + title: 'Misono Mika', + image: id('picture').files! }) -``` \ No newline at end of file +``` diff --git a/docs/essential/context.md b/docs/en/essential/context.md similarity index 73% rename from docs/essential/context.md rename to docs/en/essential/context.md index 28418d8f..9d7928cb 100644 --- a/docs/essential/context.md +++ b/docs/en/essential/context.md @@ -1,38 +1,40 @@ --- title: Handler - ElysiaJS head: - - - meta - - property: 'og:title' - content: Handler - ElysiaJS + - - meta + - property: 'og:title' + content: Handler - ElysiaJS - - - meta - - name: 'description' - content: Context is an information of each request from the client, unique to each request with global mutable store. Context can be customize by using state, decorate and derive. + - - meta + - name: 'description' + content: Context is an information of each request from the client, unique to each request with global mutable store. Context can be customize by using state, decorate and derive. - - - meta - - property: 'og:description' - content: Context is an information of each request from the client, unique to each request with global mutable store. Context can be customize by using state, decorate and derive. + - - meta + - property: 'og:description' + content: Context is an information of each request from the client, unique to each request with global mutable store. Context can be customize by using state, decorate and derive. --- # Context -Context is information of each request passed to [route handler](/essential/handler). + +Context is information of each request passed to [route handler](/en/essential/handler). Context is unique for each request, and is not shared except it's a `store` property which is a global mutable state object, (aka state). Elysia context is consists of: -- **path** - Path name of the request -- **body** - [HTTP message](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages), form or file upload. -- **query** - [Query String](https://en.wikipedia.org/wiki/Query_string), include additional parameters for search query as JavaScript Object. (Query is extract from a value after pathname starting from '?' question mark sign) -- **params** - Elysia's path parameters parsed as JavaScript object -- **headers** - [HTTP Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers), additional information about the request like User-Agent, Content-Type, Cache Hint. -- path: Pathname of the request -- **request** - [Web Standard Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) -- **store** - A global mutable store for Elysia instance -- **cookie** - A global mutatable signal store for interacting with Cookie (including get/set) -- **set** - Property to apply to Response: - - **status** - [HTTP status](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status), default to 200 if not set. - - **headers** - Response headers - - **redirect** - Response as a path to redirect to + +- **path** - Path name of the request +- **body** - [HTTP message](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages), form or file upload. +- **query** - [Query String](https://en.wikipedia.org/wiki/Query_string), include additional parameters for search query as JavaScript Object. (Query is extract from a value after pathname starting from '?' question mark sign) +- **params** - Elysia's path parameters parsed as JavaScript object +- **headers** - [HTTP Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers), additional information about the request like User-Agent, Content-Type, Cache Hint. +- path: Pathname of the request +- **request** - [Web Standard Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) +- **store** - A global mutable store for Elysia instance +- **cookie** - A global mutatable signal store for interacting with Cookie (including get/set) +- **set** - Property to apply to Response: + - **status** - [HTTP status](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status), default to 200 if not set. + - **headers** - Response headers + - **redirect** - Response as a path to redirect to ## Extending context @@ -41,9 +43,10 @@ Because Elysia only provides essential information about the Request, you can cu Extraction of a user ID or another frequently used function related to the request, for example, into Context itself. You can extend Elysia's context by using: -- **state** - Create a global mutatable state into **Context.store** -- **decorate** - Add additional function or property assigned to **Context** -- **derive** - Add additional property based on existing property or request which is uniquely assigned to every request. + +- **state** - Create a global mutatable state into **Context.store** +- **decorate** - Add additional function or property assigned to **Context** +- **derive** - Add additional property based on existing property or request which is uniquely assigned to every request. The following APIs add extra functionality to the Context. @@ -52,6 +55,7 @@ It's recommended to assign property related to request and response, or frequent ::: ## Store + **State** is a global mutable object shared across the Elysia app. If you are familiar with frontend libraries like React, Vue, or Svelte, there's a concept of Global State Management, which is also partially implemented in Elysia via state and store. @@ -61,17 +65,17 @@ If you are familiar with frontend libraries like React, Vue, or Svelte, there's **state** is a function to assign an initial value to **store**, which could be mutated later. To assign value to `store`, you can use **Elysia.state**: + ```typescript import { Elysia } from 'elysia' -new Elysia() - .state('version', 1) - .get('/', ({ store: { version } }) => version) +new Elysia().state('version', 1).get('/', ({ store: { version } }) => version) ``` Once you call **state**, value will be added to **store** property, and can be later used after in handler. Beware that you cannot use state value before being assigned. + ```typescript import { Elysia } from 'elysia' @@ -90,6 +94,7 @@ This is the magic of the Elysia-type system that does this automatically. ::: ## Decorate + Like **store**, **decorate** assigns an additional property to **Context** directly. The only difference is that the value should be read-only and not reassigned later. @@ -110,6 +115,7 @@ new Elysia() ``` ## Derive + Like `decorate`, you can assign an additional property to **Context** directly. But instead of setting the property before the server is started. **derive** assigns a property when each request happens. Allowing us to extract a piece of information into a property instead. @@ -133,43 +139,45 @@ Because **derive** is assigned once a new request starts, **derive** can access Unlike **state**, and **decorate**. Properties which assigned by **derive** is unique and not shared with another request. ## Pattern + **state**, **decorate** offers a similar APIs pattern for assigning property to Context as the following: -- key-value -- object -- remap + +- key-value +- object +- remap Where **derive** can be only used with **remap** because it depends on existing value. ### key-value + You can use **state**, and **decorate** to assign a value using a key-value pattern. ```typescript import { Elysia } from 'elysia' -new Elysia() - .state('counter', 0) - .decorate('logger', new Logger()) +new Elysia().state('counter', 0).decorate('logger', new Logger()) ``` This pattern is great for readability for setting a single property. ### Object + Assigning multiple properties is better contained in an object for a single assignment. ```typescript import { Elysia } from 'elysia' -new Elysia() - .decorate({ - logger: new Logger(), - trace: new Trace(), - telemetry: new Telemetry() - }) +new Elysia().decorate({ + logger: new Logger(), + trace: new Trace(), + telemetry: new Telemetry() +}) ``` The object offers a less repetitive API for setting multiple values. ### Remap + Remap is a function reassignment. Allowing us to create a new value from existing value like renaming or removing a property. @@ -201,23 +209,20 @@ Using remap, Elysia will treat a returned object as a new property, removing any ::: ## Affix + To provide a smoother experience, some plugins might have a lot of property value which can be overwhelming to remap one-by-one. The **Affix** function which consists of **prefix** and **suffix**, allowing us to remap all property of an instance. ```ts -const setup = new Elysia({ name: 'setup' }) - .decorate({ - argon: 'a', - boron: 'b', - carbon: 'c' - }) +const setup = new Elysia({ name: 'setup' }).decorate({ + argon: 'a', + boron: 'b', + carbon: 'c' +}) const app = new Elysia() - .use( - setup - .prefix('decorator', 'setup') - ) + .use(setup.prefix('decorator', 'setup')) .get('/', ({ setupCarbon }) => setupCarbon) ``` @@ -226,21 +231,21 @@ Allowing us to bulk remap a property of the plugin effortlessly, preventing the By default, **affix** will handle both runtime, type-level code automatically, remapping the property to camelCase as naming convention. In some condition, you can also remap `all` property of the plugin: + ```ts const app = new Elysia() - .use( - setup - .prefix('all', 'setup') - ) + .use(setup.prefix('all', 'setup')) .get('/', ({ setupCarbon }) => setupCarbon) ``` ## Reference and value + To mutate the state, it's recommended to use **reference** to mutate rather than using an actual value. When accessing the property from JavaScript, if you define a primitive value from an object property as a new value, the reference is lost, the value is treat as new separate value instead. For example: + ```typescript const store = { counter: 0 @@ -253,6 +258,7 @@ console.log(store.counter) // ✅ 1 We can use **store.counter** to access and mutate the property. However, if we define a counter as a new value + ```typescript const store = { counter: 0 diff --git a/docs/essential/handler.md b/docs/en/essential/handler.md similarity index 100% rename from docs/essential/handler.md rename to docs/en/essential/handler.md diff --git a/docs/essential/life-cycle.md b/docs/en/essential/life-cycle.md similarity index 100% rename from docs/essential/life-cycle.md rename to docs/en/essential/life-cycle.md diff --git a/docs/essential/path.md b/docs/en/essential/path.md similarity index 93% rename from docs/essential/path.md rename to docs/en/essential/path.md index c906cc14..d0a93b95 100644 --- a/docs/essential/path.md +++ b/docs/en/essential/path.md @@ -44,7 +44,7 @@ We can categorized the URL and path as follows: If the path is not specified, the browser and web server will treat the path as '/' as a default value. ::: -Each request Elysia will lookup for [route](/essential/route) and response using [handler](/essential/handler) function. +Each request Elysia will lookup for [route](/en/essential/route) and response using [handler](/en/essential/handler) function. ## Dynamic path @@ -57,9 +57,7 @@ For instance, we can extract the user ID from the pathname, we can do something ```typescript import { Elysia } from 'elysia' -new Elysia() - .get('/id/:id', ({ params: { id } }) => id) - .listen(3000) +new Elysia().get('/id/:id', ({ params: { id } }) => id).listen(3000) ``` We create a dynamic path with `/id/:id` which tells Elysia to match any path up until `/id` and after it could be any value, which is then stored as **params** object. @@ -83,10 +81,10 @@ We refer to the named variable path as **path parameter** or **params** for shor URL segment is each path that is composed into a full path. Segment is separated by `/`. -![Representation of URL segments](/essential/url-segment.webp) +![Representation of URL segments](/en/essential/url-segment.webp) Path parameters in Elysia are represented by prefixing a segment with ':' follow by a name. -![Representation of path parameter](/essential/path-parameter.webp) +![Representation of path parameter](/en/essential/path-parameter.webp) Path parameters allow Elysia to capture a specific segment of URL. @@ -128,9 +126,7 @@ However, when you need a value of the path to be more dynamic and capture the re Wildcard can capture the value after segment regardless of amount by using "\*". ```typescript -new Elysia() - .get('/id/*', ({ params }) => params['*']) - .listen(3000) +new Elysia().get('/id/*', ({ params }) => params['*']).listen(3000) ``` Sending a request to the server should return the response as the following: diff --git a/docs/essential/plugin.md b/docs/en/essential/plugin.md similarity index 85% rename from docs/essential/plugin.md rename to docs/en/essential/plugin.md index 3f8fb53f..df2df36f 100644 --- a/docs/essential/plugin.md +++ b/docs/en/essential/plugin.md @@ -25,9 +25,7 @@ const plugin = new Elysia() .decorate('plugin', 'hi') .get('/plugin', ({ plugin }) => plugin) -const app = new Elysia() - .use(plugin) - .get('/', ({ plugin }) => plugin) +const app = new Elysia().use(plugin).get('/', ({ plugin }) => plugin) ``` We can use the plugin by passing an instance to **Elysia.use**. @@ -46,15 +44,12 @@ Using a plugin pattern, you decouple your business logic into a separate file. ```typescript // plugin.ts -export const plugin = new Elysia() - .get('/plugin', () => 'hi') +export const plugin = new Elysia().get('/plugin', () => 'hi') // main.ts import { plugin } from './plugin' -const app = new Elysia() - .use(plugin) - .listen(8080) +const app = new Elysia().use(plugin).listen(8080) ``` ## Config @@ -66,12 +61,9 @@ You can create a function that accepts parameters that may change the behavior o ```typescript import { Elysia } from 'elysia' -const version = (version = 1) => new Elysia() - .get('/version', version) +const version = (version = 1) => new Elysia().get('/version', version) -const app = new Elysia() - .use(version(1)) - .listen(8080) +const app = new Elysia().use(version(1)).listen(8080) ``` ## Functional callback​ @@ -86,14 +78,10 @@ To define a functional callback, create a function that accepts Elysia as a para const plugin = (app: Elysia) => { if ('counter' in app.store) return app - return app - .state('counter', 0) - .get('/plugin', () => 'Hi') + return app.state('counter', 0).get('/plugin', () => 'Hi') } -const app = new Elysia() - .use(plugin) - .listen(8080) +const app = new Elysia().use(plugin).listen(8080) ``` Once passed to `Elysia.use`, functional callback behaves as a normal plugin except the property is assigned directly to @@ -115,11 +103,11 @@ Elysia avoids this by differentiating the instance by using **name** and **optio ```typescript import { Elysia } from 'elysia' -const plugin = (config) => new Elysia({ +const plugin = (config) => + new Elysia({ name: 'my-plugin', // [!code ++] - seed: config, // [!code ++] - }) - .get(`${config.prefix}/hi`, () => 'Hi') + seed: config // [!code ++] + }).get(`${config.prefix}/hi`, () => 'Hi') const app = new Elysia() .use( @@ -156,6 +144,7 @@ If the provided value is class, Elysia will then try to use `.toString` method t ::: ## Service Locator + When you apply multiple state and decorators plugin to an instance, the instance will gain type safety. However, you may notice that when you are trying to use the decorated value in other instance without decorator, you may realize that the type is missing. @@ -163,9 +152,7 @@ However, you may notice that when you are trying to use the decorated value in o ```typescript import { Elysia } from 'elysia' -const main = new Elysia() - .decorate('a', 'a') - .use(child) +const main = new Elysia().decorate('a', 'a').use(child) const child = new Elysia() // ❌ 'a' is missing @@ -182,26 +169,23 @@ Simply put, we need to provide the plugin reference for Elysia to find the servi ```typescript // setup.ts -const setup = new Elysia({ name: 'setup' }) - .decorate('a', 'a') +const setup = new Elysia({ name: 'setup' }).decorate('a', 'a') // index.ts -const main = new Elysia() - .use(child) +const main = new Elysia().use(child) // child.ts -const child = new Elysia() - .use(setup) - .get('/', ({ a }) => a) +const child = new Elysia().use(setup).get('/', ({ a }) => a) ``` ## Official Plugins -You can find an officially maintained plugin at Elysia's [plugins](/plugins/overview). +You can find an officially maintained plugin at Elysia's [plugins](/en/plugins/overview). Some plugins include: -- GraphQL -- Swagger -- Server Sent Event + +- GraphQL +- Swagger +- Server Sent Event And various community plugins. diff --git a/docs/essential/route.md b/docs/en/essential/route.md similarity index 98% rename from docs/essential/route.md rename to docs/en/essential/route.md index 8024c7f6..b65be62b 100644 --- a/docs/essential/route.md +++ b/docs/en/essential/route.md @@ -188,4 +188,4 @@ When navigating to your web server, you should see the result as the following: | / | POST | Route not found :\( | | /hi | GET | Route not found :\( | -You can learn more about lifecycle and error handling in [Lifecycle Event](/essential/lifecycle-event) and [error handling](/concept/error-handling) +You can learn more about lifecycle and error handling in [Lifecycle Event](/en/essential/lifecycle-event) and [error handling](/en/concept/error-handling) diff --git a/docs/essential/schema.md b/docs/en/essential/schema.md similarity index 100% rename from docs/essential/schema.md rename to docs/en/essential/schema.md diff --git a/docs/essential/scope.md b/docs/en/essential/scope.md similarity index 100% rename from docs/essential/scope.md rename to docs/en/essential/scope.md diff --git a/docs/index.md b/docs/en/index.md similarity index 100% rename from docs/index.md rename to docs/en/index.md diff --git a/docs/integrations/astro.md b/docs/en/integrations/astro.md similarity index 100% rename from docs/integrations/astro.md rename to docs/en/integrations/astro.md diff --git a/docs/integrations/cheat-sheet.md b/docs/en/integrations/cheat-sheet.md similarity index 100% rename from docs/integrations/cheat-sheet.md rename to docs/en/integrations/cheat-sheet.md diff --git a/docs/integrations/docker.md b/docs/en/integrations/docker.md similarity index 100% rename from docs/integrations/docker.md rename to docs/en/integrations/docker.md diff --git a/docs/integrations/nextjs.md b/docs/en/integrations/nextjs.md similarity index 100% rename from docs/integrations/nextjs.md rename to docs/en/integrations/nextjs.md diff --git a/docs/introduction.md b/docs/en/introduction.md similarity index 100% rename from docs/introduction.md rename to docs/en/introduction.md diff --git a/docs/life-cycle/after-handle.md b/docs/en/life-cycle/after-handle.md similarity index 100% rename from docs/life-cycle/after-handle.md rename to docs/en/life-cycle/after-handle.md diff --git a/docs/life-cycle/before-handle.md b/docs/en/life-cycle/before-handle.md similarity index 93% rename from docs/life-cycle/before-handle.md rename to docs/en/life-cycle/before-handle.md index 0fbba39f..4fa8f215 100644 --- a/docs/life-cycle/before-handle.md +++ b/docs/en/life-cycle/before-handle.md @@ -54,7 +54,7 @@ The response should be listed as follows: ## Guard -When we need to apply the same before handle to multiple routes, we can use [guard](/new/essential/guard) to apply the same before handle to multiple routes. +When we need to apply the same before handle to multiple routes, we can use [guard](/en/new/essential/guard) to apply the same before handle to multiple routes. ```typescript import { Elysia } from 'elysia' @@ -80,11 +80,11 @@ new Elysia() ## Resolve -A "safe" version of [derive](/life-cycle/before-handle#derive). +A "safe" version of [derive](/en/life-cycle/before-handle#derive). Designed to append new value to context after validation process storing in the same stack as **beforeHandle**. -Resolve syntax is identical to [derive](/life-cycle/before-handle#derive), below is an example of retrieving a bearer header from Authorization plugin. +Resolve syntax is identical to [derive](/en/life-cycle/before-handle#derive), below is an example of retrieving a bearer header from Authorization plugin. ```typescript import { Elysia } from 'elysia' diff --git a/docs/life-cycle/map-response.md b/docs/en/life-cycle/map-response.md similarity index 100% rename from docs/life-cycle/map-response.md rename to docs/en/life-cycle/map-response.md diff --git a/docs/life-cycle/on-error.md b/docs/en/life-cycle/on-error.md similarity index 66% rename from docs/life-cycle/on-error.md rename to docs/en/life-cycle/on-error.md index 2ba03806..df737274 100644 --- a/docs/life-cycle/on-error.md +++ b/docs/en/life-cycle/on-error.md @@ -1,28 +1,31 @@ --- title: Error Handling - ElysiaJS head: - - - meta - - property: 'og:title' - content: Error Handling - ElysiaJS + - - meta + - property: 'og:title' + content: Error Handling - ElysiaJS - - - meta - - name: 'description' - content: Execute when an error is thrown in any other life-cycle at least once. Designed to capture and resolve an unexpected error, its recommended to use on Error in the following sitaution. To provide custom error message. Fail safe or an error handler or retrying a request. Logging and analytic. + - - meta + - name: 'description' + content: Execute when an error is thrown in any other life-cycle at least once. Designed to capture and resolve an unexpected error, its recommended to use on Error in the following sitaution. To provide custom error message. Fail safe or an error handler or retrying a request. Logging and analytic. - - - meta - - property: 'og:description' - content: Execute when an error is thrown in any other life-cycle at least once. Designed to capture and resolve an unexpected error, its recommended to use on Error in the following sitaution. To provide custom error message. Fail safe or an error handler or retrying a request. Logging and analytic. + - - meta + - property: 'og:description' + content: Execute when an error is thrown in any other life-cycle at least once. Designed to capture and resolve an unexpected error, its recommended to use on Error in the following sitaution. To provide custom error message. Fail safe or an error handler or retrying a request. Logging and analytic. --- # Error Handling + **On Error** is the only life-cycle event that is not always executed on each request, but only when an error is thrown in any other life-cycle at least once. Designed to capture and resolve an unexpected error, its recommended to use on Error in the following sitaution: -- To provide custom error message -- Fail safe or an error handler or retrying a request -- Logging and analytic + +- To provide custom error message +- Fail safe or an error handler or retrying a request +- Logging and analytic ## Example + Elysia catches all the errors thrown in the handler, classifies the error code, and pipes them to `onError` middleware. ```typescript @@ -44,6 +47,7 @@ It's important that `onError` must be called before the handler we want to apply ::: For example, returning custom 404 messages: + ```typescript import { Elysia, NotFoundError } from 'elysia' @@ -55,24 +59,28 @@ new Elysia() return 'Not Found :(' } }) - .post('/', () => { - throw new NotFoundError(); - }) + .post('/', () => { + throw new NotFoundError() + }) .listen(8080) ``` ## Context + `onError` Context is extends from `Context` with additional properties of the following: -- error: Error object thrown -- code: Error Code + +- error: Error object thrown +- code: Error Code ### Error Code + Elysia error code consists of: -- NOT_FOUND -- INTERNAL_SERVER_ERROR -- VALIDATION -- PARSE -- UNKNOWN + +- NOT_FOUND +- INTERNAL_SERVER_ERROR +- VALIDATION +- PARSE +- UNKNOWN By default, user thrown error code is `unknown`. @@ -81,6 +89,7 @@ If no error response is returned, the error will be returned using `error.name`. ::: ## Custom Error + Elysia supports custom error both in the type-level and implementation level. To provide a custom error code, we can use `Eylsia.error` to add a custom error code, helping us to easily classify and narrow down the error type for full type safety with auto-complete as the following: @@ -97,7 +106,7 @@ new Elysia() MyError }) .onError(({ code, error }) => { - switch(code) { + switch (code) { // With auto-completion case 'MyError': // With type narrowing @@ -105,23 +114,25 @@ new Elysia() return error } }) - .get('/', () => { - throw new MyError('Hello Error'); - }) + .get('/', () => { + throw new MyError('Hello Error') + }) ``` Properties of `error` code is based on the properties of `error`, the said properties will be used to classify the error code. ## Local Error -Same as others life-cycle, we provide an error into an [scope](/new/essential/scope) using guard: + +Same as others life-cycle, we provide an error into an [scope](/en/new/essential/scope) using guard: + ```typescript new Elysia() .get('/', () => 'Hello', { beforeHandle({ set, request: { headers } }) { - if(!isSignIn(headers)) { + if (!isSignIn(headers)) { set.status = 401 - throw new Error("Unauthorized") + throw new Error('Unauthorized') } }, error({ error }) { diff --git a/docs/life-cycle/on-response.md b/docs/en/life-cycle/on-response.md similarity index 100% rename from docs/life-cycle/on-response.md rename to docs/en/life-cycle/on-response.md diff --git a/docs/life-cycle/overview.md b/docs/en/life-cycle/overview.md similarity index 95% rename from docs/life-cycle/overview.md rename to docs/en/life-cycle/overview.md index 766a2484..9cf3831b 100644 --- a/docs/life-cycle/overview.md +++ b/docs/en/life-cycle/overview.md @@ -20,7 +20,8 @@ head: # Life Cycle -It's recommended that you have read [Essential life-cycle](/new/essential/life-cycle) for better understanding of Elysia's Life Cycle. + +It's recommended that you have read [Essential life-cycle](/en/new/essential/life-cycle) for better understanding of Elysia's Life Cycle. Life Cycle allow us to intercept an important event at the predefined point allowing us to customize the behavior of our server as need. @@ -62,6 +63,7 @@ Below are the request lifecycle available in Elysia: --- Every life-cycle could be apply at both: + 1. Local Hook (route) 2. Global Hook diff --git a/docs/life-cycle/parse.md b/docs/en/life-cycle/parse.md similarity index 100% rename from docs/life-cycle/parse.md rename to docs/en/life-cycle/parse.md diff --git a/docs/life-cycle/request.md b/docs/en/life-cycle/request.md similarity index 100% rename from docs/life-cycle/request.md rename to docs/en/life-cycle/request.md diff --git a/docs/life-cycle/trace.md b/docs/en/life-cycle/trace.md similarity index 53% rename from docs/life-cycle/trace.md rename to docs/en/life-cycle/trace.md index 8de71a16..61498c79 100644 --- a/docs/life-cycle/trace.md +++ b/docs/en/life-cycle/trace.md @@ -1,20 +1,21 @@ --- title: Trace - ElysiaJS head: - - - meta - - property: 'og:title' - content: Trace - ElysiaJS + - - meta + - property: 'og:title' + content: Trace - ElysiaJS - - - meta - - name: 'description' - 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. + - - meta + - name: 'description' + 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. - - - meta - - name: 'og:description' - 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. + - - meta + - name: 'og:description' + 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. --- # Trace + Trace is an API to measure the performance of your server. Trace allows 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. @@ -28,60 +29,63 @@ We don't want to be fast for benchmarking purposes, we want you to have a real f There are many factors that can slow down our app - and it's hard to identify them, but **trace** can helps solve that problem ## Trace + Trace can measure lifecycle execution time of each function to audit the performance bottleneck of each cycle. ```ts import { Elysia } from 'elysia' const app = new Elysia() - .trace(async ({ handle }) => { - const { time, end } = await handle + .trace(async ({ handle }) => { + const { time, end } = await handle - console.log('beforeHandle took', (await end) - time) - }) - .get('/', () => 'Hi') - .listen(3000) + console.log('beforeHandle took', (await end) - time) + }) + .get('/', () => 'Hi') + .listen(3000) ``` You can trace lifecycle of the following: -- **request** - get notified of every new request -- **parse** - array of functions to parse the body -- **transform** - transform request and context before validation -- **beforeHandle** - custom requirement to check before the main handler, can skip the main handler if response returned. -- **handle** - function assigned to the path -- **afterHandle** - map returned value into a proper response -- **error** - handle error thrown during processing request -- **response** - send a Response back to the client - -Please refers to [lifecycle event](/concept/life-cycle) for more information: + +- **request** - get notified of every new request +- **parse** - array of functions to parse the body +- **transform** - transform request and context before validation +- **beforeHandle** - custom requirement to check before the main handler, can skip the main handler if response returned. +- **handle** - function assigned to the path +- **afterHandle** - map returned value into a proper response +- **error** - handle error thrown during processing request +- **response** - send a Response back to the client + +Please refers to [lifecycle event](/en/concept/life-cycle) for more information: ![Elysia Life Cycle](/assets/lifecycle.webp) ## Children + You can tap deeper and measure each function of a life-cycle event by using the **children** property of a life-cycle event ```ts import { Elysia } from 'elysia' const sleep = (time = 1000) => - new Promise((resolve) => setTimeout(resolve, time)) + new Promise((resolve) => setTimeout(resolve, time)) const app = new Elysia() - .trace(async ({ beforeHandle }) => { - for (const child of children) { - const { time: start, end, name } = await child - - console.log(name, 'took', (await end) - start, 'ms') - } - }) - .get('/', () => 'Hi', { - beforeHandle: [ - function setup() {}, - async function delay() { - await sleep() - } - ] - }) - .listen(3000) + .trace(async ({ beforeHandle }) => { + for (const child of children) { + const { time: start, end, name } = await child + + console.log(name, 'took', (await end) - start, 'ms') + } + }) + .get('/', () => 'Hi', { + beforeHandle: [ + function setup() {}, + async function delay() { + await sleep() + } + ] + }) + .listen(3000) ``` ::: tip @@ -89,28 +93,26 @@ Every life cycle has support for children except for `handle` ::: ## Name + Measuring functions by index can be hard to trace back to the function code, that's why trace provides a **name** property to easily identify the function by name. ```ts import { Elysia } from 'elysia' const app = new Elysia() - .trace(async ({ beforeHandle }) => { - for (const child of children) { - const { name } = await child + .trace(async ({ beforeHandle }) => { + for (const child of children) { + const { name } = await child - console.log(name) + console.log(name) // setup // anonymous - } - }) - .get('/', () => 'Hi', { - beforeHandle: [ - function setup() {}, - () => {} - ] - }) - .listen(3000) + } + }) + .get('/', () => 'Hi', { + beforeHandle: [function setup() {}, () => {}] + }) + .listen(3000) ``` ::: tip @@ -118,6 +120,7 @@ If you are using an arrow function or unnamed function, **name** will become **" ::: ## Set + Inside the trace callback, you can access `Context` of the request, and can mutate the value of the request itself, for example using `set.headers` to update headers. This is useful when you need support an API like Server-Timing. @@ -128,13 +131,13 @@ This is useful when you need support an API like Server-Timing. import { Elysia } from 'elysia' const app = new Elysia() - .trace(async ({ handle, set }) => { + .trace(async ({ handle, set }) => { const { time, end } = await handle set.headers['Server-Timing'] = `handle;dur=${(await end) - time}` - }) - .get('/', () => 'Hi') - .listen(3000) + }) + .get('/', () => 'Hi') + .listen(3000) ``` ::: tip @@ -142,6 +145,7 @@ Using `set` inside `trace` can affect performance, as Elysia defers the executio ::: ## Skip + Sometimes, `beforeHandle` or handler can throw an error, skipping the execution of some lifecycles. By default if this happens, each life-cycle will be resolved automatically, and you can track if the API is executed or not by using `skip` property @@ -150,15 +154,15 @@ By default if this happens, each life-cycle will be resolved automatically, and import { Elysia } from 'elysia' const app = new Elysia() - .trace(async ({ handle, set }) => { + .trace(async ({ handle, set }) => { const { time, end, skip } = await handle console.log(skip) - }) - .get('/', () => 'Hi', { + }) + .get('/', () => 'Hi', { beforeHandle() { throw new Error("I'm a teapot") } }) - .listen(3000) + .listen(3000) ``` diff --git a/docs/life-cycle/transform.md b/docs/en/life-cycle/transform.md similarity index 100% rename from docs/life-cycle/transform.md rename to docs/en/life-cycle/transform.md diff --git a/docs/patterns/cookie-signature.md b/docs/en/patterns/cookie-signature.md similarity index 100% rename from docs/patterns/cookie-signature.md rename to docs/en/patterns/cookie-signature.md diff --git a/docs/patterns/cookie.md b/docs/en/patterns/cookie.md similarity index 65% rename from docs/patterns/cookie.md rename to docs/en/patterns/cookie.md index e60a1862..c95dd0ba 100644 --- a/docs/patterns/cookie.md +++ b/docs/en/patterns/cookie.md @@ -1,30 +1,32 @@ --- title: Reactive Cookie - ElysiaJS head: - - - meta - - property: 'og:title' - content: Reactive Cookie - ElysiaJS + - - meta + - property: 'og:title' + content: Reactive Cookie - ElysiaJS - - - meta - - name: 'description' - content: Reactive Cookie take a more modern approach like signal to handle cookie with an ergonomic API. There's no 'getCookie', 'setCookie', everything is just a cookie object. When you want to use cookie, you just extract the name and value directly. + - - meta + - name: 'description' + content: Reactive Cookie take a more modern approach like signal to handle cookie with an ergonomic API. There's no 'getCookie', 'setCookie', everything is just a cookie object. When you want to use cookie, you just extract the name and value directly. - - - meta - - property: 'og:description' - content: Reactive Cookie take a more modern approach like signal to handle cookie with an ergonomic API. There's no 'getCookie', 'setCookie', everything is just a cookie object. When you want to use cookie, you just extract the name and value directly. + - - meta + - property: 'og:description' + content: Reactive Cookie take a more modern approach like signal to handle cookie with an ergonomic API. There's no 'getCookie', 'setCookie', everything is just a cookie object. When you want to use cookie, you just extract the name and value directly. --- # Cookie + To use Cookie, you can extract the cookie property and access its name and value directly. There's no get/set, you can extract the cookie name and retrieve or update its value directly. + ```ts app.get('/', ({ cookie: { name } }) => { // Get name.value // Set - name.value = "New Value" + name.value = 'New Value' name.value = { hello: 'world' } @@ -34,6 +36,7 @@ app.get('/', ({ cookie: { name } }) => { By default, Reactive Cookie can encode/decode type of object automatically allowing us to treat cookie as an object without worrying about the encoding/decoding. **It just works**. ## Reactivity + The Elysia cookie is reactive. This means that when you change the cookie value, the cookie will be updated automatically based on approach like signal. A single source of truth for handling cookies is provided by Elysia cookies, which have the ability to automatically set headers and sync cookie values. @@ -43,14 +46,16 @@ Since cookies are Proxy-dependent objects by default, the extract value can neve We can treat the cookie jar as a regular object, iteration over it will only iterate over an already-existing cookie value. ## Cookie Attribute + To use Cookie attribute, you can either use one of the following: 1. Setting the property directly 2. Using `set` or `add` to update cookie property. -See [cookie attribute config](/patterns/cookie-signature#config) for more information. +See [cookie attribute config](/en/patterns/cookie-signature#config) for more information. ### Assign Property + You can get/set the property of a cookie as if it's a normal object, the reactivity model will sync the cookie value automatically. ```ts @@ -65,6 +70,7 @@ app.get('/', ({ cookie: { name } }) => { ``` ## set + **set** allow us to set update multiple cookie property all at once, by **reset all property** and overwrite it with a new value. ```ts @@ -77,10 +83,13 @@ app.get('/', ({ cookie: { name } }) => { ``` ## add + Like **set**, **add** allow us to update multiple cookie property at once, but instead, will only overwrite the property defined instead of resetting. ## remove + To remove a cookie, you can either use: + 1. name.remove 2. delete cookie.name @@ -93,43 +102,53 @@ app.get('/', ({ cookie, cookie: { name } }) => { ``` ## Cookie Schema + You can strictly validate cookie type and providing type inference for cookie by using cookie schema with `t.Cookie`. ```ts -app.get('/', ({ cookie: { name } }) => { - // Set - name.value = { - id: 617, - name: 'Summoning 101' - } -}, { - cookie: t.Cookie({ - name: t.Object({ - id: t.Numeric(), - name: t.String() +app.get( + '/', + ({ cookie: { name } }) => { + // Set + name.value = { + id: 617, + name: 'Summoning 101' + } + }, + { + cookie: t.Cookie({ + name: t.Object({ + id: t.Numeric(), + name: t.String() + }) }) - }) -}) + } +) ``` ## Nullable Cookie + To handle nullable cookie value, you can use `t.Optional` on cookie name you want to be nullable. ```ts -app.get('/', ({ cookie: { name } }) => { - // Set - name.value = { - id: 617, - name: 'Summoning 101' +app.get( + '/', + ({ cookie: { name } }) => { + // Set + name.value = { + id: 617, + name: 'Summoning 101' + } + }, + { + cookie: t.Cookie({ + value: t.Optional( + t.Object({ + id: t.Numeric(), + name: t.String() + }) + ) + }) } -}, { - cookie: t.Cookie({ - value: t.Optional( - t.Object({ - id: t.Numeric(), - name: t.String() - }) - ) - }) -}) +) ``` diff --git a/docs/patterns/documentation.md b/docs/en/patterns/documentation.md similarity index 51% rename from docs/patterns/documentation.md rename to docs/en/patterns/documentation.md index ba5b04a1..ef660e43 100644 --- a/docs/patterns/documentation.md +++ b/docs/en/patterns/documentation.md @@ -1,62 +1,64 @@ --- title: Creating Documentation - ElysiaJS head: - - - meta - - property: 'og:title' - content: Creating Documentation - ElysiaJS + - - meta + - property: 'og:title' + content: Creating Documentation - ElysiaJS - - - meta - - name: 'description' - 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. + - - meta + - name: 'description' + 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. - - - meta - - property: 'og:description' - 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. + - - meta + - property: 'og:description' + 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. --- # Creating Documentation + 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. To generate the Swagger page, install the plugin: + ```bash bun add @elysiajs/swagger ``` And register the plugin to the server: + ```typescript import { Elysia } from 'elysia' import { swagger } from '@elysiajs/swagger' -const app = new Elysia() - .use(swagger()) +const app = new Elysia().use(swagger()) ``` -For more information about Swagger plugin, see the [Swagger plugin page](/plugins/swagger). +For more information about Swagger plugin, see the [Swagger plugin page](/en/plugins/swagger). ## Route definitions + `schema` is used to customize the route definition, not only that it will generate an OpenAPI schema and Swagger definitions, but also type validation, type-inference and auto-completion. However, sometime defining a type only isn't clear what the route might work. You can use `schema.detail` fields to explictly define what the route is all about. ```typescript -app - .post('/sign-in', ({ body }) => body, { - body: t.Object( - { - username: t.String(), - password: t.String() - }, - { - description: 'Expected an username and password' - } - ), - detail: { - summary: 'Sign in the user', - tags: ['authentication'] +app.post('/sign-in', ({ body }) => body, { + body: t.Object( + { + username: t.String(), + password: t.String() + }, + { + description: 'Expected an username and password' } - }) + ), + detail: { + summary: 'Sign in the user', + tags: ['authentication'] + } +}) ``` The detail fields follows an OpenAPI V3 definition with auto-completion and type-safety by default. diff --git a/docs/patterns/group.md b/docs/en/patterns/group.md similarity index 98% rename from docs/patterns/group.md rename to docs/en/patterns/group.md index a5a78c43..bea18be7 100644 --- a/docs/patterns/group.md +++ b/docs/en/patterns/group.md @@ -60,7 +60,7 @@ new Elysia() .listen(3000) ``` -You may find more information about groupped guard in [scope](/essential/scope.html). +You may find more information about groupped guard in [scope](/en/essential/scope.html). ## Prefix diff --git a/docs/patterns/lazy-loading-module.md b/docs/en/patterns/lazy-loading-module.md similarity index 100% rename from docs/patterns/lazy-loading-module.md rename to docs/en/patterns/lazy-loading-module.md diff --git a/docs/patterns/macro.md b/docs/en/patterns/macro.md similarity index 100% rename from docs/patterns/macro.md rename to docs/en/patterns/macro.md diff --git a/docs/patterns/mount.md b/docs/en/patterns/mount.md similarity index 100% rename from docs/patterns/mount.md rename to docs/en/patterns/mount.md diff --git a/docs/patterns/unit-test.md b/docs/en/patterns/unit-test.md similarity index 94% rename from docs/patterns/unit-test.md rename to docs/en/patterns/unit-test.md index 490b208d..de983134 100644 --- a/docs/patterns/unit-test.md +++ b/docs/en/patterns/unit-test.md @@ -47,7 +47,7 @@ Then we can perform tests by running **bun test** bun test ``` -New requests to an Elysia server must be a fully valid URL, **NOT** a part of a URL. +New requests to an Elysia server must be a fully valid URL, **NOT** a part of a URL. The request must provide URL as the following: @@ -59,6 +59,7 @@ The request must provide URL as the following: We can also use other testing libraries like Jest or testing library to create Elysia unit tests. ## Eden Test + We can simplify the tests by using Eden Treaty to create a unit-test with support for end-to-end type safety and auto-completion. ```typescript @@ -67,9 +68,7 @@ import { describe, expect, it } from 'bun:test' import { edenTreaty } from '@elysiajs/eden' -const app = new Elysia() - .get('/', () => 'hi') - .listen(3000) +const app = new Elysia().get('/', () => 'hi').listen(3000) const api = edenTreaty('http://localhost:3000') @@ -82,4 +81,4 @@ describe('Elysia', () => { }) ``` -See [Eden Test](/eden/test) for setup and more information. +See [Eden Test](/en/eden/test) for setup and more information. diff --git a/docs/patterns/websocket.md b/docs/en/patterns/websocket.md similarity index 100% rename from docs/patterns/websocket.md rename to docs/en/patterns/websocket.md diff --git a/docs/plugins/bearer.md b/docs/en/plugins/bearer.md similarity index 100% rename from docs/plugins/bearer.md rename to docs/en/plugins/bearer.md diff --git a/docs/plugins/cors.md b/docs/en/plugins/cors.md similarity index 100% rename from docs/plugins/cors.md rename to docs/en/plugins/cors.md diff --git a/docs/plugins/cron.md b/docs/en/plugins/cron.md similarity index 100% rename from docs/plugins/cron.md rename to docs/en/plugins/cron.md diff --git a/docs/plugins/graphql-apollo.md b/docs/en/plugins/graphql-apollo.md similarity index 100% rename from docs/plugins/graphql-apollo.md rename to docs/en/plugins/graphql-apollo.md diff --git a/docs/plugins/graphql-yoga.md b/docs/en/plugins/graphql-yoga.md similarity index 100% rename from docs/plugins/graphql-yoga.md rename to docs/en/plugins/graphql-yoga.md diff --git a/docs/plugins/html.md b/docs/en/plugins/html.md similarity index 100% rename from docs/plugins/html.md rename to docs/en/plugins/html.md diff --git a/docs/plugins/jwt.md b/docs/en/plugins/jwt.md similarity index 100% rename from docs/plugins/jwt.md rename to docs/en/plugins/jwt.md diff --git a/docs/en/plugins/overview.md b/docs/en/plugins/overview.md new file mode 100644 index 00000000..ebcf64d9 --- /dev/null +++ b/docs/en/plugins/overview.md @@ -0,0 +1,78 @@ +--- +title: Plugin Overview - ElysiaJS +head: + - - meta + - property: 'og:title' + content: Swagger Plugin - ElysiaJS + + - - meta + - name: 'description' + content: Elysia is designed to be modular and lightweight, which is why Elysia includes pre-built plugins involving common patterns for convenient developer usage. Elysia is enhanced by community plugins which customize it even further. + + - - meta + - name: 'og:description' + content: Elysia is designed to be modular and lightweight, which is why Elysia includes pre-built plugins involving common patterns for convenient developer usage. Elysia is enhanced by community plugins which customize it even further. +--- + +# Overview + +Elysia is designed to be modular and lightweight. + +Following the same idea as Arch Linux (btw, I use Arch): + +> Design decisions are made on a case-by-case basis through developer consensus + +This is to ensure developers end up with a performant web server they intend to create. By extension, Elysia includes pre-built common pattern plugins for convenient developer usage: + +## Official plugins: + +- [Bearer](/en/plugins/bearer) - retrieve [Bearer](https://swagger.io/docs/specification/authentication/bearer-authentication/) token automatically +- [CORS](/en/plugins/cors) - set up [Cross-origin resource sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) +- [Cron](/en/plugins/cron) - set up [cron](https://en.wikipedia.org/wiki/Cron) job +- [Eden](/en/plugins/eden/overview) - end-to-end type safety client for Elysia +- [GraphQL Apollo](/en/plugins/graphql-apollo) - run [Apollo GraphQL](https://www.apollographql.com/) on Elysia +- [GraphQL Yoga](/en/plugins/graphql-yoga) - run [GraphQL Yoga](https://github.com/dotansimha/graphql-yoga) on Elysia +- [HTML](/en/plugins/html) - handle HTML responses +- [JWT](/en/plugins/jwt) - authenticate with [JWTs](https://jwt.io/) +- [Server Timing](/en/plugins/server-timing) - audit performance bottlenecks with the [Server-Timing API](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing) +- [Static](/en/plugins/static) - serve static files/folders +- [Stream](/en/plugins/stream) - integrate response streaming and [server-sent events (SSEs)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) +- [Swagger](/en/plugins/swagger) - generate [Swagger](https://swagger.io/) documentation +- [tRPC](/en/plugins/trpc) - support [tRPC](https://trpc.io/) +- [WebSocket](/en/patterns/websocket) - support [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) + +## Community plugins: + +- [Lucia Auth](https://github.com/pilcrowOnPaper/lucia) - authentication, simple and clean +- [Elysia Clerk](https://github.com/wobsoriano/elysia-clerk) - unofficial Clerk authentication plugin +- [Elysia Polyfills](https://github.com/bogeychan/elysia-polyfills) - run Elysia ecosystem on Node.js and Deno +- [Vite](https://github.com/timnghg/elysia-vite) - serve entry HTML file with Vite's scripts injected +- [Nuxt](https://github.com/trylovetom/elysiajs-nuxt) - easily integrate elysia with nuxt! +- [Elysia Helmet](https://github.com/DevTobias/elysia-helmet) - secure Elysia apps with various HTTP headers +- [Vite Plugin SSR](https://github.com/timnghg/elysia-vite-plugin-ssr) - Vite SSR plugin using Elysia server +- [OAuth2](https://github.com/bogeychan/elysia-oauth2) - handle OAuth 2.0 authorization code flow +- [Rate Limit](https://github.com/rayriffy/elysia-rate-limit) - simple, lightweight rate limiter +- [Logysia](https://github.com/tristanisham/logysia) - classic logging middleware +- [Logger](https://github.com/bogeychan/elysia-logger) - [pino](https://github.com/pinojs/pino)-based logging middleware +- [Elysia Lambda](https://github.com/TotalTechGeek/elysia-lambda) - deploy on AWS Lambda +- [Decorators](https://github.com/gaurishhs/elysia-decorators) - use TypeScript decorators +- [Autoroutes](https://github.com/wobsoriano/elysia-autoroutes) - filesystem routes +- [Group Router](https://github.com/itsyoboieltr/elysia-group-router) - filesystem and folder-based router for groups +- [Basic Auth](https://github.com/itsyoboieltr/elysia-basic-auth) - basic HTTP authentication +- [ETag](https://github.com/bogeychan/elysia-etag) - automatic HTTP [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) generation +- [Basic Auth](https://github.com/eelkevdbos/elysia-basic-auth) - basic HTTP authentication (using `request` event) +- [i18n](https://github.com/eelkevdbos/elysia-i18next) - [i18n](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n) wrapper based on [i18next](https://www.i18next.com/) +- [Elysia Request ID](https://github.com/gtramontina/elysia-requestid) - add/forward request IDs (`X-Request-ID` or custom) +- [Elysia HTMX](https://github.com/gtramontina/elysia-htmx) - context helpers for [HTMX](https://htmx.org/) +- [Elysia HMR HTML](https://github.com/gtrabanco/elysia-hmr-html) - reload HTML files when changing any file in a directory +- [Elysia Inject HTML](https://github.com/gtrabanco/elysia-inject-html) - inject HTML code in HTML files +- [Elysia HTTP Error](https://github.com/yfrans/elysia-http-error) - return HTTP errors from Elysia handlers +- [Elysia Http Status Code](https://github.com/sylvain12/elysia-http-status-code) - integrate HTTP status codes +- [NoCache](https://github.com/gaurishhs/elysia-nocache) - disable caching +- [Elysia Tailwind](https://github.com/gtramontina/elysia-tailwind) - compile [Tailwindcss](https://tailwindcss.com/) in a plugin. +- [Elysia Compression](https://github.com/gusb3ll/elysia-compression) - compress response +- [Elysia IP](https://github.com/gaurishhs/elysia-ip) - get the IP Address + +--- + +If you have a plugin written for Elysia, feels free to add your plugin to the list by **clicking Edit this page on GitHub** below 👇 diff --git a/docs/plugins/server-timing.md b/docs/en/plugins/server-timing.md similarity index 78% rename from docs/plugins/server-timing.md rename to docs/en/plugins/server-timing.md index 55052f1e..ebca6754 100644 --- a/docs/plugins/server-timing.md +++ b/docs/en/plugins/server-timing.md @@ -15,14 +15,17 @@ head: --- # Server Timing Plugin + This plugin add support for auditing performance bottleneck with Server Timing API Install with: + ```bash bun add @elysiajs/server-timing ``` Then use it: + ```typescript import { Elysia } from 'elysia' import { serverTiming } from '@elysiajs/server-timing' @@ -42,50 +45,56 @@ To inspect, open browser developer tools > Network > [Request made through Elysi Now you can effortlessly audit performance bottleneck of your server. ## Config + Below is a config which is accepted by the plugin ### enabled + @default `NODE_ENV !== 'production'` Determine whether or not Server Timing should be enabled ### allow + @default `undefined` A condition whether server timing should be log ### trace + @default `undefined` Allow Server Timing to log specified life-cycle events: Trace accepts object of the following: -- request: capture duration from request -- parse: capture duration from parse -- transform: capture duration from transform -- beforeHandle: capture duration from beforeHandle -- handle: capture duration from handle -- afterHandle: capture duration from afterHandle -- total: capture total duration from start to finish + +- request: capture duration from request +- parse: capture duration from parse +- transform: capture duration from transform +- beforeHandle: capture duration from beforeHandle +- handle: capture duration from handle +- afterHandle: capture duration from afterHandle +- total: capture total duration from start to finish ## Pattern + Below you can find the common patterns to use the plugin. -- [Allow Condition](#allow-condition) +- [Allow Condition](#allow-condition) ## Allow Condition + You may disabled Server Timing on specific route via `allow` property ```ts import { Elysia } from 'elysia' import { serverTiming } from '@elysiajs/server-timing' -new Elysia() - .use( - serverTiming({ - allow: ({ request }) => { - return new URL(request.url).pathname !== '/no-trace' - } - }) - ) -``` \ No newline at end of file +new Elysia().use( + serverTiming({ + allow: ({ request }) => { + return new URL(request.url).pathname !== '/no-trace' + } + }) +) +``` diff --git a/docs/plugins/static.md b/docs/en/plugins/static.md similarity index 100% rename from docs/plugins/static.md rename to docs/en/plugins/static.md diff --git a/docs/plugins/stream.md b/docs/en/plugins/stream.md similarity index 100% rename from docs/plugins/stream.md rename to docs/en/plugins/stream.md diff --git a/docs/plugins/swagger.md b/docs/en/plugins/swagger.md similarity index 100% rename from docs/plugins/swagger.md rename to docs/en/plugins/swagger.md diff --git a/docs/plugins/trpc.md b/docs/en/plugins/trpc.md similarity index 100% rename from docs/plugins/trpc.md rename to docs/en/plugins/trpc.md diff --git a/docs/quick-start.md b/docs/en/quick-start.md similarity index 100% rename from docs/quick-start.md rename to docs/en/quick-start.md diff --git a/docs/table-of-content.md b/docs/en/table-of-content.md similarity index 100% rename from docs/table-of-content.md rename to docs/en/table-of-content.md diff --git a/docs/validation/elysia-type.md b/docs/en/validation/elysia-type.md similarity index 100% rename from docs/validation/elysia-type.md rename to docs/en/validation/elysia-type.md diff --git a/docs/validation/error-provider.md b/docs/en/validation/error-provider.md similarity index 68% rename from docs/validation/error-provider.md rename to docs/en/validation/error-provider.md index f7813fab..22b76b01 100644 --- a/docs/validation/error-provider.md +++ b/docs/en/validation/error-provider.md @@ -19,26 +19,27 @@ head: There are 2 ways to provide a custom error message when the validation failed: 1. inline `message` property -2. Using [onError](/life-cycle/on-error) event +2. Using [onError](/en/life-cycle/on-error) event ## Message Property + TypeBox offers an additional "**error**" property, allowing us to return a custom error message if the field is invalid. ```typescript import { Elysia, t } from 'elysia' new Elysia() - .get('/', () => 'Hello World!', { - body: t.Object( - { - x: t.Number() - }, - { - error: 'x must be a number' - } - ) - }) - .listen(3000) + .get('/', () => 'Hello World!', { + body: t.Object( + { + x: t.Number() + }, + { + error: 'x must be a number' + } + ) + }) + .listen(3000) ``` The following are an example of usage of the error property on various types: @@ -73,12 +74,9 @@ Invalid Email :( ```typescript -t.Array( - t.String(), - { - error: 'All members must be a string' - } -) +t.Array(t.String(), { + error: 'All members must be a string' +}) ``` @@ -95,11 +93,14 @@ All members must be a string ```typescript -t.Object({ - x: t.Number() -}, { - error: 'Invalid object UwU' -}) +t.Object( + { + x: t.Number() + }, + { + error: 'Invalid object UwU' + } +) ``` @@ -116,17 +117,16 @@ Invalid object UwU ## onError -We can customize the behavior of validation based on [onError](/new/lifecycle/on-error) event by narrowing down the error code call "**VALIDATION**". +We can customize the behavior of validation based on [onError](/en/new/lifecycle/on-error) event by narrowing down the error code call "**VALIDATION**". ```typescript import { Elysia, t } from 'elysia' new Elysia() - .onError(({ code, error }) => { - if (code === 'VALIDATION') - return error.message - }) - .listen(3000) + .onError(({ code, error }) => { + if (code === 'VALIDATION') return error.message + }) + .listen(3000) ``` Narrowed down error type, will be typed as `ValidationError` imported from 'elysia/error'. @@ -137,40 +137,40 @@ Narrowed down error type, will be typed as `ValidationError` imported from 'elys import { Elysia, t } from 'elysia' new Elysia() - .onError(({ code, error }) => { - if (code === 'VALIDATION') - return error.validator.Errors(error.value).First().message - }) - .listen(3000) + .onError(({ code, error }) => { + if (code === 'VALIDATION') + return error.validator.Errors(error.value).First().message + }) + .listen(3000) ``` ## Error list + **ValidationError** provides a method `ValidatorError.all`, allowing us to list all of the error causes. ```typescript import { Elysia, t } from 'elysia' new Elysia() - .post('/', ({ body }) => body, { - body: t.Object({ - name: t.String(), - age: t.Number() - }), - error({ code, error }) { - switch (code) { - case 'VALIDATION': + .post('/', ({ body }) => body, { + body: t.Object({ + name: t.String(), + age: t.Number() + }), + error({ code, error }) { + switch (code) { + case 'VALIDATION': console.log(error.all) // Find a specific error name (path is OpenAPI Schema compliance) - const name = error.all.find((x) => x.path === '/name') + const name = error.all.find((x) => x.path === '/name') // If has a validation error, then log it - if(name) - console.log(name) - } - } - }) - .listen(3000) + if (name) console.log(name) + } + } + }) + .listen(3000) ``` For more information about TypeBox's validator, see [TypeCheck](https://github.com/sinclairzx81/typebox#typecheck) diff --git a/docs/validation/overview.md b/docs/en/validation/overview.md similarity index 100% rename from docs/validation/overview.md rename to docs/en/validation/overview.md diff --git a/docs/validation/primitive-type.md b/docs/en/validation/primitive-type.md similarity index 91% rename from docs/validation/primitive-type.md rename to docs/en/validation/primitive-type.md index 508eeddd..3f29c237 100644 --- a/docs/validation/primitive-type.md +++ b/docs/en/validation/primitive-type.md @@ -107,9 +107,7 @@ boolean ```typescript -t.Array( - t.Number() -) +t.Array(t.Number()) ``` @@ -184,6 +182,7 @@ Elysia extends all type from TypeBox allowing you to reference most of the API f See [TypeBox's Type](https://github.com/sinclairzx81/typebox#json-types) for additional types that are supported by TypeBox. ## Attribute + TypeBox can accept an argument for more comprehensive behavior based on JSON Schema 7 specification. @@ -235,26 +234,23 @@ t.Number({ @@ -265,18 +261,18 @@ t.Array( ```typescript t.Object( - { - x: t.Number() - }, - { - /** - * @default false - * Accept additional properties - * that not specified in schema - * but still match the type - */ - additionalProperties: true - } + { + x: t.Number() + }, + { + /** + * @default false + * Accept additional properties + * that not specified in schema + * but still match the type + */ + additionalProperties: true + } ) ``` @@ -296,12 +292,15 @@ y: 200 See [JSON Schema 7 specification](https://json-schema.org/draft/2020-12/json-schema-validation) For more explaination for each attribute. --- +
# Honorable Mention + The following are common patterns that are often found useful when creating a schema. ## Union + Allow multiple types via union.
```typescript -t.Array( - t.Number(), - { - /** - * Minimum number of items - */ - minItems: 1, - /** - * Maximum number of items - */ - maxItems: 5 - } -) +t.Array(t.Number(), { + /** + * Minimum number of items + */ + minItems: 1, + /** + * Maximum number of items + */ + maxItems: 5 +}) ``` ```typescript -[1,2,3,4,5] +;[1, 2, 3, 4, 5] ```
@@ -315,10 +314,7 @@ Allow multiple types via union. @@ -343,6 +339,7 @@ Hello
```typescript -t.Union([ - t.String(), - t.Number() -]) +t.Union([t.String(), t.Number()]) ```
## Optional + Provided in a property of `t.Object`, allowing the field to be undefined or optional. @@ -388,6 +385,7 @@ t.Object({
## Partial + Allowing all of the field in `t.Object` to be optional. @@ -468,11 +466,14 @@ Invalid Email :( diff --git a/docs/validation/reference-model.md b/docs/en/validation/reference-model.md similarity index 100% rename from docs/validation/reference-model.md rename to docs/en/validation/reference-model.md diff --git a/docs/validation/schema-type.md b/docs/en/validation/schema-type.md similarity index 97% rename from docs/validation/schema-type.md rename to docs/en/validation/schema-type.md index 2fa7a5f9..d6bc4cfa 100644 --- a/docs/validation/schema-type.md +++ b/docs/en/validation/schema-type.md @@ -14,7 +14,6 @@ head: content: Elysia supports declarative schema with the following types. Body for validate an incoming HTTP message. Query for query string or URL parameter. Params for path parameters. Header for request headers. Cookie for cookies. Response for validating response. --- - Named after Camellia's song[「大地の閾を探して [Looking for Edge of Ground]」](https://youtu.be/oyJf72je2U0)ft. Hatsune Miku, is the last track of my most favorite's Camellia album,「U.U.F.O」. This song has a high impact on me personally, so I'm not taking the name lightly. @@ -45,7 +44,6 @@ This is the most challenging update, bringing the biggest release of Elysia yet, I'm pleased to announce the release candidate of Elysia 0.3 with exciting new features coming right up. ## Elysia Fn - Introducing Elysia Fn, run any backend function on the frontend with full auto-completion and full type support. + \ No newline at end of file diff --git a/docs/blog/elysia-05.md b/docs/blog/elysia-05.md index 85a03b5f..b78c0147 100644 --- a/docs/blog/elysia-05.md +++ b/docs/blog/elysia-05.md @@ -30,12 +30,11 @@ head: Named after Arknights' original music, 「[Radiant](https://youtu.be/QhUjD--UUV4)」composed by Monster Sirent Records. @@ -43,7 +42,6 @@ Named after Arknights' original music, 「[Radiant](https://youtu.be/QhUjD--UUV4 Radiant push the boundary of performance with more stability improvement especially types, and dynamic routes. ## Static Code Analysis - With Elysia 0.4 introducing Ahead of Time compliation, allowing Elysia to optimize function calls, and eliminate many over head we previously had. Today we are expanding Ahead of Time compliation to be even faster wtih Static Code Analysis, to be the fastest Bun web framework. @@ -86,12 +84,11 @@ app.post('/id/:id', ({ params: { id } }) => id, { With Static Code Analysis, and Ahead of Time compilation, you can rest assure that Elysia is very good at reading your code and adjust itself to maximize the performance automatically. Static Code Analysis allows us to improve Elysia performance beyond we have imagined, here's a notable mention: - -- overall improvement by ~15% -- static router fast ~33% -- empty query parsing ~50% -- strict type body parsing faster by ~100% -- empty body parsing faster by ~150% +- overall improvement by ~15% +- static router fast ~33% +- empty query parsing ~50% +- strict type body parsing faster by ~100% +- empty body parsing faster by ~150% With this improvement, we are able to surpass **Stricjs** in term of performance, compared using Elysia 0.5.0-beta.0 and Stricjs 2.0.4 @@ -121,7 +118,7 @@ TypeBox is a core library that powered Elysia's strict type system known as **El In this update, we update TypeBox from 0.26 to 0.28 to make even more fine-grained Type System near strictly typed language. -We update Typebox to improve Elysia typing system to match new TypeBox feature with newer version of TypeScript like **Constant Generic** +We update Typebox to improve Elysia typing system to match new TypeBox feature with newer version of TypeScript like **Constant Generic** ```ts new Elysia() @@ -130,7 +127,10 @@ new Elysia() 'name', Type.TemplateLiteral([ Type.Literal('Elysia '), - Type.Union([Type.Literal('The Blessing'), Type.Literal('Radiant')]) + Type.Union([ + Type.Literal('The Blessing'), + Type.Literal('Radiant') + ]) ]) ) // Strictly check for template literal @@ -148,14 +148,12 @@ That's why we introduced a new Type, **URLEncoded**. As we previously mentioned before, Elysia now can take an advantage of schema and optimize itself Ahead of Time, body parsing is one of more expensive area in Elysia, that's why we introduce a dedicated type for parsing body like URLEncoded. By default, Elysia will parse body based on body's schema type as the following: - -- t.URLEncoded -> `application/x-www-form-urlencoded` -- t.Object -> `application/json` -- t.File -> `multipart/form-data` -- the rest -> `text/plain` +- t.URLEncoded -> `application/x-www-form-urlencoded` +- t.Object -> `application/json` +- t.File -> `multipart/form-data` +- the rest -> `text/plain` However, you can explictly tells Elysia to parse body with the specific method using `type` as the following: - ```ts app.post('/', ({ body }) => body, { type: 'json' @@ -163,7 +161,6 @@ app.post('/', ({ body }) => body, { ``` `type` may be one of the following: - ```ts type ContentType = | // Shorthand for 'text/plain' @@ -183,13 +180,11 @@ type ContentType = | You can find more detail at the [explicit body](/concept/explicit-body) page in concept. ### Numeric Type - We found that one of the redundant task our developers found using Elysia is to parse numeric string. That's we introduce a new **Numeric** Type. Previously on Elysia 0.4, to parse numeric string, we need to use `transform` to manually parse the string ourself. - ```ts app.get('/id/:id', ({ params: { id } }) => id, { schema: { @@ -200,14 +195,15 @@ app.get('/id/:id', ({ params: { id } }) => id, { transform({ params }) { const id = +params.id - if (!Number.isNaN(id)) params.id = id + if(!Number.isNaN(id)) + params.id = id } }) ``` We found that this step is redundant, and full of boiler-plate, we want to tap into this problem and solve it in a declarative way. -Thanks to Static Code Analysis, Numeric type allow you to defined a numeric string and parse it to number automatically. +Thanks to Static Code Analysis, Numeric type allow you to defined a numeric string and parse it to number automatically. Once validated, a numeric type will be parsed as number automatically both on runtime and type level to fits our need. @@ -220,12 +216,11 @@ app.get('/id/:id', ({ params: { id } }) => id, { ``` You can use numeric type on any property that support schema typing, including: - -- params -- query -- headers -- body -- response +- params +- query +- headers +- body +- response We hope that you will find this new Numeric type useful in your server. @@ -234,7 +229,6 @@ You can find more detail at [numeric type](/concept/numeric) page in concept. With TypeBox 0.28, we are making Elysia type system we more complete, and we excited to see how it play out on your end. ## Inline Schema - You might have notice already that our example are not using a `schema.type` to create a type anymore, because we are making a **breaking change** to move schema and inline it to hook statement instead. ```ts @@ -244,7 +238,7 @@ app.get('/id/:id', ({ params: { id } }) => id, { params: t.Object({ id: t.Number() }) - } + }, }) // ? To @@ -262,7 +256,6 @@ Based on a lot of tinkering and real-world usage, we try to suggest this new cha But we also listen the the rest of our community, and try to get around with the argument against this decision: ### Clear separation - With the old syntax, you have to explicitly tells Elysia that the part you are creating are a schema using `Elysia.t`. Creating a clear separation between life-cycle and schema are more clear and has a better readability. @@ -270,7 +263,6 @@ Creating a clear separation between life-cycle and schema are more clear and has But from our intense test, we found that most people don't have any problem struggling reading a new syntax, separating life-cycle hook from schema type, we found that it still has clear separation with `t.Type` and function, and a different syntax highlight when reviewing the code, although not as good as clear as explicit schema, but people can get used to the new syntax very quickly especially if they are familiar the Elysia. ### Auto completion - One of the other area that people are concerned about are reading auto-completion. Merging schema and life-cycle hook caused the auto-completion to have around 10 properties for auto-complete to suggest, and based on many proven general User Experience research, it can be frastating for user to that many options to choose from, and can cause a steeper learning curve. @@ -282,7 +274,6 @@ For example, if you want to access a headers, you can acceess `headers` in Conte With this, Elysia might have a little more learning curve, however it's a trade-off that we are willing to take for better developer experience. ## "headers" fields - Previously, you can get headers field by accessing `request.headers.get`, and you might wonder why we don't ship headers by default. ```ts @@ -304,7 +295,6 @@ app.post('/headers', ({ headers }) => headers['content-type']) Parsed headers will be available as plain object with a lower-case key of the header name. ## State, Decorate, Model rework - One of the main feature of Elysia is able to customize Elysia to your need. We revisits `state`, `decorate`, and `setModel`, and we saw that api is not consistant, and can be improved. @@ -317,23 +307,23 @@ So we renamed `setModel` to `model`, and add support for setting single and mult import { Elysia, t } from 'elysia' const app = new Elysia() - // ? set model using label - .model('string', t.String()) - .model({ - number: t.Number() - }) - .state('visitor', 1) - // ? set model using object - .state({ - multiple: 'value', - are: 'now supported!' - }) - .decorate('visitor', 1) - // ? set model using object - .decorate({ - name: 'world', - number: 2 - }) + // ? set model using label + .model('string', t.String()) + .model({ + number: t.Number() + }) + .state('visitor', 1) + // ? set model using object + .state({ + multiple: 'value', + are: 'now supported!' + }) + .decorate('visitor', 1) + // ? set model using object + .decorate({ + name: 'world', + number: 2 + }) ``` And as we raised minimum support of TypeScript to 5.0 to improve strictly typed with **Constant Generic**. @@ -341,15 +331,14 @@ And as we raised minimum support of TypeScript to 5.0 to improve strictly typed `state`, `decorate`, and `model` now support literal type, and template string to strictly validate type both runtime and type-level. ```ts -// ? state, decorate, now support literal + // ? state, decorate, now support literal app.get('/', ({ body }) => number, { - body: t.Literal(1), - response: t.Literal(2) -}) + body: t.Literal(1), + response: t.Literal(2) + }) ``` ### Group and Guard - We found that many developers often use `group` with `guard`, we found that nesting them can be later redundant and maybe boilerplate full. Starting with Elysia 0.5, we add a guard scope for `.group` as an optional second parameter. @@ -367,21 +356,19 @@ app.group('/v1', (app) => // ✅ new, compatible with old syntax app.group( - '/v1', - { + '/v1', { body: t.Literal('Rikuhachima Aru') - }, - (app) => app.get('/student', () => 'Rikuhachima Aru') + }, + app => app.get('/student', () => 'Rikuhachima Aru') ) // ✅ compatible with function overload -app.group('/v1', (app) => app.get('/student', () => 'Rikuhachima Aru')) +app.group('/v1', app => app.get('/student', () => 'Rikuhachima Aru')) ``` We hope that you will find all these new revisited API more useful and fits more to your use-case. ## Type Stability - Elysia Type System is complex. We can declare variable on type-level, reference type by name, apply multiple Elysia instance, and even have support for clousure-like at type level, which is really complex to make you have the best developer experience especially with Eden. @@ -395,30 +382,26 @@ Which means that you can now rely on us to check for type integrity for every re --- ### Notable Improvement: - -- Add CommonJS support for running Elysia with Node adapter -- Remove manual fragment mapping to speed up path extraction -- Inline validator in `composeHandler` to improve performance -- Use one time context assignment -- Add support for lazy context injection via Static Code Analysis -- Ensure response non nullability -- Add unioned body validator check -- Set default object handler to inherits -- Using `constructor.name` mapping instead of `instanceof` to improve speed -- Add dedicated error constructor to improve performance -- Conditional literal fn for checking onRequest iteration -- improve WebSocket type +- Add CommonJS support for running Elysia with Node adapter +- Remove manual fragment mapping to speed up path extraction +- Inline validator in `composeHandler` to improve performance +- Use one time context assignment +- Add support for lazy context injection via Static Code Analysis +- Ensure response non nullability +- Add unioned body validator check +- Set default object handler to inherits +- Using `constructor.name` mapping instead of `instanceof` to improve speed +- Add dedicated error constructor to improve performance +- Conditional literal fn for checking onRequest iteration +- improve WebSocket type Breaking Change: - -- Rename `innerHandle` to `fetch` - - to migrate: rename `.innerHandle` to `fetch` -- Rename `.setModel` to `.model` - - to migrate: rename `setModel` to `model` -- Remove `hook.schema` to `hook` - - - to migrate: remove schema and curly brace `schema.type`: - +- Rename `innerHandle` to `fetch` + - to migrate: rename `.innerHandle` to `fetch` +- Rename `.setModel` to `.model` + - to migrate: rename `setModel` to `model` +- Remove `hook.schema` to `hook` + - to migrate: remove schema and curly brace `schema.type`: ```ts // from app.post('/', ({ body }) => body, { @@ -436,11 +419,9 @@ Breaking Change: }) }) ``` - -- remove `mapPathnameRegex` (internal) +- remove `mapPathnameRegex` (internal) ## Afterward - Pushing performance boundary of JavaScript with Bun is what we really excited! Even with the new features every release, Elysia keeps getting faster, with an improved reliabilty and stability, we hope that Elysia will become one of the choice for the next generation TypeScript framework. @@ -459,4 +440,4 @@ Thanks for your continuous support for Elysia, and we hope to see you on the nex > > Yeah, you know it's **full speed ahead** - + \ No newline at end of file diff --git a/docs/blog/elysia-06.md b/docs/blog/elysia-06.md index 87a4dc7e..a9fc24f8 100644 --- a/docs/blog/elysia-06.md +++ b/docs/blog/elysia-06.md @@ -30,12 +30,11 @@ head: Named after the opening of the legendary anime, **"No Game No Life"**, 「[This Game](https://youtu.be/kJ04dMmimn8)」composed by Konomi Suzuki. @@ -45,45 +44,43 @@ This Game push the boundary of medium-size project to large-scale app with re-im ###### (We are still waiting for No Game No Life season 2) ## New Plugin Model - This Game introduce a new syntax for plugin registration, and come up with a new plugin model internally. Previously you can register plugin by defining a callback function for Elysia instance like this: - ```ts const plugin = (app: Elysia) => app.get('/', () => 'hello') ``` With the new plugin, you can now turns and Elysia instance into a plugin: - ```ts -const plugin = new Elysia().get('/', () => 'hello') +const plugin = new Elysia() + .get('/', () => 'hello') ``` This allows any Elysia instance and even existing one to be used across application, removing any possible addition callback and tab spacing. This improved Developer Experience significantly when working and nested group - ```ts // < 0.6 -const group = (app: Elysia) => - app.group('/v1', (app) => app.get('/hello', () => 'Hello World')) +const group = (app: Elysia) => app + .group('/v1', (app) => app + .get('/hello', () => 'Hello World') + ) // >= 0.6 -const group = new Elysia({ prefix: '/v1' }).get('/hello', () => 'Hello World') +const group = new Elysia({ prefix: '/v1' }) + .get('/hello', () => 'Hello World') ``` We encourage you to use the new model of Elysia plugin instance, as we can take advantage of Plugin Checksum and new possible features in the future. However, we are **NOT deprecating** the callback function method as there's some case function model is useful like: - -- Inline function -- Plugins that required an information of main instance (for example accessing OpenAPI schema) +- Inline function +- Plugins that required an information of main instance (for example accessing OpenAPI schema) With this new plugin model, we hope that you can make your codebase even easier to maintain. ## Plugin Checksum - By default, Elysia plugin use function callback to register plugin. This means that if you register a plugin for type declaration, it will duplicate itself for just providing a type support, leading to duplication of plugin used in production. @@ -91,7 +88,6 @@ This means that if you register a plugin for type declaration, it will duplicate Which is why Plugin Checksum is introduced, to de-duplicated plugin registered for type declaration. To opt-in to Plugin Checksum, you need to use a new plugin model, and provide a `name` property to tell Elysia to prevent the plugin from being deduplicate - ```ts const plugin = new Elysia({ name: 'plugin' @@ -105,10 +101,10 @@ Any duplicated name will be registered only once but type-safety will be provide In case your plugin needs configuration, you can provide the configuration into a **seed** property to generate a checksum for deduplicating the plugin. ```ts -const plugin = (config = new Elysia({ +const plugin = (config) = new Elysia({ name: 'plugin', seed: config -})) +}) ``` Name and seed will be used to generate a checksum to de-duplicated registration, which leads to even better performance improvement. @@ -118,7 +114,6 @@ This update also fixed the deduplication of the plugin's lifecycle accidentally As always, means performance improvement for an app that's larger than "Hello World". ## Mount and WinterCG Compliance - WinterCG is a standard for web-interoperable runtimes supports by Cloudflare, Deno, Vercel Edge Runtime, Netlify Function and various more. WinterCG is a standard to allows web server to runs interoperable across runtime, which use Web Standard definitions like Fetch, Request, and Response. @@ -130,7 +125,6 @@ This allows any framework and code that is WinterCG compliance to be run togethe By this, we implemented the same logic for Elysia by introducing `.mount` method to runs any framework or code that is WinterCG compliant. To use `.mount`, [simply pass a `fetch` function](https://twitter.com/saltyAom/status/1684786233594290176): - ```ts const app = new Elysia() .get('/', () => 'Hello from Elysia') @@ -138,7 +132,6 @@ const app = new Elysia() ``` A **fetch** function is a function that accept Web Standard Request and return Web Standard Response as the definition of: - ```ts // Web Standard Request-like object // Web Standard Response @@ -146,20 +139,19 @@ type fetch = (request: RequestLike) => Response ``` By default, this declaration is used by: - -- Bun -- Deno -- Vercel Edge Runtime -- Cloudflare Worker -- Netlify Edge Function -- Remix Function Handler +- Bun +- Deno +- Vercel Edge Runtime +- Cloudflare Worker +- Netlify Edge Function +- Remix Function Handler Which means you can run all of the above code to interlop with Elysia all in a single server, or re-used and existing function all in one deployment, no need to setting up Reverse Proxy for handling multiple server. If the framework also support a **.mount** function, you can deeply nested a framework that support it infinitely. - ```ts -const elysia = new Elysia().get('/Hello from Elysia inside Hono inside Elysia') +const elysia = new Elysia() + .get('/Hello from Elysia inside Hono inside Elysia') const hono = new Hono() .get('/', (c) => c.text('Hello from Hono!')) @@ -178,7 +170,10 @@ import A from 'project-a/elysia' import B from 'project-b/elysia' import C from 'project-c/elysia' -new Elysia().mount(A).mount(B).mount(C) +new Elysia() + .mount(A) + .mount(B) + .mount(C) ``` If an instance passed to mount is an Elysia instance, it will resolve to `use` automatically, providing type-safety and support for Eden by default. @@ -186,7 +181,6 @@ If an instance passed to mount is an Elysia instance, it will resolve to `use` a This made the possibility of interlopable framework and runtime to a reality. ## Improved starts up time - Starts up time is an important metric in a serverless environment which Elysia excels it incredibly, but we have taken it even further. By default, Elysia generates OpenAPI schema for every route automatically and stored it internally when if not used. @@ -196,7 +190,6 @@ In this version, Elysia defers the compilation and moved to `@elysiajs/swagger` And with various micro-optimization, and made possible by new Plugin model, starts up time is now up to 35% faster. ## Dynamic Mode - Elysia introduces Static Code Analysis and Ahead of Time compilation to push the boundary of performance. Static Code Analysis allow Elysia to read your code then produce the most optimized version code, allowing Elysia to push the performance to its limit. @@ -206,7 +199,6 @@ Even if Elysia is WinterCG compliance, environment like Cloudflare worker doesn' This means that Ahead of Time Compliation isn't possible, leading us to create a dynamic mode which use JIT compilation instead of AoT, allowing Elysia to run in Cloudflare Worker as well. To enable dynamic mode, set `aot` to false. - ```ts new Elysia({ aot: false @@ -226,7 +218,6 @@ Elysia is able to register 10,000 routes in just 78ms which means in an average That being said, we are leaving a choice for you to decided yourself. ## Declarative Custom Error - This update adds support for adding type support for handling custom error. ```ts @@ -241,7 +232,7 @@ new Elysia() MyError: CustomError }) .onError(({ code, error }) => { - switch (code) { + switch(code) { // With auto-completion case 'MyError': // With type narrowing @@ -260,7 +251,6 @@ Elysia Type System is complex, yet we try to refrained our users need to write a It just works, and all the code looks just like JavaScript. ## TypeBox 0.30 - TypeBox is a core library that powers Elysia's strict type system known as **Elysia.t**. In this update, we update TypeBox from 0.28 to 0.30 to make even more fine-grained Type System nearly strictly typed language. @@ -268,41 +258,37 @@ In this update, we update TypeBox from 0.28 to 0.30 to make even more fine-grain These updates introduce new features and many interesting changes, for example **Iterator** type, reducing packages size, TypeScript code generation. And with support for Utility Types like: - -- `t.Awaited` -- `t.Uppercase` -- `t.Capitlized` +- `t.Awaited` +- `t.Uppercase` +- `t.Capitlized` ## Strict Path - We got a lot of requests for handling loose path. By default, Elysia handle path strictly, which means that if you have to support path with or without optional `/` , it will not be resolved and you have to duplicate the pathname twice. ```ts -new Elysia().group('/v1', (app) => - app +new Elysia() + .group('/v1', (app) => app // Handle /v1 .get('', handle) // Handle /v1/ .get('/', handle) -) + ) ``` By this, many have been requesting that `/v1/` should also resolved `/v1` as well. With this update, we add support for loose path matching by default, to opt-in into this feature automatically. - ```ts -new Elysia().group('/v1', (app) => - app +new Elysia() + .group('/v1', (app) => app // Handle /v1 and /v1/ .get('/', handle) -) + ) ``` To disable loosePath mapping, you can set `strictPath` to true to used the previous behavior: - ```ts new Elysia({ strictPath: false @@ -312,7 +298,6 @@ new Elysia({ We hope that this will clear any questions regards to path matching and its expected behavior ## onResponse - This update introduce a new lifecycle hook called `onResponse`. This is a proposal proposed by [elysia#67](https://github.com/elysiajs/elysia/issues/67) @@ -327,7 +312,6 @@ Which is why we introduced `onResponse` to handle all cases of Response. You can use `onRequest`, and `onResponse` together to measure a metric of performance or any required logging. Quoted - > However, the onAfterHandle function only fires on successful responses. For instance, if the route is not found, or the body is invalid, or an error is thrown, it is not fired. How can I listen to both successful and non-successful requests? This is why I suggested onResponse. > > Based on the drawing, I would suggest the following: @@ -336,30 +320,26 @@ Quoted --- ### Notable Improvement: - -- Added an error field to the Elysia type system for adding custom error messages -- Support Cloudflare worker with Dynamic Mode (and ENV) -- AfterHandle now automatically maps the value -- Using bun build to target Bun environment, improving the overall performance by 5-10% -- Deduplicated inline lifecycle when using plugin registration -- Support for setting `prefix` -- Recursive path typing -- Slightly improved type checking speed -- Recursive schema collision causing infinite types +- Added an error field to the Elysia type system for adding custom error messages +- Support Cloudflare worker with Dynamic Mode (and ENV) +- AfterHandle now automatically maps the value +- Using bun build to target Bun environment, improving the overall performance by 5-10% +- Deduplicated inline lifecycle when using plugin registration +- Support for setting `prefix` +- Recursive path typing +- Slightly improved type checking speed +- Recursive schema collision causing infinite types ### Change: - -- Moved **registerSchemaPath** to @elysiajs/swagger -- [Internal] Add qi (queryIndex) to context +- Moved **registerSchemaPath** to @elysiajs/swagger +- [Internal] Add qi (queryIndex) to context ### Breaking Change: - -- [Internal] Removed Elysia Symbol -- [Internal] Refactored `getSchemaValidator`, `getResponseSchemaValidator` to named parameters -- [Internal] Moved `registerSchemaPath` to `@elysiajs/swagger` +- [Internal] Removed Elysia Symbol +- [Internal] Refactored `getSchemaValidator`, `getResponseSchemaValidator` to named parameters +- [Internal] Moved `registerSchemaPath` to `@elysiajs/swagger` ## Afterward - We have just passed a one year milestone, and really excited how Elysia and Bun have improved over the year! Pushing the performance boundaries of JavaScript with Bun, and developer experience with Elysia, we are thrilled to have kept in touch with you and our community. @@ -367,9 +347,8 @@ Pushing the performance boundaries of JavaScript with Bun, and developer experie Every updates, keeps making Elysia even more stable, and gradually providing a better developer experience without a drop in performance and features. We're thrilled to see our community of open-source developers bringing Elysia to life with their projects like. - -- [Elysia Vite Plugin SSR](https://github.com/timnghg/elysia-vite-plugin-ssr) allowing us to use Vite Server Side Rendering using Elysia as the server. -- [Elysia Connect](https://github.com/timnghg/elysia-connect) which made Connect's plugin compatible with Elysia +- [Elysia Vite Plugin SSR](https://github.com/timnghg/elysia-vite-plugin-ssr) allowing us to use Vite Server Side Rendering using Elysia as the server. +- [Elysia Connect](https://github.com/timnghg/elysia-connect) which made Connect's plugin compatible with Elysia And much more developers that choose Elysia for their next big project. @@ -392,7 +371,7 @@ We incredibly thankful for your overwhelming continous support for Elysia, and w > We are maverick > > We won't give in, until we win this game -> +> > Though I don't know what tomorrow holds > > I'll make a bet any play my cards to win this game @@ -403,4 +382,4 @@ We incredibly thankful for your overwhelming continous support for Elysia, and w > > I put all my fate in used let **the game begin** - + \ No newline at end of file diff --git a/docs/blog/elysia-07.md b/docs/blog/elysia-07.md index 622f1816..9850cfee 100644 --- a/docs/blog/elysia-07.md +++ b/docs/blog/elysia-07.md @@ -30,12 +30,11 @@ head: Name after our never giving up spirit, our beloved Virtual YouTuber, ~~Suicopath~~ Hoshimachi Suisei, and her brilliance voice: 「[Stellar Stellar](https://youtu.be/AAsRtnbDs-0)」from her first album:「Still Still Stellar」 @@ -43,14 +42,13 @@ Name after our never giving up spirit, our beloved Virtual YouTuber, ~~Suicopath For once being forgotten, she really is a star that truly shine in the dark. **Stellar Stellar** brings many exciting new update to help Elysia solid the foundation, and handle complexity with ease, featuring: - -- Entirely rewrite type, up to 13x faster type inference. -- "Trace" for declarative telemetry and better performance audit. -- Reactive Cookie model and cookie valiation to simplify cookie handling. -- TypeBox 0.31 with a custom decoder support. -- Rewritten Web Socket for even better support. -- Definitions remapping, and declarative affix for preventing name collision. -- Text based status +- Entirely rewrite type, up to 13x faster type inference. +- "Trace" for declarative telemetry and better performance audit. +- Reactive Cookie model and cookie valiation to simplify cookie handling. +- TypeBox 0.31 with a custom decoder support. +- Rewritten Web Socket for even better support. +- Definitions remapping, and declarative affix for preventing name collision. +- Text based status ## Rewritten Type @@ -106,7 +104,6 @@ This API allows us to effortlessly auditing performance bottleneck of your Elysi By default, Trace use AoT compilation and Dynamic Code injection to conditionally report and even that you actually use automatically, which means there's no performance impact at all. ## Reactive Cookie - We merged our cookie plugin into Elysia core. Same as Trace, Reactive Cookie use AoT compilation and Dynamic Code injection to conditionally inject the cookie usage code, leading to no performance impact if you don't use one. @@ -118,14 +115,13 @@ Reactive Cookie take a more modern approach like signal to handle cookie with an There's no `getCookie`, `setCookie`, everything is just a cookie object. When you want to use cookie, you just extract the name get/set its value like: - ```typescript app.get('/', ({ cookie: { name } }) => { // Get name.value // Set - name.value = 'New Value' + name.value = "New Value" }) ``` @@ -134,36 +130,30 @@ Then cookie will be automatically sync the value with headers, and the cookie ja The Cookie Jar is reactive, which means that if you don't set the new value for the cookie, the `Set-Cookie` header will not be send to keep the same cookie value and reduce performance bottleneck. ### Cookie Schema - With the merge of cookie into the core of Elysia, we introduce a new **Cookie Schema** for validating cookie value. This is useful when you have to strictly validate cookie session or want to have a strict type or type inference for handling cookie. ```typescript -app.get( - '/', - ({ cookie: { name } }) => { - // Set - name.value = { - id: 617, - name: 'Summoning 101' - } - }, - { - cookie: t.Cookie({ - value: t.Object({ - id: t.Numeric(), - name: t.String() - }) - }) +app.get('/', ({ cookie: { name } }) => { + // Set + name.value = { + id: 617, + name: 'Summoning 101' } -) +}, { + cookie: t.Cookie({ + value: t.Object({ + id: t.Numeric(), + name: t.String() + }) + }) +}) ``` Elysia encode and decode cookie value for you automatically, so if you want to store JSON in a cookie like decoded JWT value, or just want to make sure if the value is a numeric string, you can do that effortlessly. ### Cookie Signature - And lastly, with an introduction of Cookie Schema, and `t.Cookie` type. We are able to create a unified type for handling sign/verify cookie signature automatically. Cookie signature is a cryptographic hash appended to a cookie's value, generated using a secret key and the content of the cookie to enhance security by adding a signature to the cookie. @@ -171,34 +161,27 @@ Cookie signature is a cryptographic hash appended to a cookie's value, generated This make sure that the cookie value is not modified by malicious actor, helps in verifying the authenticity and integrity of the cookie data. To handle cookie signature in Elysia, it's a simple as providing a `secert` and `sign` property: - ```typescript new Elysia({ cookie: { secret: 'Fischl von Luftschloss Narfidort' } -}).get( - '/', - ({ cookie: { profile } }) => { +}) + .get('/', ({ cookie: { profile } }) => { profile.value = { id: 617, name: 'Summoning 101' } - }, - { - cookie: t.Cookie( - { - profile: t.Object({ - id: t.Numeric(), - name: t.String() - }) - }, - { - sign: ['profile'] - } - ) - } -) + }, { + cookie: t.Cookie({ + profile: t.Object({ + id: t.Numeric(), + name: t.String() + }) + }, { + sign: ['profile'] + }) + }) ``` By provide a cookie secret, and `sign` property to indicate which cookie should have a signature verification. @@ -206,7 +189,6 @@ By provide a cookie secret, and `sign` property to indicate which cookie should Elysia then sign and unsign cookie value automatically, eliminate the need of **sign** / **unsign** function manually. Elysia handle Cookie's secret rotation automatically, so if you have to migrate to a new cookie secret, you can just append the secret, and Elysia will use the first value to sign a new cookie, while trying to unsign cookie with the rest of the secret if match. - ```typescript new Elysia({ cookie: { @@ -218,7 +200,6 @@ new Elysia({ The Reactive Cookie API is declarative and straigth forward, and there's some magical thing about the ergonomic it provide, and we really looking forward for you to try it. ## TypeBox 0.31 - With the release of 0.7, we are updating to TypeBox 0.31 to brings even more feature to Elysia. This brings new exciting feature like support for TypeBox's `Decode` in Elysia natively. @@ -226,7 +207,6 @@ This brings new exciting feature like support for TypeBox's `Decode` in Elysia n Previously, a custom type like `Numeric` require a dynamic code injection to convert numeric string to number, but with the use of TypeBox's decode, we are allow to define a custom function to encode and decode the value of a type automatically. Allowing us to simplify type to: - ```typescript Numeric: (property?: NumericOptions) => Type.Transform(Type.Union([Type.String(), Type.Number(property)])) @@ -248,42 +228,31 @@ Not only limited to that, with `t.Transform` you can now also define a custom ty We can't wait to see what you will brings with the introduction of `t.Transform`. ### New Type - With an introduction **Transform**, we have add a new type like `t.ObjectString` to automatically decode a value of Object in request. This is useful when you have to use **multipart/formdata** for handling file uploading but doesn't support object. You can now just use `t.ObjectString()` to tells Elysia that the field is a stringified JSON, so Elysia can decode it automatically. - ```typescript new Elysia({ cookie: { secret: 'Fischl von Luftschloss Narfidort' } -}).post( - '/', - ({ - body: { - data: { name } - } - }) => name, - { +}) + .post('/', ({ body: { data: { name } } }) => name, { body: t.Object({ image: t.File(), data: t.ObjectString({ name: t.String() }) }) - } -) + }) ``` We hope that this will simplify the need for JSON with **multipart**. ## Rewritten Web Socket - Aside from entirely rewritten type, we also entirely rewritten Web Socket as well. Previously, we found that Web Socket has 3 major problem: - 1. Schema is not strictly validated 2. Slow type inference 3. The need for `.use(ws())` in every plugin @@ -302,55 +271,56 @@ Bringing the performance to near Bun native Web Socket performance. Thanks to [Bogeychan](https://github.com/bogeychan) for providing the test case for Elysia Web Socket, helping us to rewrite Web Socket with confidence. ## Definitions Remap - Proposed on [#83](https://github.com/elysiajs/elysia/issues/83) by [Bogeychan](https://github.com/bogeychan) To summarize, Elysia allows us to decorate and request and store with any value we desire, however some plugin might a duplicate name with the value we have, and sometime plugin has a name collision but we can't rename the property at all. ### Remapping - As the name suggest, this allow us to remap existing `state`, `decorate`, `model`, `derive` to anything we like to prevent name collision, or just wanting to rename a property. By providing a function as a first parameters, the callback will accept current value, allowing us to remap the value to anything we like. - ```typescript new Elysia() .state({ - a: 'a', - b: 'b' + a: "a", + b: "b" }) // Exclude b state .state(({ b, ...rest }) => rest) ``` This is useful when you have to deal with a plugin that has some duplicate name, allowing you to remap the name of the plugin: - ```typescript -new Elysia().use( - plugin.decorate(({ logger, ...rest }) => ({ - pluginLogger: logger, - ...rest - })) -) +new Elysia() + .use( + plugin + .decorate(({ logger, ...rest }) => ({ + pluginLogger: logger, + ...rest + })) + ) ``` Remap function can be use with `state`, `decorate`, `model`, `derive` to helps you define a correct property name and preventing name collision. ### Affix - To provide a smoother experience, some plugins might have a lot of property value which can be overwhelming to remap one-by-one. The **Affix** function, which consists of a **prefix** and **suffix**, allows us to remap all properties of an instance, preventing the name collision of the plugin. ```typescript -const setup = new Elysia({ name: 'setup' }).decorate({ - argon: 'a', - boron: 'b', - carbon: 'c' -}) +const setup = new Elysia({ name: 'setup' }) + .decorate({ + argon: 'a', + boron: 'b', + carbon: 'c' + }) const app = new Elysia() - .use(setup.prefix('decorator', 'setup')) + .use( + setup + .prefix('decorator', 'setup') + ) .get('/', ({ setupCarbon }) => setupCarbon) ``` @@ -359,17 +329,18 @@ Allowing us to bulk remap a property of the plugin effortlessly, preventing the By default, **affix** will handle both runtime, type-level code automatically, remapping the property to camelCase as naming convention. In some condition, you can also remap `all` property of the plugin: - ```typescript const app = new Elysia() - .use(setup.prefix('all', 'setup')) + .use( + setup + .prefix('all', 'setup') + ) .get('/', ({ setupCarbon }) => setupCarbon) ``` We hope that remapping and affix will provide a powerful API for you to handle multiple complex plugin with ease. ## True Encapsulation Scope - With the introduction of Elysia 0.7, Elysia can now truly encapsulate an instance by treating a scoped instance as another instance. The new scope model can even prevent event like `onRequest` to be resolve on a main instance which is not possible. @@ -392,7 +363,6 @@ Further more, scoped is now truly scoped down both in runtime, and type level wh This is exciting from maintainer side because previously, it's almost impossible to truly encapsulate the scope the an instance, but using `mount` and WinterCG compilance, we are finally able to truly encapsulate the instance of the plugin while providing a soft link with main instance property like `state`, `decorate`. ## Text based status - There are over 64 standard HTTP status codes to remember, and I admit that sometime we also forget the status we want to use. This is why we ship 64 HTTP Status codes in text-based form with autocompletion for you. @@ -408,45 +378,39 @@ As you type, there should be auto-completion for text popup automatically for yo This is a small ergonomic feature to helps you develop your server without switching between IDE and MDN to search for a correct status code. ## Notable Improvement - Improvement: - -- `onRequest` can now be async -- add `Context` to `onError` -- lifecycle hook now accept array function -- static Code Analysis now support rest parameter -- breakdown dynamic router into single pipeline instead of inlining to static router to reduce memory usage -- set `t.File` and `t.Files` to `File` instead of `Blob` -- skip class instance merging -- handle `UnknownContextPassToFunction` -- [#157](https://github.com/elysiajs/elysia/pull/179) WebSocket - added unit tests and fixed example & api by @bogeychan -- [#179](https://github.com/elysiajs/elysia/pull/179) add github action to run bun test by @arthurfiorette +- `onRequest` can now be async +- add `Context` to `onError` +- lifecycle hook now accept array function +- static Code Analysis now support rest parameter +- breakdown dynamic router into single pipeline instead of inlining to static router to reduce memory usage +- set `t.File` and `t.Files` to `File` instead of `Blob` +- skip class instance merging +- handle `UnknownContextPassToFunction` +- [#157](https://github.com/elysiajs/elysia/pull/179) WebSocket - added unit tests and fixed example & api by @bogeychan +- [#179](https://github.com/elysiajs/elysia/pull/179) add github action to run bun test by @arthurfiorette Breaking Change: - -- remove `ws` plugin, migrate to core -- rename `addError` to `error` +- remove `ws` plugin, migrate to core +- rename `addError` to `error` Change: - -- using single findDynamicRoute instead of inlining to static map -- remove `mergician` -- remove array routes due to problem with TypeScript -- rewrite Type.ElysiaMeta to use TypeBox.Transform +- using single findDynamicRoute instead of inlining to static map +- remove `mergician` +- remove array routes due to problem with TypeScript +- rewrite Type.ElysiaMeta to use TypeBox.Transform Bug fix: - -- strictly validate response by default -- `t.Numeric` not working on headers / query / params -- `t.Optional(t.Object({ [name]: t.Numeric }))` causing error -- add null check before converting `Numeric` -- inherits store to instance plugin -- handle class overlapping -- [#187](https://github.com/elysiajs/elysia/pull/187) InternalServerError message fixed to "INTERNAL_SERVER_ERROR" instead of "NOT_FOUND" by @bogeychan -- [#167](https://github.com/elysiajs/elysia/pull/167) mapEarlyResponse with aot on after handle +- strictly validate response by default +- `t.Numeric` not working on headers / query / params +- `t.Optional(t.Object({ [name]: t.Numeric }))` causing error +- add null check before converting `Numeric` +- inherits store to instance plugin +- handle class overlapping +- [#187](https://github.com/elysiajs/elysia/pull/187) InternalServerError message fixed to "INTERNAL_SERVER_ERROR" instead of "NOT_FOUND" by @bogeychan +- [#167](https://github.com/elysiajs/elysia/pull/167) mapEarlyResponse with aot on after handle ## Afterward - Since the latest release, we have gained over 2,000 stars on GitHub! Taking a look back, we have progressed more than we have ever imagined back then. @@ -459,11 +423,10 @@ A future where we can freely create anything we want with an astonishing develop We feels truly thanksful to be loved by you and lovely community of TypeScript and Bun. -It's exciting to see Elysia is bring to live with amazing developer like: - -- [Ethan Niser with his amazing BETH Stack](https://youtu.be/aDYYn9R-JyE?si=hgvGgbywu_-jsmhR) -- Being mentioned by [Fireship](https://youtu.be/dWqNgzZwVJQ?si=AeCmcMsTZtNwmhm2) -- Having official integration for [Lucia Auth](https://github.com/pilcrowOnPaper/lucia) +It's exciting to see Elysia is bring to live with amazing developer like: +- [Ethan Niser with his amazing BETH Stack](https://youtu.be/aDYYn9R-JyE?si=hgvGgbywu_-jsmhR) +- Being mentioned by [Fireship](https://youtu.be/dWqNgzZwVJQ?si=AeCmcMsTZtNwmhm2) +- Having official integration for [Lucia Auth](https://github.com/pilcrowOnPaper/lucia) And much more developers that choose Elysia for their next project. @@ -490,9 +453,9 @@ Thanks you and your love and overwhelming support for Elysia, we hope we can pai > Not Cinderella, forever waiting > > But the prince that came to for her -> +> > Cause I'm a star, that's why > > Stellar Stellar - + \ No newline at end of file diff --git a/docs/blog/elysia-08.md b/docs/blog/elysia-08.md index c2ebf524..34e2e6d4 100644 --- a/docs/blog/elysia-08.md +++ b/docs/blog/elysia-08.md @@ -30,12 +30,11 @@ head: Named after the ending song of Steins;Gate Zero, [**"Gate of Steiner"**](https://youtu.be/S5fnglcM5VI). @@ -43,17 +42,15 @@ Named after the ending song of Steins;Gate Zero, [**"Gate of Steiner"**](https:/ Gate of Steiner isn't focused on new exciting APIs and features but on API stability and a solid foundation to make sure that the API will be stable once Elysia 1.0 is released. However, we do bring improvement and new features including: - -- [Macro API](#macro-api) -- [New Lifecycle: resolve, mapResponse](#new-life-cycle) -- [Error Function](#error-function) -- [Static Content](#static-content) -- [Default Property](#default-property) -- [Default Header](#default-header) -- [Performance and Notable Improvement](#performance-and-notable-improvement) +- [Macro API](#macro-api) +- [New Lifecycle: resolve, mapResponse](#new-life-cycle) +- [Error Function](#error-function) +- [Static Content](#static-content) +- [Default Property](#default-property) +- [Default Header](#default-header) +- [Performance and Notable Improvement](#performance-and-notable-improvement) ## Macro API - Macro allows us to define a custom field to hook and guard by exposing full control of the life cycle event stack. Allowing us to compose custom logic into a simple configuration with full type safety. @@ -64,23 +61,24 @@ Suppose we have an authentication plugin to restrict access based on role, we ca import { Elysia } from 'elysia' import { auth } from '@services/auth' -const app = new Elysia().use(auth).get('/', ({ user }) => user.profile, { - role: 'admin' -}) +const app = new Elysia() + .use(auth) + .get('/', ({ user }) => user.profile, { + role: 'admin' + }) ``` Macro has full access to the life cycle stack, allowing us to add, modify, or delete existing events directly for each route. - ```typescript const plugin = new Elysia({ name: 'plugin' }).macro(({ beforeHandle }) => { return { role(type: 'admin' | 'user') { beforeHandle( - { insert: 'before' }, + { insert: 'before' }, async ({ cookie: { session } }) => { - const user = await validateSession(session.value) - await validateRole('admin', user) - } + const user = await validateSession(session.value) + await validateRole('admin', user) +} ) } } @@ -94,13 +92,11 @@ The documentation of [Macro API](/patterns/macro) is now available in **pattern* The next generation of customizability is now only a reach away from your keyboard and imagination. ## New Life Cycle - Elysia introduced a new life cycle to to fix an existing problem and highly requested API including **Resolve** and **MapResponse**: resolve: a safe version of **derive**. Execute in the same queue as **beforeHandle** mapResponse: Execute just after **afterResponse** for providing transform function from primitive value to Web Standard Response ### Resolve - A "safe" version of [derive](/essential/context.html#derive). Designed to append new value to context after validation process storing in the same stack as **beforeHandle**. @@ -130,7 +126,6 @@ new Elysia() ``` ### MapResponse - Executed just after **"afterHandle"**, designed to provide custom response mapping from primitive value into a Web Standard Response. Below is an example of using mapResponse to provide Response compression. @@ -154,16 +149,15 @@ new Elysia() Why not use **afterHandle** but introduce a new API? -Because **afterHandle** is designed to read and modify a primitive value. Storing plugins like HTML, and Compression which depends on creating Web Standard Response. +Because **afterHandle** is designed to read and modify a primitive value. Storing plugins like HTML, and Compression which depends on creating Web Standard Response. This means that plugins registered after this type of plugin will be unable to read a value or modify the value making the plugin behavior incorrect. This is why we introduce a new life-cycle run after **afterHandle** dedicated to providing a custom response mapping instead of mixing the response mapping and primitive value mutation in the same queue. -## Error Function +## Error Function We can set the status code by using either **set.status** or returning a new Response. - ```typescript import { Elysia } from 'elysia' @@ -178,7 +172,7 @@ new Elysia() This aligns with our goal, to just the literal value to the client instead of worrying about how the server should behave. -However, this is proven to have a challenging integration with Eden. Since we return a literal value, we can't infer the status code from the response making Eden unable to differentiate the response from the status code. +However, this is proven to have a challenging integration with Eden. Since we return a literal value, we can't infer the status code from the response making Eden unable to differentiate the response from the status code. This results in Eden not being able to use its full potential, especially in error handling as it cannot infer type without declaring explicit response type for each status. @@ -195,7 +189,6 @@ new Elysia() ``` Which is an equivalent to: - ```typescript import { Elysia } from 'elysia' @@ -216,27 +209,20 @@ This means that by using **error**, we don't have to include the explicit respon import { Elysia, error, t } from 'elysia' new Elysia() - .get( - '/', - ({ set }) => { - set.status = 418 - return "I'm a teapot" - }, - { - // [!code --] - response: { - // [!code --] - 418: t.String() // [!code --] - } // [!code --] - } - ) // [!code --] + .get('/', ({ set }) => { + set.status = 418 + return "I'm a teapot" + }, { // [!code --] + response: { // [!code --] + 418: t.String() // [!code --] + } // [!code --] + }) // [!code --] .listen(3000) ``` We recommended using `error` function to return a response with the status code for the correct type inference, however, we do not intend to remove the usage of **set.status** from Elysia to keep existing servers working. ## Static Content - Static Content refers to a response that almost always returns the same value regardless of the incoming request. This type of resource on the server is usually something like a public **File**, **video** or hardcode value that is rarely changed unless the server is updated. @@ -257,7 +243,6 @@ Notice that the handler now isn't a function but is an inline value instead. This will improve the performance by around 20-25% by compiling the response ahead of time. ## Default Property - Elysia 0.8 updates to [TypeBox to 0.32](https://github.com/sinclairzx81/typebox/blob/index/changelog/0.32.0.md) which introduces many new features including dedicated RegEx, Deref but most importantly the most requested feature in Elysia, **default** field support. Now defining a default field in Type Builder, Elysia will provide a default value if the value is not provided, supporting schema types from type to body. @@ -270,7 +255,7 @@ new Elysia() query: t.Object({ name: t.String({ default: 'Elysia' - }) + }) }) }) .listen(3000) @@ -279,7 +264,6 @@ new Elysia() This allows us to provide a default value from schema directly, especially useful when using reference schema. ## Default Header - We can set headers using **set.headers**, which Elysia always creates a default empty object for every request. Previously we could use **onRequest** to append desired values into set.headers, but this will always have some overhead because a function is called. @@ -287,31 +271,27 @@ Previously we could use **onRequest** to append desired values into set.headers, Stacking functions to mutate an object can be a little slower than having the desired value set in the first hand if the value is always the same for every request like CORS or cache header. This is why we now support setting default headers out of the box instead of creating an empty object for every new request. - ```typescript -new Elysia().headers({ - 'X-Powered-By': 'Elysia' -}) +new Elysia() + .headers({ + 'X-Powered-By': 'Elysia' + }) ``` Elysia CORS plugin also has an update to use this new API to improve this performance. ## Performance and notable improvement - As usual, we found a way to optimize Elysia even more to make sure you have the best performance out of the box. ### Removable of bind - We found that **.bind** is slowing down the path lookup by around ~5%, with the removal of bind from our codebase we can speed up that process a little bit. ### Static Query Analysis - Elysia Static Code Analysis is now able to infer a query if the query name is referenced in the code. This usually results in a speed-up of 15-20% by default. ### Video Stream - Elysia now adds **content-range** header to File and Blob by default to fix problems with large files like videos that require to be sent by chunk. To fix this, Elysia now adds **content-range** header to by default. @@ -323,61 +303,56 @@ It's recommended to use [ETag plugin](https://github.com/bogeychan/elysia-etag) This is an initial support for **content-range** header, we have created a discussion to implement more accurate behavior based on [RPC-7233](https://datatracker.ietf.org/doc/html/rfc7233#section-4.2) in the future. Feels free to join the discussion to propose a new behavior for Elysia with **content-range** and **etag generation** at [Discussion 371](https://github.com/elysiajs/elysia/discussions/371). ### Runtime Memory improvement - Elysia now reuses the return value of the life cycle event instead of declaring a new dedicated value. This reduces the memory usage of Elysia by a little bit better for peak concurrent requests a little better. ### Runtime Memory improvement - Elysia now reuses the return value of the life cycle event instead of declaring a new dedicated value. This reduces the memory usage of Elysia by a little bit better for peak concurrent requests a little better. ### Plugins - Most official plugins now take advantage of newer **Elysia.headers**, Static Content, **MapResponse** ,and revised code to comply with static code analysis even more to improve the overall performance. Plugins that are improved by this are the following: Static, HTML, and CORS. ### Validation Error - Elysia now returns validation error as JSON instead of text. Showing current errors and all errors and expected values instead, to help you identify an error easier. Example: - ```json { - "type": "query", - "at": "password", - "message": "Required property", - "expected": { - "email": "eden@elysiajs.com", - "password": "" + "type": "query", + "at": "password", + "message": "Required property", + "expected": { + "email": "eden@elysiajs.com", + "password": "" + }, + "found": { + "email": "eden@elysiajs.com" + }, + "errors": [ + { + "type": 45, + "schema": { + "type": "string" + }, + "path": "/password", + "message": "Required property" }, - "found": { - "email": "eden@elysiajs.com" - }, - "errors": [ - { - "type": 45, - "schema": { - "type": "string" - }, - "path": "/password", - "message": "Required property" - }, - { - "type": 54, - "schema": { - "type": "string" - }, - "path": "/password", - "message": "Expected string" - } - ] + { + "type": 54, + "schema": { + "type": "string" + }, + "path": "/password", + "message": "Expected string" + } + ] } ``` @@ -386,30 +361,25 @@ Example: ## Notable Improvement **Improvement** - -- lazy query reference -- add content-range header to `Blob` by default -- update TypeBox to 0.32 -- override lifecycle response of `be` and `af` +- lazy query reference +- add content-range header to `Blob` by default +- update TypeBox to 0.32 +- override lifecycle response of `be` and `af` **Breaking Change** - -- `afterHandle` no longer early return +- `afterHandle` no longer early return **Change** - -- change validation response to JSON -- differentiate derive from `decorator['request']` as `decorator['derive']` -- `derive` now don't show infer type in onRequest +- change validation response to JSON +- differentiate derive from `decorator['request']` as `decorator['derive']` +- `derive` now don't show infer type in onRequest **Bug fix** - -- remove `headers`, `path` from `PreContext` -- remove `derive` from `PreContext` -- Elysia type doesn't output custom `error` +- remove `headers`, `path` from `PreContext` +- remove `derive` from `PreContext` +- Elysia type doesn't output custom `error` ## Afterward - It has been a great journey since the first release. Elysia evolved from a generic REST API framework to an ergonomic framework to support End-to-end type safety, OpenAPI documentation generation, we we would like to keep introduce more exciting features in the future. @@ -418,25 +388,24 @@ Elysia evolved from a generic REST API framework to an ergonomic framework to su We are happy to have you, and the developers to build amazing stuff with Elysia and your overwhelming continuous support for Elysia encourages us to keep going. It's exciting to see Elysia grow more as a community: - -- [Scalar's Elysia theme](https://x.com/saltyAom/status/1737468941696421908?s=20) for new documentation instead of Swagger UI. -- [pkgx](https://pkgx.dev/) supports Elysia out of the box. -- Community submitted Elysia to [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r22&hw=ph&test=composite) ranking at 32 out of all frameworks in composite score, even surpassing Go's Gin and Echo. +- [Scalar's Elysia theme](https://x.com/saltyAom/status/1737468941696421908?s=20) for new documentation instead of Swagger UI. +- [pkgx](https://pkgx.dev/) supports Elysia out of the box. +- Community submitted Elysia to [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r22&hw=ph&test=composite) ranking at 32 out of all frameworks in composite score, even surpassing Go's Gin and Echo. We are now trying to provide more support for each runtime, plugin, and integration to return the kindness you have given us, starting with the rewrite of the documentation with more detailed and beginner-friendliness, [Integration with Nextjs](/integrations/nextj), [Astro](/integrations/astro) and more to come in the future. And since the release of 0.7, we have seen fewer issues compared to the previous releases. Now **we are preparing for the first stable release of Elysia**, Elysia 1.0 aiming to release **in Q1 2024** to repay your kindness. -Elysia will now enter soft API lockdown mode, to prevent an API change and make sure that there will be no or less breaking change once the stable release arrives. +Elysia will now enter soft API lockdown mode, to prevent an API change and make sure that there will be no or less breaking change once the stable release arrives. So you can expect your Elysia app to work starting from 0.7 with no or minimal change to support the stable release of Elysia. We again thank your continuous support for Elysia, and we hope to see you again on the stable release day. -_Keep fighting for all that is beautiful in this world_. +*Keep fighting for all that is beautiful in this world*. -Until then, _El Psy Congroo_. +Until then, *El Psy Congroo*. > A drop in the darkness 小さな命 > @@ -454,4 +423,4 @@ Until then, _El Psy Congroo_. > > Shed a tear and leap to a new world - + \ No newline at end of file diff --git a/docs/blog/elysia-supabase.md b/docs/blog/elysia-supabase.md index f4cd4cd1..df2bbf43 100644 --- a/docs/blog/elysia-supabase.md +++ b/docs/blog/elysia-supabase.md @@ -30,12 +30,11 @@ head: Supabase, an Open Source alternative to Firebase, has become one of the developers' favorite toolkits known for rapid development. @@ -60,7 +59,7 @@ Things that take many hours to redo in every project are now a matter of a minut If you haven't heard, Elysia is a Bun-first web framework built with speed and Developer Experience in mind. -Elysia outperforms Express by nearly ~20x faster, while having almost the same syntax as Express and Fastify. +Elysia outperforms Express by nearly ~20x faster, while having almost the same syntax as Express and Fastify. ###### (Performance may vary per machine, we recommended you run [the benchmark](https://github.com/SaltyAom/bun-http-framework-benchmark) on your machine before deciding the performance) @@ -157,27 +156,26 @@ And now, let's apply Supabase to authenticate our user. ```ts // src/modules/authen.ts import { Elysia } from 'elysia' -import { supabase } from '../../libs' // [!code ++] +import { supabase } from '../../libs' // [!code ++] const authen = (app: Elysia) => app.group('/auth', (app) => app .post('/sign-up', async ({ body }) => { const { data, error } = await supabase.auth.signUp(body) // [!code ++] - // [!code ++] + // [!code ++] if (error) return error // [!code ++] return data.user // [!code ++] return 'This route is expected to sign up a user' // [!code --] }) .post('/sign-in', async ({ body }) => { - const { data, error } = await supabase.auth.signInWithPassword( - // [!code ++] + const { data, error } = await supabase.auth.signInWithPassword( // [!code ++] body // [!code ++] ) // [!code ++] - // [!code ++] + // [!code ++] if (error) return error // [!code ++] - // [!code ++] + // [!code ++] return data.user // [!code ++] return 'This route is expected to sign in a user' // [!code --] }) @@ -207,18 +205,13 @@ const authen = (app: Elysia) => return data.user }, - { - // [!code ++] - schema: { - // [!code ++] - body: t.Object({ - // [!code ++] - email: t.String({ - // [!code ++] + { // [!code ++] + schema: { // [!code ++] + body: t.Object({ // [!code ++] + email: t.String({ // [!code ++] format: 'email' // [!code ++] }), // [!code ++] - password: t.String({ - // [!code ++] + password: t.String({ // [!code ++] minLength: 8 // [!code ++] }) // [!code ++] }) // [!code ++] @@ -235,18 +228,13 @@ const authen = (app: Elysia) => return data.user }, - { - // [!code ++] - schema: { - // [!code ++] - body: t.Object({ - // [!code ++] - email: t.String({ - // [!code ++] + { // [!code ++] + schema: { // [!code ++] + body: t.Object({ // [!code ++] + email: t.String({ // [!code ++] format: 'email' // [!code ++] }), // [!code ++] - password: t.String({ - // [!code ++] + password: t.String({ // [!code ++] minLength: 8 // [!code ++] }) // [!code ++] }) // [!code ++] @@ -276,16 +264,12 @@ import { supabase } from '../../libs' const authen = (app: Elysia) => app.group('/auth', (app) => app - .setModel({ - // [!code ++] - sign: t.Object({ - // [!code ++] - email: t.String({ - // [!code ++] + .setModel({ // [!code ++] + sign: t.Object({ // [!code ++] + email: t.String({ // [!code ++] format: 'email' // [!code ++] }), // [!code ++] - password: t.String({ - // [!code ++] + password: t.String({ // [!code ++] minLength: 8 // [!code ++] }) // [!code ++] }) // [!code ++] @@ -301,14 +285,11 @@ const authen = (app: Elysia) => { schema: { body: 'sign', // [!code ++] - body: t.Object({ - // [!code --] - email: t.String({ - // [!code --] + body: t.Object({ // [!code --] + email: t.String({ // [!code --] format: 'email' // [!code --] }), // [!code --] - password: t.String({ - // [!code --] + password: t.String({ // [!code --] minLength: 8 // [!code --] }) // [!code --] }) // [!code --] @@ -328,14 +309,11 @@ const authen = (app: Elysia) => { schema: { body: 'sign', // [!code ++] - body: t.Object({ - // [!code --] - email: t.String({ - // [!code --] + body: t.Object({ // [!code --] + email: t.String({ // [!code --] format: 'email' // [!code --] }), // [!code --] - password: t.String({ - // [!code --] + password: t.String({ // [!code --] minLength: 8 // [!code --] }) // [!code --] }) // [!code --] @@ -374,37 +352,33 @@ import { cookie } from '@elysiajs/cookie' // [!code ++] import { supabase } from '../../libs' const authen = (app: Elysia) => - app.group( - '/auth', - (app) => - app - .use( - // [!code ++] - cookie({ - // [!code ++] - httpOnly: true // [!code ++] - // If you need cookie to deliver via https only // [!code ++] - // secure: true, // [!code ++] - // // [!code ++] - // If you need a cookie to be available for same-site only // [!code ++] - // sameSite: "strict", // [!code ++] - // // [!code ++] - // If you want to encrypt a cookie // [!code ++] - // signed: true, // [!code ++] - // secret: process.env.COOKIE_SECRET, // [!code ++] - }) // [!code ++] - ) // [!code ++] - .setModel({ - sign: t.Object({ - email: t.String({ - format: 'email' - }), - password: t.String({ - minLength: 8 - }) + app.group('/auth', (app) => + app + .use( // [!code ++] + cookie({ // [!code ++] + httpOnly: true, // [!code ++] + // If you need cookie to deliver via https only // [!code ++] + // secure: true, // [!code ++] + // // [!code ++] + // If you need a cookie to be available for same-site only // [!code ++] + // sameSite: "strict", // [!code ++] + // // [!code ++] + // If you want to encrypt a cookie // [!code ++] + // signed: true, // [!code ++] + // secret: process.env.COOKIE_SECRET, // [!code ++] + }) // [!code ++] + ) // [!code ++] + .setModel({ + sign: t.Object({ + email: t.String({ + format: 'email' + }), + password: t.String({ + minLength: 8 }) }) - // rest of the code + }) + // rest of the code ) ``` @@ -424,73 +398,66 @@ import { Elysia, t } from 'elysia' import { supabase } from '../../libs' const authen = (app: Elysia) => - app.group( - '/auth', - (app) => - app - .setModel({ - sign: t.Object({ - email: t.String({ - format: 'email' - }), - password: t.String({ - minLength: 8 - }) + app.group('/auth', (app) => + app + .setModel({ + sign: t.Object({ + email: t.String({ + format: 'email' + }), + password: t.String({ + minLength: 8 }) }) - .post( - '/sign-up', - async ({ body }) => { - const { data, error } = await supabase.auth.signUp(body) - - if (error) return error - return data.user - }, - { - schema: { - body: 'sign' - } + }) + .post( + '/sign-up', + async ({ body }) => { + const { data, error } = await supabase.auth.signUp(body) + + if (error) return error + return data.user + }, + { + schema: { + body: 'sign' } - ) - .post( - '/sign-in', - async ({ body }) => { - const { data, error } = - await supabase.auth.signInWithPassword(body) - - if (error) return error - - return data.user - }, - { - schema: { - body: 'sign' - } + } + ) + .post( + '/sign-in', + async ({ body }) => { + const { data, error } = + await supabase.auth.signInWithPassword(body) + + if (error) return error + + return data.user + }, + { + schema: { + body: 'sign' } - ) - .get( - // [!code ++] - '/refresh', // [!code ++] - async ({ setCookie, cookie: { refresh_token } }) => { - // [!code ++] - const { data, error } = - await supabase.auth.refreshSession({ - // [!code ++] - refresh_token // [!code ++] - }) // [!code ++] - // [!code ++] - if (error) return error // [!code ++] - // [!code ++] - setCookie('refresh_token', data.session!.refresh_token) // [!code ++] - // [!code ++] - return data.user // [!code ++] - } // [!code ++] - ) // [!code ++] + } + ) + .get( // [!code ++] + '/refresh', // [!code ++] + async ({ setCookie, cookie: { refresh_token } }) => { // [!code ++] + const { data, error } = await supabase.auth.refreshSession({ // [!code ++] + refresh_token // [!code ++] + }) // [!code ++] + // [!code ++] + if (error) return error // [!code ++] + // [!code ++] + setCookie('refresh_token', data.session!.refresh_token) // [!code ++] + // [!code ++] + return data.user // [!code ++] + } // [!code ++] + ) // [!code ++] ) ``` Finally, add routes to the main server. - ```ts import { Elysia, t } from 'elysia' @@ -575,21 +542,18 @@ export const post = (app: Elysia) => '/create', async ({ body }) => { let userId: string // [!code ++] - // [!code ++] - const { data, error } = await supabase.auth.getUser( - // [!code ++] + // [!code ++] + const { data, error } = await supabase.auth.getUser( // [!code ++] access_token // [!code ++] ) // [!code ++] - // [!code ++] - if (error) { - // [!code ++] - const { data, error } = await supabase.auth.refreshSession({ - // [!code ++] + // [!code ++] + if(error) { // [!code ++] + const { data, error } = await supabase.auth.refreshSession({ // [!code ++] refresh_token // [!code ++] }) // [!code ++] - // [!code ++] + // [!code ++] if (error) throw error // [!code ++] - // [!code ++] + // [!code ++] userId = data.user!.id // [!code ++] } // [!code ++] @@ -620,7 +584,6 @@ export const post = (app: Elysia) => Great! Now we can extract `user_id` from our cookie using **supabase.auth.getUser** ## Derive - Our code work fine for now, but let's paint a little picture. Let's say you have so many routes that require authorization like this, requiring you to extract the `userId`, it means that you will have a lot of duplicated code here, right? @@ -702,25 +665,20 @@ export const post = (app: Elysia) => .use(authen) // [!code ++] .put( '/create', - async ({ body, userId }) => { - // [!code ++] + async ({ body, userId }) => { // [!code ++] let userId: string // [!code --] - // [!code --] - const { data, error } = await supabase.auth.getUser( - // [!code --] + // [!code --] + const { data, error } = await supabase.auth.getUser( // [!code --] access_token // [!code --] ) // [!code --] - // [!code --] - if (error) { - // [!code --] - const { data, error } = - await supabase.auth.refreshSession({ - // [!code --] - refresh_token // [!code --] - }) // [!code --] - // [!code --] + // [!code --] + if(error) { // [!code --] + const { data, error } = await supabase.auth.refreshSession({ // [!code --] + refresh_token // [!code --] + }) // [!code --] + // [!code --] if (error) throw error // [!code --] - // [!code --] + // [!code --] userId = data.user!.id // [!code --] } // [!code --] @@ -745,16 +703,16 @@ export const post = (app: Elysia) => } ) ) + ``` -Great right? We don't even see that we handled the authorization by looking at the code like magic. +Great right? We don't even see that we handled the authorization by looking at the code like magic. Putting our focus back on our core business logic instead. Using Rest Client to create post ## Non-authorized scope - Now let's create one more route to fetch the post from the database. ```ts @@ -765,17 +723,15 @@ import { authen, supabase } from '../../libs' export const post = (app: Elysia) => app.group('/post', (app) => app - .get('/:id', async ({ params: { id } }) => { - // [!code ++] + .get('/:id', async ({ params: { id } }) => { // [!code ++] const { data, error } = await supabase // [!code ++] .from('post') // [!code ++] .select() // [!code ++] .eq('id', id) // [!code ++] - // [!code ++] + // [!code ++] if (error) return error // [!code ++] - // [!code ++] - return { - // [!code ++] + // [!code ++] + return { // [!code ++] success: !!data[0], // [!code ++] data: data[0] ?? null // [!code ++] } // [!code ++] @@ -817,7 +773,6 @@ If not, we are going to return `success: false` and `data: null` instead. As we mentioned before, the `.use(authen)` is applied to the scoped **but** with only the one declared after itself, which means that anything before isn't affected, and what came after is now authorized only route. And one last thing, don't forget to add routes to the main server. - ```ts import { Elysia, t } from 'elysia' @@ -833,6 +788,7 @@ console.log( ) ``` + ## Bonus: Documentation As a bonus, after all of what we create, instead of telling exactly route by route, we can create documentation for our frontend devs in 1 line. @@ -884,4 +840,4 @@ Elysia is on a journey for creating a Bun-first web framework with new technolog If you're interested in Elysia, feel free to check out our [Discord server](https://discord.gg/eaFJ2KDJck) or see [Elysia on GitHub](https://github.com/elysiajs/elysia) Also, you might want to checkout out [Elysia Eden](/plugins/eden/overview), a fully type-safe, no-code-gen fetch client like tRPC for Elysia server. - + \ No newline at end of file diff --git a/docs/blog/integrate-trpc-with-elysia.md b/docs/blog/integrate-trpc-with-elysia.md index ef074edd..df2bbf43 100644 --- a/docs/blog/integrate-trpc-with-elysia.md +++ b/docs/blog/integrate-trpc-with-elysia.md @@ -1,28 +1,28 @@ --- -title: Integrate tRPC server to Bun with Elysia +title: Elysia with Supabase. Your next backend at sonic speed sidebar: false editLink: false search: false head: - - meta - property: 'og:title' - content: Integrate tRPC server to Bun with Elysia + content: Elysia with Supabase. Your next backend at sonic speed - - meta - name: 'description' - content: 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. + content: 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. - - meta - property: 'og:description' - content: 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. + content: 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. - - meta - property: 'og:image' - content: https://elysiajs.com/blog/integrate-trpc-with-elysia/elysia-trpc.webp + content: https://elysiajs.com/blog/elysia-supabase/elysia-supabase.webp - - meta - property: 'twitter:image' - content: https://elysiajs.com/blog/integrate-trpc-with-elysia/elysia-trpc.webp + content: https://elysiajs.com/blog/elysia-supabase/elysia-supabase.webp --- -tRPC has been a popular choice for web development recently, thanks to its end-to-end type-safety approach to accelerate development by blurring the line between front and backend, and inferring types from the backend automatically. +Supabase, an Open Source alternative to Firebase, has become one of the developers' favorite toolkits known for rapid development. -Helping developers develop faster and safer code, knowing instantly when things break while migrating data structure, and removing redundant steps of re-creating type for frontend once again. +Featuring PostgreSQL, ready-to-use user authentication, Serverless Edge function, Cloud Storage, and more, ready to use. -But we can when extending tRPC more. +Because Supabase already has pre-built and composed the situation where you find yourself redoing the same feature for the 100th time into less than 10 lines of code. -## Elysia +For example, authentication, which would take require you to rewrite a hundred lines of code for every project you did to just: -Elysia is a web framework optimized for Bun, inspired by many frameworks including tRPC. Elysia supports end-to-end type safety by default, but unlike tRPC, Elysia uses Express-like syntax that many already know, removing the learning curve of tRPC. +```ts +supabase.auth.signUp(body) -With Bun being the runtime for Elysia, the speed and throughput for Elysia server are fast and even outperforming [Express up to 21x and Fastify up to 12x on mirroring JSON body (see benchmark)](https://github.com/SaltyAom/bun-http-framework-benchmark/tree/655fe7f87f0f4f73f2121433f4741a9d6cf00de4). +supabase.auth.signInWithPassword(body) +``` -The ability to combine the existing tRPC server into Elysia has been one of the very first objectives of Elysia since its start. +Then Supabase will handle the rest, confirming email by sending a confirmation link, or authentication with a magic link or OTP, securing your database with row-level authentication, you name it. -The reason why you might want to switch from tRPC to Bun: +Things that take many hours to redo in every project are now a matter of a minute to accomplish. -- Significantly faster, even outperform many popular web frameworks running in Nodejs without changing a single piece of code. -- Extend tRPC with RESTful or GraphQL, both co-existing in the same server. -- Elysia has end-to-end type-safety like tRPC but with almost no-learning curve for most developer. -- Using Elysia is the great first start experimenting/investing in Bun runtime. +## Elysia -## Creating Elysia Server +If you haven't heard, Elysia is a Bun-first web framework built with speed and Developer Experience in mind. -To get started, let's create a new Elysia server, make sure you have [Bun](https://bun.sh) installed first, then run this command to scaffold Elysia project. +Elysia outperforms Express by nearly ~20x faster, while having almost the same syntax as Express and Fastify. -``` -bun create elysia elysia-trpc && cd elysia-trpc && bun add elysia -``` +###### (Performance may vary per machine, we recommended you run [the benchmark](https://github.com/SaltyAom/bun-http-framework-benchmark) on your machine before deciding the performance) -::: tip -Sometimes Bun doesn't resolve the latest field correctly, so we are using `bun add elysia` to specify the latest version of Elysia instead -::: +Elysia has a very snappy Developer Experience. +Not only that you can define a single source of truth for type, but also detects and warns when you accidentally create a change in data. -This should create a folder name **"elysia-trpc"** with Elysia pre-configured. +All done in a declaratively small line of code. -Let's start a development server by running the dev command: +## Setting things up -``` -bun run dev -``` +You can use Supabase Cloud for a quick start. -This command should start a development server on port :3000 +Supabase Cloud will handle setting up the database, scaling, and all things you need in the Cloud in a single click. -## Elysia tRPC plugin +Supabase landing page -Building on top of the tRPC Web Standard adapter, Elysia has a plugin for integrating the existing tRPC server into Elysia. +Creating a project, you should see something like this, fill all the requests you need, and if you're in Asia, Supabase has a server in Singapore and Tokyo -```bash -bun add @trpc/server zod @elysiajs/trpc @elysiajs/cors -``` +##### (sometimes this is a tie-breaker for developers living in Asia because of latency) -Suppose that this is an existing tRPC server: +Creating new Supabase project -```typescript -import { initTRPC } from '@trpc/server' -import { observable } from '@trpc/server/observable' +After creating a project, you should greet with a welcome screen where you can copy the project URL and service role. -import { z } from 'zod' +Both are use to indicate which Supabase project you're using in your project. -const t = initTRPC.create() +And if you missed the welcome page, you navigate to **Settings > API**, copy **Project URL** and **Project API keys** -export const router = t.router({ - mirror: t.procedure.input(z.string()).query(({ input }) => input) -}) +Supabase Config Page -export type Router = typeof router -``` +Now in your command line, you can start creating the Elysia project by running: -Normally all we need to use tRPC is to export the type of router, but to integrate tRPC with Elysia, we need to export the instance of router too. +```bash +bun create elysia elysia-supabase +``` -Then in the Elysia server, we import the router and register tRPC router with `.use(trpc)` +The last argument is our folder name for Bun to create, feel free to change the name to anything you like. -```typescript -import { Elysia } from 'elysia' -import { cors } from '@elysiajs/cors' // [!code ++] -import { trpc } from '@elysiajs/trpc' // [!code ++] +Now, **cd** into our folder, as we are going to use a newer feature in Elysia 0.3 (RC), we need to install the Elysia RC channel first, and let's grab a cookie plugin and Supabase client that we are going to use later here too. -import { router } from './trpc' // [!code ++] +```bash +bun add elysia@rc @elysiajs/cookie@rc @supabase/supabase-js +``` -const app = new Elysia() - .use(cors()) // [!code ++] - .get('/', () => 'Hello Elysia') - .use( - // [!code ++] - trpc(router) // [!code ++] - ) // [!code ++] - .listen(3000) +Let's create a **.env** file to load our Supabase service load as a secret. -console.log( - `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` -) +```bash +# .env +supabase_url=https://********************.supabase.co +supabase_service_role=**** **** **** **** ``` -And that's it! 🎉 +You don't have to install any plugin to load the env file as Bun load **.env** file by default + +Now let's open our project in our favorite IDE, and create a file inside `src/libs/supabase.ts` -That's all it takes to integrate tRPC with Elysia, making tRPC run on Bun. +```ts +// src/libs/supabase.ts +import { createClient } from '@supabase/supabase-js' -## tRPC config and Context +const { supabase_url, supabase_service_role } = process.env -To create context, `trpc` can accept 2nd parameters that can configure tRPC as same as `createHTTPServer`. +export const supabase = createClient(supabase_url!, supabase_service_role!) +``` + +And that's it! That's all you need to set up Supabase and a Elysia project. -For example, adding `createContext` into tRPC server: +Now let's dive into implementation! -```typescript -// trpc.ts -import { initTRPC } from '@trpc/server' -import { observable } from '@trpc/server/observable' -import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch' // [!code ++] -import { z } from 'zod' +## Authentication -export const createContext = async (opts: FetchCreateContextFnOptions) => { - // [!code ++] - return { - // [!code ++] - name: 'elysia' // [!code ++] - } // [!code ++] -} // [!code ++] +Now let's create authentication routes separated from the main file. -const t = initTRPC.context>>().create() // [!code ++] +Inside `src/modules/authen.ts`, let's create an outline for our routes first. -export const router = t.router({ - mirror: t.procedure.input(z.string()).query(({ input }) => input) -}) +```ts +// src/modules/authen.ts +import { Elysia } from 'elysia' -export type Router = typeof router +const authen = (app: Elysia) => + app.group('/auth', (app) => + app + .post('/sign-up', () => { + return 'This route is expected to sign up a user' + }) + .post('/sign-in', () => { + return 'This route is expected to sign in a user' + }) + ) ``` -And in the Elysia server +And now, let's apply Supabase to authenticate our user. -```typescript +```ts +// src/modules/authen.ts import { Elysia } from 'elysia' -import { cors } from '@elysiajs/cors' -import '@elysiajs/trpc' - -import { router, createContext } from './trpc' // [!code ++] +import { supabase } from '../../libs' // [!code ++] -const app = new Elysia() - .use(cors()) - .get('/', () => 'Hello Elysia') - .use( - trpc(router, { - // [!code ++] - createContext // [!code ++] - }) // [!code ++] +const authen = (app: Elysia) => + app.group('/auth', (app) => + app + .post('/sign-up', async ({ body }) => { + const { data, error } = await supabase.auth.signUp(body) // [!code ++] + // [!code ++] + if (error) return error // [!code ++] + + return data.user // [!code ++] + return 'This route is expected to sign up a user' // [!code --] + }) + .post('/sign-in', async ({ body }) => { + const { data, error } = await supabase.auth.signInWithPassword( // [!code ++] + body // [!code ++] + ) // [!code ++] + // [!code ++] + if (error) return error // [!code ++] + // [!code ++] + return data.user // [!code ++] + return 'This route is expected to sign in a user' // [!code --] + }) ) - .listen(3000) - -console.log( - `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` -) ``` -And we can specify a custom endpoint of tRPC by using `endpoint`: +And- done! That's all it needs to create **sign-in** and **sign-up** route for our user. -```typescript -import { Elysia } from 'elysia' -import { cors } from '@elysiajs/cors' -import { trpc } from '@elysiajs/trpc' +But we have a little problem here, you see, our route can accept **any** body and put it into a Supabase parameter, even an invalid one. -import { router, createContext } from './trpc' +So, to make sure that we put the correct data, we can define a schema for our body. -const app = new Elysia() - .use(cors()) - .get('/', () => 'Hello Elysia') - .use( - trpc(router, { - createContext, - endpoint: '/v2/trpc' // [!code ++] - }) +```ts +// src/modules/authen.ts +import { Elysia, t } from 'elysia' +import { supabase } from '../../libs' + +const authen = (app: Elysia) => + app.group('/auth', (app) => + app + .post( + '/sign-up', + async ({ body }) => { + const { data, error } = await supabase.auth.signUp(body) + + if (error) return error + + return data.user + }, + { // [!code ++] + schema: { // [!code ++] + body: t.Object({ // [!code ++] + email: t.String({ // [!code ++] + format: 'email' // [!code ++] + }), // [!code ++] + password: t.String({ // [!code ++] + minLength: 8 // [!code ++] + }) // [!code ++] + }) // [!code ++] + } // [!code ++] + } // [!code ++] + ) + .post( + '/sign-in', + async ({ body }) => { + const { data, error } = + await supabase.auth.signInWithPassword(body) + + if (error) return error + + return data.user + }, + { // [!code ++] + schema: { // [!code ++] + body: t.Object({ // [!code ++] + email: t.String({ // [!code ++] + format: 'email' // [!code ++] + }), // [!code ++] + password: t.String({ // [!code ++] + minLength: 8 // [!code ++] + }) // [!code ++] + }) // [!code ++] + } // [!code ++] + } // [!code ++] + ) ) - .listen(3000) - -console.log( - `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` -) ``` -## Subscription +And now we declare a schema in both **sign-in** and **sign-up**, Elysia is going to make sure that an incoming body is going to have the same form as we declare, preventing an invalid argument from passing into `supabase.auth`. -By default, tRPC uses WebSocketServer to support `subscription`, but unfortunately as Bun 0.5.4 doesn't support WebSocketServer yet, we can't directly use WebSocket Server. +Elysia also understands the schema, so instead of declaring TypeScript's type separately, Elysia types the `body` automatically as the schema you defined. -However, Bun does support Web Socket using `Bun.serve`, and with Elysia tRPC plugin has wired all the usage of tRPC's Web Socket into `Bun.serve`, you can directly use tRPC's `subscription` with Elysia Web Socket plugin directly: +So if you accidentally create a breaking change in the future, Elysia going to warn you about the data type. -Start by installing the Web Socket plugin: +The code we have are great, it did the job that we expected, but we can step it up a little bit further. -```bash -bun add @elysiajs/websocket +You see, both **sign-in** and **sign-up** accept the same shape of data, in the future, you might also find yourself duplicating a long schema in multiple routes. + +We can fix that by telling Elysia to memorize our schema, then we can use by telling Elysia the name of the schema we want to use. + +```ts +// src/modules/authen.ts +import { Elysia, t } from 'elysia' +import { supabase } from '../../libs' + +const authen = (app: Elysia) => + app.group('/auth', (app) => + app + .setModel({ // [!code ++] + sign: t.Object({ // [!code ++] + email: t.String({ // [!code ++] + format: 'email' // [!code ++] + }), // [!code ++] + password: t.String({ // [!code ++] + minLength: 8 // [!code ++] + }) // [!code ++] + }) // [!code ++] + }) // [!code ++] + .post( + '/sign-up', + async ({ body }) => { + const { data, error } = await supabase.auth.signUp(body) + + if (error) return error + return data.user + }, + { + schema: { + body: 'sign', // [!code ++] + body: t.Object({ // [!code --] + email: t.String({ // [!code --] + format: 'email' // [!code --] + }), // [!code --] + password: t.String({ // [!code --] + minLength: 8 // [!code --] + }) // [!code --] + }) // [!code --] + } + } + ) + .post( + '/sign-in', + async ({ body }) => { + const { data, error } = + await supabase.auth.signInWithPassword(body) + + if (error) return error + + return data.user + }, + { + schema: { + body: 'sign', // [!code ++] + body: t.Object({ // [!code --] + email: t.String({ // [!code --] + format: 'email' // [!code --] + }), // [!code --] + password: t.String({ // [!code --] + minLength: 8 // [!code --] + }) // [!code --] + }) // [!code --] + } + } + ) + ) ``` -Then inside tRPC server: +Great! We have just used name reference on our route! + +::: tip +If you found yourself with a long schema, you can declare them in a separate file and re-use them in any Elysia route to put the focus back on business logic instead. +::: -```typescript -import { initTRPC } from '@trpc/server' -import { observable } from '@trpc/server/observable' // [!code ++] -import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch' +## Storing user session -import { EventEmitter } from 'stream' // [!code ++] +Great, now the last thing we need to do to complete the authentication system is to store the user session, after a user is signed in, the token is known as `access_token` and `refresh_token` in Supabase. -import { zod } from 'zod' +access_token is a short live JWT access token. Use to authenticate a user in a short amount of time. +refresh_token is a one-time-used never-expired token to renew access_token. So as long as we have this token, we can create a new access token to extend our user session. -export const createContext = async (opts: FetchCreateContextFnOptions) => { - return { - name: 'elysia' - } -} +We can store both values inside a cookie. -const t = initTRPC.context>>().create() -const ee = new EventEmitter() // [!code ++] +Now, some might not like the idea of storing the access token inside a cookie and might use Bearer instead. but for simplicity, we are going to use a cookie here. -export const router = t.router({ - mirror: t.procedure.input(z.string()).query(({ input }) => { - ee.emit('listen', input) // [!code ++] +::: tip +We can set a cookie as **HttpOnly** to prevent XSS, **Secure**, **Same-Site**, and also encrypt a cookie to prevent a man-in-the-middle attack. +::: - return input - }), - listen: t.procedure.subscription( - () => - // [!code ++] - observable((emit) => { - // [!code ++] - ee.on('listen', (input) => { - // [!code ++] - emit.next(input) // [!code ++] +```ts +// src/modules/authen.ts +import { Elysia, t } from 'elysia' +import { cookie } from '@elysiajs/cookie' // [!code ++] + +import { supabase } from '../../libs' + +const authen = (app: Elysia) => + app.group('/auth', (app) => + app + .use( // [!code ++] + cookie({ // [!code ++] + httpOnly: true, // [!code ++] + // If you need cookie to deliver via https only // [!code ++] + // secure: true, // [!code ++] + // // [!code ++] + // If you need a cookie to be available for same-site only // [!code ++] + // sameSite: "strict", // [!code ++] + // // [!code ++] + // If you want to encrypt a cookie // [!code ++] + // signed: true, // [!code ++] + // secret: process.env.COOKIE_SECRET, // [!code ++] }) // [!code ++] - }) // [!code ++] - ) // [!code ++] -}) - -export type Router = typeof router + ) // [!code ++] + .setModel({ + sign: t.Object({ + email: t.String({ + format: 'email' + }), + password: t.String({ + minLength: 8 + }) + }) + }) + // rest of the code + ) ``` -And then we register: +And-- That's all it takes to create a **sign-in** and **sign-up** route for Elysia and Supabase! + +Using Rest Client to sign in + +## Refreshing a token + +Now, as mentioned, access_token is short-lived, and we might need to renew the token now and then. -```typescript -import { Elysia, ws } from 'elysia' -import { cors } from '@elysiajs/cors' -import '@elysiajs/trpc' +Luckily, we can do that with a one-liner from Supabase. + +```ts +// src/modules/authen.ts +import { Elysia, t } from 'elysia' +import { supabase } from '../../libs' + +const authen = (app: Elysia) => + app.group('/auth', (app) => + app + .setModel({ + sign: t.Object({ + email: t.String({ + format: 'email' + }), + password: t.String({ + minLength: 8 + }) + }) + }) + .post( + '/sign-up', + async ({ body }) => { + const { data, error } = await supabase.auth.signUp(body) + + if (error) return error + return data.user + }, + { + schema: { + body: 'sign' + } + } + ) + .post( + '/sign-in', + async ({ body }) => { + const { data, error } = + await supabase.auth.signInWithPassword(body) + + if (error) return error + + return data.user + }, + { + schema: { + body: 'sign' + } + } + ) + .get( // [!code ++] + '/refresh', // [!code ++] + async ({ setCookie, cookie: { refresh_token } }) => { // [!code ++] + const { data, error } = await supabase.auth.refreshSession({ // [!code ++] + refresh_token // [!code ++] + }) // [!code ++] + // [!code ++] + if (error) return error // [!code ++] + // [!code ++] + setCookie('refresh_token', data.session!.refresh_token) // [!code ++] + // [!code ++] + return data.user // [!code ++] + } // [!code ++] + ) // [!code ++] + ) +``` + +Finally, add routes to the main server. +```ts +import { Elysia, t } from 'elysia' -import { router, createContext } from './trpc' +import { auth } from './modules' // [!code ++] const app = new Elysia() - .use(cors()) - .use(ws()) // [!code ++] - .get('/', () => 'Hello Elysia') - .trpc(router, { - createContext - }) + .use(auth) // [!code ++] .listen(3000) console.log( @@ -292,156 +472,372 @@ console.log( ) ``` -And that's all it takes to integrate the existing fully functional tRPC server to Elysia Server thus making tRPC run on Bun 🥳. +And that's it! -Elysia is excellent when you need both tRPC and REST API, as they can co-exist together in one server. +## Authorization route -## Bonus: Type-Safe Elysia with Eden +We have just implemented user authentication which is fun and game, but now you might find yourself in need of authorization for each route, and duplicating the same code to check for cookies all over the place. -As Elysia is inspired by tRPC, Elysia also supports end-to-end type-safety like tRPC by default using **"Eden"**. +Luckily, we can re-use the function in Elysia. -This means that you can use Express-like syntax to create RESTful API with full-type support on a client like tRPC. +Let's paint the example by saying that we might want a user to create a simple blog post that can have the database schema as the following: - +Inside the Supabse console, we are going to create a Postgres table name 'post' as the following: +Creating table using Supabase UI, in the public table with the name of 'post', and a columns of 'id' with type of 'int8' as a primary value, 'created_at' with type of 'timestamp' with default value of 'now()', 'user_id' linked to Supabase's user schema linked as 'user.id', and 'post' with type of 'text' -To get started, let's export the app type. +**user_id** is linked to Supabase's generated **auth** table linked as **user.id**, using this relation, we can create row-level security to only allow the owner of the post to modify the data. -```typescript -import { Elysia, ws } from 'elysia' -import { cors } from '@elysiajs/cors' -import { trpc } from '@elysiajs/trpc' +Linking the 'user_id' field with Supabase's user schema as 'user.id' -import { router, createContext } from './trpc' +Now, let's create a new separated Elysia route in another folder to separate the code from auth route, inside `src/modules/post/index.ts` -const app = new Elysia() - .use(cors()) - .use(ws()) - .get('/', () => 'Hello Elysia') - .use( - trpc(router, { - createContext - }) +```ts +// src/modules/post/index.ts +import { Elysia, t } from 'elysia' + +import { supabase } from '../../libs' + +export const post = (app: Elysia) => + app.group('/post', (app) => + app.put( + '/create', + async ({ body }) => { + const { data, error } = await supabase + .from('post') + .insert({ + // Add user_id somehow + // user_id: userId, + ...body + }) + .select('id') + + if (error) throw error + + return data[0] + }, + { + schema: { + body: t.Object({ + detail: t.String() + }) + } + } + ) ) - .listen(3000) +``` -export type App = typeof app // [!code ++] +Now, this route can accept the body and put it into the database, the only thing we are left to do is handle authorization and extract the `user_id`. -console.log( - `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` -) +Luckily we can do that easily, thanks to Supabase, and our cookies. + +```ts +import { Elysia, t } from 'elysia' +import { cookie } from '@elysiajs/cookie' // [!code ++] + +import { supabase } from '../../libs' + +export const post = (app: Elysia) => + app.group('/post', (app) => + app.put( + '/create', + async ({ body }) => { + let userId: string // [!code ++] + // [!code ++] + const { data, error } = await supabase.auth.getUser( // [!code ++] + access_token // [!code ++] + ) // [!code ++] + // [!code ++] + if(error) { // [!code ++] + const { data, error } = await supabase.auth.refreshSession({ // [!code ++] + refresh_token // [!code ++] + }) // [!code ++] + // [!code ++] + if (error) throw error // [!code ++] + // [!code ++] + userId = data.user!.id // [!code ++] + } // [!code ++] + + const { data, error } = await supabase + .from('post') + .insert({ + // Add user_id somehow + // user_id: userId, + ...body + }) + .select('id') + + if (error) throw error + + return data[0] + }, + { + schema: { + body: t.Object({ + detail: t.String() + }) + } + } + ) + ) ``` -And on the client side: +Great! Now we can extract `user_id` from our cookie using **supabase.auth.getUser** -```bash -bun add @elysia/eden && bun add -d elysia +## Derive +Our code work fine for now, but let's paint a little picture. + +Let's say you have so many routes that require authorization like this, requiring you to extract the `userId`, it means that you will have a lot of duplicated code here, right? + +Luckily, Elysia is specially designed to tackle this problem. + +--- + +In Elysia, we have something called a **scope**. + +Imagine it's like a **closure** where only a variable can only be used inside a scope, or ownership if you're from Rust. + +Any life-cycle declared in scope like **group**, **guard** is going to be only available in scope. + +This means that you can declare a specific life cycle to specific routes inside the scope. + +For example, a scope of routes that need authorization while others are not. + +So, instead of reusing all that code, we defined it once and applied it to all the routes you need. + +--- + +Now, let's move this retrieving **user_id** into a plugin and apply it to all routes in the scope. + +Let's put this plugin inside `src/libs/authen.ts` + +```ts +import { Elysia } from 'elysia' +import { cookie } from '@elysiajs/cookie' + +import { supabase } from './supabase' + +export const authen = (app: Elysia) => + app + .use(cookie()) + .derive( + async ({ setCookie, cookie: { access_token, refresh_token } }) => { + const { data, error } = await supabase.auth.getUser( + access_token + ) + + if (data.user) + return { + userId: data.user.id + } + + const { data: refreshed, error: refreshError } = + await supabase.auth.refreshSession({ + refresh_token + }) + + if (refreshError) throw error + + return { + userId: refreshed.user!.id + } + } + ) ``` -And in the code: +This code attempts to extract userId, and add `userId` to `Context` of the route, otherwise, it will throw an error and skip the handler, preventing an invalid error to be put into our business logic, aka **supabase.from.insert**. -```typescript -import { edenTreaty } from '@elysiajs/eden' -import type { App } from '../server' +::: tip +We can also use **onBeforeHandle** to create a custom validation before entering the main handler too, **.derive** also does the same but any returned from **derived** will be added to **Context** while **onBeforeHandle** don't. + +Technically, **derive** use **transform** under the hood. +::: + +And with a single line, we apply all routes inside the scope into authorized-only routes, with type-safe access to **userId**. + +```ts +import { Elysia, t } from 'elysia' -// This now has all type inference from the server -const app = edenTreaty('http://localhost:3000') +import { authen, supabase } from '../../libs' // [!code ++] + +export const post = (app: Elysia) => + app.group('/post', (app) => + app + .use(authen) // [!code ++] + .put( + '/create', + async ({ body, userId }) => { // [!code ++] + let userId: string // [!code --] + // [!code --] + const { data, error } = await supabase.auth.getUser( // [!code --] + access_token // [!code --] + ) // [!code --] + // [!code --] + if(error) { // [!code --] + const { data, error } = await supabase.auth.refreshSession({ // [!code --] + refresh_token // [!code --] + }) // [!code --] + // [!code --] + if (error) throw error // [!code --] + // [!code --] + userId = data.user!.id // [!code --] + } // [!code --] + + const { data, error } = await supabase + .from('post') + .insert({ + user_id: userId, // [!code ++] + ...body + }) + .select('id') + + if (error) throw error + + return data[0] + }, + { + schema: { + body: t.Object({ + detail: t.String() + }) + } + } + ) + ) -// data will have a value of 'Hello Elysia' and has a type of 'string' -const data = await app.index.get() ``` -Elysia is a good start when you want end-to-end type-safety like tRPC but need to support more standard patterns like REST, and still have to support tRPC or need to migrate from one. +Great right? We don't even see that we handled the authorization by looking at the code like magic. -## Bonus: Extra tip for Elysia +Putting our focus back on our core business logic instead. -An additional thing you can do with Elysia is not only that it has support for tRPC and end-to-end type-safety, but also has a variety of support for many essential plugins configured especially for Bun. +Using Rest Client to create post -For example, you can generate documentation with Swagger only in 1 line using [Swagger plugin](/plugins/swagger). +## Non-authorized scope +Now let's create one more route to fetch the post from the database. -```typescript +```ts import { Elysia, t } from 'elysia' -import { swagger } from '@elysiajs/swagger' // [!code ++] + +import { authen, supabase } from '../../libs' + +export const post = (app: Elysia) => + app.group('/post', (app) => + app + .get('/:id', async ({ params: { id } }) => { // [!code ++] + const { data, error } = await supabase // [!code ++] + .from('post') // [!code ++] + .select() // [!code ++] + .eq('id', id) // [!code ++] + // [!code ++] + if (error) return error // [!code ++] + // [!code ++] + return { // [!code ++] + success: !!data[0], // [!code ++] + data: data[0] ?? null // [!code ++] + } // [!code ++] + }) // [!code ++] + .use(authen) + .put( + '/create', + async ({ body, userId }) => { + const { data, error } = await supabase + .from('post') + .insert({ + // Add user_id somehow + // user_id: userId, + ...body + }) + .select('id') + + if (error) throw error + + return data[0] + }, + { + schema: { + body: t.Object({ + detail: t.String() + }) + } + } + ) + ) +``` + +We are using success to indicate if the post is existed or not. +Using Rest Client to get post by id + +If not, we are going to return `success: false` and `data: null` instead. +Using Rest Client to get post by id but failed + +As we mentioned before, the `.use(authen)` is applied to the scoped **but** with only the one declared after itself, which means that anything before isn't affected, and what came after is now authorized only route. + +And one last thing, don't forget to add routes to the main server. +```ts +import { Elysia, t } from 'elysia' + +import { auth, post } from './modules' // [!code ++] const app = new Elysia() - .use(swagger()) // [!code ++] - .setModel({ - sign: t.Object({ - username: t.String(), - password: t.String() - }) - }) - .get('/', () => 'Hello Elysia') - .post('/typed-body', ({ body }) => body, { - schema: { - body: 'sign', - response: 'sign' - } - }) + .use(auth) + .use(post) // [!code ++] .listen(3000) -export type App = typeof app - console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` -Or when you want to use [GraphQL Apollo](/plugins/graphql-apollo) on Bun. -```typescript -import { Elysia } from 'elysia' -import { apollo, gql } from '@elysiajs/apollo' // [!code ++] +## Bonus: Documentation + +As a bonus, after all of what we create, instead of telling exactly route by route, we can create documentation for our frontend devs in 1 line. + +With the Swagger plugin, we can install: + +```bash +bun add @elysiajs/swagger@rc +``` + +And then just add the plugin: + +```ts +import { Elysia, t } from 'elysia' +import { swagger } from '@elysiajs/swagger' // [!code ++] + +import { auth, post } from './modules' const app = new Elysia() - .use( - // [!code ++] - apollo({ - // [!code ++] - typeDefs: gql` // [!code ++] - type Book { // [!code ++] - title: String // [!code ++] - author: String // [!code ++] - } // [!code ++] - // [!code ++] - type Query { // [!code ++] - books: [Book] // [!code ++] - } // [!code ++] - `, // [!code ++] - resolvers: { - // [!code ++] - Query: { - // [!code ++] - books: () => { - // [!code ++] - return [ - // [!code ++] - { - // [!code ++] - title: 'Elysia', // [!code ++] - author: 'saltyAom' // [!code ++] - } // [!code ++] - ] // [!code ++] - } // [!code ++] - } // [!code ++] - } // [!code ++] - }) // [!code ++] - ) // [!code ++] - .get('/', () => 'Hello Elysia') + .use(swagger()) // [!code ++] + .use(auth) + .use(post) .listen(3000) -export type App = typeof app - console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` -Or supporting OAuth 2.0 with a [community OAuth plugin](https://github.com/bogeychan/elysia-oauth2). +Tada 🎉 We got well-defined documentation for our API. + +Swagger documentation generated by Elysia + +And if anything more, you don't have to worry that you might forget a specification of OpenAPI Schema 3.0, we have auto-completion and type-safety too. + +We can define our route detail with `schema.detail` that also follows OpenAPI Schema 3.0, so you can properly create documentation. +Using auto-completion with `schema.detail` + +## What's next + +For the next step, we encourage you to try out and explore more with the [code we have just written in this article](https://github.com/saltyaom/elysia-supabase-example) and try adding an image-uploading post, to see explore both Supabase and Elysia ecosystems. + +As we can see, it's super easy to create a production-ready web server with Supabase, many things are just one-liners and handy for rapid development. + +Especially when paired with Elysia, you get excellent Developer Experience, declarative schema as a single source of truth, and a very well-thought design choice for creating an API, high-performance server while using TypeScript, and as a bonus, we can create documentation in just one line. -Nonetheless, Elysia is a great place to start learning/using Bun and the ecosystem around Bun. +Elysia is on a journey for creating a Bun-first web framework with new technology, and a new approach. -If you like to learn more about Elysia, [Elysia documentation](https://elysiajs.com) is a great start to start exploring the concept and patterns, and if you are stuck or need help, feel free to reach out in [Elysia Discord](https://discord.gg/eaFJ2KDJck). +If you're interested in Elysia, feel free to check out our [Discord server](https://discord.gg/eaFJ2KDJck) or see [Elysia on GitHub](https://github.com/elysiajs/elysia) -The repository for all of the code is available at [https://github.com/saltyaom/elysia-trpc-demo](https://github.com/saltyaom/elysia-trpc-demo), feels free to experiment and reach out. - +Also, you might want to checkout out [Elysia Eden](/plugins/eden/overview), a fully type-safe, no-code-gen fetch client like tRPC for Elysia server. + \ No newline at end of file diff --git a/docs/blog/with-prisma.md b/docs/blog/with-prisma.md index 832228f4..fd5a155e 100644 --- a/docs/blog/with-prisma.md +++ b/docs/blog/with-prisma.md @@ -12,6 +12,7 @@ head: - name: 'description' content: 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. + - - meta - property: 'og:description' content: 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. @@ -35,21 +36,20 @@ src="/blog/with-prisma/prism.webp" alt="Triangular Prism placing in the center" author="saltyaom" date="4 Jun 2023" - -> Prisma is a renowned TypeScript ORM for its developer experience. +> +Prisma is a renowned TypeScript ORM for its developer experience. With type-safe and intuitive API that allows us to interact with databases using a fluent and natural syntax. Writing a database query is as simple as writing a shape of data with TypeScript auto-completion, then Prisma takes care of the rest by generating efficient SQL queries and handling database connections in the background. One of the standout features of Prisma is its seamless integration with popular databases like: - -- PostgreSQL -- MySQL -- SQLite -- SQL Server -- MongoDB -- CockroachDB +- PostgreSQL +- MySQL +- SQLite +- SQL Server +- MongoDB +- CockroachDB So we have the flexibility to choose the database that best suits our project's needs, without compromising on the power and performance that Prisma brings to the table. @@ -84,13 +84,11 @@ bun create elysia elysia-prisma Where `elysia-prisma` is our project name (folder destination), feels free to change the name to anything you like. Now in our folder, and let's install Prisma CLI as dev dependency. - ```ts bun add -d prisma ``` Then we can setup prisma project with `prisma init` - ```ts bunx prisma init ``` @@ -99,10 +97,9 @@ bunx prisma init Once setup, we can see that Prisma will update `.env` file and generate a folder named **prisma** with **schema.prisma** as a file inside. -**schema.prisma** is an database model defined with Prisma's schema language. +**schema.prisma** is an database model defined with Prisma's schema language. Let's update our **schema.prisma** file like this for a demonstration: - ```ts generator client { provider = "prisma-client-js" @@ -123,31 +120,28 @@ model User { Telling Prisma that we want to create a table name **User** with column as: | Column | Type | Constraint | | --- | --- | --- | -| id | Number | Primary Key with auto increment | +| id | Number | Primary Key with auto increment | | username | String | Unique | | password | String | - | Prisma will then read the schema, and DATABASE_URL from an `.env` file, so before syncing our database we need to define the `DATABASE_URL` first. Since we don't have any database running, we can setup one using docker: - ```bash docker run -p 5432:5432 -e POSTGRES_PASSWORD=12345678 -d postgres ``` Now go into `.env` file at the root of our project then edit: - ``` DATABASE_URL="postgresql://postgres:12345678@localhost:5432/db?schema=public" ``` Then we can run `prisma migrate` to sync our database with Prisma schema: - ```bash bunx prisma migrate dev --name init ``` -Prisma then generate a strongly-typed Prisma Client code based on our schema. +Prisma then generate a strongly-typed Prisma Client code based on our schema. This means we get autocomplete and type checking in our code editor, catching potential errors at compile time rather than runtime. @@ -162,14 +156,11 @@ import { PrismaClient } from '@prisma/client' // [!code ++] const db = new PrismaClient() // [!code ++] const app = new Elysia() - .post( - // [!code ++] + .post( // [!code ++] '/sign-up', // [!code ++] - async ({ body }) => - db.user.create({ - // [!code ++] - data: body // [!code ++] - }) // [!code ++] + async ({ body }) => db.user.create({ // [!code ++] + data: body // [!code ++] + }) // [!code ++] ) // [!code ++] .listen(3000) @@ -189,7 +180,6 @@ As Prisma function doesn't return native Promise, Elysia can not dynamically han Now the problem is that the body could be anything, not limited to our expected defined type. We can improve that by using Elysia's type system. - ```ts import { Elysia, t } from 'elysia' // [!code ++] import { PrismaClient } from '@prisma/client' @@ -198,18 +188,14 @@ const db = new PrismaClient() const app = new Elysia() .post( - '/sign-up', - async ({ body }) => - db.user.create({ - data: body - }), - { - // [!code ++] - body: t.Object({ - // [!code ++] + '/sign-up', + async ({ body }) => db.user.create({ + data: body + }), + { // [!code ++] + body: t.Object({ // [!code ++] username: t.String(), // [!code ++] - password: t.String({ - // [!code ++] + password: t.String({ // [!code ++] minLength: 8 // [!code ++] }) // [!code ++] }) // [!code ++] @@ -223,7 +209,6 @@ console.log( ``` This tells Elysia to validate the body of an incoming request to match the shape, and update TypeScript's type of the `body` inside the callback to match the exact same type: - ```ts // 'body' is now typed as the following: { @@ -237,9 +222,7 @@ This means that if you the shape doesn't interlop with database table, it would Which is effective when you need to edit a table or perform a migration, Elysia can log the error immediately line by line because of a type conflict before reaching the production. ## Error Handling - Since our `username` field is unique, sometime Prisma can throw an error there could be an accidental duplication of `username` when trying to sign up like this: - ```ts Invalid `prisma.user.create()` invocation: @@ -247,7 +230,6 @@ Unique constraint failed on the fields: (`username`) ``` Default Elysia's error handler can handle the case automatically but we can improve that by specifying a custom error using Elysia's local `onError` hook: - ```ts import { Elysia, t } from 'elysia' import { PrismaClient } from '@prisma/client' @@ -257,24 +239,19 @@ const db = new PrismaClient() const app = new Elysia() .post( '/', - async ({ body }) => - db.user.create({ - data: body - }), + async ({ body }) => db.user.create({ + data: body + }), { - error({ code }) { - // [!code ++] - switch ( - code // [!code ++] - ) { + error({ code }) { // [!code ++] + switch (code) { // [!code ++] // Prisma P2002: "Unique constraint failed on the {constraint}" // [!code ++] - case 'P2002': // [!code ++] - return { - // [!code ++] - error: 'Username must be unique' // [!code ++] - } // [!code ++] - } // [!code ++] - }, // [!code ++] + case 'P2002': // [!code ++] + return { // [!code ++] + error: 'Username must be unique' // [!code ++] + } // [!code ++] + } // [!code ++] + }, // [!code ++] body: t.Object({ username: t.String(), password: t.String({ @@ -295,7 +272,6 @@ Using `error` hook, any error thown inside a callback will be populate to `error According to [Prisma documentation](https://www.prisma.io/docs/reference/api-reference/error-reference#p2002), error code 'P2002' means that by performing the query, it will failed a unique constraint. Since this table only a single `username` field that is unique, we can imply that the error is caused because username is not unique, so we return a custom erorr message of: - ```ts { error: 'Username must be unique' @@ -307,7 +283,6 @@ This will return a JSON equivalent of our custom error message when a unique con Allowing us to seemlessly define any custom error from Prisma error. ## Bonus: Reference Schema - When our server grow complex and type becoming more redundant and become a boilerplate, inlining an Elysia type can be improved by using **Reference Schema**. To put it simply, we can named our schema and reference the type by using the name. @@ -319,23 +294,19 @@ import { PrismaClient } from '@prisma/client' const db = new PrismaClient() const app = new Elysia() - .model({ - // [!code ++] - 'user.sign': t.Object({ - // [!code ++] + .model({ // [!code ++] + 'user.sign': t.Object({ // [!code ++] username: t.String(), // [!code ++] - password: t.String({ - // [!code ++] + password: t.String({ // [!code ++] minLength: 8 // [!code ++] }) // [!code ++] }) // [!code ++] }) // [!code ++] .post( '/', - async ({ body }) => - db.user.create({ - data: body - }), + async ({ body }) => db.user.create({ + data: body + }), { error({ code }) { switch (code) { @@ -347,11 +318,9 @@ const app = new Elysia() } }, body: 'user.sign', // [!code ++] - body: t.Object({ - // [!code --] + body: t.Object({ // [!code --] username: t.String(), // [!code --] - password: t.String({ - // [!code --] + password: t.String({ // [!code --] minLength: 8 // [!code --] }) // [!code --] }) // [!code --] @@ -369,7 +338,6 @@ This works as same as using an inline but instead you defined it once and refers TypeScript and validation code will works as expected. ## Bonus: Documentation - As a bonus, Elysia type system is also OpenAPI Schema 3.0 compliance, which means that it can generate documentation with tools that support OpenAPI Schema like Swagger. We can use Elysia Swagger plugin to generate an API documentation in a single line. @@ -394,8 +362,7 @@ const app = new Elysia() async ({ body }) => db.user.create({ data: body, - select: { - // [!code ++] + select: { // [!code ++] id: true, // [!code ++] username: true // [!code ++] } // [!code ++] @@ -416,8 +383,7 @@ const app = new Elysia() minLength: 8 }) }), - response: t.Object({ - // [!code ++] + response: t.Object({ // [!code ++] id: t.Number(), // [!code ++] username: t.String() // [!code ++] }) // [!code ++] @@ -443,7 +409,6 @@ And if anything more, we don't have to worry that we might forget a specificatio We can define our route detail with `detail` that also follows OpenAPI Schema 3.0, so we can properly create documentation effortlessly. ## What's next - With the support of Prisma with Bun and Elysia, we are entering a new era of a new level of developer experience. For Prisma we can accelerate our interaction with database, Elysia accelerate our creation of backend web server in term of both developer experience and performance. @@ -455,4 +420,4 @@ Elysia is on a journey to create a new standard for a better developer experienc If you're looking for a place to start learning about out Bun, consider take a look for what Elysia can offer especially with an [end-to-end type safety](/eden/overview) like tRPC but built on REST standard without any code generation. If you're interested in Elysia, feel free to check out our [Discord server](https://discord.gg/eaFJ2KDJck) or see [Elysia on GitHub](https://github.com/elysiajs/elysia) - + \ No newline at end of file From 1902bc205070e5b8d9e91c16dc157727194e61f4 Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:10:06 -0500 Subject: [PATCH 04/25] Update integrate-trpc-with-elysia.md --- docs/blog/integrate-trpc-with-elysia.md | 985 +++++++----------------- 1 file changed, 268 insertions(+), 717 deletions(-) diff --git a/docs/blog/integrate-trpc-with-elysia.md b/docs/blog/integrate-trpc-with-elysia.md index df2bbf43..52e1c22b 100644 --- a/docs/blog/integrate-trpc-with-elysia.md +++ b/docs/blog/integrate-trpc-with-elysia.md @@ -1,28 +1,28 @@ --- -title: Elysia with Supabase. Your next backend at sonic speed +title: Integrate tRPC server to Bun with Elysia sidebar: false editLink: false search: false head: - - - meta - - property: 'og:title' - content: Elysia with Supabase. Your next backend at sonic speed + - - meta + - property: 'og:title' + content: Integrate tRPC server to Bun with Elysia - - - meta - - name: 'description' - content: 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. + - - meta + - name: 'description' + content: 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. - - - meta - - property: 'og:description' - content: 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. + - - meta + - property: 'og:description' + content: 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. - - - meta - - property: 'og:image' - content: https://elysiajs.com/blog/elysia-supabase/elysia-supabase.webp + - - meta + - property: 'og:image' + content: https://elysiajs.com/blog/integrate-trpc-with-elysia/elysia-trpc.webp - - - meta - - property: 'twitter:image' - content: https://elysiajs.com/blog/elysia-supabase/elysia-supabase.webp + - - meta + - property: 'twitter:image' + content: https://elysiajs.com/blog/integrate-trpc-with-elysia/elysia-trpc.webp --- -Supabase, an Open Source alternative to Firebase, has become one of the developers' favorite toolkits known for rapid development. +tRPC has been a popular choice for web development recently, thanks to its end-to-end type-safety approach to accelerate development by blurring the line between front and backend, and inferring types from the backend automatically. -Featuring PostgreSQL, ready-to-use user authentication, Serverless Edge function, Cloud Storage, and more, ready to use. +Helping developers develop faster and safer code, knowing instantly when things break while migrating data structure, and removing redundant steps of re-creating type for frontend once again. -Because Supabase already has pre-built and composed the situation where you find yourself redoing the same feature for the 100th time into less than 10 lines of code. - -For example, authentication, which would take require you to rewrite a hundred lines of code for every project you did to just: - -```ts -supabase.auth.signUp(body) - -supabase.auth.signInWithPassword(body) -``` - -Then Supabase will handle the rest, confirming email by sending a confirmation link, or authentication with a magic link or OTP, securing your database with row-level authentication, you name it. - -Things that take many hours to redo in every project are now a matter of a minute to accomplish. +But we can when extending tRPC more. ## Elysia +Elysia is a web framework optimized for Bun, inspired by many frameworks including tRPC. Elysia supports end-to-end type safety by default, but unlike tRPC, Elysia uses Express-like syntax that many already know, removing the learning curve of tRPC. -If you haven't heard, Elysia is a Bun-first web framework built with speed and Developer Experience in mind. - -Elysia outperforms Express by nearly ~20x faster, while having almost the same syntax as Express and Fastify. - -###### (Performance may vary per machine, we recommended you run [the benchmark](https://github.com/SaltyAom/bun-http-framework-benchmark) on your machine before deciding the performance) - -Elysia has a very snappy Developer Experience. -Not only that you can define a single source of truth for type, but also detects and warns when you accidentally create a change in data. - -All done in a declaratively small line of code. - -## Setting things up - -You can use Supabase Cloud for a quick start. - -Supabase Cloud will handle setting up the database, scaling, and all things you need in the Cloud in a single click. - -Supabase landing page - -Creating a project, you should see something like this, fill all the requests you need, and if you're in Asia, Supabase has a server in Singapore and Tokyo - -##### (sometimes this is a tie-breaker for developers living in Asia because of latency) - -Creating new Supabase project - -After creating a project, you should greet with a welcome screen where you can copy the project URL and service role. - -Both are use to indicate which Supabase project you're using in your project. +With Bun being the runtime for Elysia, the speed and throughput for Elysia server are fast and even outperforming [Express up to 21x and Fastify up to 12x on mirroring JSON body (see benchmark)](https://github.com/SaltyAom/bun-http-framework-benchmark/tree/655fe7f87f0f4f73f2121433f4741a9d6cf00de4). -And if you missed the welcome page, you navigate to **Settings > API**, copy **Project URL** and **Project API keys** +The ability to combine the existing tRPC server into Elysia has been one of the very first objectives of Elysia since its start. -Supabase Config Page +The reason why you might want to switch from tRPC to Bun: +- Significantly faster, even outperform many popular web frameworks running in Nodejs without changing a single piece of code. +- Extend tRPC with RESTful or GraphQL, both co-existing in the same server. +- Elysia has end-to-end type-safety like tRPC but with almost no-learning curve for most developer. +- Using Elysia is the great first start experimenting/investing in Bun runtime. -Now in your command line, you can start creating the Elysia project by running: - -```bash -bun create elysia elysia-supabase +## Creating Elysia Server +To get started, let's create a new Elysia server, make sure you have [Bun](https://bun.sh) installed first, then run this command to scaffold Elysia project. +``` +bun create elysia elysia-trpc && cd elysia-trpc && bun add elysia ``` -The last argument is our folder name for Bun to create, feel free to change the name to anything you like. +::: tip +Sometimes Bun doesn't resolve the latest field correctly, so we are using `bun add elysia` to specify the latest version of Elysia instead +::: -Now, **cd** into our folder, as we are going to use a newer feature in Elysia 0.3 (RC), we need to install the Elysia RC channel first, and let's grab a cookie plugin and Supabase client that we are going to use later here too. +This should create a folder name **"elysia-trpc"** with Elysia pre-configured. -```bash -bun add elysia@rc @elysiajs/cookie@rc @supabase/supabase-js +Let's start a development server by running the dev command: +``` +bun run dev ``` -Let's create a **.env** file to load our Supabase service load as a secret. +This command should start a development server on port :3000 +## Elysia tRPC plugin +Building on top of the tRPC Web Standard adapter, Elysia has a plugin for integrating the existing tRPC server into Elysia. ```bash -# .env -supabase_url=https://********************.supabase.co -supabase_service_role=**** **** **** **** +bun add @trpc/server zod @elysiajs/trpc @elysiajs/cors ``` -You don't have to install any plugin to load the env file as Bun load **.env** file by default +Suppose that this is an existing tRPC server: +```typescript +import { initTRPC } from '@trpc/server' +import { observable } from '@trpc/server/observable' -Now let's open our project in our favorite IDE, and create a file inside `src/libs/supabase.ts` +import { z } from 'zod' -```ts -// src/libs/supabase.ts -import { createClient } from '@supabase/supabase-js' +const t = initTRPC.create() -const { supabase_url, supabase_service_role } = process.env +export const router = t.router({ + mirror: t.procedure.input(z.string()).query(({ input }) => input), +}) -export const supabase = createClient(supabase_url!, supabase_service_role!) +export type Router = typeof router ``` -And that's it! That's all you need to set up Supabase and a Elysia project. - -Now let's dive into implementation! - -## Authentication - -Now let's create authentication routes separated from the main file. +Normally all we need to use tRPC is to export the type of router, but to integrate tRPC with Elysia, we need to export the instance of router too. -Inside `src/modules/authen.ts`, let's create an outline for our routes first. - -```ts -// src/modules/authen.ts +Then in the Elysia server, we import the router and register tRPC router with `.use(trpc)` +```typescript import { Elysia } from 'elysia' +import { cors } from '@elysiajs/cors' // [!code ++] +import { trpc } from '@elysiajs/trpc' // [!code ++] -const authen = (app: Elysia) => - app.group('/auth', (app) => - app - .post('/sign-up', () => { - return 'This route is expected to sign up a user' - }) - .post('/sign-in', () => { - return 'This route is expected to sign in a user' - }) - ) -``` +import { router } from './trpc' // [!code ++] -And now, let's apply Supabase to authenticate our user. - -```ts -// src/modules/authen.ts -import { Elysia } from 'elysia' -import { supabase } from '../../libs' // [!code ++] +const app = new Elysia() + .use(cors()) // [!code ++] + .get('/', () => 'Hello Elysia') + .use( // [!code ++] + trpc(router) // [!code ++] + ) // [!code ++] + .listen(3000) -const authen = (app: Elysia) => - app.group('/auth', (app) => - app - .post('/sign-up', async ({ body }) => { - const { data, error } = await supabase.auth.signUp(body) // [!code ++] - // [!code ++] - if (error) return error // [!code ++] - - return data.user // [!code ++] - return 'This route is expected to sign up a user' // [!code --] - }) - .post('/sign-in', async ({ body }) => { - const { data, error } = await supabase.auth.signInWithPassword( // [!code ++] - body // [!code ++] - ) // [!code ++] - // [!code ++] - if (error) return error // [!code ++] - // [!code ++] - return data.user // [!code ++] - return 'This route is expected to sign in a user' // [!code --] - }) - ) +console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`) ``` -And- done! That's all it needs to create **sign-in** and **sign-up** route for our user. - -But we have a little problem here, you see, our route can accept **any** body and put it into a Supabase parameter, even an invalid one. +And that's it! 🎉 -So, to make sure that we put the correct data, we can define a schema for our body. +That's all it takes to integrate tRPC with Elysia, making tRPC run on Bun. -```ts -// src/modules/authen.ts -import { Elysia, t } from 'elysia' -import { supabase } from '../../libs' - -const authen = (app: Elysia) => - app.group('/auth', (app) => - app - .post( - '/sign-up', - async ({ body }) => { - const { data, error } = await supabase.auth.signUp(body) - - if (error) return error - - return data.user - }, - { // [!code ++] - schema: { // [!code ++] - body: t.Object({ // [!code ++] - email: t.String({ // [!code ++] - format: 'email' // [!code ++] - }), // [!code ++] - password: t.String({ // [!code ++] - minLength: 8 // [!code ++] - }) // [!code ++] - }) // [!code ++] - } // [!code ++] - } // [!code ++] - ) - .post( - '/sign-in', - async ({ body }) => { - const { data, error } = - await supabase.auth.signInWithPassword(body) - - if (error) return error - - return data.user - }, - { // [!code ++] - schema: { // [!code ++] - body: t.Object({ // [!code ++] - email: t.String({ // [!code ++] - format: 'email' // [!code ++] - }), // [!code ++] - password: t.String({ // [!code ++] - minLength: 8 // [!code ++] - }) // [!code ++] - }) // [!code ++] - } // [!code ++] - } // [!code ++] - ) - ) -``` +## tRPC config and Context +To create context, `trpc` can accept 2nd parameters that can configure tRPC as same as `createHTTPServer`. -And now we declare a schema in both **sign-in** and **sign-up**, Elysia is going to make sure that an incoming body is going to have the same form as we declare, preventing an invalid argument from passing into `supabase.auth`. +For example, adding `createContext` into tRPC server: +```typescript +// trpc.ts +import { initTRPC } from '@trpc/server' +import { observable } from '@trpc/server/observable' +import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch' // [!code ++] +import { z } from 'zod' -Elysia also understands the schema, so instead of declaring TypeScript's type separately, Elysia types the `body` automatically as the schema you defined. +export const createContext = async (opts: FetchCreateContextFnOptions) => { // [!code ++] + return { // [!code ++] + name: 'elysia' // [!code ++] + } // [!code ++] +} // [!code ++] -So if you accidentally create a breaking change in the future, Elysia going to warn you about the data type. +const t = initTRPC.context>>().create() // [!code ++] -The code we have are great, it did the job that we expected, but we can step it up a little bit further. +export const router = t.router({ + mirror: t.procedure.input(z.string()).query(({ input }) => input), +}) -You see, both **sign-in** and **sign-up** accept the same shape of data, in the future, you might also find yourself duplicating a long schema in multiple routes. - -We can fix that by telling Elysia to memorize our schema, then we can use by telling Elysia the name of the schema we want to use. - -```ts -// src/modules/authen.ts -import { Elysia, t } from 'elysia' -import { supabase } from '../../libs' - -const authen = (app: Elysia) => - app.group('/auth', (app) => - app - .setModel({ // [!code ++] - sign: t.Object({ // [!code ++] - email: t.String({ // [!code ++] - format: 'email' // [!code ++] - }), // [!code ++] - password: t.String({ // [!code ++] - minLength: 8 // [!code ++] - }) // [!code ++] - }) // [!code ++] - }) // [!code ++] - .post( - '/sign-up', - async ({ body }) => { - const { data, error } = await supabase.auth.signUp(body) - - if (error) return error - return data.user - }, - { - schema: { - body: 'sign', // [!code ++] - body: t.Object({ // [!code --] - email: t.String({ // [!code --] - format: 'email' // [!code --] - }), // [!code --] - password: t.String({ // [!code --] - minLength: 8 // [!code --] - }) // [!code --] - }) // [!code --] - } - } - ) - .post( - '/sign-in', - async ({ body }) => { - const { data, error } = - await supabase.auth.signInWithPassword(body) - - if (error) return error - - return data.user - }, - { - schema: { - body: 'sign', // [!code ++] - body: t.Object({ // [!code --] - email: t.String({ // [!code --] - format: 'email' // [!code --] - }), // [!code --] - password: t.String({ // [!code --] - minLength: 8 // [!code --] - }) // [!code --] - }) // [!code --] - } - } - ) - ) +export type Router = typeof router ``` -Great! We have just used name reference on our route! - -::: tip -If you found yourself with a long schema, you can declare them in a separate file and re-use them in any Elysia route to put the focus back on business logic instead. -::: - -## Storing user session - -Great, now the last thing we need to do to complete the authentication system is to store the user session, after a user is signed in, the token is known as `access_token` and `refresh_token` in Supabase. - -access_token is a short live JWT access token. Use to authenticate a user in a short amount of time. -refresh_token is a one-time-used never-expired token to renew access_token. So as long as we have this token, we can create a new access token to extend our user session. - -We can store both values inside a cookie. - -Now, some might not like the idea of storing the access token inside a cookie and might use Bearer instead. but for simplicity, we are going to use a cookie here. +And in the Elysia server +```typescript +import { Elysia } from 'elysia' +import { cors } from '@elysiajs/cors' +import '@elysiajs/trpc' -::: tip -We can set a cookie as **HttpOnly** to prevent XSS, **Secure**, **Same-Site**, and also encrypt a cookie to prevent a man-in-the-middle attack. -::: +import { router, createContext } from './trpc' // [!code ++] -```ts -// src/modules/authen.ts -import { Elysia, t } from 'elysia' -import { cookie } from '@elysiajs/cookie' // [!code ++] - -import { supabase } from '../../libs' - -const authen = (app: Elysia) => - app.group('/auth', (app) => - app - .use( // [!code ++] - cookie({ // [!code ++] - httpOnly: true, // [!code ++] - // If you need cookie to deliver via https only // [!code ++] - // secure: true, // [!code ++] - // // [!code ++] - // If you need a cookie to be available for same-site only // [!code ++] - // sameSite: "strict", // [!code ++] - // // [!code ++] - // If you want to encrypt a cookie // [!code ++] - // signed: true, // [!code ++] - // secret: process.env.COOKIE_SECRET, // [!code ++] - }) // [!code ++] - ) // [!code ++] - .setModel({ - sign: t.Object({ - email: t.String({ - format: 'email' - }), - password: t.String({ - minLength: 8 - }) - }) - }) - // rest of the code +const app = new Elysia() + .use(cors()) + .get('/', () => 'Hello Elysia') + .use( + trpc(router, { // [!code ++] + createContext // [!code ++] + }) // [!code ++] ) -``` - -And-- That's all it takes to create a **sign-in** and **sign-up** route for Elysia and Supabase! - -Using Rest Client to sign in - -## Refreshing a token - -Now, as mentioned, access_token is short-lived, and we might need to renew the token now and then. - -Luckily, we can do that with a one-liner from Supabase. + .listen(3000) -```ts -// src/modules/authen.ts -import { Elysia, t } from 'elysia' -import { supabase } from '../../libs' - -const authen = (app: Elysia) => - app.group('/auth', (app) => - app - .setModel({ - sign: t.Object({ - email: t.String({ - format: 'email' - }), - password: t.String({ - minLength: 8 - }) - }) - }) - .post( - '/sign-up', - async ({ body }) => { - const { data, error } = await supabase.auth.signUp(body) - - if (error) return error - return data.user - }, - { - schema: { - body: 'sign' - } - } - ) - .post( - '/sign-in', - async ({ body }) => { - const { data, error } = - await supabase.auth.signInWithPassword(body) - - if (error) return error - - return data.user - }, - { - schema: { - body: 'sign' - } - } - ) - .get( // [!code ++] - '/refresh', // [!code ++] - async ({ setCookie, cookie: { refresh_token } }) => { // [!code ++] - const { data, error } = await supabase.auth.refreshSession({ // [!code ++] - refresh_token // [!code ++] - }) // [!code ++] - // [!code ++] - if (error) return error // [!code ++] - // [!code ++] - setCookie('refresh_token', data.session!.refresh_token) // [!code ++] - // [!code ++] - return data.user // [!code ++] - } // [!code ++] - ) // [!code ++] - ) +console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`) ``` -Finally, add routes to the main server. -```ts -import { Elysia, t } from 'elysia' +And we can specify a custom endpoint of tRPC by using `endpoint`: +```typescript +import { Elysia } from 'elysia' +import { cors } from '@elysiajs/cors' +import { trpc } from '@elysiajs/trpc' -import { auth } from './modules' // [!code ++] +import { router, createContext } from './trpc' const app = new Elysia() - .use(auth) // [!code ++] + .use(cors()) + .get('/', () => 'Hello Elysia') + .use( + trpc(router, { + createContext, + endpoint: '/v2/trpc' // [!code ++] + }) + ) .listen(3000) -console.log( - `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` -) +console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`) ``` -And that's it! +## Subscription +By default, tRPC uses WebSocketServer to support `subscription`, but unfortunately as Bun 0.5.4 doesn't support WebSocketServer yet, we can't directly use WebSocket Server. -## Authorization route +However, Bun does support Web Socket using `Bun.serve`, and with Elysia tRPC plugin has wired all the usage of tRPC's Web Socket into `Bun.serve`, you can directly use tRPC's `subscription` with Elysia Web Socket plugin directly: -We have just implemented user authentication which is fun and game, but now you might find yourself in need of authorization for each route, and duplicating the same code to check for cookies all over the place. +Start by installing the Web Socket plugin: +```bash +bun add @elysiajs/websocket +``` -Luckily, we can re-use the function in Elysia. +Then inside tRPC server: +```typescript +import { initTRPC } from '@trpc/server' +import { observable } from '@trpc/server/observable' // [!code ++] +import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch' -Let's paint the example by saying that we might want a user to create a simple blog post that can have the database schema as the following: +import { EventEmitter } from 'stream' // [!code ++] -Inside the Supabse console, we are going to create a Postgres table name 'post' as the following: -Creating table using Supabase UI, in the public table with the name of 'post', and a columns of 'id' with type of 'int8' as a primary value, 'created_at' with type of 'timestamp' with default value of 'now()', 'user_id' linked to Supabase's user schema linked as 'user.id', and 'post' with type of 'text' +import { zod } from 'zod' -**user_id** is linked to Supabase's generated **auth** table linked as **user.id**, using this relation, we can create row-level security to only allow the owner of the post to modify the data. +export const createContext = async (opts: FetchCreateContextFnOptions) => { + return { + name: 'elysia' + } +} -Linking the 'user_id' field with Supabase's user schema as 'user.id' +const t = initTRPC.context>>().create() +const ee = new EventEmitter() // [!code ++] -Now, let's create a new separated Elysia route in another folder to separate the code from auth route, inside `src/modules/post/index.ts` +export const router = t.router({ + mirror: t.procedure.input(z.string()).query(({ input }) => { + ee.emit('listen', input) // [!code ++] -```ts -// src/modules/post/index.ts -import { Elysia, t } from 'elysia' + return input + }), + listen: t.procedure.subscription(() => // [!code ++] + observable((emit) => { // [!code ++] + ee.on('listen', (input) => { // [!code ++] + emit.next(input) // [!code ++] + }) // [!code ++] + }) // [!code ++] + ) // [!code ++] +}) -import { supabase } from '../../libs' - -export const post = (app: Elysia) => - app.group('/post', (app) => - app.put( - '/create', - async ({ body }) => { - const { data, error } = await supabase - .from('post') - .insert({ - // Add user_id somehow - // user_id: userId, - ...body - }) - .select('id') - - if (error) throw error - - return data[0] - }, - { - schema: { - body: t.Object({ - detail: t.String() - }) - } - } - ) - ) +export type Router = typeof router ``` -Now, this route can accept the body and put it into the database, the only thing we are left to do is handle authorization and extract the `user_id`. +And then we register: +```typescript +import { Elysia, ws } from 'elysia' +import { cors } from '@elysiajs/cors' +import '@elysiajs/trpc' -Luckily we can do that easily, thanks to Supabase, and our cookies. +import { router, createContext } from './trpc' -```ts -import { Elysia, t } from 'elysia' -import { cookie } from '@elysiajs/cookie' // [!code ++] - -import { supabase } from '../../libs' - -export const post = (app: Elysia) => - app.group('/post', (app) => - app.put( - '/create', - async ({ body }) => { - let userId: string // [!code ++] - // [!code ++] - const { data, error } = await supabase.auth.getUser( // [!code ++] - access_token // [!code ++] - ) // [!code ++] - // [!code ++] - if(error) { // [!code ++] - const { data, error } = await supabase.auth.refreshSession({ // [!code ++] - refresh_token // [!code ++] - }) // [!code ++] - // [!code ++] - if (error) throw error // [!code ++] - // [!code ++] - userId = data.user!.id // [!code ++] - } // [!code ++] +const app = new Elysia() + .use(cors()) + .use(ws()) // [!code ++] + .get('/', () => 'Hello Elysia') + .trpc(router, { + createContext + }) + .listen(3000) - const { data, error } = await supabase - .from('post') - .insert({ - // Add user_id somehow - // user_id: userId, - ...body - }) - .select('id') - - if (error) throw error - - return data[0] - }, - { - schema: { - body: t.Object({ - detail: t.String() - }) - } - } - ) - ) +console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`) ``` -Great! Now we can extract `user_id` from our cookie using **supabase.auth.getUser** - -## Derive -Our code work fine for now, but let's paint a little picture. - -Let's say you have so many routes that require authorization like this, requiring you to extract the `userId`, it means that you will have a lot of duplicated code here, right? - -Luckily, Elysia is specially designed to tackle this problem. +And that's all it takes to integrate the existing fully functional tRPC server to Elysia Server thus making tRPC run on Bun 🥳. ---- - -In Elysia, we have something called a **scope**. +Elysia is excellent when you need both tRPC and REST API, as they can co-exist together in one server. -Imagine it's like a **closure** where only a variable can only be used inside a scope, or ownership if you're from Rust. +## Bonus: Type-Safe Elysia with Eden +As Elysia is inspired by tRPC, Elysia also supports end-to-end type-safety like tRPC by default using **"Eden"**. -Any life-cycle declared in scope like **group**, **guard** is going to be only available in scope. +This means that you can use Express-like syntax to create RESTful API with full-type support on a client like tRPC. -This means that you can declare a specific life cycle to specific routes inside the scope. + -For example, a scope of routes that need authorization while others are not. +To get started, let's export the app type. -So, instead of reusing all that code, we defined it once and applied it to all the routes you need. +```typescript +import { Elysia, ws } from 'elysia' +import { cors } from '@elysiajs/cors' +import { trpc } from '@elysiajs/trpc' ---- +import { router, createContext } from './trpc' -Now, let's move this retrieving **user_id** into a plugin and apply it to all routes in the scope. +const app = new Elysia() + .use(cors()) + .use(ws()) + .get('/', () => 'Hello Elysia') + .use( + trpc(router, { + createContext + }) + ) + .listen(3000) -Let's put this plugin inside `src/libs/authen.ts` +export type App = typeof app // [!code ++] -```ts -import { Elysia } from 'elysia' -import { cookie } from '@elysiajs/cookie' - -import { supabase } from './supabase' - -export const authen = (app: Elysia) => - app - .use(cookie()) - .derive( - async ({ setCookie, cookie: { access_token, refresh_token } }) => { - const { data, error } = await supabase.auth.getUser( - access_token - ) - - if (data.user) - return { - userId: data.user.id - } - - const { data: refreshed, error: refreshError } = - await supabase.auth.refreshSession({ - refresh_token - }) - - if (refreshError) throw error - - return { - userId: refreshed.user!.id - } - } - ) +console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`) ``` -This code attempts to extract userId, and add `userId` to `Context` of the route, otherwise, it will throw an error and skip the handler, preventing an invalid error to be put into our business logic, aka **supabase.from.insert**. - -::: tip -We can also use **onBeforeHandle** to create a custom validation before entering the main handler too, **.derive** also does the same but any returned from **derived** will be added to **Context** while **onBeforeHandle** don't. - -Technically, **derive** use **transform** under the hood. -::: - -And with a single line, we apply all routes inside the scope into authorized-only routes, with type-safe access to **userId**. - -```ts -import { Elysia, t } from 'elysia' - -import { authen, supabase } from '../../libs' // [!code ++] - -export const post = (app: Elysia) => - app.group('/post', (app) => - app - .use(authen) // [!code ++] - .put( - '/create', - async ({ body, userId }) => { // [!code ++] - let userId: string // [!code --] - // [!code --] - const { data, error } = await supabase.auth.getUser( // [!code --] - access_token // [!code --] - ) // [!code --] - // [!code --] - if(error) { // [!code --] - const { data, error } = await supabase.auth.refreshSession({ // [!code --] - refresh_token // [!code --] - }) // [!code --] - // [!code --] - if (error) throw error // [!code --] - // [!code --] - userId = data.user!.id // [!code --] - } // [!code --] - - const { data, error } = await supabase - .from('post') - .insert({ - user_id: userId, // [!code ++] - ...body - }) - .select('id') - - if (error) throw error - - return data[0] - }, - { - schema: { - body: t.Object({ - detail: t.String() - }) - } - } - ) - ) - +And on the client side: +```bash +bun add @elysia/eden && bun add -d elysia ``` -Great right? We don't even see that we handled the authorization by looking at the code like magic. - -Putting our focus back on our core business logic instead. +And in the code: +```typescript +import { edenTreaty } from '@elysiajs/eden' +import type { App } from '../server' -Using Rest Client to create post +// This now has all type inference from the server +const app = edenTreaty('http://localhost:3000') -## Non-authorized scope -Now let's create one more route to fetch the post from the database. - -```ts -import { Elysia, t } from 'elysia' - -import { authen, supabase } from '../../libs' - -export const post = (app: Elysia) => - app.group('/post', (app) => - app - .get('/:id', async ({ params: { id } }) => { // [!code ++] - const { data, error } = await supabase // [!code ++] - .from('post') // [!code ++] - .select() // [!code ++] - .eq('id', id) // [!code ++] - // [!code ++] - if (error) return error // [!code ++] - // [!code ++] - return { // [!code ++] - success: !!data[0], // [!code ++] - data: data[0] ?? null // [!code ++] - } // [!code ++] - }) // [!code ++] - .use(authen) - .put( - '/create', - async ({ body, userId }) => { - const { data, error } = await supabase - .from('post') - .insert({ - // Add user_id somehow - // user_id: userId, - ...body - }) - .select('id') - - if (error) throw error - - return data[0] - }, - { - schema: { - body: t.Object({ - detail: t.String() - }) - } - } - ) - ) +// data will have a value of 'Hello Elysia' and has a type of 'string' +const data = await app.index.get() ``` -We are using success to indicate if the post is existed or not. -Using Rest Client to get post by id - -If not, we are going to return `success: false` and `data: null` instead. -Using Rest Client to get post by id but failed +Elysia is a good start when you want end-to-end type-safety like tRPC but need to support more standard patterns like REST, and still have to support tRPC or need to migrate from one. -As we mentioned before, the `.use(authen)` is applied to the scoped **but** with only the one declared after itself, which means that anything before isn't affected, and what came after is now authorized only route. +## Bonus: Extra tip for Elysia +An additional thing you can do with Elysia is not only that it has support for tRPC and end-to-end type-safety, but also has a variety of support for many essential plugins configured especially for Bun. -And one last thing, don't forget to add routes to the main server. -```ts +For example, you can generate documentation with Swagger only in 1 line using [Swagger plugin](/plugins/swagger). +```typescript import { Elysia, t } from 'elysia' - -import { auth, post } from './modules' // [!code ++] +import { swagger } from '@elysiajs/swagger' // [!code ++] const app = new Elysia() - .use(auth) - .use(post) // [!code ++] + .use(swagger()) // [!code ++] + .setModel({ + sign: t.Object({ + username: t.String(), + password: t.String() + }) + }) + .get('/', () => 'Hello Elysia') + .post('/typed-body', ({ body }) => body, { + schema: { + body: 'sign', + response: 'sign' + } + }) .listen(3000) -console.log( - `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` -) -``` - +export type App = typeof app -## Bonus: Documentation - -As a bonus, after all of what we create, instead of telling exactly route by route, we can create documentation for our frontend devs in 1 line. - -With the Swagger plugin, we can install: - -```bash -bun add @elysiajs/swagger@rc +console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`) ``` -And then just add the plugin: - -```ts -import { Elysia, t } from 'elysia' -import { swagger } from '@elysiajs/swagger' // [!code ++] - -import { auth, post } from './modules' +Or when you want to use [GraphQL Apollo](/plugins/graphql-apollo) on Bun. +```typescript +import { Elysia } from 'elysia' +import { apollo, gql } from '@elysiajs/apollo' // [!code ++] const app = new Elysia() - .use(swagger()) // [!code ++] - .use(auth) - .use(post) + .use( // [!code ++] + apollo({ // [!code ++] + typeDefs: gql` // [!code ++] + type Book { // [!code ++] + title: String // [!code ++] + author: String // [!code ++] + } // [!code ++] + // [!code ++] + type Query { // [!code ++] + books: [Book] // [!code ++] + } // [!code ++] + `, // [!code ++] + resolvers: { // [!code ++] + Query: { // [!code ++] + books: () => { // [!code ++] + return [ // [!code ++] + { // [!code ++] + title: 'Elysia', // [!code ++] + author: 'saltyAom' // [!code ++] + } // [!code ++] + ] // [!code ++] + } // [!code ++] + } // [!code ++] + } // [!code ++] + }) // [!code ++] + ) // [!code ++] + .get('/', () => 'Hello Elysia') .listen(3000) -console.log( - `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` -) -``` - -Tada 🎉 We got well-defined documentation for our API. - -Swagger documentation generated by Elysia - -And if anything more, you don't have to worry that you might forget a specification of OpenAPI Schema 3.0, we have auto-completion and type-safety too. +export type App = typeof app -We can define our route detail with `schema.detail` that also follows OpenAPI Schema 3.0, so you can properly create documentation. -Using auto-completion with `schema.detail` - -## What's next - -For the next step, we encourage you to try out and explore more with the [code we have just written in this article](https://github.com/saltyaom/elysia-supabase-example) and try adding an image-uploading post, to see explore both Supabase and Elysia ecosystems. - -As we can see, it's super easy to create a production-ready web server with Supabase, many things are just one-liners and handy for rapid development. +console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`) +``` -Especially when paired with Elysia, you get excellent Developer Experience, declarative schema as a single source of truth, and a very well-thought design choice for creating an API, high-performance server while using TypeScript, and as a bonus, we can create documentation in just one line. +Or supporting OAuth 2.0 with a [community OAuth plugin](https://github.com/bogeychan/elysia-oauth2). -Elysia is on a journey for creating a Bun-first web framework with new technology, and a new approach. +Nonetheless, Elysia is a great place to start learning/using Bun and the ecosystem around Bun. -If you're interested in Elysia, feel free to check out our [Discord server](https://discord.gg/eaFJ2KDJck) or see [Elysia on GitHub](https://github.com/elysiajs/elysia) +If you like to learn more about Elysia, [Elysia documentation](https://elysiajs.com) is a great start to start exploring the concept and patterns, and if you are stuck or need help, feel free to reach out in [Elysia Discord](https://discord.gg/eaFJ2KDJck). -Also, you might want to checkout out [Elysia Eden](/plugins/eden/overview), a fully type-safe, no-code-gen fetch client like tRPC for Elysia server. +The repository for all of the code is available at [https://github.com/saltyaom/elysia-trpc-demo](https://github.com/saltyaom/elysia-trpc-demo), feels free to experiment and reach out. \ No newline at end of file From 4efc37de0a6417755d02c935f3abff97ed99a602 Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:12:43 -0500 Subject: [PATCH 05/25] Update integrate-trpc-with-elysia.md --- docs/blog/integrate-trpc-with-elysia.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blog/integrate-trpc-with-elysia.md b/docs/blog/integrate-trpc-with-elysia.md index 52e1c22b..237c5652 100644 --- a/docs/blog/integrate-trpc-with-elysia.md +++ b/docs/blog/integrate-trpc-with-elysia.md @@ -391,4 +391,4 @@ Nonetheless, Elysia is a great place to start learning/using Bun and the ecosyst If you like to learn more about Elysia, [Elysia documentation](https://elysiajs.com) is a great start to start exploring the concept and patterns, and if you are stuck or need help, feel free to reach out in [Elysia Discord](https://discord.gg/eaFJ2KDJck). The repository for all of the code is available at [https://github.com/saltyaom/elysia-trpc-demo](https://github.com/saltyaom/elysia-trpc-demo), feels free to experiment and reach out. - \ No newline at end of file + From ff6b63b2a38e0845891dfb55f164b88088451b16 Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:17:08 -0500 Subject: [PATCH 06/25] whut --- docs/blog/elysia-05.md | 2 +- docs/blog/elysia-06.md | 2 +- docs/blog/elysia-07.md | 2 +- docs/blog/elysia-08.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/blog/elysia-05.md b/docs/blog/elysia-05.md index b78c0147..0688717d 100644 --- a/docs/blog/elysia-05.md +++ b/docs/blog/elysia-05.md @@ -440,4 +440,4 @@ Thanks for your continuous support for Elysia, and we hope to see you on the nex > > Yeah, you know it's **full speed ahead** - \ No newline at end of file + diff --git a/docs/blog/elysia-06.md b/docs/blog/elysia-06.md index a9fc24f8..6d89aecc 100644 --- a/docs/blog/elysia-06.md +++ b/docs/blog/elysia-06.md @@ -382,4 +382,4 @@ We incredibly thankful for your overwhelming continous support for Elysia, and w > > I put all my fate in used let **the game begin** - \ No newline at end of file + diff --git a/docs/blog/elysia-07.md b/docs/blog/elysia-07.md index 9850cfee..50906b06 100644 --- a/docs/blog/elysia-07.md +++ b/docs/blog/elysia-07.md @@ -458,4 +458,4 @@ Thanks you and your love and overwhelming support for Elysia, we hope we can pai > > Stellar Stellar - \ No newline at end of file + diff --git a/docs/blog/elysia-08.md b/docs/blog/elysia-08.md index 34e2e6d4..b05a588b 100644 --- a/docs/blog/elysia-08.md +++ b/docs/blog/elysia-08.md @@ -423,4 +423,4 @@ Until then, *El Psy Congroo*. > > Shed a tear and leap to a new world - \ No newline at end of file + From a0386e15b5876c03a9fcd5811c9d400fa20d1a5d Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:19:51 -0500 Subject: [PATCH 07/25] Update treaty.md --- docs/eden/treaty.md | 73 ++++++++++++++++----------------------------- 1 file changed, 25 insertions(+), 48 deletions(-) diff --git a/docs/eden/treaty.md b/docs/eden/treaty.md index be0497d5..95c42b27 100644 --- a/docs/eden/treaty.md +++ b/docs/eden/treaty.md @@ -1,21 +1,20 @@ --- title: Eden Treaty - ElysiaJS head: - - - meta - - property: 'og:title' - content: Eden Treaty - ElysiaJS + - - meta + - property: 'og:title' + content: Eden Treaty - ElysiaJS - - - meta - - name: 'og:description' - content: Eden Treaty is a 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. + - - meta + - name: 'og:description' + content: Eden Treaty is a 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. - - - meta - - name: 'og:description' - content: Eden Treaty is a 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. + - - meta + - name: 'og:description' + content: Eden Treaty is a 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. --- # Eden Treaty - Eden Treaty is an object-like representation of an Elysia server. Providing accessor like a normal object with type directly from the server, helping us to move faster, and make sure that nothing break @@ -23,7 +22,6 @@ Providing accessor like a normal object with type directly from the server, help --- To use Eden Treaty, first export your existing Elysia server type: - ```typescript // server.ts import { Elysia, t } from 'elysia' @@ -43,7 +41,6 @@ export type App = typeof app // [!code ++] ``` Then import the server type, and consume the Elysia API on client: - ```typescript // client.ts import { edenTreaty } from '@elysiajs/eden' @@ -65,13 +62,11 @@ const { data: nendoroid, error } = app.mirror.post({ ``` ::: tip -Eden Treaty is fully type-safe with auto-completion support. +Eden Treaty is fully type-safe with auto-completion support. ::: ## Anatomy - Eden Treaty will transform all existing paths to object-like representation, that can be described as: - ```typescript EdenTreaty.<1>.<2>..({ ...body, @@ -81,28 +76,23 @@ EdenTreaty.<1>.<2>..({ ``` ### Path - Eden will transform `/` into `.` which can be called with a registered `method`, for example: - -- **/path** -> .path -- **/nested/path** -> .nested.path +- **/path** -> .path +- **/nested/path** -> .nested.path ### Path parameters - Path parameters will be mapped to automatically by their name in the URL. -- **/id/:id** -> .id.`` -- eg: .id.hi -- eg: .id['123'] +- **/id/:id** -> .id.`` +- eg: .id.hi +- eg: .id['123'] ::: tip If a path doesn't support path parameters, TypeScript will show an error. ::: ### Query - You can append queries to path with `$query`: - ```typescript app.get({ $query: { @@ -113,9 +103,7 @@ app.get({ ``` ### Fetch - Eden Treaty is a fetch wrapper, you can add any valid [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) parameters to Eden by passing it to `$fetch`: - ```typescript app.post({ $fetch: { @@ -127,9 +115,7 @@ app.post({ ``` ## Error Handling - Eden Treaty will return a value of `data` and `error` as a result, both fully typed. - ```typescript // response type: { id: 1895, name: 'Skadi' } const { data: nendoroid, error } = app.mirror.post({ @@ -137,8 +123,8 @@ const { data: nendoroid, error } = app.mirror.post({ name: 'Skadi' }) -if (error) { - switch (error.status) { +if(error) { + switch(error.status) { case 400: case 401: warnUser(error.value) @@ -169,7 +155,6 @@ Error is wrapped with an `Error` with its value return from the server can be re ::: ### Error type based on status - Both Eden Treaty and Eden Fetch can narrow down an error type based on status code if you explictly provided an error type in the Elysia server. ```typescript @@ -202,15 +187,14 @@ export type App = typeof app ``` An on the client side: - ```typescript const { data: nendoroid, error } = app.mirror.post({ id: 1895, name: 'Skadi' }) -if (error) { - switch (error.status) { +if(error) { + switch(error.status) { case 400: case 401: // narrow down to type 'error' described in the server @@ -228,9 +212,7 @@ if (error) { ``` ## WebSocket - Eden supports WebSocket using the same API as same as normal route. - ```typescript // Server import { Elysia, t, ws } from 'elysia' @@ -250,7 +232,6 @@ type App = typeof app ``` To start listening to real-time data, call the `.subscribe` method: - ```typescript // Client import { edenTreaty } from '@elysiajs/eden' @@ -274,17 +255,14 @@ We can use [schema](/essential/schema) to enforce type-safety on WebSockets, jus If more control is need, **EdenWebSocket.raw** can be accessed to interact with the native WebSocket API. ## File Upload - You may either pass one of the following to the field to attach file: - -- **File** -- **FileList** -- **Blob** +- **File** +- **FileList** +- **Blob** Attaching a file will results **content-type** to be **multipart/form-data** Suppose we have the server as the following: - ```typescript // server.ts import { Elysia } from 'elysia' @@ -293,7 +271,7 @@ const app = new Elysia() .post('/image', ({ body: { image, title } }) => title, { body: t.Object({ title: t.String(), - image: t.Files() + image: t.Files(), }) }) .listen(3000) @@ -302,7 +280,6 @@ export type App = typeof app ``` We may use the client as follows: - ```typescript // client.ts import { edenTreaty } from '@elysia/eden' @@ -314,7 +291,7 @@ const id = (id: string) => document.getElementById(id)! as T const { data } = await client.image.post({ - title: 'Misono Mika', - image: id('picture').files! + title: "Misono Mika", + image: id('picture').files!, }) ``` From 7fa92913a8832d90273add70d9d942491c3cc5b9 Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:20:46 -0500 Subject: [PATCH 08/25] Prettier reverts --- docs/api/constructor.md | 2 +- docs/blog/elysia-supabase.md | 2 +- docs/blog/with-prisma.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api/constructor.md b/docs/api/constructor.md index 78342a0d..622fb6d3 100644 --- a/docs/api/constructor.md +++ b/docs/api/constructor.md @@ -75,4 +75,4 @@ const app = new Elysia() // `server` will be null if listen isn't called console.log(`Running at http://${app.server!.hostname}:${app.server!.port}`) -``` \ No newline at end of file +``` diff --git a/docs/blog/elysia-supabase.md b/docs/blog/elysia-supabase.md index df2bbf43..1b8413c8 100644 --- a/docs/blog/elysia-supabase.md +++ b/docs/blog/elysia-supabase.md @@ -840,4 +840,4 @@ Elysia is on a journey for creating a Bun-first web framework with new technolog If you're interested in Elysia, feel free to check out our [Discord server](https://discord.gg/eaFJ2KDJck) or see [Elysia on GitHub](https://github.com/elysiajs/elysia) Also, you might want to checkout out [Elysia Eden](/plugins/eden/overview), a fully type-safe, no-code-gen fetch client like tRPC for Elysia server. - \ No newline at end of file + diff --git a/docs/blog/with-prisma.md b/docs/blog/with-prisma.md index fd5a155e..cc527b08 100644 --- a/docs/blog/with-prisma.md +++ b/docs/blog/with-prisma.md @@ -420,4 +420,4 @@ Elysia is on a journey to create a new standard for a better developer experienc If you're looking for a place to start learning about out Bun, consider take a look for what Elysia can offer especially with an [end-to-end type safety](/eden/overview) like tRPC but built on REST standard without any code generation. If you're interested in Elysia, feel free to check out our [Discord server](https://discord.gg/eaFJ2KDJck) or see [Elysia on GitHub](https://github.com/elysiajs/elysia) - \ No newline at end of file + From a7539bcb251712af2eaa6827c866e02ab23dcc3e Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:22:56 -0500 Subject: [PATCH 09/25] Update path.md --- docs/essential/path.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/essential/path.md b/docs/essential/path.md index d0a27497..c906cc14 100644 --- a/docs/essential/path.md +++ b/docs/essential/path.md @@ -57,7 +57,9 @@ For instance, we can extract the user ID from the pathname, we can do something ```typescript import { Elysia } from 'elysia' -new Elysia().get('/id/:id', ({ params: { id } }) => id).listen(3000) +new Elysia() + .get('/id/:id', ({ params: { id } }) => id) + .listen(3000) ``` We create a dynamic path with `/id/:id` which tells Elysia to match any path up until `/id` and after it could be any value, which is then stored as **params** object. @@ -126,7 +128,9 @@ However, when you need a value of the path to be more dynamic and capture the re Wildcard can capture the value after segment regardless of amount by using "\*". ```typescript -new Elysia().get('/id/*', ({ params }) => params['*']).listen(3000) +new Elysia() + .get('/id/*', ({ params }) => params['*']) + .listen(3000) ``` Sending a request to the server should return the response as the following: From 6df3697b6ced4ac352845f1999aaa6b5d47789aa Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:23:09 -0500 Subject: [PATCH 10/25] Update context.md --- docs/essential/context.md | 114 ++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 60 deletions(-) diff --git a/docs/essential/context.md b/docs/essential/context.md index 933a85ed..28418d8f 100644 --- a/docs/essential/context.md +++ b/docs/essential/context.md @@ -1,40 +1,38 @@ --- title: Handler - ElysiaJS head: - - - meta - - property: 'og:title' - content: Handler - ElysiaJS + - - meta + - property: 'og:title' + content: Handler - ElysiaJS - - - meta - - name: 'description' - content: Context is an information of each request from the client, unique to each request with global mutable store. Context can be customize by using state, decorate and derive. + - - meta + - name: 'description' + content: Context is an information of each request from the client, unique to each request with global mutable store. Context can be customize by using state, decorate and derive. - - - meta - - property: 'og:description' - content: Context is an information of each request from the client, unique to each request with global mutable store. Context can be customize by using state, decorate and derive. + - - meta + - property: 'og:description' + content: Context is an information of each request from the client, unique to each request with global mutable store. Context can be customize by using state, decorate and derive. --- # Context - Context is information of each request passed to [route handler](/essential/handler). Context is unique for each request, and is not shared except it's a `store` property which is a global mutable state object, (aka state). Elysia context is consists of: - -- **path** - Path name of the request -- **body** - [HTTP message](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages), form or file upload. -- **query** - [Query String](https://en.wikipedia.org/wiki/Query_string), include additional parameters for search query as JavaScript Object. (Query is extract from a value after pathname starting from '?' question mark sign) -- **params** - Elysia's path parameters parsed as JavaScript object -- **headers** - [HTTP Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers), additional information about the request like User-Agent, Content-Type, Cache Hint. -- path: Pathname of the request -- **request** - [Web Standard Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) -- **store** - A global mutable store for Elysia instance -- **cookie** - A global mutatable signal store for interacting with Cookie (including get/set) -- **set** - Property to apply to Response: - - **status** - [HTTP status](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status), default to 200 if not set. - - **headers** - Response headers - - **redirect** - Response as a path to redirect to +- **path** - Path name of the request +- **body** - [HTTP message](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages), form or file upload. +- **query** - [Query String](https://en.wikipedia.org/wiki/Query_string), include additional parameters for search query as JavaScript Object. (Query is extract from a value after pathname starting from '?' question mark sign) +- **params** - Elysia's path parameters parsed as JavaScript object +- **headers** - [HTTP Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers), additional information about the request like User-Agent, Content-Type, Cache Hint. +- path: Pathname of the request +- **request** - [Web Standard Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) +- **store** - A global mutable store for Elysia instance +- **cookie** - A global mutatable signal store for interacting with Cookie (including get/set) +- **set** - Property to apply to Response: + - **status** - [HTTP status](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status), default to 200 if not set. + - **headers** - Response headers + - **redirect** - Response as a path to redirect to ## Extending context @@ -43,10 +41,9 @@ Because Elysia only provides essential information about the Request, you can cu Extraction of a user ID or another frequently used function related to the request, for example, into Context itself. You can extend Elysia's context by using: - -- **state** - Create a global mutatable state into **Context.store** -- **decorate** - Add additional function or property assigned to **Context** -- **derive** - Add additional property based on existing property or request which is uniquely assigned to every request. +- **state** - Create a global mutatable state into **Context.store** +- **decorate** - Add additional function or property assigned to **Context** +- **derive** - Add additional property based on existing property or request which is uniquely assigned to every request. The following APIs add extra functionality to the Context. @@ -55,7 +52,6 @@ It's recommended to assign property related to request and response, or frequent ::: ## Store - **State** is a global mutable object shared across the Elysia app. If you are familiar with frontend libraries like React, Vue, or Svelte, there's a concept of Global State Management, which is also partially implemented in Elysia via state and store. @@ -65,17 +61,17 @@ If you are familiar with frontend libraries like React, Vue, or Svelte, there's **state** is a function to assign an initial value to **store**, which could be mutated later. To assign value to `store`, you can use **Elysia.state**: - ```typescript import { Elysia } from 'elysia' -new Elysia().state('version', 1).get('/', ({ store: { version } }) => version) +new Elysia() + .state('version', 1) + .get('/', ({ store: { version } }) => version) ``` Once you call **state**, value will be added to **store** property, and can be later used after in handler. Beware that you cannot use state value before being assigned. - ```typescript import { Elysia } from 'elysia' @@ -94,7 +90,6 @@ This is the magic of the Elysia-type system that does this automatically. ::: ## Decorate - Like **store**, **decorate** assigns an additional property to **Context** directly. The only difference is that the value should be read-only and not reassigned later. @@ -115,7 +110,6 @@ new Elysia() ``` ## Derive - Like `decorate`, you can assign an additional property to **Context** directly. But instead of setting the property before the server is started. **derive** assigns a property when each request happens. Allowing us to extract a piece of information into a property instead. @@ -139,45 +133,43 @@ Because **derive** is assigned once a new request starts, **derive** can access Unlike **state**, and **decorate**. Properties which assigned by **derive** is unique and not shared with another request. ## Pattern - **state**, **decorate** offers a similar APIs pattern for assigning property to Context as the following: - -- key-value -- object -- remap +- key-value +- object +- remap Where **derive** can be only used with **remap** because it depends on existing value. ### key-value - You can use **state**, and **decorate** to assign a value using a key-value pattern. ```typescript import { Elysia } from 'elysia' -new Elysia().state('counter', 0).decorate('logger', new Logger()) +new Elysia() + .state('counter', 0) + .decorate('logger', new Logger()) ``` This pattern is great for readability for setting a single property. ### Object - Assigning multiple properties is better contained in an object for a single assignment. ```typescript import { Elysia } from 'elysia' -new Elysia().decorate({ - logger: new Logger(), - trace: new Trace(), - telemetry: new Telemetry() -}) +new Elysia() + .decorate({ + logger: new Logger(), + trace: new Trace(), + telemetry: new Telemetry() + }) ``` The object offers a less repetitive API for setting multiple values. ### Remap - Remap is a function reassignment. Allowing us to create a new value from existing value like renaming or removing a property. @@ -209,20 +201,23 @@ Using remap, Elysia will treat a returned object as a new property, removing any ::: ## Affix - To provide a smoother experience, some plugins might have a lot of property value which can be overwhelming to remap one-by-one. The **Affix** function which consists of **prefix** and **suffix**, allowing us to remap all property of an instance. ```ts -const setup = new Elysia({ name: 'setup' }).decorate({ - argon: 'a', - boron: 'b', - carbon: 'c' -}) +const setup = new Elysia({ name: 'setup' }) + .decorate({ + argon: 'a', + boron: 'b', + carbon: 'c' + }) const app = new Elysia() - .use(setup.prefix('decorator', 'setup')) + .use( + setup + .prefix('decorator', 'setup') + ) .get('/', ({ setupCarbon }) => setupCarbon) ``` @@ -231,21 +226,21 @@ Allowing us to bulk remap a property of the plugin effortlessly, preventing the By default, **affix** will handle both runtime, type-level code automatically, remapping the property to camelCase as naming convention. In some condition, you can also remap `all` property of the plugin: - ```ts const app = new Elysia() - .use(setup.prefix('all', 'setup')) + .use( + setup + .prefix('all', 'setup') + ) .get('/', ({ setupCarbon }) => setupCarbon) ``` ## Reference and value - To mutate the state, it's recommended to use **reference** to mutate rather than using an actual value. When accessing the property from JavaScript, if you define a primitive value from an object property as a new value, the reference is lost, the value is treat as new separate value instead. For example: - ```typescript const store = { counter: 0 @@ -258,7 +253,6 @@ console.log(store.counter) // ✅ 1 We can use **store.counter** to access and mutate the property. However, if we define a counter as a new value - ```typescript const store = { counter: 0 From 48dc056a62f0a17dc315302b2751f2b41a7e1c1a Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:23:52 -0500 Subject: [PATCH 11/25] Update treaty.md --- docs/eden/treaty.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/eden/treaty.md b/docs/eden/treaty.md index 95c42b27..eb2437d2 100644 --- a/docs/eden/treaty.md +++ b/docs/eden/treaty.md @@ -294,4 +294,4 @@ const { data } = await client.image.post({ title: "Misono Mika", image: id('picture').files!, }) -``` +``` \ No newline at end of file From fd4ee5f89631d2a97302907d39a586f568b53c3d Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:24:28 -0500 Subject: [PATCH 12/25] Update plugin.md --- docs/essential/plugin.md | 56 ++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/docs/essential/plugin.md b/docs/essential/plugin.md index 71b391d5..3f8fb53f 100644 --- a/docs/essential/plugin.md +++ b/docs/essential/plugin.md @@ -25,7 +25,9 @@ const plugin = new Elysia() .decorate('plugin', 'hi') .get('/plugin', ({ plugin }) => plugin) -const app = new Elysia().use(plugin).get('/', ({ plugin }) => plugin) +const app = new Elysia() + .use(plugin) + .get('/', ({ plugin }) => plugin) ``` We can use the plugin by passing an instance to **Elysia.use**. @@ -44,12 +46,15 @@ Using a plugin pattern, you decouple your business logic into a separate file. ```typescript // plugin.ts -export const plugin = new Elysia().get('/plugin', () => 'hi') +export const plugin = new Elysia() + .get('/plugin', () => 'hi') // main.ts import { plugin } from './plugin' -const app = new Elysia().use(plugin).listen(8080) +const app = new Elysia() + .use(plugin) + .listen(8080) ``` ## Config @@ -61,9 +66,12 @@ You can create a function that accepts parameters that may change the behavior o ```typescript import { Elysia } from 'elysia' -const version = (version = 1) => new Elysia().get('/version', version) +const version = (version = 1) => new Elysia() + .get('/version', version) -const app = new Elysia().use(version(1)).listen(8080) +const app = new Elysia() + .use(version(1)) + .listen(8080) ``` ## Functional callback​ @@ -78,10 +86,14 @@ To define a functional callback, create a function that accepts Elysia as a para const plugin = (app: Elysia) => { if ('counter' in app.store) return app - return app.state('counter', 0).get('/plugin', () => 'Hi') + return app + .state('counter', 0) + .get('/plugin', () => 'Hi') } -const app = new Elysia().use(plugin).listen(8080) +const app = new Elysia() + .use(plugin) + .listen(8080) ``` Once passed to `Elysia.use`, functional callback behaves as a normal plugin except the property is assigned directly to @@ -103,11 +115,11 @@ Elysia avoids this by differentiating the instance by using **name** and **optio ```typescript import { Elysia } from 'elysia' -const plugin = (config) => - new Elysia({ +const plugin = (config) => new Elysia({ name: 'my-plugin', // [!code ++] - seed: config // [!code ++] - }).get(`${config.prefix}/hi`, () => 'Hi') + seed: config, // [!code ++] + }) + .get(`${config.prefix}/hi`, () => 'Hi') const app = new Elysia() .use( @@ -144,7 +156,6 @@ If the provided value is class, Elysia will then try to use `.toString` method t ::: ## Service Locator - When you apply multiple state and decorators plugin to an instance, the instance will gain type safety. However, you may notice that when you are trying to use the decorated value in other instance without decorator, you may realize that the type is missing. @@ -152,7 +163,9 @@ However, you may notice that when you are trying to use the decorated value in o ```typescript import { Elysia } from 'elysia' -const main = new Elysia().decorate('a', 'a').use(child) +const main = new Elysia() + .decorate('a', 'a') + .use(child) const child = new Elysia() // ❌ 'a' is missing @@ -169,13 +182,17 @@ Simply put, we need to provide the plugin reference for Elysia to find the servi ```typescript // setup.ts -const setup = new Elysia({ name: 'setup' }).decorate('a', 'a') +const setup = new Elysia({ name: 'setup' }) + .decorate('a', 'a') // index.ts -const main = new Elysia().use(child) +const main = new Elysia() + .use(child) // child.ts -const child = new Elysia().use(setup).get('/', ({ a }) => a) +const child = new Elysia() + .use(setup) + .get('/', ({ a }) => a) ``` ## Official Plugins @@ -183,9 +200,8 @@ const child = new Elysia().use(setup).get('/', ({ a }) => a) You can find an officially maintained plugin at Elysia's [plugins](/plugins/overview). Some plugins include: - -- GraphQL -- Swagger -- Server Sent Event +- GraphQL +- Swagger +- Server Sent Event And various community plugins. From 57d34572be4aca87c7bffff864e89eb4b6d3d4e3 Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:25:06 -0500 Subject: [PATCH 13/25] Update on-error.md --- docs/life-cycle/on-error.md | 67 ++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/docs/life-cycle/on-error.md b/docs/life-cycle/on-error.md index 47cea721..2ba03806 100644 --- a/docs/life-cycle/on-error.md +++ b/docs/life-cycle/on-error.md @@ -1,31 +1,28 @@ --- title: Error Handling - ElysiaJS head: - - - meta - - property: 'og:title' - content: Error Handling - ElysiaJS + - - meta + - property: 'og:title' + content: Error Handling - ElysiaJS - - - meta - - name: 'description' - content: Execute when an error is thrown in any other life-cycle at least once. Designed to capture and resolve an unexpected error, its recommended to use on Error in the following sitaution. To provide custom error message. Fail safe or an error handler or retrying a request. Logging and analytic. + - - meta + - name: 'description' + content: Execute when an error is thrown in any other life-cycle at least once. Designed to capture and resolve an unexpected error, its recommended to use on Error in the following sitaution. To provide custom error message. Fail safe or an error handler or retrying a request. Logging and analytic. - - - meta - - property: 'og:description' - content: Execute when an error is thrown in any other life-cycle at least once. Designed to capture and resolve an unexpected error, its recommended to use on Error in the following sitaution. To provide custom error message. Fail safe or an error handler or retrying a request. Logging and analytic. + - - meta + - property: 'og:description' + content: Execute when an error is thrown in any other life-cycle at least once. Designed to capture and resolve an unexpected error, its recommended to use on Error in the following sitaution. To provide custom error message. Fail safe or an error handler or retrying a request. Logging and analytic. --- # Error Handling - **On Error** is the only life-cycle event that is not always executed on each request, but only when an error is thrown in any other life-cycle at least once. Designed to capture and resolve an unexpected error, its recommended to use on Error in the following sitaution: - -- To provide custom error message -- Fail safe or an error handler or retrying a request -- Logging and analytic +- To provide custom error message +- Fail safe or an error handler or retrying a request +- Logging and analytic ## Example - Elysia catches all the errors thrown in the handler, classifies the error code, and pipes them to `onError` middleware. ```typescript @@ -47,7 +44,6 @@ It's important that `onError` must be called before the handler we want to apply ::: For example, returning custom 404 messages: - ```typescript import { Elysia, NotFoundError } from 'elysia' @@ -59,28 +55,24 @@ new Elysia() return 'Not Found :(' } }) - .post('/', () => { - throw new NotFoundError() - }) + .post('/', () => { + throw new NotFoundError(); + }) .listen(8080) ``` ## Context - `onError` Context is extends from `Context` with additional properties of the following: - -- error: Error object thrown -- code: Error Code +- error: Error object thrown +- code: Error Code ### Error Code - Elysia error code consists of: - -- NOT_FOUND -- INTERNAL_SERVER_ERROR -- VALIDATION -- PARSE -- UNKNOWN +- NOT_FOUND +- INTERNAL_SERVER_ERROR +- VALIDATION +- PARSE +- UNKNOWN By default, user thrown error code is `unknown`. @@ -89,7 +81,6 @@ If no error response is returned, the error will be returned using `error.name`. ::: ## Custom Error - Elysia supports custom error both in the type-level and implementation level. To provide a custom error code, we can use `Eylsia.error` to add a custom error code, helping us to easily classify and narrow down the error type for full type safety with auto-complete as the following: @@ -106,7 +97,7 @@ new Elysia() MyError }) .onError(({ code, error }) => { - switch (code) { + switch(code) { // With auto-completion case 'MyError': // With type narrowing @@ -114,25 +105,23 @@ new Elysia() return error } }) - .get('/', () => { - throw new MyError('Hello Error') - }) + .get('/', () => { + throw new MyError('Hello Error'); + }) ``` Properties of `error` code is based on the properties of `error`, the said properties will be used to classify the error code. ## Local Error - Same as others life-cycle, we provide an error into an [scope](/new/essential/scope) using guard: - ```typescript new Elysia() .get('/', () => 'Hello', { beforeHandle({ set, request: { headers } }) { - if (!isSignIn(headers)) { + if(!isSignIn(headers)) { set.status = 401 - throw new Error('Unauthorized') + throw new Error("Unauthorized") } }, error({ error }) { From 863df915bb44e9289412b2365663c1d7eabfcba0 Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:25:30 -0500 Subject: [PATCH 14/25] Update overview.md --- docs/life-cycle/overview.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/life-cycle/overview.md b/docs/life-cycle/overview.md index 23c5d930..766a2484 100644 --- a/docs/life-cycle/overview.md +++ b/docs/life-cycle/overview.md @@ -20,7 +20,6 @@ head: # Life Cycle - It's recommended that you have read [Essential life-cycle](/new/essential/life-cycle) for better understanding of Elysia's Life Cycle. Life Cycle allow us to intercept an important event at the predefined point allowing us to customize the behavior of our server as need. @@ -63,7 +62,6 @@ Below are the request lifecycle available in Elysia: --- Every life-cycle could be apply at both: - 1. Local Hook (route) 2. Global Hook From f0248f1e37b06d936e61fca58ac9f0d5d6a9ebfc Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:25:47 -0500 Subject: [PATCH 15/25] Update trace.md --- docs/life-cycle/trace.md | 126 +++++++++++++++++++-------------------- 1 file changed, 61 insertions(+), 65 deletions(-) diff --git a/docs/life-cycle/trace.md b/docs/life-cycle/trace.md index 164f5b8f..8de71a16 100644 --- a/docs/life-cycle/trace.md +++ b/docs/life-cycle/trace.md @@ -1,21 +1,20 @@ --- title: Trace - ElysiaJS head: - - - meta - - property: 'og:title' - content: Trace - ElysiaJS + - - meta + - property: 'og:title' + content: Trace - ElysiaJS - - - meta - - name: 'description' - 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. + - - meta + - name: 'description' + 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. - - - meta - - name: 'og:description' - 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. + - - meta + - name: 'og:description' + 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. --- # Trace - Trace is an API to measure the performance of your server. Trace allows 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. @@ -29,63 +28,60 @@ We don't want to be fast for benchmarking purposes, we want you to have a real f There are many factors that can slow down our app - and it's hard to identify them, but **trace** can helps solve that problem ## Trace - Trace can measure lifecycle execution time of each function to audit the performance bottleneck of each cycle. ```ts import { Elysia } from 'elysia' const app = new Elysia() - .trace(async ({ handle }) => { - const { time, end } = await handle + .trace(async ({ handle }) => { + const { time, end } = await handle - console.log('beforeHandle took', (await end) - time) - }) - .get('/', () => 'Hi') - .listen(3000) + console.log('beforeHandle took', (await end) - time) + }) + .get('/', () => 'Hi') + .listen(3000) ``` You can trace lifecycle of the following: - -- **request** - get notified of every new request -- **parse** - array of functions to parse the body -- **transform** - transform request and context before validation -- **beforeHandle** - custom requirement to check before the main handler, can skip the main handler if response returned. -- **handle** - function assigned to the path -- **afterHandle** - map returned value into a proper response -- **error** - handle error thrown during processing request -- **response** - send a Response back to the client +- **request** - get notified of every new request +- **parse** - array of functions to parse the body +- **transform** - transform request and context before validation +- **beforeHandle** - custom requirement to check before the main handler, can skip the main handler if response returned. +- **handle** - function assigned to the path +- **afterHandle** - map returned value into a proper response +- **error** - handle error thrown during processing request +- **response** - send a Response back to the client Please refers to [lifecycle event](/concept/life-cycle) for more information: ![Elysia Life Cycle](/assets/lifecycle.webp) ## Children - You can tap deeper and measure each function of a life-cycle event by using the **children** property of a life-cycle event ```ts import { Elysia } from 'elysia' const sleep = (time = 1000) => - new Promise((resolve) => setTimeout(resolve, time)) + new Promise((resolve) => setTimeout(resolve, time)) const app = new Elysia() - .trace(async ({ beforeHandle }) => { - for (const child of children) { - const { time: start, end, name } = await child - - console.log(name, 'took', (await end) - start, 'ms') - } - }) - .get('/', () => 'Hi', { - beforeHandle: [ - function setup() {}, - async function delay() { - await sleep() - } - ] - }) - .listen(3000) + .trace(async ({ beforeHandle }) => { + for (const child of children) { + const { time: start, end, name } = await child + + console.log(name, 'took', (await end) - start, 'ms') + } + }) + .get('/', () => 'Hi', { + beforeHandle: [ + function setup() {}, + async function delay() { + await sleep() + } + ] + }) + .listen(3000) ``` ::: tip @@ -93,26 +89,28 @@ Every life cycle has support for children except for `handle` ::: ## Name - Measuring functions by index can be hard to trace back to the function code, that's why trace provides a **name** property to easily identify the function by name. ```ts import { Elysia } from 'elysia' const app = new Elysia() - .trace(async ({ beforeHandle }) => { - for (const child of children) { - const { name } = await child + .trace(async ({ beforeHandle }) => { + for (const child of children) { + const { name } = await child - console.log(name) + console.log(name) // setup // anonymous - } - }) - .get('/', () => 'Hi', { - beforeHandle: [function setup() {}, () => {}] - }) - .listen(3000) + } + }) + .get('/', () => 'Hi', { + beforeHandle: [ + function setup() {}, + () => {} + ] + }) + .listen(3000) ``` ::: tip @@ -120,7 +118,6 @@ If you are using an arrow function or unnamed function, **name** will become **" ::: ## Set - Inside the trace callback, you can access `Context` of the request, and can mutate the value of the request itself, for example using `set.headers` to update headers. This is useful when you need support an API like Server-Timing. @@ -131,13 +128,13 @@ This is useful when you need support an API like Server-Timing. import { Elysia } from 'elysia' const app = new Elysia() - .trace(async ({ handle, set }) => { + .trace(async ({ handle, set }) => { const { time, end } = await handle set.headers['Server-Timing'] = `handle;dur=${(await end) - time}` - }) - .get('/', () => 'Hi') - .listen(3000) + }) + .get('/', () => 'Hi') + .listen(3000) ``` ::: tip @@ -145,7 +142,6 @@ Using `set` inside `trace` can affect performance, as Elysia defers the executio ::: ## Skip - Sometimes, `beforeHandle` or handler can throw an error, skipping the execution of some lifecycles. By default if this happens, each life-cycle will be resolved automatically, and you can track if the API is executed or not by using `skip` property @@ -154,15 +150,15 @@ By default if this happens, each life-cycle will be resolved automatically, and import { Elysia } from 'elysia' const app = new Elysia() - .trace(async ({ handle, set }) => { + .trace(async ({ handle, set }) => { const { time, end, skip } = await handle console.log(skip) - }) - .get('/', () => 'Hi', { + }) + .get('/', () => 'Hi', { beforeHandle() { throw new Error("I'm a teapot") } }) - .listen(3000) + .listen(3000) ``` From 849446405c534d29b090db42e0e708e9ef91b6d8 Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:26:16 -0500 Subject: [PATCH 16/25] Update cookie.md --- docs/patterns/cookie.md | 95 +++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 57 deletions(-) diff --git a/docs/patterns/cookie.md b/docs/patterns/cookie.md index 6f0cf619..e60a1862 100644 --- a/docs/patterns/cookie.md +++ b/docs/patterns/cookie.md @@ -1,32 +1,30 @@ --- title: Reactive Cookie - ElysiaJS head: - - - meta - - property: 'og:title' - content: Reactive Cookie - ElysiaJS + - - meta + - property: 'og:title' + content: Reactive Cookie - ElysiaJS - - - meta - - name: 'description' - content: Reactive Cookie take a more modern approach like signal to handle cookie with an ergonomic API. There's no 'getCookie', 'setCookie', everything is just a cookie object. When you want to use cookie, you just extract the name and value directly. + - - meta + - name: 'description' + content: Reactive Cookie take a more modern approach like signal to handle cookie with an ergonomic API. There's no 'getCookie', 'setCookie', everything is just a cookie object. When you want to use cookie, you just extract the name and value directly. - - - meta - - property: 'og:description' - content: Reactive Cookie take a more modern approach like signal to handle cookie with an ergonomic API. There's no 'getCookie', 'setCookie', everything is just a cookie object. When you want to use cookie, you just extract the name and value directly. + - - meta + - property: 'og:description' + content: Reactive Cookie take a more modern approach like signal to handle cookie with an ergonomic API. There's no 'getCookie', 'setCookie', everything is just a cookie object. When you want to use cookie, you just extract the name and value directly. --- # Cookie - To use Cookie, you can extract the cookie property and access its name and value directly. There's no get/set, you can extract the cookie name and retrieve or update its value directly. - ```ts app.get('/', ({ cookie: { name } }) => { // Get name.value // Set - name.value = 'New Value' + name.value = "New Value" name.value = { hello: 'world' } @@ -36,7 +34,6 @@ app.get('/', ({ cookie: { name } }) => { By default, Reactive Cookie can encode/decode type of object automatically allowing us to treat cookie as an object without worrying about the encoding/decoding. **It just works**. ## Reactivity - The Elysia cookie is reactive. This means that when you change the cookie value, the cookie will be updated automatically based on approach like signal. A single source of truth for handling cookies is provided by Elysia cookies, which have the ability to automatically set headers and sync cookie values. @@ -46,7 +43,6 @@ Since cookies are Proxy-dependent objects by default, the extract value can neve We can treat the cookie jar as a regular object, iteration over it will only iterate over an already-existing cookie value. ## Cookie Attribute - To use Cookie attribute, you can either use one of the following: 1. Setting the property directly @@ -55,7 +51,6 @@ To use Cookie attribute, you can either use one of the following: See [cookie attribute config](/patterns/cookie-signature#config) for more information. ### Assign Property - You can get/set the property of a cookie as if it's a normal object, the reactivity model will sync the cookie value automatically. ```ts @@ -70,7 +65,6 @@ app.get('/', ({ cookie: { name } }) => { ``` ## set - **set** allow us to set update multiple cookie property all at once, by **reset all property** and overwrite it with a new value. ```ts @@ -83,13 +77,10 @@ app.get('/', ({ cookie: { name } }) => { ``` ## add - Like **set**, **add** allow us to update multiple cookie property at once, but instead, will only overwrite the property defined instead of resetting. ## remove - To remove a cookie, you can either use: - 1. name.remove 2. delete cookie.name @@ -102,53 +93,43 @@ app.get('/', ({ cookie, cookie: { name } }) => { ``` ## Cookie Schema - You can strictly validate cookie type and providing type inference for cookie by using cookie schema with `t.Cookie`. ```ts -app.get( - '/', - ({ cookie: { name } }) => { - // Set - name.value = { - id: 617, - name: 'Summoning 101' - } - }, - { - cookie: t.Cookie({ - name: t.Object({ - id: t.Numeric(), - name: t.String() - }) - }) +app.get('/', ({ cookie: { name } }) => { + // Set + name.value = { + id: 617, + name: 'Summoning 101' } -) +}, { + cookie: t.Cookie({ + name: t.Object({ + id: t.Numeric(), + name: t.String() + }) + }) +}) ``` ## Nullable Cookie - To handle nullable cookie value, you can use `t.Optional` on cookie name you want to be nullable. ```ts -app.get( - '/', - ({ cookie: { name } }) => { - // Set - name.value = { - id: 617, - name: 'Summoning 101' - } - }, - { - cookie: t.Cookie({ - value: t.Optional( - t.Object({ - id: t.Numeric(), - name: t.String() - }) - ) - }) +app.get('/', ({ cookie: { name } }) => { + // Set + name.value = { + id: 617, + name: 'Summoning 101' } -) +}, { + cookie: t.Cookie({ + value: t.Optional( + t.Object({ + id: t.Numeric(), + name: t.String() + }) + ) + }) +}) ``` From 14e09f694076e7ccc58efc5e1768dd96f9ff1820 Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:26:38 -0500 Subject: [PATCH 17/25] Update documentation.md --- docs/patterns/documentation.md | 54 ++++++++++++++++------------------ 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/docs/patterns/documentation.md b/docs/patterns/documentation.md index 5349331c..ba5b04a1 100644 --- a/docs/patterns/documentation.md +++ b/docs/patterns/documentation.md @@ -1,64 +1,62 @@ --- title: Creating Documentation - ElysiaJS head: - - - meta - - property: 'og:title' - content: Creating Documentation - ElysiaJS + - - meta + - property: 'og:title' + content: Creating Documentation - ElysiaJS - - - meta - - name: 'description' - 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. + - - meta + - name: 'description' + 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. - - - meta - - property: 'og:description' - 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. + - - meta + - property: 'og:description' + 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. --- # Creating Documentation - 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. To generate the Swagger page, install the plugin: - ```bash bun add @elysiajs/swagger ``` And register the plugin to the server: - ```typescript import { Elysia } from 'elysia' import { swagger } from '@elysiajs/swagger' -const app = new Elysia().use(swagger()) +const app = new Elysia() + .use(swagger()) ``` For more information about Swagger plugin, see the [Swagger plugin page](/plugins/swagger). ## Route definitions - `schema` is used to customize the route definition, not only that it will generate an OpenAPI schema and Swagger definitions, but also type validation, type-inference and auto-completion. However, sometime defining a type only isn't clear what the route might work. You can use `schema.detail` fields to explictly define what the route is all about. ```typescript -app.post('/sign-in', ({ body }) => body, { - body: t.Object( - { - username: t.String(), - password: t.String() - }, - { - description: 'Expected an username and password' +app + .post('/sign-in', ({ body }) => body, { + body: t.Object( + { + username: t.String(), + password: t.String() + }, + { + description: 'Expected an username and password' + } + ), + detail: { + summary: 'Sign in the user', + tags: ['authentication'] } - ), - detail: { - summary: 'Sign in the user', - tags: ['authentication'] - } -}) + }) ``` The detail fields follows an OpenAPI V3 definition with auto-completion and type-safety by default. From 989fad05b682bfbf03b67933e172feab0861708d Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:26:56 -0500 Subject: [PATCH 18/25] Update unit-test.md --- docs/patterns/unit-test.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/patterns/unit-test.md b/docs/patterns/unit-test.md index a1eb43c2..490b208d 100644 --- a/docs/patterns/unit-test.md +++ b/docs/patterns/unit-test.md @@ -47,7 +47,7 @@ Then we can perform tests by running **bun test** bun test ``` -New requests to an Elysia server must be a fully valid URL, **NOT** a part of a URL. +New requests to an Elysia server must be a fully valid URL, **NOT** a part of a URL. The request must provide URL as the following: @@ -59,7 +59,6 @@ The request must provide URL as the following: We can also use other testing libraries like Jest or testing library to create Elysia unit tests. ## Eden Test - We can simplify the tests by using Eden Treaty to create a unit-test with support for end-to-end type safety and auto-completion. ```typescript @@ -68,7 +67,9 @@ import { describe, expect, it } from 'bun:test' import { edenTreaty } from '@elysiajs/eden' -const app = new Elysia().get('/', () => 'hi').listen(3000) +const app = new Elysia() + .get('/', () => 'hi') + .listen(3000) const api = edenTreaty('http://localhost:3000') From 038d5868255d9020d06a10e5d29bc00be6363e79 Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:27:30 -0500 Subject: [PATCH 19/25] Update overview.md --- docs/plugins/overview.md | 90 +++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 47 deletions(-) diff --git a/docs/plugins/overview.md b/docs/plugins/overview.md index 95a9878f..70bf03a4 100644 --- a/docs/plugins/overview.md +++ b/docs/plugins/overview.md @@ -15,7 +15,6 @@ head: --- # Overview - Elysia is designed to be modular and lightweight. Following the same idea as Arch Linux (btw, I use Arch): @@ -25,54 +24,51 @@ Following the same idea as Arch Linux (btw, I use Arch): This is to ensure developers end up with a performant web server they intend to create. By extension, Elysia includes pre-built common pattern plugins for convenient developer usage: ## Official plugins: - -- [Bearer](/plugins/bearer) - retrieve [Bearer](https://swagger.io/docs/specification/authentication/bearer-authentication/) token automatically -- [CORS](/plugins/cors) - set up [Cross-origin resource sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) -- [Cron](/plugins/cron) - set up [cron](https://en.wikipedia.org/wiki/Cron) job -- [Eden](/plugins/eden/overview) - end-to-end type safety client for Elysia -- [GraphQL Apollo](/plugins/graphql-apollo) - run [Apollo GraphQL](https://www.apollographql.com/) on Elysia -- [GraphQL Yoga](/plugins/graphql-yoga) - run [GraphQL Yoga](https://github.com/dotansimha/graphql-yoga) on Elysia -- [HTML](/plugins/html) - handle HTML responses -- [JWT](/plugins/jwt) - authenticate with [JWTs](https://jwt.io/) -- [Server Timing](/plugins/server-timing) - audit performance bottlenecks with the [Server-Timing API](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing) -- [Static](/plugins/static) - serve static files/folders -- [Stream](/plugins/stream) - integrate response streaming and [server-sent events (SSEs)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) -- [Swagger](/plugins/swagger) - generate [Swagger](https://swagger.io/) documentation -- [tRPC](/plugins/trpc) - support [tRPC](https://trpc.io/) -- [WebSocket](/patterns/websocket) - support [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) +- [Bearer](/plugins/bearer) - retrieve [Bearer](https://swagger.io/docs/specification/authentication/bearer-authentication/) token automatically +- [CORS](/plugins/cors) - set up [Cross-origin resource sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) +- [Cron](/plugins/cron) - set up [cron](https://en.wikipedia.org/wiki/Cron) job +- [Eden](/plugins/eden/overview) - end-to-end type safety client for Elysia +- [GraphQL Apollo](/plugins/graphql-apollo) - run [Apollo GraphQL](https://www.apollographql.com/) on Elysia +- [GraphQL Yoga](/plugins/graphql-yoga) - run [GraphQL Yoga](https://github.com/dotansimha/graphql-yoga) on Elysia +- [HTML](/plugins/html) - handle HTML responses +- [JWT](/plugins/jwt) - authenticate with [JWTs](https://jwt.io/) +- [Server Timing](/plugins/server-timing) - audit performance bottlenecks with the [Server-Timing API](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing) +- [Static](/plugins/static) - serve static files/folders +- [Stream](/plugins/stream) - integrate response streaming and [server-sent events (SSEs)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) +- [Swagger](/plugins/swagger) - generate [Swagger](https://swagger.io/) documentation +- [tRPC](/plugins/trpc) - support [tRPC](https://trpc.io/) +- [WebSocket](/patterns/websocket) - support [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) ## Community plugins: - -- [Lucia Auth](https://github.com/pilcrowOnPaper/lucia) - authentication, simple and clean -- [Elysia Clerk](https://github.com/wobsoriano/elysia-clerk) - unofficial Clerk authentication plugin -- [Elysia Polyfills](https://github.com/bogeychan/elysia-polyfills) - run Elysia ecosystem on Node.js and Deno -- [Vite](https://github.com/timnghg/elysia-vite) - serve entry HTML file with Vite's scripts injected -- [Nuxt](https://github.com/trylovetom/elysiajs-nuxt) - easily integrate elysia with nuxt! -- [Elysia Helmet](https://github.com/DevTobias/elysia-helmet) - secure Elysia apps with various HTTP headers -- [Vite Plugin SSR](https://github.com/timnghg/elysia-vite-plugin-ssr) - Vite SSR plugin using Elysia server -- [OAuth2](https://github.com/bogeychan/elysia-oauth2) - handle OAuth 2.0 authorization code flow -- [Rate Limit](https://github.com/rayriffy/elysia-rate-limit) - simple, lightweight rate limiter -- [Logysia](https://github.com/tristanisham/logysia) - classic logging middleware -- [Logger](https://github.com/bogeychan/elysia-logger) - [pino](https://github.com/pinojs/pino)-based logging middleware -- [Elysia Lambda](https://github.com/TotalTechGeek/elysia-lambda) - deploy on AWS Lambda -- [Decorators](https://github.com/gaurishhs/elysia-decorators) - use TypeScript decorators -- [Autoroutes](https://github.com/wobsoriano/elysia-autoroutes) - filesystem routes -- [Group Router](https://github.com/itsyoboieltr/elysia-group-router) - filesystem and folder-based router for groups -- [Basic Auth](https://github.com/itsyoboieltr/elysia-basic-auth) - basic HTTP authentication -- [ETag](https://github.com/bogeychan/elysia-etag) - automatic HTTP [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) generation -- [Basic Auth](https://github.com/eelkevdbos/elysia-basic-auth) - basic HTTP authentication (using `request` event) -- [i18n](https://github.com/eelkevdbos/elysia-i18next) - [i18n](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n) wrapper based on [i18next](https://www.i18next.com/) -- [Elysia Request ID](https://github.com/gtramontina/elysia-requestid) - add/forward request IDs (`X-Request-ID` or custom) -- [Elysia HTMX](https://github.com/gtramontina/elysia-htmx) - context helpers for [HTMX](https://htmx.org/) -- [Elysia HMR HTML](https://github.com/gtrabanco/elysia-hmr-html) - reload HTML files when changing any file in a directory -- [Elysia Inject HTML](https://github.com/gtrabanco/elysia-inject-html) - inject HTML code in HTML files -- [Elysia HTTP Error](https://github.com/yfrans/elysia-http-error) - return HTTP errors from Elysia handlers -- [Elysia Http Status Code](https://github.com/sylvain12/elysia-http-status-code) - integrate HTTP status codes -- [NoCache](https://github.com/gaurishhs/elysia-nocache) - disable caching -- [Elysia Tailwind](https://github.com/gtramontina/elysia-tailwind) - compile [Tailwindcss](https://tailwindcss.com/) in a plugin. -- [Elysia Compression](https://github.com/gusb3ll/elysia-compression) - compress response -- [Elysia IP](https://github.com/gaurishhs/elysia-ip) - get the IP Address - +- [Lucia Auth](https://github.com/pilcrowOnPaper/lucia) - authentication, simple and clean +- [Elysia Clerk](https://github.com/wobsoriano/elysia-clerk) - unofficial Clerk authentication plugin +- [Elysia Polyfills](https://github.com/bogeychan/elysia-polyfills) - run Elysia ecosystem on Node.js and Deno +- [Vite](https://github.com/timnghg/elysia-vite) - serve entry HTML file with Vite's scripts injected +- [Nuxt](https://github.com/trylovetom/elysiajs-nuxt) - easily integrate elysia with nuxt! +- [Elysia Helmet](https://github.com/DevTobias/elysia-helmet) - secure Elysia apps with various HTTP headers +- [Vite Plugin SSR](https://github.com/timnghg/elysia-vite-plugin-ssr) - Vite SSR plugin using Elysia server +- [OAuth2](https://github.com/bogeychan/elysia-oauth2) - handle OAuth 2.0 authorization code flow +- [Rate Limit](https://github.com/rayriffy/elysia-rate-limit) - simple, lightweight rate limiter +- [Logysia](https://github.com/tristanisham/logysia) - classic logging middleware +- [Logger](https://github.com/bogeychan/elysia-logger) - [pino](https://github.com/pinojs/pino)-based logging middleware +- [Elysia Lambda](https://github.com/TotalTechGeek/elysia-lambda) - deploy on AWS Lambda +- [Decorators](https://github.com/gaurishhs/elysia-decorators) - use TypeScript decorators +- [Autoroutes](https://github.com/wobsoriano/elysia-autoroutes) - filesystem routes +- [Group Router](https://github.com/itsyoboieltr/elysia-group-router) - filesystem and folder-based router for groups +- [Basic Auth](https://github.com/itsyoboieltr/elysia-basic-auth) - basic HTTP authentication +- [ETag](https://github.com/bogeychan/elysia-etag) - automatic HTTP [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) generation +- [Basic Auth](https://github.com/eelkevdbos/elysia-basic-auth) - basic HTTP authentication (using `request` event) +- [i18n](https://github.com/eelkevdbos/elysia-i18next) - [i18n](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n) wrapper based on [i18next](https://www.i18next.com/) +- [Elysia Request ID](https://github.com/gtramontina/elysia-requestid) - add/forward request IDs (`X-Request-ID` or custom) +- [Elysia HTMX](https://github.com/gtramontina/elysia-htmx) - context helpers for [HTMX](https://htmx.org/) +- [Elysia HMR HTML](https://github.com/gtrabanco/elysia-hmr-html) - reload HTML files when changing any file in a directory +- [Elysia Inject HTML](https://github.com/gtrabanco/elysia-inject-html) - inject HTML code in HTML files +- [Elysia HTTP Error](https://github.com/yfrans/elysia-http-error) - return HTTP errors from Elysia handlers +- [Elysia Http Status Code](https://github.com/sylvain12/elysia-http-status-code) - integrate HTTP status codes +- [NoCache](https://github.com/gaurishhs/elysia-nocache) - disable caching +- [Elysia Tailwind](https://github.com/gtramontina/elysia-tailwind) - compile [Tailwindcss](https://tailwindcss.com/) in a plugin. +- [Elysia Compression](https://github.com/gusb3ll/elysia-compression) - compress response +- [Elysia IP](https://github.com/gaurishhs/elysia-ip) - get the IP Address --- If you have a plugin written for Elysia, feels free to add your plugin to the list by **clicking Edit this page on GitHub** below 👇 From dd6fb3c26e7c6890c46d0ed5bd59f4c042974a2e Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:27:51 -0500 Subject: [PATCH 20/25] Update server-timing.md --- docs/plugins/server-timing.md | 43 ++++++++++++++--------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/docs/plugins/server-timing.md b/docs/plugins/server-timing.md index ebca6754..55052f1e 100644 --- a/docs/plugins/server-timing.md +++ b/docs/plugins/server-timing.md @@ -15,17 +15,14 @@ head: --- # Server Timing Plugin - This plugin add support for auditing performance bottleneck with Server Timing API Install with: - ```bash bun add @elysiajs/server-timing ``` Then use it: - ```typescript import { Elysia } from 'elysia' import { serverTiming } from '@elysiajs/server-timing' @@ -45,56 +42,50 @@ To inspect, open browser developer tools > Network > [Request made through Elysi Now you can effortlessly audit performance bottleneck of your server. ## Config - Below is a config which is accepted by the plugin ### enabled - @default `NODE_ENV !== 'production'` Determine whether or not Server Timing should be enabled ### allow - @default `undefined` A condition whether server timing should be log ### trace - @default `undefined` Allow Server Timing to log specified life-cycle events: Trace accepts object of the following: - -- request: capture duration from request -- parse: capture duration from parse -- transform: capture duration from transform -- beforeHandle: capture duration from beforeHandle -- handle: capture duration from handle -- afterHandle: capture duration from afterHandle -- total: capture total duration from start to finish +- request: capture duration from request +- parse: capture duration from parse +- transform: capture duration from transform +- beforeHandle: capture duration from beforeHandle +- handle: capture duration from handle +- afterHandle: capture duration from afterHandle +- total: capture total duration from start to finish ## Pattern - Below you can find the common patterns to use the plugin. -- [Allow Condition](#allow-condition) +- [Allow Condition](#allow-condition) ## Allow Condition - You may disabled Server Timing on specific route via `allow` property ```ts import { Elysia } from 'elysia' import { serverTiming } from '@elysiajs/server-timing' -new Elysia().use( - serverTiming({ - allow: ({ request }) => { - return new URL(request.url).pathname !== '/no-trace' - } - }) -) -``` +new Elysia() + .use( + serverTiming({ + allow: ({ request }) => { + return new URL(request.url).pathname !== '/no-trace' + } + }) + ) +``` \ No newline at end of file From 5d77e9c60bad792fed6d40a3a92c5b602e91db2b Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:28:15 -0500 Subject: [PATCH 21/25] Update error-provider.md --- docs/validation/error-provider.md | 94 +++++++++++++++---------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/docs/validation/error-provider.md b/docs/validation/error-provider.md index 5a59353f..f7813fab 100644 --- a/docs/validation/error-provider.md +++ b/docs/validation/error-provider.md @@ -22,24 +22,23 @@ There are 2 ways to provide a custom error message when the validation failed: 2. Using [onError](/life-cycle/on-error) event ## Message Property - TypeBox offers an additional "**error**" property, allowing us to return a custom error message if the field is invalid. ```typescript import { Elysia, t } from 'elysia' new Elysia() - .get('/', () => 'Hello World!', { - body: t.Object( - { - x: t.Number() - }, - { - error: 'x must be a number' - } - ) - }) - .listen(3000) + .get('/', () => 'Hello World!', { + body: t.Object( + { + x: t.Number() + }, + { + error: 'x must be a number' + } + ) + }) + .listen(3000) ``` The following are an example of usage of the error property on various types: @@ -74,9 +73,12 @@ Invalid Email :( @@ -93,14 +95,11 @@ All members must be a string @@ -123,10 +122,11 @@ We can customize the behavior of validation based on [onError](/new/lifecycle/on import { Elysia, t } from 'elysia' new Elysia() - .onError(({ code, error }) => { - if (code === 'VALIDATION') return error.message - }) - .listen(3000) + .onError(({ code, error }) => { + if (code === 'VALIDATION') + return error.message + }) + .listen(3000) ``` Narrowed down error type, will be typed as `ValidationError` imported from 'elysia/error'. @@ -137,40 +137,40 @@ Narrowed down error type, will be typed as `ValidationError` imported from 'elys import { Elysia, t } from 'elysia' new Elysia() - .onError(({ code, error }) => { - if (code === 'VALIDATION') - return error.validator.Errors(error.value).First().message - }) - .listen(3000) + .onError(({ code, error }) => { + if (code === 'VALIDATION') + return error.validator.Errors(error.value).First().message + }) + .listen(3000) ``` ## Error list - **ValidationError** provides a method `ValidatorError.all`, allowing us to list all of the error causes. ```typescript import { Elysia, t } from 'elysia' new Elysia() - .post('/', ({ body }) => body, { - body: t.Object({ - name: t.String(), - age: t.Number() - }), - error({ code, error }) { - switch (code) { - case 'VALIDATION': + .post('/', ({ body }) => body, { + body: t.Object({ + name: t.String(), + age: t.Number() + }), + error({ code, error }) { + switch (code) { + case 'VALIDATION': console.log(error.all) // Find a specific error name (path is OpenAPI Schema compliance) - const name = error.all.find((x) => x.path === '/name') + const name = error.all.find((x) => x.path === '/name') // If has a validation error, then log it - if (name) console.log(name) - } - } - }) - .listen(3000) + if(name) + console.log(name) + } + } + }) + .listen(3000) ``` For more information about TypeBox's validator, see [TypeCheck](https://github.com/sinclairzx81/typebox#typecheck) From 7b27d9a21e306a8319bbd85dd3907f09cd92cad3 Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:28:45 -0500 Subject: [PATCH 22/25] Update primitive-type.md --- docs/validation/primitive-type.md | 77 +++++++++++++++---------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/docs/validation/primitive-type.md b/docs/validation/primitive-type.md index 3f29c237..508eeddd 100644 --- a/docs/validation/primitive-type.md +++ b/docs/validation/primitive-type.md @@ -107,7 +107,9 @@ boolean @@ -182,7 +184,6 @@ Elysia extends all type from TypeBox allowing you to reference most of the API f See [TypeBox's Type](https://github.com/sinclairzx81/typebox#json-types) for additional types that are supported by TypeBox. ## Attribute - TypeBox can accept an argument for more comprehensive behavior based on JSON Schema 7 specification.
```typescript -t.Object({ - x: t.Number() -}, { - error: 'Invalid object UwU' -}) +t.Object( + { + x: t.Number() + }, + { + error: 'Invalid object UwU' + } +) ``` ```typescript -t.Array(t.String(), { - error: 'All members must be a string' -}) +t.Array( + t.String(), + { + error: 'All members must be a string' + } +) ``` ```typescript -t.Object( - { - x: t.Number() - }, - { - error: 'Invalid object UwU' - } -) +t.Object({ + x: t.Number() +}, { + error: 'Invalid object UwU' +}) ``` ```typescript -t.Array(t.Number()) +t.Array( + t.Number() +) ```
@@ -234,23 +235,26 @@ t.Number({ @@ -261,18 +265,18 @@ t.Array(t.Number(), { ```typescript t.Object( - { - x: t.Number() - }, - { - /** - * @default false - * Accept additional properties - * that not specified in schema - * but still match the type - */ - additionalProperties: true - } + { + x: t.Number() + }, + { + /** + * @default false + * Accept additional properties + * that not specified in schema + * but still match the type + */ + additionalProperties: true + } ) ``` @@ -292,15 +296,12 @@ y: 200 See [JSON Schema 7 specification](https://json-schema.org/draft/2020-12/json-schema-validation) For more explaination for each attribute. --- -
# Honorable Mention - The following are common patterns that are often found useful when creating a schema. ## Union - Allow multiple types via union.
```typescript -t.Array(t.Number(), { - /** - * Minimum number of items - */ - minItems: 1, - /** - * Maximum number of items - */ - maxItems: 5 -}) +t.Array( + t.Number(), + { + /** + * Minimum number of items + */ + minItems: 1, + /** + * Maximum number of items + */ + maxItems: 5 + } +) ``` ```typescript -;[1, 2, 3, 4, 5] +[1,2,3,4,5] ```
@@ -314,7 +315,10 @@ Allow multiple types via union. @@ -339,7 +343,6 @@ Hello
```typescript -t.Union([t.String(), t.Number()]) +t.Union([ + t.String(), + t.Number() +]) ```
## Optional - Provided in a property of `t.Object`, allowing the field to be undefined or optional. @@ -385,7 +388,6 @@ t.Object({
## Partial - Allowing all of the field in `t.Object` to be optional. @@ -466,14 +468,11 @@ Invalid Email :( From 0baf12ccf5d1321a20557b19137cabb3dcf7b667 Mon Sep 17 00:00:00 2001 From: ChrisLaRocque Date: Wed, 27 Dec 2023 18:28:59 -0500 Subject: [PATCH 23/25] Update schema-type.md --- docs/validation/schema-type.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/validation/schema-type.md b/docs/validation/schema-type.md index edde45d7..2fa7a5f9 100644 --- a/docs/validation/schema-type.md +++ b/docs/validation/schema-type.md @@ -14,6 +14,7 @@ head: content: Elysia supports declarative schema with the following types. Body for validate an incoming HTTP message. Query for query string or URL parameter. Params for path parameters. Header for request headers. Cookie for cookies. Response for validating response. --- +
```typescript -t.Object( - { - x: t.Number() - }, - { - error: 'Invalid object UwU' - } -) +t.Object({ + x: t.Number() +}, { + error: 'Invalid object UwU' +}) ```