The first production ready, and most loved Bun framework
diff --git a/docs/components/fern/sponsor.constant.ts b/docs/components/fern/sponsor.constant.ts
new file mode 100644
index 00000000..0487ff07
--- /dev/null
+++ b/docs/components/fern/sponsor.constant.ts
@@ -0,0 +1,12 @@
+export const sponsorOverride = {
+ class: {
+ // logo has padding
+ sfcompute: 'p-0.5 dark:p-2.25'
+ },
+ href: {
+ sfcompute: 'https://sfcompute.com?from=elysia',
+ 'oven-sh': 'https://bun.sh?from=elysia',
+ coderabbitai: 'https://coderabbit.ai?from=elysia',
+ scalar: 'https://scalar.com?from=elysia',
+ }
+} as const
diff --git a/docs/components/fern/sponsor.data.ts b/docs/components/fern/sponsor.data.ts
index 6fcb0b5e..0f73b245 100644
--- a/docs/components/fern/sponsor.data.ts
+++ b/docs/components/fern/sponsor.data.ts
@@ -20,9 +20,25 @@ export interface Sponsor {
duration: string
}
+export interface GoldSponsorDetail {
+ url: string
+ caption: string
+}
+
declare const data: Sponsor[]
export { data }
+// Key is sponsorEntity.login
+export const goldSponsorDetail: Record<
+ string,
+ Sponsor['sponsorEntity']['login']
+> = {}
+
+// Sponsors contact to not be displayed
+// although you will be able to see it here
+// but you won't see the duration, and tier
+const hidden = ['l2D']
+
export default defineLoader({
async load(): Promise {
try {
@@ -38,9 +54,9 @@ export default defineLoader({
query: `query {
user(login: "saltyaom") {
sponsorshipsAsMaintainer(
- first: 100
+ first: 100,
+ activeOnly: true
) {
- totalRecurringMonthlyPriceInDollars
nodes {
sponsorEntity {
... on User {
@@ -71,7 +87,7 @@ export default defineLoader({
result.data?.user?.sponsorshipsAsMaintainer?.nodes || []
return data
- .filter((x) => !x.tier.isOneTime)
+ .filter((x) => !x.tier.isOneTime && !hidden.includes(x.sponsorEntity.login))
.sort(
(a, b) =>
b?.tier?.monthlyPriceInDollars -
diff --git a/docs/components/fern/sponsor.vue b/docs/components/fern/sponsor.vue
index c5753ae7..5054ba26 100644
--- a/docs/components/fern/sponsor.vue
+++ b/docs/components/fern/sponsor.vue
@@ -1,69 +1,48 @@
- Made possible by you
+ Because of You
Elysia is
not owned by an organization , but is driven by the community.
+ >, driven by volunteer, and community.
-
- Elysia development is only possible thanks to your support.
+
+ Elysia is possible by these awesome sponsors.
+
+ Elysia offers a robust built-in validation, but you can also
+ bring your favorite validator, like
+ Zod, Valibot, ArkType, Effect , and more
+
+
+ With seamless support for type inference, and OpenAPI. You
+ will feels
+ right at home.
+
+
+
+
+
+
+
- If you found yourself writing code for the framework, then
- there's something wrong with the framework.
-
-
-
- That's why Elysia invests time to experiment with design
- decisions to craft the most ergonomic way possible for
- everyone
-
-
- From built-in strict-type validation to a unified type
- system, and documentation generation, making an ideal
- framework for building servers with TypeScript.
-
{{ response }}
@@ -116,10 +116,10 @@ watch([current], compute)
}
& > .input {
- @apply flex justify-start items-center gap-1 w-full max-w-sm px-3 rounded-lg bg-neutral-100 dark:bg-slate-700 overflow-hidden;
+ @apply flex justify-start items-center gap-1 w-full max-w-sm px-3 py-0.75 rounded-xl bg-neutral-100 dark:bg-slate-700 overflow-hidden;
& > .select {
- @apply text-blue-500 font-bold px-1 bg-blue-500/10 hover:bg-blue-500/20 dark:bg-blue-500/25 dark:hover:bg-blue-500/40 border border-solid border-blue-500/50 dark:border-blue-500/75 rounded cursor-pointer transition-colors
+ @apply text-blue-500 font-bold px-1 py-0.25 bg-blue-500/10 hover:bg-blue-500/20 dark:bg-blue-500/25 dark:hover:bg-blue-500/40 border border-solid border-blue-500/50 dark:border-blue-500/75 rounded cursor-pointer transition-colors
}
}
}
diff --git a/docs/eden/fetch.md b/docs/eden/fetch.md
index af25cea0..e89a363b 100644
--- a/docs/eden/fetch.md
+++ b/docs/eden/fetch.md
@@ -15,9 +15,9 @@ head:
---
# Eden Fetch
-A fetch-like alternative to Eden Treaty .
+A fetch-like alternative to Eden Treaty.
-With Eden Fetch can interact with Elysia server in a type-safe manner using Fetch API.
+With Eden Fetch, you can interact with Elysia server in a type-safe manner using Fetch API.
---
diff --git a/docs/eden/installation.md b/docs/eden/installation.md
index 7b1da5cd..d5ac42d6 100644
--- a/docs/eden/installation.md
+++ b/docs/eden/installation.md
@@ -90,7 +90,7 @@ client.
```
## Gotcha
-Sometimes Eden may not infer type from Elysia correctly, the following are the most common workaround to fix Eden type inference.
+Sometimes, Eden may not infer types from Elysia correctly, the following are the most common workarounds to fix Eden type inference.
### Type Strict
Make sure to enable strict mode in **tsconfig.json**
@@ -103,9 +103,9 @@ Make sure to enable strict mode in **tsconfig.json**
```
### Unmatch Elysia version
-Eden depends Elysia class to import Elysia instance and infers type correctly.
+Eden depends on Elysia class to import Elysia instance and infer types correctly.
-Make sure that both client and server have a matching Elysia version.
+Make sure that both client and server have the matching Elysia version.
You can check it with [`npm why`](https://docs.npmjs.com/cli/v10/commands/npm-explain) command:
@@ -135,12 +135,12 @@ node_modules/elysia
### TypeScript version
-Elysia uses newer features and syntax of TypeScript to infer types in a the most performant way. Features like Const Generic and Template Literal are heavily used.
+Elysia uses newer features and syntax of TypeScript to infer types in the most performant way. Features like Const Generic and Template Literal are heavily used.
Make sure your client has a **minimum TypeScript version if >= 5.0**
### Method Chaining
-To make Eden works, Elysia must be using **method chaining**
+To make Eden work, Elysia must use **method chaining**
Elysia's type system is complex, methods usually introduce a new type to the instance.
@@ -156,9 +156,9 @@ new Elysia()
.get('/', ({ store: { build } }) => build)
.listen(3000)
```
-Using this, **state** now returns a new **ElysiaInstance** type, introducing **build** into store and replace the current one.
+Using this, **state** now returns a new **ElysiaInstance** type, introducing **build** into store replacing the current one.
-Without using method chaining, Elysia doesn't save the new type when introduced, leading to no type inference.
+Without method chaining, Elysia doesn't save the new type when introduced, leading to no type inference.
```typescript twoslash
// @errors: 2339
import { Elysia } from 'elysia'
@@ -173,14 +173,14 @@ app.listen(3000)
```
### Type Definitions
-If you are using a Bun specific feature like `Bun.file` or similar API and return it from a handler, you may need to install Bun type definitions to the client as well.
+If you are using a Bun specific feature, like `Bun.file` or similar API and return it from a handler, you may need to install Bun type definitions to the client as well.
```bash
bun add -d @types/bun
```
### Path alias (monorepo)
-If you are using path alias in your monorepo, make sure that frontend are able to resolve the path as same as backend.
+If you are using path alias in your monorepo, make sure that frontend is able to resolve the path as same as backend.
::: tip
Setting up path alias in monorepo is a bit tricky, you can fork our example template: [Kozeki Template](https://github.com/SaltyAom/kozeki-template) and modify it to your needs.
@@ -211,7 +211,7 @@ const app = new Elysia()
export type app = typeof app
```
-You **must** make sure that your frontend code is able to resolve the same path alias otherwise type inference will be resolved as any.
+You **must** make sure that your frontend code is able to resolve the same path alias. Otherwise, type inference will be resolved as any.
```typescript
import { treaty } from '@elysiajs/eden'
@@ -220,12 +220,12 @@ import type { app } from '@/index'
const client = treaty('localhost:3000')
// This should be able to resolve the same module both frontend and backend, and not `any`
-import { a, b } from '@/controllers'
+import { a, b } from '@/controllers' // [!code ++]
```
To fix this, you must make sure that path alias is resolved to the same file in both frontend and backend.
-So you must change the path alias in **tsconfig.json** to:
+So, you must change the path alias in **tsconfig.json** to:
```json
{
"compilerOptions": {
@@ -243,8 +243,8 @@ If configured correctly, you should be able to resolve the same module in both f
import { a, b } from '@/controllers'
```
-#### Scope
-We recommended to add a **scope** prefix for each modules in your monorepo to avoid any confusion and conflict that may happen.
+#### Namespace
+We recommended adding a **namespace** prefix for each module in your monorepo to avoid any confusion and conflict that may happen.
```json
{
@@ -258,12 +258,12 @@ We recommended to add a **scope** prefix for each modules in your monorepo to av
}
```
-Then you can import the module like this:
+Then, you can import the module like this:
```typescript
// Should work in both frontend and backend and not return `any`
import { a, b } from '@backend/controllers'
```
-We recommended creating a **single tsconfig.json** that define a `baseUrl` as the root of your repo, provide a path according to the module location, and create a **tsconfig.json** for each module that inherits the root **tsconfig.json** which has the path alias.
+We recommend creating a **single tsconfig.json** that defines a `baseUrl` as the root of your repo, provide a path according to the module location, and create a **tsconfig.json** for each module that inherits the root **tsconfig.json** which has the path alias.
-You may find a working example of in this [path alias example repo](https://github.com/SaltyAom/elysia-monorepo-path-alias) or [Kozeki Template](https://github.com/SaltyAom/kozeki-template)
+You may find a working example of in this [path alias example repo](https://github.com/SaltyAom/elysia-monorepo-path-alias) or [Kozeki Template](https://github.com/SaltyAom/kozeki-template).
diff --git a/docs/eden/overview.md b/docs/eden/overview.md
index 413f2f4e..d5ae8ed8 100644
--- a/docs/eden/overview.md
+++ b/docs/eden/overview.md
@@ -7,11 +7,11 @@ head:
- - meta
- name: 'description'
- content: Elysia supports end-to-end type safety with Elysia Eden since start. End-to-end type-safety refers to a system in which every component of the system is checked for type consistency, meaning that data is passed between components only if the types of the data are compatible.
+ content: Elysia supports end-to-end type safety with Elysia Eden from the start. End-to-end type-safety refers to a system in which every component of the system is checked for type consistency, meaning that data is passed between components only if the types of the data are compatible.
- - meta
- property: 'og:description'
- content: Elysia supports end-to-end type safety with Elysia Eden since start. End-to-end type-safety refers to a system in which every component of the system is checked for type consistency, meaning that data is passed between components only if the types of the data are compatible.
+ content: Elysia supports end-to-end type safety with Elysia Eden from the start. End-to-end type-safety refers to a system in which every component of the system is checked for type consistency, meaning that data is passed between components only if the types of the data are compatible.
---
# End-to-End Type Safety
@@ -23,14 +23,14 @@ End-to-end type safety is like making sure all the pieces of the track match up
For a framework to have end-to-end type safety means you can connect client and server in a type-safe manner.
-Elysia provide end-to-end type safety **without code generation** out of the box with RPC-like connector, **Eden**
+Elysia provides end-to-end type safety **without code generation** out of the box with an RPC-like connector, **Eden**
-Others framework that support e2e type safety:
+Other frameworks that support e2e type safety:
- tRPC
- Remix
- SvelteKit
@@ -52,20 +52,20 @@ Hover over variable and function to see type definition.
Elysia allows you to change the type on the server and it will be instantly reflected on the client, helping with auto-completion and type-enforcement.
## Eden
-Eden is a RPC-like client to connect Elysia **end-to-end type safety** using only TypeScript's type inference instead of code generation.
+Eden is an RPC-like client to connect Elysia with **end-to-end type safety** using only TypeScript's type inference instead of code generation.
-Allowing you to sync client and server types effortlessly, weighing less than 2KB.
+It allows you to sync client and server types effortlessly, weighing less than 2KB.
-Eden is consists of 2 modules:
+Eden consists of 2 modules:
1. Eden Treaty **(recommended)**: an improved version RFC version of Eden Treaty
-2. Eden Fetch: Fetch-like client with type safety.
+2. Eden Fetch: Fetch-like client with type safety
Below is an overview, use-case and comparison for each module.
## Eden Treaty (Recommended)
Eden Treaty is an object-like representation of an Elysia server providing end-to-end type safety and a significantly improved developer experience.
-With Eden Treaty we can connect interact Elysia server with full-type support and auto-completion, error handling with type narrowing, and creating type safe unit test.
+With Eden Treaty we can interact with an Elysia server with full-type support and auto-completion, error handling with type narrowing, and create type-safe unit tests.
Example usage of Eden Treaty:
```typescript twoslash
@@ -131,5 +131,5 @@ const { data } = await fetch('/name/:name', {
```
::: tip NOTE
-Unlike Eden Treaty, Eden Fetch doesn't provide Web Socket implementation for Elysia server
+Unlike Eden Treaty, Eden Fetch doesn't provide Web Socket implementation for Elysia server.
:::
diff --git a/docs/eden/treaty/config.md b/docs/eden/treaty/config.md
index 720cb4c4..1cca14bb 100644
--- a/docs/eden/treaty/config.md
+++ b/docs/eden/treaty/config.md
@@ -34,21 +34,21 @@ import type { App } from './server'
const api = treaty('localhost:3000')
```
-You may or may not specified a protocol for URL endpoint.
+You may or may not specify a protocol for URL endpoint.
-Elysia will appends the endpoints automatically as follows:
+Elysia will append the endpoints automatically as follows:
1. If protocol is specified, use the URL directly
2. If the URL is localhost and ENV is not production, use http
3. Otherwise use https
-This also apply to Web Socket as well for determining between **ws://** or **wss://**.
+This also applies to Web Socket as well for determining between **ws://** or **wss://**.
---
### Elysia Instance
If Elysia instance is passed, Eden Treaty will create a `Request` class and pass to `Elysia.handle` directly without creating a network request.
-This allows us to interact with Elysia server directly without request overhead, or the need start a server.
+This allows us to interact with Elysia server directly without request overhead, or the need to start a server.
```typescript
import { Elysia } from 'elysia'
@@ -61,13 +61,13 @@ const app = new Elysia()
const api = treaty(app)
```
-If an instance is passed, generic is not needed to be pass as Eden Treaty can infers the type from a parameter directly.
+If an instance is passed, generic is not needed to be passed as Eden Treaty can infer the type from a parameter directly.
-This patterns is recommended for performing unit tests, or creating a type-safe reverse proxy server or micro-services.
+This pattern is recommended for performing unit tests, or creating a type-safe reverse proxy server or micro-services.
## Options
-2nd optional parameters for Eden Treaty to customize fetch behavior, accepting parameters as follows:
-- [fetch](#fetch) - add default parameters to fetch intialization (RequestInit)
+2nd optional parameter for Eden Treaty to customize fetch behavior, accepting parameters as follows:
+- [fetch](#fetch) - add default parameters to fetch initialization (RequestInit)
- [headers](#headers) - define default headers
- [fetcher](#fetcher) - custom fetch function eg. Axios, unfetch
- [onRequest](#onrequest) - Intercept and modify fetch request before firing
@@ -87,7 +87,7 @@ treaty('localhost:3000', {
})
```
-All parameters that passed to fetch, will be passed to fetcher, which is an equivalent to:
+All parameters that are passed to fetch will be passed to fetcher, which is equivalent to:
```typescript
fetch('http://localhost:3000', {
credentials: 'include'
@@ -114,7 +114,7 @@ fetch('localhost:3000', {
})
```
-headers may accepts the following as parameters:
+headers may accept the following as parameters:
- Object
- Function
@@ -130,7 +130,7 @@ treaty('localhost:3000', {
```
### Function
-You may specify a headers as a function to return custom headers based on condition
+You may specify headers as a function to return custom headers based on condition
```typescript
treaty('localhost:3000', {
@@ -151,7 +151,7 @@ headers function accepts 2 parameters:
- options `RequestInit`: Parameters that passed through 2nd parameter of fetch
### Array
-You may define a headers function as an array if multiple conditions are need.
+You may define a headers function as an array if multiple conditions are needed.
```typescript
treaty('localhost:3000', {
@@ -172,7 +172,7 @@ Eden Treaty will **run all functions** even if the value is already returned.
Eden Treaty will prioritize the order headers if duplicated as follows:
1. Inline method - Passed in method function directly
2. headers - Passed in `config.headers`
- - If `config.headers` is array, parameters that come after will be prioritize
+ - If `config.headers` is array, parameters that come after will be prioritized
3. fetch - Passed in `config.fetch.headers`
For example, for the following example:
@@ -190,7 +190,7 @@ api.profile.get({
})
```
-This will be results in:
+This will result in:
```typescript
fetch('http://localhost:3000', {
headers: {
diff --git a/docs/eden/treaty/parameters.md b/docs/eden/treaty/parameters.md
index 854938ff..87dbbe2d 100644
--- a/docs/eden/treaty/parameters.md
+++ b/docs/eden/treaty/parameters.md
@@ -20,7 +20,7 @@ We need to send a payload to server eventually.
To handle this, Eden Treaty's methods accept 2 parameters to send data to server.
-Both parameters is type safe and will be guided by TypeScript automatically:
+Both parameters are type safe and will be guided by TypeScript automatically:
1. body
2. additional parameters
diff --git a/docs/eden/treaty/response.md b/docs/eden/treaty/response.md
index e46860a8..7aca25db 100644
--- a/docs/eden/treaty/response.md
+++ b/docs/eden/treaty/response.md
@@ -7,15 +7,15 @@ head:
- - meta
- name: 'og:description'
- content: Eden Treaty is an object-like representation of an Elysia server, providing an end-to-end type safety, and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
+ content: Eden Treaty is an object-like representation of an Elysia server, providing end-to-end type safety and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
- - meta
- name: 'og:description'
- content: Eden Treaty is an object-like representation of an Elysia server, providing an end-to-end type safety, and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
+ content: Eden Treaty is an object-like representation of an Elysia server, providing end-to-end type safety and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
---
# Response
-Once the fetch method is called, Eden Treaty return a promise containing an object with the following properties:
+Once the fetch method is called, Eden Treaty returns a `Promise` containing an object with the following properties:
- data - returned value of the response (2xx)
- error - returned value from the response (>= 3xx)
- response `Response` - Web Standard Response class
@@ -30,8 +30,7 @@ import { treaty } from '@elysiajs/eden'
const app = new Elysia()
.post('/user', ({ body: { name }, status }) => {
- if(name === 'Otto')
- return status(400, 'Bad Request')
+ if(name === 'Otto') return status(400)
return name
}, {
@@ -67,19 +66,20 @@ const submit = async (name: string) => {
}
```
-By default, Elysia infers `error` and `response` type to TypeScript automatically, and Eden will be providing auto-completion and type narrowing for accurate behavior.
+By default, Elysia infers `error` and `response` types to TypeScript automatically, and Eden will be providing auto-completion and type narrowing for accurate behavior.
::: tip
-If the server responds with an HTTP status >= 300, then value will be always be null, and `error` will have a returned value instead.
+If the server responds with an HTTP status >= 300, then the value will always be `null`, and `error` will have a returned value instead.
-Otherwise, response will be passed to data.
+Otherwise, response will be passed to `data`.
:::
## Stream response
-Eden will interpret a stream response as `AsyncGenerator` allowing us to use `for await` loop to consume the stream.
+Eden will interpret a stream response or [Server-Sent Events](/essential/handler.html#server-sent-events-sse) as `AsyncGenerator` allowing us to use `for await` loop to consume the stream.
+::: code-group
-```typescript twoslash
+```typescript twoslash [Stream]
import { Elysia } from 'elysia'
import { treaty } from '@elysiajs/eden'
@@ -97,3 +97,95 @@ for await (const chunk of data)
console.log(chunk)
// ^?
```
+
+```typescript twoslash [Server-Sent Events]
+import { Elysia, sse } from 'elysia'
+import { treaty } from '@elysiajs/eden'
+
+const app = new Elysia()
+ .get('/ok', function* () {
+ yield sse({
+ event: 'message',
+ data: 1
+ })
+ yield sse({
+ event: 'message',
+ data: 2
+ })
+ yield sse({
+ event: 'end'
+ })
+ })
+
+const { data, error } = await treaty(app).ok.get()
+if (error) throw error
+
+for await (const chunk of data)
+ console.log(chunk)
+ // ^?
+
+
+
+
+
+
+
+//
+```
+
+:::
+
+
+## Utility type
+Eden Treaty provides a utility type `Treaty.Data` and `Treaty.Error` to extract the `data` and `error` type from the response.
+
+```typescript twoslash
+import { Elysia, t } from 'elysia'
+
+import { treaty, Treaty } from '@elysiajs/eden'
+
+const app = new Elysia()
+ .post('/user', ({ body: { name }, status }) => {
+ if(name === 'Otto') return status(400)
+
+ return name
+ }, {
+ body: t.Object({
+ name: t.String()
+ })
+ })
+ .listen(3000)
+
+const api =
+ treaty('localhost:3000')
+
+type UserData = Treaty.Data
+// ^?
+
+
+// Alternatively you can also pass a response
+const response = await api.user.post({
+ name: 'Saltyaom'
+})
+
+type UserDataFromResponse = Treaty.Data
+// ^?
+
+
+
+type UserError = Treaty.Error
+// ^?
+
+
+
+
+
+
+
+
+
+
+
+
+//
+```
diff --git a/docs/eden/treaty/unit-test.md b/docs/eden/treaty/unit-test.md
index 95a7f8af..13b44b95 100644
--- a/docs/eden/treaty/unit-test.md
+++ b/docs/eden/treaty/unit-test.md
@@ -7,17 +7,17 @@ head:
- - meta
- name: 'og:description'
- content: Eden Treaty is an object-like representation of an Elysia server, providing an end-to-end type safety, and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
+ content: Eden Treaty is an object-like representation of an Elysia server, providing end-to-end type safety and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
- - meta
- name: 'og:description'
- content: Eden Treaty is an object-like representation of an Elysia server, providing an end-to-end type safety, and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
+ content: Eden Treaty is an object-like representation of an Elysia server, providing end-to-end type safety and a significantly improved developer experience. With Eden, we can fetch an API from Elysia server fully type-safe without code generation.
---
# Unit Test
According to [Eden Treaty config](/eden/treaty/config.html#urlorinstance) and [Unit Test](/patterns/unit-test), we may pass an Elysia instance to Eden Treaty directly to interact with Elysia server directly without sending a network request.
-We may use this patterns to create a unit test with end-to-end type safety and type-level test all at once.
+We may use this pattern to create a unit test with end-to-end type safety and type-level test all at once.
```typescript twoslash
// test/index.test.ts
@@ -29,7 +29,7 @@ const app = new Elysia().get('/hello', 'hi')
const api = treaty(app)
describe('Elysia', () => {
- it('return a response', async () => {
+ it('returns a response', async () => {
const { data } = await api.hello.get()
expect(data).toBe('hi')
@@ -40,7 +40,7 @@ describe('Elysia', () => {
```
## Type safety test
-To perform a type safety test, simply run **tsc** to test folders.
+To perform a type safety test, simply run **tsc** on test folders.
```bash
tsc --noEmit test/**/*.ts
diff --git a/docs/essential/best-practice.md b/docs/essential/best-practice.md
index fd50411b..35e83e9b 100644
--- a/docs/essential/best-practice.md
+++ b/docs/essential/best-practice.md
@@ -7,57 +7,144 @@ head:
- - meta
- name: 'description'
- content: Elysia is pattern agnostic framework, we leave the decision up to you and your team for coding patterns to use. However, we found that there are several who are using MVC pattern (Model-View-Controller) on Elysia, and found it's hard to decouple and handling with types. This page is a guide to use Elysia with MVC pattern.
+ content: Elysia is a pattern agnostic framework, we leave the decision up to you and your team for coding patterns to use. However, we found that there are several who are using MVC pattern (Model-View-Controller) on Elysia, and found it's hard to decouple and handle types. This page is a guide to use Elysia with MVC pattern.
- - meta
- property: 'og:description'
- content: Elysia is pattern agnostic framework, we the decision up to you and your team for coding patterns to use. However, we found that there are several who are using MVC pattern (Model-View-Controller) on Elysia, and found it's hard to decouple and handling with types. This page is a guide to use Elysia with MVC pattern.
+ content: Elysia is a pattern agnostic framework, we leave the decision up to you and your team for coding patterns to use. However, we found that there are several who are using MVC pattern (Model-View-Controller) on Elysia, and found it's hard to decouple and handle types. This page is a guide to use Elysia with MVC pattern.
---
# Best Practice
Elysia is a pattern-agnostic framework, leaving the decision of which coding patterns to use up to you and your team.
-However, there are several concern from trying to adapt an MVC pattern [(Model-View-Controller)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) with Elysia, and found it's hard to decouple and handle types.
+However, there are several concerns when trying to adapt an MVC pattern [(Model-View-Controller)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) with Elysia, and we found it hard to decouple and handle types.
-This page is a guide to on how to follows Elysia structure best practice combined with MVC pattern but can be adapted to any coding pattern you like.
+This page is a guide on how to follow Elysia structure best practices combined with the MVC pattern, but it can be adapted to any coding pattern you prefer.
-## Method Chaining
-Elysia code should always use **method chaining**.
+## Folder Structure
-As Elysia type system is complex, every methods in Elysia returns a new type reference.
+Elysia is unopinionated about folder structure, leaving you to **decide** how to organize your code yourself.
-**This is important** to ensure type integrity and inference.
+However, **if you don't have a specific structure in mind**, we recommend a feature-based folder structure where each feature has its own folder containing controllers, services, and models.
-```typescript twoslash
+```
+| src
+ | modules
+ | auth
+ | index.ts (Elysia controller)
+ | service.ts (service)
+ | model.ts (model)
+ | user
+ | index.ts (Elysia controller)
+ | service.ts (service)
+ | model.ts (model)
+ | utils
+ | a
+ | index.ts
+ | b
+ | index.ts
+```
+
+This structure allows you to easily find and manage your code and keep related code together.
+
+Here's an example code of how to distribute your code into a feature-based folder structure:
+
+::: code-group
+
+```typescript [auth/index.ts]
+// Controller handle HTTP related eg. routing, request validation
import { Elysia } from 'elysia'
-new Elysia()
- .state('build', 1)
- // Store is strictly typed // [!code ++]
- .get('/', ({ store: { build } }) => build)
- .listen(3000)
+import { Auth } from './service'
+import { AuthModel } from './model'
+
+export const auth = new Elysia({ prefix: '/auth' })
+ .get(
+ '/sign-in',
+ async ({ body, cookie: { session } }) => {
+ const response = await Auth.signIn(body)
+
+ // Set session cookie
+ session.value = response.token
+
+ return response
+ }, {
+ body: AuthModel.signInBody,
+ response: {
+ 200: AuthModel.signInResponse,
+ 400: AuthModel.signInInvalid
+ }
+ }
+ )
```
-In the code above **state** returns a new **ElysiaInstance** type, adding a `build` type.
+```typescript [auth/service.ts]
+// Service handle business logic, decoupled from Elysia controller
+import { status } from 'elysia'
+
+import type { AuthModel } from './model'
+
+// If the class doesn't need to store a property,
+// you may use `abstract class` to avoid class allocation
+export abstract class Auth {
+ static async signIn({ username, password }: AuthModel.signInBody) {
+ const user = await sql`
+ SELECT password
+ FROM users
+ WHERE username = ${username}
+ LIMIT 1`
+
+ if (await Bun.password.verify(password, user.password))
+ // You can throw an HTTP error directly
+ throw status(
+ 400,
+ 'Invalid username or password' satisfies AuthModel.signInInvalid
+ )
+
+ return {
+ username,
+ token: await generateAndSaveTokenToDB(user.id)
+ }
+ }
+}
+```
-### ❌ Don't: Use Elysia without method chaining
-Without using method chaining, Elysia doesn't save these new types, leading to no type inference.
+```typescript [auth/model.ts]
+// Model define the data structure and validation for the request and response
+import { t } from 'elysia'
-```typescript twoslash
-// @errors: 2339
-import { Elysia } from 'elysia'
+export namespace AuthModel {
+ // Define a DTO for Elysia validation
+ export const signInBody = t.Object({
+ username: t.String(),
+ password: t.String(),
+ })
-const app = new Elysia()
+ // Define it as TypeScript type
+ export type signInBody = typeof signInBody.static
-app.state('build', 1)
+ // Repeat for other models
+ export const signInResponse = t.Object({
+ username: t.String(),
+ token: t.String(),
+ })
-app.get('/', ({ store: { build } }) => build)
+ export type signInResponse = typeof signInResponse.static
-app.listen(3000)
+ export const signInInvalid = t.Literal('Invalid username or password')
+ export type signInInvalid = typeof signInInvalid.static
+}
```
-We recommend to **always use method chaining** to provide an accurate type inference.
+:::
+
+Each file has its own responsibility as follows:
+- **Controller**: Handle HTTP routing, request validation, and cookie.
+- **Service**: Handle business logic, decoupled from Elysia controller if possible.
+- **Model**: Define the data structure and validation for the request and response.
+
+Feel free to adapt this structure to your needs and use any coding pattern you prefer.
## Controller
> 1 Elysia instance = 1 controller
@@ -99,6 +186,21 @@ new Elysia()
})
```
+Otherwise, if you really want to separate the controller, you may create a controller class that is not tied with HTTP request at all.
+
+```typescript
+import { Elysia } from 'elysia'
+
+abstract class Controller {
+ static doStuff(stuff: string) {
+ return Service.doStuff(stuff)
+ }
+}
+
+new Elysia()
+ .get('/', ({ stuff }) => Controller.doStuff(stuff))
+```
+
### Testing
You can test your controller using `handle` to directly call a function (and it's lifecycle)
@@ -106,7 +208,7 @@ You can test your controller using `handle` to directly call a function (and it'
import { Elysia } from 'elysia'
import { Service } from './service'
-import { describe, it, should } from 'bun:test'
+import { describe, it, expect } from 'bun:test'
const app = new Elysia()
.get('/', ({ stuff }) => {
@@ -133,12 +235,15 @@ Service is a set of utility/helper functions decoupled as a business logic to us
Any technical logic that can be decoupled from controller may live inside a **Service**.
-There're 2 types of service in Elysia:
+There are 2 types of service in Elysia:
1. Non-request dependent service
2. Request dependent service
-### ✅ Do: Non-request dependent service
-This kind of service doesn't need to access any property from the request or `Context`, and can be initiated as a static class same as usual MVC service pattern.
+### ✅ Do: Abstract away non-request dependent service
+
+We recommend abstracting a service class/function away from Elysia.
+
+If the service or function isn't tied to an HTTP request or doesn't access a `Context`, it's recommended to implement it as a static class or function.
```typescript
import { Elysia, t } from 'elysia'
@@ -162,8 +267,55 @@ new Elysia()
If your service doesn't need to store a property, you may use `abstract class` and `static` instead to avoid allocating class instance.
-### Request Dependent Service
-This kind of service may require some property from the request, and should be **initiated as an Elysia instance**.
+### ✅ Do: Request dependent service as Elysia instance
+
+**If the service is a request-dependent service** or needs to process HTTP requests, we recommend abstracting it as an Elysia instance to ensure type integrity and inference:
+
+```typescript
+import { Elysia } from 'elysia'
+
+// ✅ Do
+const AuthService = new Elysia({ name: 'Auth.Service' })
+ .macro({
+ isSignIn: {
+ resolve({ cookie, status }) {
+ if (!cookie.session.value) return status(401)
+
+ return {
+ session: cookie.session.value,
+ }
+ }
+ }
+ })
+
+const UserController = new Elysia()
+ .use(AuthService)
+ .get('/profile', ({ Auth: { user } }) => user, {
+ isSignIn: true
+ })
+```
+
+::: tip
+Elysia handles [plugin deduplication](/essential/plugin.html#plugin-deduplication) by default, so you don't have to worry about performance, as it will be a singleton if you specify a **"name"** property.
+:::
+
+### ✅ Do: Decorate only request dependent property
+
+It's recommended to `decorate` only request-dependent properties, such as `requestIP`, `requestTime`, or `session`.
+
+Overusing decorators may tie your code to Elysia, making it harder to test and reuse.
+
+```typescript
+import { Elysia } from 'elysia'
+
+new Elysia()
+ .decorate('requestIP', ({ request }) => request.headers.get('x-forwarded-for') || request.ip)
+ .decorate('requestTime', () => Date.now())
+ .decorate('session', ({ cookie }) => cookie.session.value)
+ .get('/', ({ requestIP, requestTime, session }) => {
+ return { requestIP, requestTime, session }
+ })
+```
### ❌ Don't: Pass entire `Context` to a service
**Context is a highly dynamic type** that can be inferred from Elysia instance.
@@ -185,40 +337,6 @@ class AuthService {
As Elysia type is complex, and heavily depends on plugin and multiple level of chaining, it can be challenging to manually type as it's highly dynamic.
-### ✅ Do: Use Elysia instance as a service
-
-We recommended to use Elysia instance as a service to ensure type integrity and inference:
-```typescript
-import { Elysia } from 'elysia'
-
-// ✅ Do
-const AuthService = new Elysia({ name: 'Service.Auth' })
- .derive({ as: 'scoped' }, ({ cookie: { session } }) => ({
- // This is equivalent to dependency injection
- Auth: {
- user: session.value
- }
- }))
- .macro(({ onBeforeHandle }) => ({
- // This is declaring a service method
- isSignIn(value: boolean) {
- onBeforeHandle(({ Auth, status }) => {
- if (!Auth?.user || !Auth.user) return status(401)
- })
- }
- }))
-
-const UserController = new Elysia()
- .use(AuthService)
- .get('/profile', ({ Auth: { user } }) => user, {
- isSignIn: true
- })
-```
-
-::: tip
-Elysia handle [plugin deduplication](/essential/plugin.html#plugin-deduplication) by default so you don't have to worry about performance, as it's going to be Singleton if you specified a **"name"** property.
-:::
-
### ⚠️ Infers Context from Elysia instance
In case of **absolute necessity**, you may infer the `Context` type from the Elysia instance itself:
@@ -390,7 +508,7 @@ const UserController = new Elysia({ prefix: '/auth' })
This approach provide several benefits:
1. Allow us to name a model and provide auto-completion.
2. Modify schema for later usage, or perform a [remap](/essential/handler.html#remap).
-3. Show up as "models" in OpenAPI compliance client, eg. Swagger.
+3. Show up as "models" in OpenAPI compliance client, eg. OpenAPI.
4. Improve TypeScript inference speed as model type will be cached during registration.
## Reuse a plugin
diff --git a/docs/essential/handler.md b/docs/essential/handler.md
index 0e6be213..b7afa893 100644
--- a/docs/essential/handler.md
+++ b/docs/essential/handler.md
@@ -7,11 +7,11 @@ head:
- - meta
- name: 'description'
- content: handler is a function that responds to the request for each route. Accepting request information and returning a response to the client. Handler can be registered through Elysia.get / Elysia.post
+ content: A handler is a function that responds to the request for each route. Accepting request information and returning a response to the client. Handler can be registered through Elysia.get / Elysia.post
- - meta
- property: 'og:description'
- content: handler is a function that responds to the request for each route. Accepting request information and returning a response to the client. Handler can be registered through Elysia.get / Elysia.post
+ content: A handler is a function that responds to the request for each route. Accepting request information and returning a response to the client. Handler can be registered through Elysia.get / Elysia.post
---
# Plugin
Plugin is a pattern that decouples functionality into smaller parts. Creating reusable components for our web server.
-Defining a plugin is to define a separate instance.
+To create a plugin is to create a separate instance.
```typescript twoslash
import { Elysia } from 'elysia'
@@ -137,21 +155,17 @@ We can use the plugin by passing an instance to **Elysia.use**.
-The plugin will inherit all properties of the plugin instance, including **state**, **decorate**, **derive**, **route**, **lifecycle**, etc.
+The plugin will inherit all properties of the plugin instance like `state`, `decorate` but **WILL NOT inherit plugin lifecycle** as it's [isolated by default](#scope).
-Elysia will also handle the type inference automatically as well, so you can imagine as if you call all of the other instances on the main one.
-
-::: tip
-Notice that the plugin doesn't contain **.listen**, because **.listen** will allocate a port for the usage, and we only want the main instance to allocate the port.
-:::
+Elysia will also handle the type inference automatically as well.
## Plugin
Every Elysia instance can be a plugin.
-We can decouple our logic into a new separate Elysia instance and use it as a plugin.
+We decouple our logic into a separate Elysia instance and reuse it across multiple instances.
-First, we define an instance in a difference file:
+To create a plugin, simply define an instance in a separate file:
```typescript twoslash
// plugin.ts
import { Elysia } from 'elysia'
@@ -163,14 +177,150 @@ export const plugin = new Elysia()
And then we import the instance into the main file:
```typescript
import { Elysia } from 'elysia'
-import { plugin } from './plugin'
+import { plugin } from './plugin' // [!code ++]
const app = new Elysia()
- .use(plugin)
+ .use(plugin) // [!code ++]
.listen(3000)
```
-### Config
+
+## Scope
+
+Elysia lifecycle methods are **encapsulated** to its own instance only.
+
+Which means if you create a new instance, it will not share the lifecycle methods with others.
+
+```ts
+import { Elysia } from 'elysia'
+
+const profile = new Elysia()
+ .onBeforeHandle(({ cookie }) => {
+ throwIfNotSignIn(cookie)
+ })
+ .get('/profile', () => 'Hi there!')
+
+const app = new Elysia()
+ .use(profile)
+ // ⚠️ This will NOT have sign in check
+ .patch('/rename', ({ body }) => updateProfile(body))
+```
+
+
+In this example, the `isSignIn` check will only apply to `profile` but not `app`.
+
+
+
+> Try changing the path in the URL bar to **/rename** and see the result
+
+
+
+**Elysia isolate lifecycle by default** unless explicitly stated. This is similar to **export** in JavaScript, where you need to export the function to make it available outside the module.
+
+To **"export"** the lifecycle to other instances, you must add specify the scope.
+
+```ts
+import { Elysia } from 'elysia'
+
+const profile = new Elysia()
+ .onBeforeHandle(
+ { as: 'global' }, // [!code ++]
+ ({ cookie }) => {
+ throwIfNotSignIn(cookie)
+ }
+ )
+ .get('/profile', () => 'Hi there!')
+
+const app = new Elysia()
+ .use(profile)
+ // This has sign in check
+ .patch('/rename', ({ body }) => updateProfile(body))
+```
+
+
+
+Casting lifecycle to **"global"** will export lifecycle to **every instance**.
+
+### Scope level
+Elysia has 3 levels of scope as the following:
+
+Scope type are as the following:
+1. **local** (default) - apply to only current instance and descendant only
+2. **scoped** - apply to parent, current instance and descendants
+3. **global** - apply to all instance that apply the plugin (all parents, current, and descendants)
+
+Let's review what each scope type does by using the following example:
+```typescript
+import { Elysia } from 'elysia'
+
+
+const child = new Elysia()
+ .get('/child', 'hi')
+
+const current = new Elysia()
+ // ? Value based on table value provided below
+ .onBeforeHandle({ as: 'local' }, () => { // [!code ++]
+ console.log('hi')
+ })
+ .use(child)
+ .get('/current', 'hi')
+
+const parent = new Elysia()
+ .use(current)
+ .get('/parent', 'hi')
+
+const main = new Elysia()
+ .use(parent)
+ .get('/main', 'hi')
+```
+
+By changing the `type` value, the result should be as follows:
+
+| type | child | current | parent | main |
+| ---------- | ----- | ------- | ------ | ---- |
+| local | ✅ | ✅ | ❌ | ❌ |
+| scoped | ✅ | ✅ | ✅ | ❌ |
+| global | ✅ | ✅ | ✅ | ✅ |
+
+### Descendant
+
+By default plugin will **apply hook to itself and descendants** only.
+
+If the hook is registered in a plugin, instances that inherit the plugin will **NOT** inherit hooks and schema.
+
+```typescript
+import { Elysia } from 'elysia'
+
+const plugin = new Elysia()
+ .onBeforeHandle(() => {
+ console.log('hi')
+ })
+ .get('/child', 'log hi')
+
+const main = new Elysia()
+ .use(plugin)
+ .get('/parent', 'not log hi')
+```
+
+To apply hook to globally, we need to specify hook as global.
+```typescript
+import { Elysia } from 'elysia'
+
+const plugin = new Elysia()
+ .onBeforeHandle(() => {
+ return 'hi'
+ })
+ .get('/child', 'child')
+ .as('scoped')
+
+const main = new Elysia()
+ .use(plugin)
+ .get('/parent', 'parent')
+```
+
+
+
+## Config
To make the plugin more useful, allowing customization via config is recommended.
@@ -191,7 +341,7 @@ const app = new Elysia()
It's recommended to define a new plugin instance instead of using a function callback.
-Functional callback allows us to access the existing property of the main instance. For example, checking if specific routes or stores existed.
+Functional callback allows us to access the existing property of the main instance. For example, checking if specific routes or stores existed but harder to handle encapsulation and scope correctly.
To define a functional callback, create a function that accepts Elysia as a parameter.
@@ -210,7 +360,7 @@ const app = new Elysia()
-Once passed to `Elysia.use`, functional callback behaves as a normal plugin except the property is assigned directly to
+Once passed to `Elysia.use`, functional callback behaves as a normal plugin except the property is assigned directly to the main instance.
::: tip
You shall not worry about the performance difference between a functional callback and creating an instance.
@@ -307,11 +457,14 @@ const setup = new Elysia({ name: 'setup' })
const error = new Elysia()
.get('/', ({ a }) => a)
-const main = new Elysia()
- // With `setup`, type will be inferred
+// With `setup`, type will be inferred
+const child = new Elysia()
.use(setup) // [!code ++]
.get('/', ({ a }) => a)
// ^?
+
+const main = new Elysia()
+ .use(child)
```
@@ -448,98 +601,13 @@ new Elysia()
-## Scope
-
-By default, hook and schema will apply to **current instance only**.
-
-Elysia has an encapsulation scope for to prevent unintentional side effects.
-
-Scope type is to specify the scope of hook whether is should be encapsulated or global.
-
-```typescript twoslash
-// @errors: 2339
-import { Elysia } from 'elysia'
-
-const plugin = new Elysia()
- .derive(() => {
- return { hi: 'ok' }
- })
- .get('/child', ({ hi }) => hi)
-
-const main = new Elysia()
- .use(plugin)
- // ⚠️ Hi is missing
- .get('/parent', ({ hi }) => hi)
-```
-
-From the above code, we can see that `hi` is missing from the parent instance because the scope is local by default if not specified, and will not apply to parent.
-
-To apply the hook to the parent instance, we can use the `as` to specify scope of the hook.
-
-```typescript twoslash
-// @errors: 2339
-import { Elysia } from 'elysia'
-
-const plugin = new Elysia()
- .derive({ as: 'scoped' }, () => { // [!code ++]
- return { hi: 'ok' }
- })
- .get('/child', ({ hi }) => hi)
-
-const main = new Elysia()
- .use(plugin)
- // ✅ Hi is now available
- .get('/parent', ({ hi }) => hi)
-```
-
-### Scope level
-Elysia has 3 levels of scope as the following:
-Scope type are as the following:
-- **local** (default) - apply to only current instance and descendant only
-- **scoped** - apply to parent, current instance and descendants
-- **global** - apply to all instance that apply the plugin (all parents, current, and descendants)
-
-Let's review what each scope type does by using the following example:
-```typescript
-import { Elysia } from 'elysia'
-
-// ? Value base on table value provided below
-const type = 'local'
-
-const child = new Elysia()
- .get('/child', 'hi')
-
-const current = new Elysia()
- .onBeforeHandle({ as: type }, () => { // [!code ++]
- console.log('hi')
- })
- .use(child)
- .get('/current', 'hi')
-
-const parent = new Elysia()
- .use(current)
- .get('/parent', 'hi')
-
-const main = new Elysia()
- .use(parent)
- .get('/main', 'hi')
-```
-
-By changing the `type` value, the result should be as follows:
-
-| type | child | current | parent | main |
-| ---------- | ----- | ------- | ------ | ---- |
-| 'local' | ✅ | ✅ | ❌ | ❌ |
-| 'scoped' | ✅ | ✅ | ✅ | ❌ |
-| 'global' | ✅ | ✅ | ✅ | ✅ |
-
-### Scope cast
+## Scope cast Advanced Concept
To apply hook to parent may use one of the following:
-1. `inline as` apply only to a single hook
-2. `guard as` apply to all hook in a guard
-3. `instance as` apply to all hook in an instance
+1. [inline as](#inline-as) apply only to a single hook
+2. [guard as](#guard-as) apply to all hook in a guard
+3. [instance as](#instance-as) apply to all hook in an instance
-### 1. Inline as
+### Inline
Every event listener will accept `as` parameter to specify the scope of the hook.
```typescript twoslash
@@ -559,7 +627,7 @@ const main = new Elysia()
However, this method is apply to only a single hook, and may not be suitable for multiple hooks.
-### 2. Guard as
+### Guard as
Every event listener will accept `as` parameter to specify the scope of the hook.
```typescript
@@ -584,7 +652,7 @@ Guard alllowing us to apply `schema` and `hook` to multiple routes all at once w
However, it doesn't support `derive` and `resolve` method.
-### 3. Instance as
+### Instance as
`as` will read all hooks and schema scope of the current instance, modify.
```typescript twoslash
@@ -632,58 +700,14 @@ const parent = new Elysia()
.get('/ok', () => 3)
```
-### Descendant
-
-By default plugin will **apply hook to itself and descendants** only.
-
-If the hook is registered in a plugin, instances that inherit the plugin will **NOT** inherit hooks and schema.
-
-```typescript
-import { Elysia } from 'elysia'
-
-const plugin = new Elysia()
- .onBeforeHandle(() => {
- console.log('hi')
- })
- .get('/child', 'log hi')
-
-const main = new Elysia()
- .use(plugin)
- .get('/parent', 'not log hi')
-```
-
-To apply hook to globally, we need to specify hook as global.
-```typescript
-import { Elysia } from 'elysia'
-
-const plugin = new Elysia()
- .onBeforeHandle(() => {
- return 'hi'
- })
- .get('/child', 'child')
- .as('scoped')
-
-const main = new Elysia()
- .use(plugin)
- .get('/parent', 'parent')
-```
-
-
-
## Lazy Load
Modules are eagerly loaded by default.
-Elysia loads all modules then registers and indexes all of them before starting the server. This enforces that all the modules have loaded before it starts accepting requests.
-
-While this is fine for most applications, it may become a bottleneck for a server running in a serverless environment or an edge function, in which the startup time is important.
+Elysia will make sure that all modules are registered before the server starts.
-Lazy-loading can help decrease startup time by deferring modules to be gradually indexed after the server start.
+However, some modules may be computationally heavy or blocking, making the server startup slow.
-Lazy-loading modules are a good option when some modules are heavy and importing startup time is crucial.
-
-By default, any async plugin without await is treated as a deferred module and the import statement as a lazy-loading module.
-
-Both will be registered after the server is started.
+To solve this, Elysia allows you to provide an async plugin that will not block the server startup.
### Deferred Module
The deferred module is an async plugin that can be registered after the server is started.
@@ -713,8 +737,6 @@ const app = new Elysia()
.use(loadStatic)
```
-Elysia static plugin is also a deferred module, as it loads files and registers files path asynchronously.
-
### Lazy Load Module
Same as the async plugin, the lazy-load module will be registered after the server is started.
diff --git a/docs/essential/route.md b/docs/essential/route.md
index 473ef553..e79d6a47 100644
--- a/docs/essential/route.md
+++ b/docs/essential/route.md
@@ -36,20 +36,20 @@ const demo4 = new Elysia()
const demo5 = new Elysia()
.get('/', () => 'hello')
- .get('/hi', ({ error }) => error(404, 'Route not found :('))
+ .get('/hi', ({ status }) => status(404, 'Route not found :('))
const demo6 = new Elysia()
.get('/id/:id', ({ params: { id } }) => id)
.get('/id/123', '123')
.get('/id/anything', 'anything')
- .get('/id', ({ error }) => error(404))
- .get('/id/anything/test', ({ error }) => error(404))
+ .get('/id', ({ status }) => status(404))
+ .get('/id/anything/test', ({ status }) => status(404))
const demo7 = new Elysia()
.get('/id/:id', ({ params: { id } }) => id)
.get('/id/123', '123')
.get('/id/anything', 'anything')
- .get('/id', ({ error }) => error(404))
+ .get('/id', ({ status }) => status(404))
.get('/id/:id/:name', ({ params: { id, name } }) => id + ' ' + name)
const demo8 = new Elysia()
@@ -61,7 +61,7 @@ const demo9 = new Elysia()
.get('/id/:id', ({ params: { id } }) => id)
.get('/id/123', '123')
.get('/id/anything', 'anything')
- .get('/id', ({ error }) => error(404))
+ .get('/id', ({ status }) => status(404))
.get('/id/:id/:name', ({ params: { id, name } }) => id + '/' + name)
const demo10 = new Elysia()
@@ -94,9 +94,9 @@ const demo13 = new Elysia()
# Routing
-Web servers use the request's **path and HTTP method** to look up the correct resource, refers as **"routing"**.
+Web servers use the request's **path and method** to look up the correct resource, known as **"routing"**.
-We can define a route by calling a **method named after HTTP verbs**, passing a path and a function to execute when matched.
+We can define a route with **HTTP verb method**, a path and a function to execute when matched.
```typescript
import { Elysia } from 'elysia'
@@ -114,7 +114,7 @@ By default, web browsers will send a GET method when visiting a page.
::: tip
-Using an interactive browser above, hover on a blue highlight area to see difference result between each path
+Using the interactive browser above, hover on the blue highlight area to see different results between each path.
:::
## Path type
@@ -127,14 +127,6 @@ Path in Elysia can be grouped into 3 types:
You can use all of the path types together to compose a behavior for your web server.
-The priorities are as follows:
-
-1. static paths
-2. dynamic paths
-3. wildcards
-
-If the path is resolved as the static wild dynamic path is presented, Elysia will resolve the static path rather than the dynamic path
-
```typescript
import { Elysia } from 'elysia'
@@ -158,20 +150,31 @@ new Elysia()
}"
/>
-Here the server will respond as follows:
+
## Static Path
-A path or pathname is an identifier to locate resources of a server.
+Static path is a hardcoded string to locate the resource on the server.
+
+```ts
+import { Elysia } from 'elysia'
+
+new Elysia()
+ .get('/hello', 'hello')
+ .get('/hi', 'hi')
+ .listen(3000)
+```
+
+
-| URL | Path |
+
## Dynamic path
-URLs can be both static and dynamic.
-
-Static paths are hardcoded strings that can be used to locate resources of the server, while dynamic paths match some part and captures the value to extract extra information.
+Dynamic paths match some part and capture the value to extract extra information.
-For instance, we can extract the user ID from the pathname. For example:
+To define a dynamic path, we can use a colon `:` followed by a name.
```typescript twoslash
import { Elysia } from 'elysia'
@@ -217,7 +219,7 @@ new Elysia()
-Here dynamic path is created with `/id/:id` which tells Elysia to match any path up until `/id`. What comes after that is then stored as **params** object.
+Here, a dynamic path is created with `/id/:id`. Which tells Elysia to capture the value `:id` segment with value like **/id/1**, **/id/123**, **/id/anything**.
-## Multiple path parameters
+### Multiple path parameters
You can have as many path parameters as you like, which will then be stored into a `params` object.
@@ -338,20 +340,18 @@ new Elysia()
}"
/>
-The server will respond as follows:
+
## Wildcards
-Dynamic paths allow capturing certain segments of the URL.
+Dynamic paths allow capturing a single segment while wildcards allow capturing the rest of the path.
-However, when you need a value of the path to be more dynamic and want to capture the rest of the URL segment, a wildcard can be used.
-
-Wildcards can capture the value after segment regardless of amount by using "\*".
+To define a wildcard, we can use an asterisk `*`.
```typescript twoslash
import { Elysia } from 'elysia'
@@ -380,7 +380,7 @@ new Elysia()
}"
/>
-In this case the server will respond as follows:
+
-Wildcards are useful for capturing a path until a specific point.
+## Path priority
+Elysia has a path priorities as follows:
-::: tip
-You can use a wildcard with a path parameter.
-:::
+1. static paths
+2. dynamic paths
+3. wildcards
+
+If the path is resolved as the static wild dynamic path is presented, Elysia will resolve the static path rather than the dynamic path
+
+```typescript
+import { Elysia } from 'elysia'
+
+new Elysia()
+ .get('/id/1', 'static path')
+ .get('/id/:id', 'dynamic path')
+ .get('/id/*', 'wildcard path')
+ .listen(3000)
+```
+
+
## HTTP Verb
@@ -469,12 +495,12 @@ const app = new Elysia()
- **function**: Function to response to the client
- **hook**: Additional metadata
-When navigating to each method, you should see the results as the following:
+
::: tip
Based on [RFC 7231](https://www.rfc-editor.org/rfc/rfc7231#section-4.1), HTTP Verb is case-sensitive.
@@ -482,7 +508,7 @@ Based on [RFC 7231](https://www.rfc-editor.org/rfc/rfc7231#section-4.1), HTTP Ve
It's recommended to use the UPPERCASE convention for defining a custom HTTP Verb with Elysia.
:::
-## Elysia.all
+### ALL method
Elysia provides an `Elysia.all` for handling any HTTP method for a specified path using the same API like **Elysia.get** and **Elysia.post**
@@ -528,7 +554,7 @@ Unlike unit test's mock, **you can expect it to behave like an actual request**
But also useful for simulating or creating unit tests.
:::
-## 404
+
## Group
diff --git a/docs/essential/structure.md b/docs/essential/structure.md
index ee161c81..506d730c 100644
--- a/docs/essential/structure.md
+++ b/docs/essential/structure.md
@@ -7,11 +7,11 @@ head:
- - meta
- name: 'description'
- content: Elysia is pattern agnostic framework, we leave the decision up to you and your team for coding patterns to use. However, we found that there are several who are using MVC pattern (Model-View-Controller) on Elysia, and found it's hard to decouple and handling with types. This page is a guide to use Elysia with MVC pattern.
+ content: Elysia is a pattern agnostic framework, we leave the decision up to you and your team for coding patterns to use. However, we found that there are several who are using MVC pattern (Model-View-Controller) on Elysia, and found it's hard to decouple and handle types. This page is a guide to use Elysia with MVC pattern.
- - meta
- property: 'og:description'
- content: Elysia is pattern agnostic framework, we the decision up to you and your team for coding patterns to use. However, we found that there are several who are using MVC pattern (Model-View-Controller) on Elysia, and found it's hard to decouple and handling with types. This page is a guide to use Elysia with MVC pattern.
+ content: Elysia is a pattern agnostic framework, we leave the decision up to you and your team for coding patterns to use. However, we found that there are several who are using MVC pattern (Model-View-Controller) on Elysia, and found it's hard to decouple and handle types. This page is a guide to use Elysia with MVC pattern.
---
#### This page has been moved to [best practice](/essential/best-practice)
@@ -20,14 +20,14 @@ head:
Elysia is a pattern-agnostic framework, leaving the decision of which coding patterns to use up to you and your team.
-However, there are several concern from trying to adapt an MVC pattern [(Model-View-Controller)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) with Elysia, and found it's hard to decouple and handle types.
+However, there are several concerns about trying to adapt an MVC pattern [(Model-View-Controller)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) with Elysia, and we found it's hard to decouple and handle types.
-This page is a guide to on how to follows Elysia structure best practice combined with MVC pattern but can be adapted to any coding pattern you like.
+This page is a guide on how to follow Elysia structure best practices combined with MVC pattern but can be adapted to any coding pattern you like.
## Method Chaining
Elysia code should always use **method chaining**.
-As Elysia type system is complex, every methods in Elysia returns a new type reference.
+As Elysia's type system is complex, every method in Elysia returns a new type reference.
**This is important** to ensure type integrity and inference.
@@ -59,16 +59,16 @@ app.get('/', ({ store: { build } }) => build)
app.listen(3000)
```
-We recommend to **always use method chaining** to provide an accurate type inference.
+We recommend **always using method chaining** to provide an accurate type inference.
## Controller
> 1 Elysia instance = 1 controller
Elysia does a lot to ensure type integrity, if you pass an entire `Context` type to a controller, these might be the problems:
-1. Elysia type is complex and heavily depends on plugin and multiple level of chaining.
-2. Hard to type, Elysia type could change at anytime, especially with decorators, and store
-3. Type casting may lead to a loss of type integrity or an inability to ensure consistency between types and runtime code.
+1. Elysia type is complex and heavily depends on plugin and multiple level of chaining
+2. Hard to type, Elysia type could change at any time, especially with decorators, and store
+3. Type casting may lead to a loss of type integrity or an inability to ensure consistency between types and runtime code
4. This makes it more challenging for [Sucrose](/blog/elysia-10#sucrose) *(Elysia's "kind of" compiler)* to statically analyze your code
### ❌ Don't: Create a separate controller
@@ -87,10 +87,10 @@ new Elysia()
.get('/', Controller.hi)
```
-By passing an entire `Controller.method` to Elysia is an equivalent of having 2 controllers passing data back and forth. It's against the design of framework and MVC pattern itself.
+Passing an entire `Controller.method` to Elysia is equivalent to having 2 controllers passing data back and forth. It's against the design of the framework and MVC pattern itself.
### ✅ Do: Use Elysia as a controller
-Instead treat an Elysia instance as a controller itself instead.
+Instead, treat an Elysia instance as a controller itself.
```typescript
import { Elysia } from 'elysia'
import { Service } from './service'
@@ -158,9 +158,12 @@ class AuthService {
As Elysia type is complex, and heavily depends on plugin and multiple level of chaining, it can be challenging to manually type as it's highly dynamic.
-### ✅ Do: Use Elysia instance as a service
+### ✅ Do: Request dependent service as Elysia instance
+
+We recommend abstracting service classes away from Elysia.
+
+However, **if the service is a request dependent service** or needs to process HTTP requests, we recommend abstracting it as an Elysia instance to ensure type integrity and inference:
-We recommended to use Elysia instance as a service to ensure type integrity and inference:
```typescript
import { Elysia } from 'elysia'
@@ -194,7 +197,7 @@ Elysia handle [plugin deduplication](/essential/plugin.html#plugin-deduplication
### ⚠️ Infers Context from Elysia instance
-In case of **absolute necessity**, you may infer the `Context` type from the Elysia instance itself:
+If **absolutely necessary**, you may infer the `Context` type from the Elysia instance itself:
```typescript
import { Elysia, type InferContext } from 'elysia'
@@ -213,9 +216,9 @@ class AuthService {
}
```
-However we recommend to avoid this if possible, and use [Elysia as a service](✅-do-use-elysia-instance-as-a-service) instead.
+However, we recommend avoiding this if possible, and using [Elysia as a service](✅-do-use-elysia-instance-as-a-service) instead.
-You may find more about [InferContext](/essential/handler#infercontext) in [Essential: Handler](/essential/handler).
+You can learn more about [InferContext](/essential/handler#infercontext) in [Essential: Handler](/essential/handler).
## Model
Model or [DTO (Data Transfer Object)](https://en.wikipedia.org/wiki/Data_transfer_object) is handle by [Elysia.t (Validation)](/validation/overview.html#data-validation).
@@ -359,7 +362,7 @@ const UserController = new Elysia({ prefix: '/auth' })
This approach provide several benefits:
1. Allow us to name a model and provide auto-completion.
2. Modify schema for later usage, or perform [remapping](/patterns/remapping.html#remapping).
-3. Show up as "models" in OpenAPI compliance client, eg. Swagger.
+3. Show up as "models" in OpenAPI compliance client, eg. OpenAPI.
4. Improve TypeScript inference speed as model type will be cached during registration.
---
diff --git a/docs/essential/validation.md b/docs/essential/validation.md
index 3da35b8d..885b5d46 100644
--- a/docs/essential/validation.md
+++ b/docs/essential/validation.md
@@ -68,7 +68,7 @@ const demo4 = new Elysia()
The purpose of creating an API server is to take an input and process it.
-JavaScript allows any data to be any type. Elysia provides a tool to validate data out of the box to ensure that the data is in the correct format.
+JavaScript allows any data to be of any type. Elysia provides a tool to validate data out of the box to ensure that the data is in the correct format.
```typescript twoslash
import { Elysia, t } from 'elysia'
@@ -84,39 +84,41 @@ new Elysia()
### TypeBox
-**Elysia.t** is a schema builder based on [TypeBox](https://github.com/sinclairzx81/typebox) that provides type-safety at runtime, compile-time, and for OpenAPI schemas, enabling the generation of OpenAPI/Swagger documentation.
+**Elysia.t** is a schema builder based on [TypeBox](https://github.com/sinclairzx81/typebox) that provides type-safety at runtime, compile-time, and OpenAPI schema generation from a single source of truth.
-TypeBox is a very fast, lightweight, and type-safe runtime validation library for TypeScript. Elysia extends and customizes the default behavior of TypeBox to match server-side validation requirements.
+Elysia tailor TypeBox for server-side validation for a seamless experience.
-We believe that validation should be handled by the framework natively, rather than relying on the user to set up a custom type for every project.
+### Standard Schema
+Elysia also support [Standard Schema](https://github.com/standard-schema/standard-schema), allowing you to use your favorite validation library:
+- Zod
+- Valibot
+- ArkType
+- Effect Schema
+- Yup
+- Joi
+- [and more](https://github.com/standard-schema/standard-schema)
-### TypeScript
-We can get a type definitions of every Elysia/TypeBox's type by accessing `static` property as follows:
-
-```ts twoslash
-import { t } from 'elysia'
-
-const MyType = t.Object({
- hello: t.Literal('Elysia')
-})
+To use Standard Schema, simply import the schema and provide it to the route handler.
-type MyType = typeof MyType.static
-// ^?
-````
-
-
-
-
-
-This allows Elysia to infer and provide type automatically, reducing the need to declare duplicate schema
+```typescript twoslash
+import { Elysia } from 'elysia'
+import { z } from 'zod'
+import * as v from 'valibot'
-A single Elysia/TypeBox schema can be used for:
-- Runtime validation
-- Data coercion
-- TypeScript type
-- OpenAPI schema
+new Elysia()
+ .get('/id/:id', ({ params: { id }, query: { name } }) => id, {
+ // ^?
+ params: z.object({
+ id: z.coerce.number()
+ }),
+ query: v.object({
+ name: v.literal('Lilith')
+ })
+ })
+ .listen(3000)
+```
-This allows us to make a schema as a **single source of truth**.
+You can use any validator together in the same handler without any issue.
## Schema type
Elysia supports declarative schemas with the following types:
@@ -172,7 +174,7 @@ The response should be as follows:
| /id/a?name=Elysia | ✅ | ❌ |
| /id/a?alias=Elysia | ❌ | ❌ |
-When a schema is provided, the type will be inferred from the schema automatically and an OpenAPI type will be generated for Swagger documentation, eliminating the redundant task of providing the type manually.
+When a schema is provided, the type will be inferred from the schema automatically and an OpenAPI type will be generated for an API documentation, eliminating the redundant task of providing the type manually.
## Guard
@@ -322,6 +324,25 @@ new Elysia()
By providing a file type, Elysia will automatically assume that the content-type is `multipart/form-data`.
+### File (Standard Schema)
+If you're using Standard Schema, it's important that Elysia will not be able to valiate content type automatically similar to `t.File`.
+
+But Elysia export a `fileType` that can be used to validate file type by using magic number.
+
+```typescript twoslash
+import { Elysia, fileType } from 'elysia'
+import { z } from 'zod'
+
+new Elysia()
+ .post('/body', ({ body }) => body, {
+ body: z.object({
+ file: z.file().refine((file) => fileType(file, 'image/jpeg')) // [!code ++]
+ })
+ })
+```
+
+It's very important that you **should use** `fileType` to validate the file type as **most validator doesn't actually validate the file** correctly, like checking the content type the value of it which can lead to security vulnerability.
+
## Query
Query is the data sent through the URL. It can be in the form of `?key=value`.
@@ -753,7 +774,7 @@ All members must be a string
t.Object({
x: t.Number()
}, {
- error: 'Invalid object UwU'
+ error: 'Invalid object UnU'
})
```
@@ -761,7 +782,7 @@ t.Object({
@@ -990,13 +1011,13 @@ The narrowed-down error type will be typed as `ValidationError` imported from **
**ValidationError** exposes a property named **validator**, typed as [TypeCheck](https://github.com/sinclairzx81/typebox#typecheck), allowing us to interact with TypeBox functionality out of the box.
-```typescript twoslash
+```typescript
import { Elysia, t } from 'elysia'
new Elysia()
.onError(({ code, error }) => {
if (code === 'VALIDATION')
- return error.validator.Errors(error.value).First().message
+ return error.all[0].message
})
.listen(3000)
```
@@ -1005,7 +1026,7 @@ new Elysia()
**ValidationError** provides a method `ValidatorError.all`, allowing us to list all of the error causes.
-```typescript twoslash
+```typescript
import { Elysia, t } from 'elysia'
new Elysia()
@@ -1141,7 +1162,7 @@ const app = new Elysia()
})
```
-This approach not only allows us to separate concerns but also enables us to reuse the model in multiple places while integrating the model into Swagger documentation.
+This approach not only allows us to separate concerns but also enables us to reuse the model in multiple places while integrating the model into OpenAPI documentation.
### Multiple Models
`model` accepts an object with the key as a model name and the value as the model definition. Multiple models are supported by default.
@@ -1190,3 +1211,31 @@ export const userModels = new Elysia()
This can prevent naming duplication to some extent, but ultimately, it's best to let your team decide on the naming convention.
Elysia provides an opinionated option to help prevent decision fatigue.
+
+### TypeScript
+We can get type definitions of every Elysia/TypeBox's type by accessing the `static` property as follows:
+
+```ts twoslash
+import { t } from 'elysia'
+
+const MyType = t.Object({
+ hello: t.Literal('Elysia')
+})
+
+type MyType = typeof MyType.static
+// ^?
+````
+
+
+
+
+
+This allows Elysia to infer and provide type automatically, reducing the need to declare duplicate schema
+
+A single Elysia/TypeBox schema can be used for:
+- Runtime validation
+- Data coercion
+- TypeScript type
+- OpenAPI schema
+
+This allows us to make a schema as a **single source of truth**.
diff --git a/docs/index.md b/docs/index.md
index 95804d83..59e4faa2 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -9,11 +9,11 @@ head:
- - meta
- name: 'description'
- content: Elysia is an ergonomic framework for Humans. With end-to-end type safety and great developer experience. Elysia is familiar, fast, and first class TypeScript support with well-thought integration between services whether it's tRPC, Swagger or WebSocket. Elysia got you covered, start building next generation TypeScript web servers today.
+ content: Elysia is an ergonomic framework for Humans. With end-to-end type safety and great developer experience. Elysia is familiar, fast, and has first-class TypeScript support with well-thought integration between services whether it's tRPC, Swagger or WebSocket. Elysia has got you covered, start building next generation TypeScript web servers today.
- - meta
- property: 'og:description'
- content: Elysia is an ergonomic framework for Humans. With end-to-end type safety and great developer experience. Elysia is familiar, fast, and first class TypeScript support with well-thought integration between services whether it's tRPC, Swagger or WebSocket. Elysia got you covered, start building next generation TypeScript web servers today.
+ content: Elysia is an ergonomic framework for Humans. With end-to-end type safety and great developer experience. Elysia is familiar, fast, and has first-class TypeScript support with well-thought integration between services whether it's tRPC, Swagger or WebSocket. Elysia has got you covered, start building next generation TypeScript web servers today.
---
+
+# Key Concept
+
+Elysia has a every important concepts that you need to understand to use.
+
+This page covers most concepts that you should know before getting started.
+
+## Encapsulation
+Elysia lifecycle methods are **encapsulated** to its own instance only.
+
+Which means if you create a new instance, it will not share the lifecycle methods with others.
-Although Elysia is a simple library, it has some key concepts that you need to understand to use it effectively.
+```ts
+import { Elysia } from 'elysia'
+
+const profile = new Elysia()
+ .onBeforeHandle(({ cookie }) => {
+ throwIfNotSignIn(cookie)
+ })
+ .get('/profile', () => 'Hi there!')
+
+const app = new Elysia()
+ .use(profile)
+ // ⚠️ This will NOT have sign in check
+ .patch('/rename', ({ body }) => updateProfile(body))
+```
+
+
+In this example, the `isSignIn` check will only apply to `profile` but not `app`.
+
+
-This page covers most important concepts of Elysia that you should know.
+> Try changing the path in the URL bar to **/rename** and see the result
+
+
-::: tip
-We __highly recommend__ you to read this page before learn more about Elysia.
-:::
+**Elysia isolate lifecycle by default** unless explicitly stated. This is similar to **export** in JavaScript, where you need to export the function to make it available outside the module.
-## Everything is a component
+To **"export"** the lifecycle to other instances, you must add specify the scope.
+
+```ts
+import { Elysia } from 'elysia'
+
+const profile = new Elysia()
+ .onBeforeHandle(
+ { as: 'global' }, // [!code ++]
+ ({ cookie }) => {
+ throwIfNotSignIn(cookie)
+ }
+ )
+ .get('/profile', () => 'Hi there!')
+
+const app = new Elysia()
+ .use(profile)
+ // This has sign in check
+ .patch('/rename', ({ body }) => updateProfile(body))
+```
+
+
+
+Casting lifecycle to **"global"** will export lifecycle to **every instance**.
+
+Learn more about this in [scope](/essential/plugin.html#scope-level).
+
+
-As Elysia type system is complex, every methods in Elysia returns a new type reference.
+## Method Chaining
+Elysia code should **ALWAYS** use method chaining.
-**This is important** to ensure type integrity and inference.
+This is **important to ensure type safety**.
```typescript twoslash
import { Elysia } from 'elysia'
@@ -72,7 +146,9 @@ new Elysia()
In the code above, **state** returns a new **ElysiaInstance** type, adding a typed `build` property.
-### Don't use Elysia without method chaining
+### Without method chaining
+As Elysia type system is complex, every method in Elysia returns a new type reference.
+
Without using method chaining, Elysia doesn't save these new types, leading to no type inference.
```typescript twoslash
@@ -90,63 +166,19 @@ app.listen(3000)
We recommend to **always use method chaining** to provide an accurate type inference.
-## Scope
-By default, event/life-cycle in each instance is isolated from each other.
+## Dependency
+Each plugin will be re-executed **EVERY TIME** when apply to another instance.
-```ts twoslash
-// @errors: 2339
-import { Elysia } from 'elysia'
+If the plugin is applied multiple time, it will cause an unnecessary duplication.
-const ip = new Elysia()
- .derive(({ server, request }) => ({
- ip: server?.requestIP(request)
- }))
- .get('/ip', ({ ip }) => ip)
+It's important that some methods, like **lifecycle** or **routes**, should only be called once.
-const server = new Elysia()
- .use(ip)
- .get('/ip', ({ ip }) => ip)
- .listen(3000)
-```
-
-In this example, the `ip` property is only shared in its own instance but not in the `server` instance.
-
-To share the lifecycle, in our case, an `ip` property with `server` instance, we need to **explicitly say** that it could be shared.
-
-```ts twoslash
-import { Elysia } from 'elysia'
-
-const ip = new Elysia()
- .derive(
- { as: 'global' }, // [!code ++]
- ({ server, request }) => ({
- ip: server?.requestIP(request)
- })
- )
- .get('/ip', ({ ip }) => ip)
-
-const server = new Elysia()
- .use(ip)
- .get('/ip', ({ ip }) => ip)
- .listen(3000)
-```
-
-In this example, `ip` property is shared between `ip` and `server` instance because we define it as `global`.
-
-This forces you to think about the scope of each property, preventing you from accidentally sharing the property between instances.
-
-Learn more about this in [scope](/essential/plugin.html#scope).
-
-## Dependency
-By default, each instance will be re-executed every time it's applied to another instance.
-
-This can cause a duplication of the same method being applied multiple times, whereas some methods, like **lifecycle** or **routes**, should only be called once.
-
-To prevent lifecycle methods from being duplicated, we can add **a unique identifier** to the instance.
+To prevent this, Elysia can deduplicate lifecycle with **an unique identifier**.
```ts twoslash
import { Elysia } from 'elysia'
+// `name` is an unique identifier
const ip = new Elysia({ name: 'ip' }) // [!code ++]
.derive(
{ as: 'global' },
@@ -169,14 +201,12 @@ const server = new Elysia()
.use(router2)
```
-This will prevent the `ip` property from being called multiple times by applying deduplication using a unique name.
-
-This allows us to reuse the same instance multiple times without the performance penalty. Forcing you to think about the dependencies of each instance.
+Adding the `name` property to the instance will make it a unique identifier prevent it from being called multiple times.
Learn more about this in [plugin deduplication](/essential/plugin.html#plugin-deduplication).
-### Service Locator
-When you apply a plugin with state/decorators to an instance, the instance will gain type safety.
+### Service Locator
+When you apply a plugin with to an instance, the instance will gain type safety.
But if you don't apply the plugin to another instance, it will not be able to infer the type.
@@ -195,7 +225,7 @@ const main = new Elysia()
Elysia introduces the **Service Locator** pattern to counteract this.
-We simply provide the plugin reference for Elysia to find the service to add type safety.
+By simply provide the plugin reference for Elysia to find the service to add type safety.
```typescript twoslash
// @errors: 2339
@@ -208,16 +238,23 @@ const setup = new Elysia({ name: 'setup' })
const error = new Elysia()
.get('/', ({ a }) => a)
-const main = new Elysia()
- // With `setup`, type will be inferred
+// With `setup`, type will be inferred
+const child = new Elysia()
.use(setup) // [!code ++]
.get('/', ({ a }) => a)
// ^?
+
+
+
+// ---cut-after---
+console.log()
```
-As mentioned in [dependencies](#dependencies), we can use the `name` property to deduplicate the instance so it will not have any performance penalty or lifecycle duplication.
+This is equivalent to TypeScript's **type import**, where you import the type without actually importing the code to run.
+
+As mentioned in Elysia already handle deduplication, this will not have any performance penalty or lifecycle duplication.
-## Order of code
+## Order of code
The order of Elysia's life-cycle code is very important.
@@ -268,9 +305,9 @@ const app = new Elysia()
})
```
-If possible, **always use an inline function** to provide an accurate type inference.
+You should **always use an inline function** to provide an accurate type inference.
-If you need to apply a separate function, eg. MVC's controller pattern, it's recommended to destructure properties from inline function to prevent unnecessary type inference.
+If you need to apply a separate function, eg. MVC's controller pattern, it's recommended to destructure properties from inline function to prevent unnecessary type inference as follows:
```ts twoslash
import { Elysia, t } from 'elysia'
@@ -289,6 +326,8 @@ const app = new Elysia()
})
```
+See [Best practice: MVC Controller](/essential/best-practice.html#controller).
+
### TypeScript
We can get a type definitions of every Elysia/TypeBox's type by accessing `static` property as follows:
@@ -316,5 +355,3 @@ A single Elysia/TypeBox schema can be used for:
- OpenAPI schema
This allows us to make a schema as a **single source of truth**.
-
-Learn more about this in [Best practice: MVC Controller](/essential/best-practice.html#controller).
diff --git a/docs/midori.md b/docs/midori.md
deleted file mode 100644
index 3572c14d..00000000
--- a/docs/midori.md
+++ /dev/null
@@ -1,181 +0,0 @@
----
-title: Elysia - Ergonomic Framework for Humans
-layout: page
-sidebar: false
-head:
- - - meta
- - property: 'og:title'
- content: Elysia - Ergonomic Framework for Humans
-
- - - meta
- - name: 'description'
- content: Elysia is an ergonomic framework for Humans. With end-to-end type safety and great developer experience. Elysia is familiar, fast, and first class TypeScript support with well-thought integration between services whether it's tRPC, Swagger or WebSocket. Elysia got you covered, start building next generation TypeScript web servers today.
-
- - - meta
- - property: 'og:description'
- content: Elysia is an ergonomic framework for Humans. With end-to-end type safety and great developer experience. Elysia is familiar, fast, and first class TypeScript support with well-thought integration between services whether it's tRPC, Swagger or WebSocket. Elysia got you covered, start building next generation TypeScript web servers today.
----
-
-
-
-
-
-
-```typescript twoslash
-import { Elysia } from 'elysia'
-
-new Elysia()
- .get('/', 'Hello World')
- .get('/json', {
- hello: 'world'
- })
- .get('/id/:id', ({ params: { id } }) => id)
- .listen(3000)
-
-```
-
-
-
-
-
-```typescript twoslash
-import { Elysia, t } from 'elysia'
-
-new Elysia()
- .post(
- '/profile',
- // ↓ hover me ↓
- ({ body }) => body,
- {
- body: t.Object({
- username: t.String()
- })
- }
- )
- .listen(3000)
-
-```
-
-
-
-
-```ts twoslash
-// @filename: controllers.ts
-import { Elysia } from 'elysia'
-
-export const users = new Elysia()
- .get('/users', 'Dreamy Euphony')
-
-export const feed = new Elysia()
- .get('/feed', ['Hoshino', 'Griseo', 'Astro'])
-
-// @filename: server.ts
-// ---cut---
-import { Elysia, t } from 'elysia'
-import { swagger } from '@elysiajs/swagger'
-import { users, feed } from './controllers'
-
-new Elysia()
- .use(swagger())
- .use(users)
- .use(feed)
- .listen(3000)
-```
-
-
-
-
-```typescript twoslash
-// @filename: server.ts
-// ---cut---
-// server.ts
-import { Elysia, t } from 'elysia'
-
-const app = new Elysia()
- .patch(
- '/user/profile',
- ({ body, status }) => {
- if(body.age < 18)
- return status(400, "Oh no")
-
- if(body.name === 'Nagisa')
- return status(418)
-
- return body
- },
- {
- body: t.Object({
- name: t.String(),
- age: t.Number()
- })
- }
- )
- .listen(80)
-
-export type App = typeof app
-```
-
-
-
-
-```typescript twoslash
-// @errors: 2322 1003
-// @filename: server.ts
-import { Elysia, t } from 'elysia'
-
-const app = new Elysia()
- .patch(
- '/user/profile',
- ({ body, status }) => {
- if(body.age < 18)
- return status(400, "Oh no")
-
- if(body.name === 'Nagisa')
- return status(418)
-
- return body
- },
- {
- body: t.Object({
- name: t.String(),
- age: t.Number()
- })
- }
- )
- .listen(80)
-
-export type App = typeof app
-
-// @filename: client.ts
-// ---cut---
-// client.ts
-import { treaty } from '@elysiajs/eden'
-import type { App } from './server'
-
-const api = treaty('localhost')
-
-const { data, error } = await api.user.profile.patch({
- name: 'saltyaom',
- age: '21'
-})
-
-if(error)
- switch(error.status) {
- case 400:
- throw error.value
-// ^?
-
- case 418:
- throw error.value
-// ^?
-}
-
-data
-// ^?
-```
-
-
-
-
diff --git a/docs/migrate/from-express.md b/docs/migrate/from-express.md
index 1df127d7..25d227b8 100644
--- a/docs/migrate/from-express.md
+++ b/docs/migrate/from-express.md
@@ -13,11 +13,11 @@ head:
- - meta
- name: 'description'
- content: This guide is for Express users who want to see a differences from Express including syntax, and how to migrate your application from Express to Elysia by example.
+ content: This guide is for Express users who want to see the differences from Express including syntax, and how to migrate your application from Express to Elysia by example.
- - meta
- property: 'og:description'
- content: This guide is for Express users who want to see a differences from Express including syntax, and how to migrate your application from Express to Elysia by example.
+ content: This guide is for Express users who want to see the differences from Express including syntax, and how to migrate your application from Express to Elysia by example.
---
+
+# From tRPC to Elysia
+
+This guide is for tRPC users who want to see a differences from Elysia including syntax, and how to migrate your application from tRPC to Elysia by example.
+
+**tRPC** is a typesafe RPC framework for building APIs using TypeScript. It provides a way to create end-to-end type-safe APIs with type-safe contract between frontend and backend.
+
+**Elysia** is an ergonomic web framework. Designed to be ergonomic and developer-friendly with a focus on **sound type safety** and performance.
+
+## Overview
+tRPC is primarily designed as RPC communication with proprietary abstraction over RESTful API, while Elysia is focused on RESTful API.
+
+Main feature of tRPC is end-to-end type safety contract between frontend and backend which Elysia also offers via [Eden](/eden/overview).
+
+Making Elysia a better fit for building a universal API with RESTful standard that developers already know instead of learning a new proprietary abstraction while having the end-to-end type safety that tRPC offers.
+
+## Routing
+
+Elysia use a syntax similar to Express, and Hono like `app.get()` and `app.post()` methods to define routes and similar path parameters syntax.
+
+While tRPC use a nested router approach to define routes.
+
+
+
+
+
+::: code-group
+
+```ts [tRPC]
+import { initTRPC } from '@trpc/server'
+import { createHTTPServer } from '@trpc/server/adapters/standalone'
+
+const t = initTRPC.create()
+
+const appRouter = t.router({
+ hello: t.procedure.query(() => 'Hello World'),
+ user: t.router({
+ getById: t.procedure
+ .input((id: string) => id)
+ .query(({ input }) => {
+ return { id: input }
+ })
+ })
+})
+
+const server = createHTTPServer({
+ router: appRouter
+})
+
+server.listen(3000)
+```
+
+:::
+
+
+
+
+> tRPC use nested router and procedure to define routes
+
+
+
+
+
+::: code-group
+
+```ts [Elysia]
+import { Elysia } from 'elysia'
+
+const app = new Elysia()
+ .get('/', 'Hello World')
+ .post(
+ '/id/:id',
+ ({ status, params: { id } }) => {
+ return status(201, id)
+ }
+ )
+ .listen(3000)
+```
+
+:::
+
+
+
+
+> Elysia use HTTP method, and path parameters to define routes
+
+
+
+
+
+While tRPC use proprietary abstraction over RESTful API with procedure and router, Elysia use a syntax similar to Express, and Hono like `app.get()` and `app.post()` methods to define routes and similar path parameters syntax.
+
+## Handler
+
+tRPC handler is called `procedure` which can be either `query` or `mutation`, while Elysia use HTTP method like `get`, `post`, `put`, `delete` and so on.
+
+tRPC is doesn't have a concept of HTTP property like query, headers, status code, and so on, only `input` and `output`.
+
+
+
+
+
+::: code-group
+
+```ts [tRPC]
+import { initTRPC } from '@trpc/server'
+
+const t = initTRPC.create()
+
+const appRouter = t.router({
+ user: t.procedure
+ .input((val: { limit?: number; name: string; authorization?: string }) => val)
+ .mutation(({ input }) => {
+ const limit = input.limit
+ const name = input.name
+ const auth = input.authorization
+
+ return { limit, name, auth }
+ })
+})
+```
+
+:::
+
+
+
+
+> tRPC use single `input` for all properties
+
+
+
+
+
+::: code-group
+
+```ts [Elysia]
+import { Elysia } from 'elysia'
+
+const app = new Elysia()
+ .post('/user', (ctx) => {
+ const limit = ctx.query.limit
+ const name = ctx.body.name
+ const auth = ctx.headers.authorization
+
+ return { limit, name, auth }
+ })
+```
+
+:::
+
+
+
+
+> Elysia use specific property for each HTTP property
+
+
+
+
+
+Elysia use **static code analysis** to determine what to parse, and only parse the required properties.
+
+This is useful for performance and type safety.
+
+## Subrouter
+
+tRPC use nested router to define subrouter, while Elysia use `.use()` method to define a subrouter.
+
+
+
+
+
+::: code-group
+
+```ts [tRPC]
+import { initTRPC } from '@trpc/server'
+
+const t = initTRPC.create()
+
+const subRouter = t.router({
+ user: t.procedure.query(() => 'Hello User')
+})
+
+const appRouter = t.router({
+ api: subRouter
+})
+```
+
+:::
+
+
+
+
+> tRPC use nested router to define subrouter
+
+
+
+
+
+::: code-group
+
+```ts [Elysia]
+import { Elysia } from 'elysia'
+
+const subRouter = new Elysia()
+ .get('/user', 'Hello User')
+
+const app = new Elysia()
+ .use(subRouter)
+```
+
+:::
+
+
+
+
+> Elysia use a `.use()` method to define a subrouter
+
+
+
+
+
+While you can inline the subrouter in tRPC, Elysia use `.use()` method to define a subrouter.
+
+## Validation
+Both support Standard Schema for validation. Allowing you to use various validation library like Zod, Yup, Valibot, and so on.
+
+
+
+
+
+::: code-group
+
+```ts [tRPC]
+import { initTRPC } from '@trpc/server'
+import { z } from 'zod'
+
+const t = initTRPC.create()
+
+const appRouter = t.router({
+ user: t.procedure
+ .input(
+ z.object({
+ id: z.number(),
+ name: z.string()
+ })
+ )
+ .mutation(({ input }) => input)
+// ^?
+})
+```
+
+:::
+
+
+
+
+> tRPC use `input` to define validation schema
+
+
+
+
+
+::: code-group
+
+```ts twoslash [Elysia TypeBox]
+import { Elysia, t } from 'elysia'
+
+const app = new Elysia()
+ .patch('/user/:id', ({ params, body }) => ({
+// ^?
+ params,
+ body
+// ^?
+ }),
+
+
+
+ {
+ params: t.Object({
+ id: t.Number()
+ }),
+ body: t.Object({
+ name: t.String()
+ })
+ })
+```
+
+```ts twoslash [Elysia Zod]
+import { Elysia } from 'elysia'
+import { z } from 'zod'
+
+const app = new Elysia()
+ .patch('/user/:id', ({ params, body }) => ({
+// ^?
+ params,
+ body
+// ^?
+ }),
+
+
+
+ {
+ params: z.object({
+ id: z.number()
+ }),
+ body: z.object({
+ name: z.string()
+ })
+ })
+```
+
+```ts twoslash [Elysia Valibot]
+import { Elysia } from 'elysia'
+import * as v from 'zod'
+
+const app = new Elysia()
+ .patch('/user/:id', ({ params, body }) => ({
+// ^?
+ params,
+ body
+// ^?
+ }),
+
+
+
+ {
+ params: v.object({
+ id: v.number()
+ }),
+ body: v.object({
+ name: v.string()
+ })
+ })
+```
+
+:::
+
+
+
+
+> Elysia use specific property to define validation schema
+
+
+
+
+
+Both offers type inference from schema to context automatically.
+
+## File upload
+tRPC doesn't support file upload out-of-the-box and require you to use `base64` string as input which is inefficient, and doesn't support mimetype validation.
+
+While Elysia has built-in support for file upload using Web Standard API.
+
+
+
+
+
+::: code-group
+
+```ts [tRPC]
+import { initTRPC } from '@trpc/server'
+import { z } from 'zod'
+
+import { fileTypeFromBuffer } from 'file-type'
+
+const t = initTRPC.create()
+
+export const uploadRouter = t.router({
+ uploadImage: t.procedure
+ .input(z.base64())
+ .mutation(({ input }) => {
+ const buffer = Buffer.from(input, 'base64')
+
+ const type = await fileTypeFromBuffer(buffer)
+ if (!type || !type.mime.startsWith('image/'))
+ throw new TRPCError({
+ code: 'UNPROCESSABLE_CONTENT',
+ message: 'Invalid file type',
+ })
+
+ return input
+ })
+})
+```
+
+:::
+
+
+
+
+> tRPC
+
+
+
+
+
+::: code-group
+
+```ts [Elysia]
+import { Elysia, t } from 'elysia'
+
+const app = new Elysia()
+ .post('/upload', ({ body }) => body.file, {
+ body: t.Object({
+ file: t.File({
+ type: 'image'
+ })
+ })
+ })
+```
+
+:::
+
+
+
+
+> Elysia handle file, and mimetype validation declaratively
+
+
+
+
+
+As doesn't validate mimetype out-of-the-box, you need to use a third-party library like `file-type` to validate an actual type.
+
+## Middleware
+
+tRPC middleware use a single queue-based order with `next` similar to Express, while Elysia give you a more granular control using an **event-based** lifecycle.
+
+Elysia's Life Cycle event can be illustrated as the following.
+
+> Click on image to enlarge
+
+While tRPC has a single flow for request pipeline in order, Elysia can intercept each event in a request pipeline.
+
+
+
+
+
+::: code-group
+
+```ts [tRPC]
+import { initTRPC } from '@trpc/server'
+
+const t = initTRPC.create()
+
+const log = t.middleware(async ({ ctx, next }) => {
+ console.log('Request started')
+
+ const result = await next()
+
+ console.log('Request ended')
+
+ return result
+})
+
+const appRouter = t.router({
+ hello: log
+ .procedure
+ .query(() => 'Hello World')
+})
+```
+
+:::
+
+
+
+
+> tRPC use a single middleware queue defined as a procedure
+
+
+
+
+
+::: code-group
+
+```ts [Elysia]
+import { Elysia } from 'elysia'
+
+const app = new Elysia()
+ // Global middleware
+ .onRequest(({ method, path }) => {
+ console.log(`${method} ${path}`)
+ })
+ // Route-specific middleware
+ .get('/protected', () => 'protected', {
+ beforeHandle({ status, headers }) {
+ if (!headers.authorizaton)
+ return status(401)
+ }
+ })
+```
+
+:::
+
+
+
+
+> Elysia use a specific event interceptor for each point in the request pipeline
+
+
+
+
+
+While tRPC has a `next` function to call the next middleware in the queue, Elysia use specific event interceptor for each point in the request pipeline.
+
+## Sounds type safety
+Elysia is designed to be sounds type safety.
+
+For example, you can customize context in a **type safe** manner using [derive](/essential/life-cycle.html#derive) and [resolve](/essential/life-cycle.html#resolve) while tRPC offers one by using `context` by type case which is doesn't ensure 100% type safety, making it unsounds.
+
+
+
+
+
+::: code-group
+
+```ts twoslash [tRPC]
+import { initTRPC } from '@trpc/server'
+
+const t = initTRPC.context<{
+ version: number
+ token: string
+}>().create()
+
+const appRouter = t.router({
+ version: t.procedure.query(({ ctx: { version } }) => version),
+ // ^?
+
+
+ token: t.procedure.query(({ ctx: { token, version } }) => {
+ version
+ // ^?
+
+ return token
+ // ^?
+ })
+})
+```
+
+:::
+
+
+
+
+> tRPC use `context` to extend context but doesn't have sounds type safety
+
+
+
+
+
+::: code-group
+
+```ts twoslash [Elysia]
+import { Elysia } from 'elysia'
+
+const app = new Elysia()
+ .decorate('version', 2)
+ .get('/version', ({ version }) => version)
+ .resolve(({ status, headers: { authorization } }) => {
+ if(!authorization?.startsWith('Bearer '))
+ return status(401)
+
+ return {
+ token: authorization.split(' ')[1]
+ }
+ })
+ .get('/token', ({ token, version }) => {
+ version
+ // ^?
+
+
+ return token
+ // ^?
+ })
+```
+
+:::
+
+
+
+
+> Elysia use a specific event interceptor for each point in the request pipeline
+
+
+
+
+
+## Middleware parameter
+Both support custom middleware, but Elysia use macro to pass custom argument to custom middleware while tRPC use higher-order-function which is not type safe.
+
+
+
+
+
+::: code-group
+
+```ts twoslash [tRPC]
+import { initTRPC, TRPCError } from '@trpc/server'
+
+const t = initTRPC.create()
+
+const findUser = (authorization?: string) => {
+ return {
+ name: 'Jane Doe',
+ role: 'admin' as const
+ }
+}
+
+const role = (role: 'user' | 'admin') =>
+ t.middleware(({ next, input }) => {
+ const user = findUser(input as string)
+ // ^?
+
+
+ if(user.role !== role)
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'Unauthorized',
+ })
+
+ return next({
+ ctx: {
+ user
+ }
+ })
+ })
+
+const appRouter = t.router({
+ token: t.procedure
+ .use(role('admin'))
+ .query(({ ctx: { user } }) => user)
+ // ^?
+})
+
+
+
+// ---cut-after---
+// Unused
+```
+
+:::
+
+
+
+
+> tRPC use higher-order-function to pass custom argument to custom middleware
+
+
+
+
+
+::: code-group
+
+```ts twoslash [Elysia]
+const findUser = (authorization?: string) => {
+ return {
+ name: 'Jane Doe',
+ role: 'admin' as const
+ }
+}
+// ---cut---
+import { Elysia } from 'elysia'
+
+const app = new Elysia()
+ .macro({
+ role: (role: 'user' | 'admin') => ({
+ resolve({ status, headers: { authorization } }) {
+ const user = findUser(authorization)
+
+ if(user.role !== role)
+ return status(401)
+
+ return {
+ user
+ }
+ }
+ })
+ })
+ .get('/token', ({ user }) => user, {
+ // ^?
+ role: 'admin'
+ })
+```
+
+:::
+
+
+
+
+> Elysia use macro to pass custom argument to custom middleware
+
+
+
+
+
+## Error handling
+
+tRPC use middleware-like to handle error, while Elysia provide custom error with type safety, and error interceptor for both global and route specific error handler.
+
+
+
+
+
+::: code-group
+
+```ts [trpc]
+import { initTRPC, TRPCError } from '@trpc/server'
+
+const t = initTRPC.create()
+
+class CustomError extends Error {
+ constructor(message: string) {
+ super(message)
+ this.name = 'CustomError'
+ }
+}
+
+const appRouter = t.router()
+ .middleware(async ({ next }) => {
+ try {
+ return await next()
+ } catch (error) {
+ console.log(error)
+
+ throw new TRPCError({
+ code: 'INTERNAL_SERVER_ERROR',
+ message: error.message
+ })
+ }
+ })
+ .query('error', () => {
+ throw new CustomError('oh uh')
+ })
+```
+
+:::
+
+
+
+
+> tRPC use middleware-like to handle error
+
+
+
+
+
+::: code-group
+
+```ts twoslash [Elysia]
+import { Elysia } from 'elysia'
+
+class CustomError extends Error {
+ // Optional: custom HTTP status code
+ status = 500
+
+ constructor(message: string) {
+ super(message)
+ this.name = 'CustomError'
+ }
+
+ // Optional: what should be sent to the client
+ toResponse() {
+ return {
+ message: "If you're seeing this, our dev forgot to handle this error",
+ error: this
+ }
+ }
+}
+
+const app = new Elysia()
+ // Optional: register custom error class
+ .error({
+ CUSTOM: CustomError,
+ })
+ // Global error handler
+ .onError(({ error, code }) => {
+ if(code === 'CUSTOM')
+ // ^?
+
+
+
+
+ return {
+ message: 'Something went wrong!',
+ error
+ }
+ })
+ .get('/error', () => {
+ throw new CustomError('oh uh')
+ }, {
+ // Optional: route specific error handler
+ error({ error }) {
+ return {
+ message: 'Only for this route!',
+ error
+ }
+ }
+ })
+```
+
+:::
+
+
+
+
+> Elysia provide more granular control over error handling, and scoping mechanism
+
+
+
+
+
+While tRPC offers error handling using middleware-like, Elysia provide:
+
+1. Both global and route specific error handler
+2. Shorthand for mapping HTTP status and `toResponse` for mapping error to a response
+3. Provide a custom error code for each error
+
+The error code is useful for logging and debugging, and is important when differentiating between different error types extending the same class.
+
+Elysia provides all of this with type safety while tRPC doesn't.
+
+## Encapsulation
+
+tRPC encapsulate side-effect of a by procedure or router making it always isolated, while Elysia give you a control over side-effect of a plugin via explicit scoping mechanism, and order-of-code.
+
+
+
+
+
+::: code-group
+
+```ts [tRPC]
+import { initTRPC } from '@trpc/server'
+
+const t = initTRPC.create()
+
+const subRouter = t.router()
+ .middleware(({ ctx, next }) => {
+ if(!ctx.headers.authorization?.startsWith('Bearer '))
+ throw new TRPCError({
+ code: 'UNAUTHORIZED',
+ message: 'Unauthorized',
+ })
+
+ return next()
+ })
+
+const appRouter = t.router({
+ // doesn't have side-effect from subRouter
+ hello: t.procedure.query(() => 'Hello World'),
+ api: subRouter
+ .mutation('side-effect', () => 'hi')
+})
+```
+
+:::
+
+
+
+
+> tRPC encapsulate side-effect of a plugin into the procedure or router
+
+
+
+
+
+::: code-group
+
+```ts [Elysia]
+import { Elysia } from 'elysia'
+
+const subRouter = new Elysia()
+ .onBeforeHandle(({ status, headers: { authorization } }) => {
+ if(!authorization?.startsWith('Bearer '))
+ return status(401)
+ })
+
+const app = new Elysia()
+ .get('/', 'Hello World')
+ .use(subRouter)
+ // doesn't have side-effect from subRouter
+ .get('/side-effect', () => 'hi')
+```
+
+:::
+
+
+
+
+> Elysia encapsulate side-effect of a plugin unless explicitly stated
+
+
+
+
+
+Both has a encapsulate mechanism of a plugin to prevent side-effect.
+
+However, Elysia can explicitly stated which plugin should have side-effect by declaring a scoped while Fastify always encapsulate it.
+
+```ts [Elysia]
+import { Elysia } from 'elysia'
+
+const subRouter = new Elysia()
+ .onBeforeHandle(({ status, headers: { authorization } }) => {
+ if(!authorization?.startsWith('Bearer '))
+ return status(401)
+ })
+ // Scoped to parent instance but not beyond
+ .as('scoped') // [!code ++]
+
+const app = new Elysia()
+ .get('/', 'Hello World')
+ .use(subRouter)
+ // [!code ++]
+ // now have side-effect from subRouter
+ .get('/side-effect', () => 'hi')
+```
+
+Elysia offers 3 type of scoping mechanism:
+1. **local** - Apply to current instance only, no side-effect (default)
+2. **scoped** - Scoped side-effect to the parent instance but not beyond
+3. **global** - Affects every instances
+
+## OpenAPI
+tRPC doesn't offers OpenAPI first party, and relying on third-party library like `trpc-to-openapi` which is not a streamlined solution.
+
+While Elysia has built-in support for OpenAPI using [@elysiajs/openapi](/plugins/openapi) from a single line of code.
+
+
+
+
+
+::: code-group
+
+```ts [tRPC]
+import { initTRPC } from '@trpc/server'
+import { createHTTPServer } from '@trpc/server/adapters/standalone'
+
+import { OpenApiMeta } from 'trpc-to-openapi';
+
+const t = initTRPC.meta().create()
+
+const appRouter = t.router({
+ user: t.procedure
+ .meta({
+ openapi: {
+ method: 'post',
+ path: '/users',
+ tags: ['User'],
+ summary: 'Create user',
+ }
+ })
+ .input(
+ t.array(
+ t.object({
+ name: t.string(),
+ age: t.number()
+ })
+ )
+ )
+ .output(
+ t.array(
+ t.object({
+ name: t.string(),
+ age: t.number()
+ })
+ )
+ )
+ .mutation(({ input }) => input)
+})
+
+export const openApiDocument = generateOpenApiDocument(appRouter, {
+ title: 'tRPC OpenAPI',
+ version: '1.0.0',
+ baseUrl: 'http://localhost:3000'
+})
+```
+
+:::
+
+
+
+
+> tRPC rely on third-party library to generate OpenAPI spec
+
+
+
+
+
+::: code-group
+
+```ts twoslash [Elysia]
+import { Elysia, t } from 'elysia'
+import { openapi } from '@elysiajs/openapi' // [!code ++]
+
+const app = new Elysia()
+ .use(openapi()) // [!code ++]
+ .model({
+ user: t.Array(
+ t.Object({
+ name: t.String(),
+ age: t.Number()
+ })
+ )
+ })
+ .post('/users', ({ body }) => body, {
+ // ^?
+ body: 'user',
+ response: {
+ 201: 'user'
+ },
+ detail: {
+ summary: 'Create user'
+ }
+ })
+
+```
+
+:::
+
+
+
+
+> Elysia seamlessly integrate the specification into the schema
+
+
+
+
+
+tRPC rely on third-party library to generate OpenAPI spec, and **MUST** require you to define a correct path name and HTTP method in the metadata which is force you to be **consistently aware** of how you place a router, and procedure.
+
+While Elysia use schema you provide to generate the OpenAPI specification, and validate the request/response, and infer type automatically all from a **single source of truth**.
+
+Elysia also appends the schema registered in `model` to the OpenAPI spec, allowing you to reference the model in a dedicated section in Swagger or Scalar UI while this is missing on tRPC inline the schema to the route.
+
+## Testing
+
+Elysia use Web Standard API to handle request and response while tRPC require a lot of ceremony to run the request using `createCallerFactory`.
+
+
+
+
+
+::: code-group
+
+```ts [tRPC]
+import { describe, it, expect } from 'vitest'
+
+import { initTRPC } from '@trpc/server'
+import { z } from 'zod'
+
+const t = initTRPC.create()
+
+const publicProcedure = t.procedure
+const { createCallerFactory, router } = t
+
+const appRouter = router({
+ post: router({
+ add: publicProcedure
+ .input(
+ z.object({
+ title: z.string().min(2)
+ })
+ )
+ .mutation(({ input }) => input)
+ })
+})
+
+const createCaller = createCallerFactory(appRouter)
+
+const caller = createCaller({})
+
+describe('GET /', () => {
+ it('should return Hello World', async () => {
+ const newPost = await caller.post.add({
+ title: '74 Itoki Hana'
+ })
+
+ expect(newPost).toEqual({
+ title: '74 Itoki Hana'
+ })
+ })
+})
+```
+
+:::
+
+
+
+
+> tRPC require `createCallerFactory`, and a lot of ceremony to run the request
+
+
+
+
+
+::: code-group
+
+```ts [Elysia]
+import { Elysia, t } from 'elysia'
+import { describe, it, expect } from 'vitest'
+
+const app = new Elysia()
+ .post('/add', ({ body }) => body, {
+ body: t.Object({
+ title: t.String({ minLength: 2 })
+ })
+ })
+
+describe('GET /', () => {
+ it('should return Hello World', async () => {
+ const res = await app.handle(
+ new Request('http://localhost/add', {
+ method: 'POST',
+ body: JSON.stringify({ title: '74 Itoki Hana' }),
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ })
+ )
+
+ expect(res.status).toBe(200)
+ expect(await res.res()).toEqual({
+ title: '74 Itoki Hana'
+ })
+ })
+})
+```
+
+:::
+
+
+
+
+> Elysia use Web Standard API to handle request and response
+
+
+
+
+
+Alternatively, Elysia also offers a helper library called [Eden](/eden/overview) for End-to-end type safety which is similar to `tRPC.createCallerFactory`, allowing us to test with auto-completion, and full type safety like tRPC without the ceremony.
+
+```ts twoslash [Elysia]
+import { Elysia } from 'elysia'
+import { treaty } from '@elysiajs/eden'
+import { describe, expect, it } from 'bun:test'
+
+const app = new Elysia().get('/hello', 'Hello World')
+const api = treaty(app)
+
+describe('GET /', () => {
+ it('should return Hello World', async () => {
+ const { data, error, status } = await api.hello.get()
+
+ expect(status).toBe(200)
+ expect(data).toBe('Hello World')
+ // ^?
+ })
+})
+```
+
+## End-to-end type safety
+Both offers end-to-end type safety for client-server communication.
+
+
+
+
+
+::: code-group
+
+```ts twoslash [tRPC]
+import { initTRPC } from '@trpc/server'
+import { createHTTPServer } from '@trpc/server/adapters/standalone'
+import { z } from 'zod'
+
+import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'
+
+const t = initTRPC.create()
+
+const appRouter = t.router({
+ mirror: t.procedure
+ .input(
+ z.object({
+ message: z.string()
+ })
+ )
+ .output(
+ z.object({
+ message: z.string()
+ })
+ )
+ .mutation(({ input }) => input)
+})
+
+const server = createHTTPServer({
+ router: appRouter
+})
+
+server.listen(3000)
+
+const client = createTRPCProxyClient({
+ links: [
+ httpBatchLink({
+ url: 'http://localhost:3000'
+ })
+ ]
+})
+
+const { message } = await client.mirror.mutate({
+ message: 'Hello World'
+})
+
+message
+// ^?
+
+
+
+
+// ---cut-after---
+console.log('ok')
+```
+
+:::
+
+
+
+
+> tRPC use `createTRPCProxyClient` to create a client with end-to-end type safety
+
+
+
+
+
+::: code-group
+
+```ts twoslash [Elysia]
+import { Elysia, t } from 'elysia'
+import { treaty } from '@elysiajs/eden'
+
+const app = new Elysia()
+ .post('/mirror', ({ body }) => body, {
+ body: t.Object({
+ message: t.String()
+ })
+ })
+
+const api = treaty(app)
+
+const { data, error } = await api.mirror.post({
+ message: 'Hello World'
+})
+
+if(error)
+ throw error
+ // ^?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+console.log(data)
+// ^?
+
+
+
+// ---cut-after---
+console.log('ok')
+```
+
+:::
+
+
+
+
+> Elysia use `treaty` to run the request, and offers end-to-end type safety
+
+
+
+
+
+While both offers end-to-end type safety, tRPC only handle **happy path** where the request is successful, and doesn't have a type soundness of error handling, making it unsound.
+
+If type soundness is important for you, then Elysia is the right choice.
+
+---
+
+While tRPC is a great framework for building type-safe APIs, it has its limitations in terms of RESTful compliance, and type soundness.
+
+Elysia is designed to be ergonomic and developer-friendly with a focus on developer experience, and **type soundness** complying with RESTful, OpenAPI, and WinterTC Standard making it a better fit for building a universal API.
+
+Alternatively, if you are coming from a different framework, you can check out:
+
+
+
+ Comparison between tRPC and Elysia
+
+
+ Comparison between Fastify and Elysia
+
+
+ Comparison between tRPC and Elysia
+
+
diff --git a/docs/migrate/index.md b/docs/migrate/index.md
new file mode 100644
index 00000000..bdede92c
--- /dev/null
+++ b/docs/migrate/index.md
@@ -0,0 +1,31 @@
+
+
+
+
+ The core concept of Elysia and how to use it.
+
+
+
+# Comparison with Other Frameworks
+
+Elysia is designed to be intuitive and easy to use, especially for those familiar with other web frameworks.
+
+If you have used other popular frameworks like Express, Fastify, or Hono, you will find Elysia right at home with just a few differences.
+
+
+
+ Comparison between tRPC and Elysia
+
+
+ Comparison between Fastify and Elysia
+
+
+ Comparison between tRPC and Elysia
+
+
+ Comparison between tRPC and Elysia
+
+
diff --git a/docs/patterns/configuration.md b/docs/patterns/configuration.md
index c719bfe3..d83856f1 100644
--- a/docs/patterns/configuration.md
+++ b/docs/patterns/configuration.md
@@ -162,7 +162,7 @@ new Elysia({
})
```
-When unknown properties that is not specified in schema is found on either input and output, how should Elysia handle the field?
+When unknown properties that are not specified in schema are found on either input and output, how should Elysia handle the field?
Options - @default `true`
@@ -176,7 +176,7 @@ Options - @default `true`
###### Since 1.0.0
-Whether should Elysia should [precompile all routes](/blog/elysia-10.html#improved-startup-time) a head of time before starting the server.
+Whether Elysia should [precompile all routes](/blog/elysia-10.html#improved-startup-time) ahead of time before starting the server.
```ts twoslash
import { Elysia } from 'elysia'
@@ -216,7 +216,7 @@ import { Elysia, t } from 'elysia'
new Elysia({ prefix: '/v1' }).get('/name', 'elysia') // Path is /v1/name
```
-## santize
+## sanitize
A function or an array of function that calls and intercepts on every `t.String` while validation.
@@ -226,7 +226,7 @@ Allowing us to read and transform a string into a new value.
import { Elysia, t } from 'elysia'
new Elysia({
- santize: (value) => Bun.escapeHTML(value)
+ sanitize: (value) => Bun.escapeHTML(value)
})
```
@@ -340,6 +340,28 @@ new Elysia({
})
```
+### Example: Increase timeout
+
+We can increase the idle timeout by setting [`serve.idleTimeout`](#serve-idletimeout) in the `serve` configuration.
+
+```ts
+import { Elysia } from 'elysia'
+
+new Elysia({
+ serve: {
+ // Increase idle timeout to 30 seconds
+ idleTimeout: 30
+ }
+})
+```
+
+By default the idle timeout is 10 seconds (on Bun).
+
+---
+
+## serve
+HTTP server configuration.
+
Elysia extends Bun configuration which supports TLS out of the box, powered by BoringSSL.
See [serve.tls](#serve-tls) for available configuration.
@@ -354,6 +376,11 @@ Uniquely identify a server instance with an ID
This string will be used to hot reload the server without interrupting pending requests or websockets. If not provided, a value will be generated. To disable hot reloading, set this value to `null`.
+### serve.idleTimeout
+@default `10` (10 seconds)
+
+By default, Bun set idle timeout to 10 seconds, which means that if a request is not completed within 10 seconds, it will be aborted.
+
### serve.maxRequestBodySize
@default `1024 * 1024 * 128` (128MB)
diff --git a/docs/patterns/cookie.md b/docs/patterns/cookie.md
index 9470c92a..c03e3024 100644
--- a/docs/patterns/cookie.md
+++ b/docs/patterns/cookie.md
@@ -7,15 +7,15 @@ head:
- - 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.
+ content: Reactive Cookie takes a more modern approach like signals to handle cookies with an ergonomic API. There's no 'getCookie', 'setCookie', everything is just a cookie object. When you want to use cookies, 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.
+ content: Reactive Cookie takes a more modern approach like signals to handle cookies with an ergonomic API. There's no 'getCookie', 'setCookie', everything is just a cookie object. When you want to use cookies, 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.
+Elysia provides a mutable signal for interacting with Cookie.
There's no get/set, you can extract the cookie name and retrieve or update its value directly.
```ts
@@ -31,10 +31,10 @@ new Elysia()
})
```
-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**.
+By default, Reactive Cookie can encode/decode object types automatically allowing us to treat cookies as objects 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.
+The Elysia cookie is reactive. This means that when you change the cookie value, the cookie will be updated automatically based on an approach like signals.
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.
diff --git a/docs/patterns/deploy.md b/docs/patterns/deploy.md
index 92db6f11..371eda82 100644
--- a/docs/patterns/deploy.md
+++ b/docs/patterns/deploy.md
@@ -17,33 +17,70 @@ head:
# Deploy to production
This page is a guide on how to deploy Elysia to production.
+## Cluster mode
+Elysia is a single-threaded by default. To take advantage of multi-core CPU, we can run Elysia in cluster mode.
+
+Let's create a **index.ts** file that import our main server from **server.ts** and fork multiple workers based on the number of CPU cores available.
+
+::: code-group
+
+```ts [src/index.ts]
+import cluster from 'node:cluster'
+import os from 'node:os'
+import process from 'node:process'
+
+if (cluster.isPrimary) {
+ for (let i = 0; i < os.availableParallelism(); i++)
+ cluster.fork()
+} else {
+ await import('./server')
+ console.log(`Worker ${process.pid} started`)
+}
+```
+
+```ts [src/server.ts]
+import { Elysia } from 'elysia'
+
+new Elysia()
+ .get('/', () => 'Hello World!')
+ .listen(3000)
+```
+
+:::
+
+This will make sure that Elysia is running on multiple CPU cores.
+
+::: tip
+Elysia on Bun use SO_REUSEPORT by default, which allows multiple instances to listen on the same port. This only works on Linux.
+:::
+
## Compile to binary
-We recommended running a build command before deploying to production as it could potentially reduce memory usage and file size significantly.
+We recommend running a build command before deploying to production as it could potentially reduce memory usage and file size significantly.
-We recommended compile Elysia into a single binary using the command as follows:
+We recommend compiling Elysia into a single binary using the command as follows:
```bash
bun build \
--compile \
--minify-whitespace \
--minify-syntax \
- --target bun \
+ --target bun
--outfile server \
- ./src/index.ts
+ src/index.ts
```
This will generate a portable binary `server` which we can run to start our server.
-Compiling server to binary usually significantly reduce memory usage by 2-3x compared to development environment.
+Compiling server to binary usually significantly reduces memory usage by 2-3x compared to development environment.
This command is a bit long, so let's break it down:
-1. `--compile` - Compile TypeScript to binary
-2. `--minify-whitespace` - Remove unnecessary whitespace
-3. `--minify-syntax` - Minify JavaScript syntax to reduce file size
-4. `--target bun` - Target the `bun` platform, this can optimize the binary for the target platform
-5. `--outfile server` - Output the binary as `server`
-6. `./src/index.ts` - The entry file of our server (codebase)
-
-To start our server, simly run the binary.
+1. **--compile** Compile TypeScript to binary
+2. **--minify-whitespace** Remove unnecessary whitespace
+3. **--minify-syntax** Minify JavaScript syntax to reduce file size
+4. **--target bun** Optimize the binary for Bun runtime
+5. **--outfile server** Output the binary as `server`
+6. **src/index.ts** The entry file of our server (codebase)
+
+To start our server, simply run the binary.
```bash
./server
```
@@ -52,25 +89,49 @@ Once binary is compiled, you don't need `Bun` installed on the machine to run th
This is great as the deployment server doesn't need to install an extra runtime to run making binary portable.
+### Target
+You can also add a `--target` flag to optimize the binary for the target platform.
+
+```bash
+bun build \
+ --compile \
+ --minify-whitespace \
+ --minify-syntax \
+ --target bun-linux-x64 \
+ --outfile server \
+ src/index.ts
+```
+
+Here's a list of available targets:
+| Target | Operating System | Architecture | Modern | Baseline | Libc |
+|--------------------------|------------------|--------------|--------|----------|-------|
+| bun-linux-x64 | Linux | x64 | ✅ | ✅ | glibc |
+| bun-linux-arm64 | Linux | arm64 | ✅ | N/A | glibc |
+| bun-windows-x64 | Windows | x64 | ✅ | ✅ | - |
+| bun-windows-arm64 | Windows | arm64 | ❌ | ❌ | - |
+| bun-darwin-x64 | macOS | x64 | ✅ | ✅ | - |
+| bun-darwin-arm64 | macOS | arm64 | ✅ | N/A | - |
+| bun-linux-x64-musl | Linux | x64 | ✅ | ✅ | musl |
+| bun-linux-arm64-musl | Linux | arm64 | ✅ | N/A | musl |
+
### Why not --minify
Bun does have `--minify` flag that will minify the binary.
However if we are using [OpenTelemetry](/plugins/opentelemetry), it's going to reduce a function name to a single character.
-This make tracing harder than it should as OpenTelemetry rely on a function name.
+This makes tracing harder than it should as OpenTelemetry relies on a function name.
However, if you're not using OpenTelemetry, you may opt in for `--minify` instead
```bash
bun build \
--compile \
--minify \
- --target bun \
--outfile server \
- ./src/index.ts
+ src/index.ts
```
### Permission
-Some Linux distro might not be able to run the binary, we suggest enable executable permission to a binary if you're on Linux:
+Some Linux distros might not be able to run the binary, we suggest enabling executable permission to a binary if you're on Linux:
```bash
chmod +x ./server
@@ -82,7 +143,7 @@ If you're trying to deploy a binary to your server but unable to run with random
It means that the machine you're running on **doesn't support AVX2**.
-Unfortunately, Bun require machine that has an `AVX2` hardware support.
+Unfortunately, Bun requires a machine that has `AVX2` hardware support.
There's no workaround as far as we know.
@@ -93,12 +154,10 @@ You may bundle your server to a JavaScript file instead.
```bash
bun build \
- --compile \ // [!code --]
--minify-whitespace \
--minify-syntax \
- --target bun \
--outfile ./dist/index.js \
- ./src/index.ts
+ src/index.ts
```
This will generate a single portable JavaScript file that you can deploy on your server.
@@ -129,9 +188,8 @@ RUN bun build \
--compile \
--minify-whitespace \
--minify-syntax \
- --target bun \
--outfile server \
- ./src/index.ts
+ src/index.ts
FROM gcr.io/distroless/base
@@ -224,9 +282,8 @@ RUN bun build \
--compile \
--minify-whitespace \
--minify-syntax \
- --target bun \
--outfile server \
- ./src/index.ts
+ src/index.ts
FROM gcr.io/distroless/base
@@ -244,9 +301,9 @@ EXPOSE 3000
## Railway
[Railway](https://railway.app) is one of the popular deployment platform.
-Railway assign **random port** to expose for each deployment that can be access via `PORT` environment variable.
+Railway assigns a **random port** to expose for each deployment, which can be accessed via the `PORT` environment variable.
-We need to modify our Elysia server to accept `PORT` environment to comply with Railway port.
+We need to modify our Elysia server to accept the `PORT` environment variable to comply with Railway port.
Instead of a fixed port, we may use `process.env.PORT` and provide a fallback on development instead.
```ts
diff --git a/docs/patterns/error-handling.md b/docs/patterns/error-handling.md
new file mode 100644
index 00000000..870de687
--- /dev/null
+++ b/docs/patterns/error-handling.md
@@ -0,0 +1,310 @@
+---
+title: Error Handling - ElysiaJS
+head:
+ - - meta
+ - property: 'og:title'
+ content: Error Handling - ElysiaJS
+
+ - - meta
+ - name: 'description'
+ content: 'Learn how to handle errors in ElysiaJS applications effectively. This guide covers best practices for error handling, including custom error classes and middleware integration.'
+
+ - - meta
+ - property: 'og:description'
+ content: 'Learn how to handle errors in ElysiaJS applications effectively. This guide covers best practices for error handling, including custom error classes and middleware integration.'
+---
+
+
+
+# Error Handling
+
+This page provide a more advance guide for effectively handling errors with Elysia.
+
+If you haven't read **"Life Cycle (onError)"** yet, we recommend you to read it first.
+
+
+
+ Life cycle for handling errors in Elysia.
+
+
+
+## Custom Validation Message
+
+When defining a schema, you can provide a custom validation message for each field.
+
+This message will be returned as-is when the validation fails.
+
+```ts
+import { Elysia } from 'elysia'
+
+new Elysia().get('/:id', ({ params: { id } }) => id, {
+ params: t.Object({
+ id: t.Number({
+ error: 'id must be a number' // [!code ++]
+ })
+ })
+})
+```
+
+If the validation fails on the `id` field, the response will be return as `id must be a number`.
+
+
+
+### Validation Detail
+
+Returning as value from `schema.error` will return the validation as-is, but sometimes you may also want to return the validation details, such as the field name and the expected type
+
+You can do this by using the `validationDetail` option.
+
+```ts
+import { Elysia, validationDetail } from 'elysia' // [!code ++]
+
+new Elysia().get('/:id', ({ params: { id } }) => id, {
+ params: t.Object({
+ id: t.Number({
+ error: validationDetail('id must be a number') // [!code ++]
+ })
+ })
+})
+```
+
+This will include all of the validation details in the response, such as the field name and the expected type.
+
+
+
+But if you're planned to use `validationDetail` in every field, adding it manually can be annoying.
+
+You can automatically add validation detail by handling it in `onError` hook.
+
+```ts
+new Elysia()
+ .onError(({ error, code }) => {
+ if (code === 'VALIDATION') return error.detail(error.message) // [!code ++]
+ })
+ .get('/:id', ({ params: { id } }) => id, {
+ params: t.Object({
+ id: t.Number({
+ error: 'id must be a number'
+ })
+ })
+ })
+ .listen(3000)
+```
+
+This will apply every validation error with a custom message with custom validation message.
+
+## Validation Detail on production
+
+By default, Elysia will omitted all validation detail if `NODE_ENV` is `production`.
+
+This is done to prevent leaking sensitive information about the validation schema, such as field names and expected types, which could be exploited by an attacker.
+
+Elysia will only return that validation failed without any details.
+
+```json
+{
+ "type": "validation",
+ "on": "body",
+ "found": {},
+ // Only shown for custom error
+ "message": "x must be a number"
+}
+```
+
+The `message` property is optional and is omitted by default unless you provide a custom error message in the schema.
+
+## Custom Error
+
+Elysia supports custom error both in the type-level and implementation level.
+
+By default, Elysia have a set of built-in error types like `VALIDATION`, `NOT_FOUND` which will narrow down the type automatically.
+
+If Elysia doesn't know the error, the error code will be `UNKNOWN` with default status of `500`
+
+But you can also add a custom error with type safety with `Elysia.error` which will help narrow down the error type for full type safety with auto-complete, and custom status code as follows:
+
+```typescript twoslash
+import { Elysia } from 'elysia'
+
+class MyError extends Error {
+ constructor(public message: string) {
+ super(message)
+ }
+}
+
+new Elysia()
+ .error({
+ MyError
+ })
+ .onError(({ code, error }) => {
+ switch (code) {
+ // With auto-completion
+ case 'MyError':
+ // With type narrowing
+ // Hover to see error is typed as `CustomError`
+ return error
+ }
+ })
+ .get('/:id', () => {
+ throw new MyError('Hello Error')
+ })
+```
+
+### Custom Status Code
+
+You can also provide a custom status code for your custom error by adding `status` property in your custom error class.
+
+```typescript
+import { Elysia } from 'elysia'
+
+class MyError extends Error {
+ status = 418
+
+ constructor(public message: string) {
+ super(message)
+ }
+}
+```
+
+Elysia will then use this status code when the error is thrown.
+
+Otherwise you can also set the status code manually in the `onError` hook.
+
+```typescript
+import { Elysia } from 'elysia'
+
+class MyError extends Error {
+ constructor(public message: string) {
+ super(message)
+ }
+}
+
+new Elysia()
+ .error({
+ MyError
+ })
+ .onError(({ code, error, status }) => {
+ switch (code) {
+ case 'MyError':
+ return status(418, error.message)
+ }
+ })
+ .get('/:id', () => {
+ throw new MyError('Hello Error')
+ })
+```
+
+### Custom Error Response
+You can also provide a custom `toResponse` method in your custom error class to return a custom response when the error is thrown.
+
+```typescript
+import { Elysia } from 'elysia'
+
+class MyError extends Error {
+ status = 418
+
+ constructor(public message: string) {
+ super(message)
+ }
+
+ toResponse() {
+ return Response.json({
+ error: this.message,
+ code: this.status
+ }, {
+ status: 418
+ })
+ }
+}
+```
+
+## To Throw or Return
+
+Most of an error handling in Elysia can be done by throwing an error and will be handle in `onError`.
+
+But for `status` it can be a little bit confusing, since it can be used both as a return value or throw an error.
+
+It could either be **return** or **throw** based on your specific needs.
+
+- If an `status` is **throw**, it will be caught by `onError` middleware.
+- If an `status` is **return**, it will be **NOT** caught by `onError` middleware.
+
+See the following code:
+
+```typescript
+import { Elysia, file } from 'elysia'
+
+new Elysia()
+ .onError(({ code, error, path }) => {
+ if (code === 418) return 'caught'
+ })
+ .get('/throw', ({ status }) => {
+ // This will be caught by onError
+ throw status(418)
+ })
+ .get('/return', ({ status }) => {
+ // This will NOT be caught by onError
+ return status(418)
+ })
+```
+
+
diff --git a/docs/patterns/macro.md b/docs/patterns/macro.md
index ebb66f27..18a47080 100644
--- a/docs/patterns/macro.md
+++ b/docs/patterns/macro.md
@@ -20,179 +20,62 @@ head:
import Tab from '../components/fern/tab.vue'
-Macro allows us to define a custom field to the hook.
+Macro is similar to a function that have a control over the lifecycle event, schema, context with full type safety.
-
-
-
-
-Macro v1 uses functional callback with event listener function.
-
-**Elysia.macro** allows us to compose custom heavy logic into a simple configuration available in hook, and **guard** with full type safety.
+Once defined, it will be available in hook and can be activated by adding the property.
```typescript twoslash
import { Elysia } from 'elysia'
const plugin = new Elysia({ name: 'plugin' })
- .macro(({ onBeforeHandle }) => ({
- hi(word: string) {
- onBeforeHandle(() => {
+ .macro({
+ hi: (word: string) => ({
+ beforeHandle() {
console.log(word)
- })
- }
- }))
+ }
+ })
+ })
const app = new Elysia()
.use(plugin)
.get('/', () => 'hi', {
- hi: 'Elysia'
+ hi: 'Elysia' // [!code ++]
})
```
-Accessing the path should log **"Elysia"** as the result.
-
-### API
-
-**macro** should return an object, each key is reflected to the hook, and the provided value inside the hook will be sent back as the first parameter.
-
-In previous example, we create **hi** accepting a **string**.
-
-We then assigned **hi** to **"Elysia"**, the value was then sent back to the **hi** function, and then the function added a new event to **beforeHandle** stack.
+Accessing the path should log **"Elysia"** as the results.
-Which is an equivalent of pushing function to **beforeHandle** as the following:
+## Property shorthand
+Starting from Elysia 1.2.10, each property in the macro object can be a function or an object.
+If the property is an object, it will be translated to a function that accept a boolean parameter, and will be executed if the parameter is true.
```typescript
import { Elysia } from 'elysia'
-const app = new Elysia()
- .get('/', () => 'hi', {
- beforeHandle() {
- console.log('Elysia')
- }
- })
-```
-
-**macro** shine when a logic is more complex than accepting a new function, for example creating an authorization layer for each route.
-
-```typescript twoslash
-// @filename: auth.ts
-import { Elysia } from 'elysia'
-
export const auth = new Elysia()
- .macro(() => {
- return {
- isAuth(isAuth: boolean) {},
- role(role: 'user' | 'admin') {},
- }
- })
-
-// @filename: index.ts
-// ---cut---
-import { Elysia } from 'elysia'
-import { auth } from './auth'
-
-const app = new Elysia()
- .use(auth)
- .get('/', () => 'hi', {
- isAuth: true,
- role: 'admin'
- })
-```
-
-The field can accept anything ranging from string to function, allowing us to create a custom life cycle event.
-
-**macro** will be executed in order from top-to-bottom according to definition in hook, ensure that the stack should be handle in correct order.
-
-### Parameters
-
-**Elysia.macro** parameters to interact with the life cycle event as the following:
-
-- onParse
-- onTransform
-- onBeforeHandle
-- onAfterHandle
-- onError
-- onResponse
-- events - Life cycle store
- - global: Life cycle of a global stack
- - local: Life cycle of an inline hook (route)
-
-Parameters start with **on** is a function to appends function into a life cycle stack.
-
-While **events** is an actual stack that stores an order of the life-cycle event. You may mutate the stack directly or using the helper function provided by Elysia.
-
-### Options
-
-The life cycle function of an extension API accepts additional **options** to ensure control over life cycle events.
-
-- **options** (optional) - determine which stack
-- **function** - function to execute on the event
-
-```typescript
-import { Elysia } from 'elysia'
-
-const plugin = new Elysia({ name: 'plugin' })
- .macro(({ onBeforeHandle }) => {
- return {
- hi(word: string) {
- onBeforeHandle(
- { insert: 'before' }, // [!code ++]
- () => {
- console.log(word)
- }
- )
- }
- }
- })
-```
-
-**Options** may accept the following parameter:
-
-- **insert**
- - Where should the function be added
- - value: **'before' | 'after'**
- - @default: **'after'**
-- **stack**
- - Determine which type of stack should be added
- - value: **'global' | 'local'**
- - @default: **'local'**
-
-
-
-
-
-Macro v2 use an object syntax with return lifecycle like inline hook.
-
-**Elysia.macro** allows us to compose custom heavy logic into a simple configuration available in hook, and **guard** with full type safety.
-
-```typescript twoslash
-import { Elysia } from 'elysia'
-
-const plugin = new Elysia({ name: 'plugin' })
.macro({
- hi(word: string) {
- return {
- beforeHandle() {
- console.log(word)
- }
- }
- }
- })
+ // This property shorthand
+ isAuth: {
+ resolve: () => ({
+ user: 'saltyaom'
+ })
+ },
+ // is equivalent to
+ isAuth(enabled: boolean) {
+ if(!enabled) return
-const app = new Elysia()
- .use(plugin)
- .get('/', () => 'hi', {
- hi: 'Elysia'
+ return {
+ resolve() {
+ return {
+ user
+ }
+ }
+ }
+ }
})
```
-Accessing the path should log **"Elysia"** as the results.
-
-### API
+## API
**macro** has the same API as hook.
@@ -247,7 +130,7 @@ const app = new Elysia()
})
```
-Macro v2 can also register a new property to the context, allowing us to access the value directly from the context.
+Macro can also register a new property to the context, allowing us to access the value directly from the context.
The field can accept anything ranging from string to function, allowing us to create a custom life cycle event.
@@ -281,38 +164,138 @@ Here's an example that macro resolve could be useful:
- run an additional database query and add data to the context
- add a new property to the context
-## Property shorthand
-Starting from Elysia 1.2.10, each property in the macro object can be a function or an object.
-If the property is an object, it will be translated to a function that accept a boolean parameter, and will be executed if the parameter is true.
-```typescript
-import { Elysia } from 'elysia'
+### Macro extension with resolve
+Due to TypeScript limitation, macro that extends other macro cannot infer type into **resolve** function.
-export const auth = new Elysia()
- .macro({
- // This property shorthand
- isAuth: {
- resolve() {
- return {
- user: 'saltyaom'
- }
- }
- },
- // is equivalent to
- isAuth(enabled: boolean) {
- if(!enabled) return
+We provide a named single macro as a workaround to this limitation.
- return {
- resolve() {
- return {
- user
- }
- }
- }
- }
- })
+```typescript twoslash
+import { Elysia, t } from 'elysia'
+new Elysia()
+ .macro('user', {
+ resolve: () => ({
+ user: 'lilith' as const
+ })
+ })
+ .macro('user2', {
+ user: true,
+ resolve: ({ user }) => {
+ // ^?
+ }
+ })
```
-
+## Schema
+You can define a custom schema for your macro, to make sure that the route using the macro is passing the correct type.
+
+```typescript twoslash
+import { Elysia, t } from 'elysia'
+
+new Elysia()
+ .macro({
+ withFriends: {
+ body: t.Object({
+ friends: t.Tuple([t.Literal('Fouco'), t.Literal('Sartre')])
+ })
+ }
+ })
+ .post('/', ({ body }) => body.friends, {
+// ^?
+
+ body: t.Object({
+ name: t.Literal('Lilith')
+ }),
+ withFriends: true
+ })
+```
+
+Macro with schema will automatically validate and infer type to ensure type safety, and it can co-exist with existing schema as well.
+
+You can also stack multiple schema from different macro, or even from Standard Validator and it will work together seamlessly.
+
+### Schema with lifecycle in the same macro
+Similar to [Macro extension with resolve](#macro-extension-with-resolve),
+
+Macro schema also support type inference for **lifecycle within the same macro** **BUT** only with named single macro due to TypeScript limitation.
+
+```typescript twoslash
+import { Elysia, t } from 'elysia'
+
+new Elysia()
+ .macro('withFriends', {
+ body: t.Object({
+ friends: t.Tuple([t.Literal('Fouco'), t.Literal('Sartre')])
+ }),
+ beforeHandle({ body: { friends } }) {
+// ^?
+ }
+ })
+```
+
+If you want to use lifecycle type inference within the same macro, you might want to use a named single macro instead of multiple stacked macro
+
+> Not to confused with using macro schema to infer type into route's lifecycle event. That works just fine this limitation only apply to using lifecycle within the same macro.
+
+## Extension
+Macro can extends other macro, allowing you to build upon existing one.
+
+```typescript twoslash
+import { Elysia, t } from 'elysia'
+
+new Elysia()
+ .macro({
+ sartre: {
+ body: t.Object({
+ sartre: t.Literal('Sartre')
+ })
+ },
+ fouco: {
+ body: t.Object({
+ fouco: t.Literal('Fouco')
+ })
+ },
+ lilith: {
+ fouco: true,
+ sartre: true,
+ body: t.Object({
+ lilith: t.Literal('Lilith')
+ })
+ }
+ })
+ .post('/', ({ body }) => body, {
+// ^?
+ lilith: true
+ })
+
+
+
+// ---cut-after---
+//
+```
+
+This allow you to build upon existing macro, and add more functionality to it.
+
+## Deduplication
+Macro will automatically deduplicate the lifecycle event, ensuring that each lifecycle event is only executed once.
+
+By default, Elysia will use the property value as the seed, but you can override it by providing a custom seed.
+
+```typescript twoslash
+import { Elysia, t } from 'elysia'
+
+new Elysia()
+ .macro({
+ sartre: (role: string) => ({
+ seed: role, // [!code ++]
+ body: t.Object({
+ sartre: t.Literal('Sartre')
+ })
+ })
+ })
+```
+
+
+However, if you evert accidentally create a circular dependency, Elysia have a limit stack of 16 to prevent infinite loop in both runtime and type inference.
-
+If the route already has OpenAPI detail, it will merge the detail together but prefers the route detail over macro detail.
diff --git a/docs/patterns/mount.md b/docs/patterns/mount.md
index 00558bb8..3ba08346 100644
--- a/docs/patterns/mount.md
+++ b/docs/patterns/mount.md
@@ -7,51 +7,52 @@ head:
- - meta
- name: 'description'
- content: Applying WinterCG interplopable code to run with Elysia or vice-versa.
+ content: Applying WinterCG interoperable code to run with Elysia or vice-versa.
- - meta
- property: 'og:description'
- content: Applying WinterCG interplopable code to run with Elysia or vice-versa.
+ content: Applying WinterCG interoperable code to run with Elysia or vice-versa.
---
# Mount
-WinterCG is a standard for web-interoperable runtimes. Supported by Cloudflare, Deno, Vercel Edge Runtime, Netlify Function, and various others, it allows web servers to run interoperably across runtimes that use Web Standard definitions like `Fetch`, `Request`, and `Response`.
+[WinterTC](https://wintertc.org/) is a standard for building HTTP Server behind Cloudflare, Deno, Vercel, and others.
-Elysia is WinterCG compliant. We are optimized to run on Bun but also openly support other runtimes if possible.
+It allows web servers to run interoperably across runtimes by using [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request), and [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response).
-In theory, this allows any framework or code that is WinterCG compliant to be run together, allowing frameworks like Elysia, Hono, Remix, Itty Router to run together in a simple function.
+Elysia is WinterTC compliant. Optimized to run on Bun, but also support other runtimes if possible.
-Adhering to this, we implemented the same logic for Elysia by introducing `.mount` method to run with any framework or code that is WinterCG compliant.
+This allows any framework or code that is WinterCG compliant to be run together, allowing frameworks like Elysia, Hono, Remix, Itty Router to run together in a simple function.
## Mount
To use **.mount**, [simply pass a `fetch` function](https://twitter.com/saltyAom/status/1684786233594290176):
```ts
import { Elysia } from 'elysia'
+import { Hono } from 'hono'
+
+const hono = new Hono()
+ .get('/', (c) => c.text('Hello from Hono!'))
const app = new Elysia()
.get('/', () => 'Hello from Elysia')
.mount('/hono', hono.fetch)
```
-A **fetch** function is a function that accepts a Web Standard Request and returns a Web Standard Response with the definition of:
-```ts
-// Web Standard Request-like object
-// Web Standard Response
-type fetch = (request: RequestLike) => Response
-```
+Any framework that use `Request`, and `Response` can be interoperable with Elysia like
+- Hono
+- Nitro
+- H3
+- [Nextjs API Route](/integrations/nextjs)
+- [Nuxt API Route](/integrations/nuxt)
+- [SvelteKit API Route](/integrations/sveltekit)
-By default, this declaration is used by:
+And these can be use on multiple runtimes like:
- Bun
- Deno
- Vercel Edge Runtime
- Cloudflare Worker
- Netlify Edge Function
-- Remix Function Handler
-- etc.
-
-This allows you to execute all the aforementioned code in a single server environment, making it possible to interact seamlessly with Elysia. You can also reuse existing functions within a single deployment, eliminating the need for a reverse proxy to manage multiple servers.
-If the framework also supports a **.mount** function, you can deeply nest a framework that supports it.
+If the framework supports a **.mount** function, you can also mount Elysia inside another framework:
```ts
import { Elysia } from 'elysia'
import { Hono } from 'hono'
diff --git a/docs/patterns/openapi.md b/docs/patterns/openapi.md
new file mode 100644
index 00000000..839f2fe2
--- /dev/null
+++ b/docs/patterns/openapi.md
@@ -0,0 +1,523 @@
+---
+title: OpenAPI - ElysiaJS
+head:
+ - - meta
+ - property: 'og:title'
+ content: OpenAPI - ElysiaJS
+
+ - - meta
+ - name: 'description'
+ content: Elysia has first-class support and follows OpenAPI schema by default. Allowing any Elysia server to generate an API documentation page and serve as documentation automatically by using just 1 line of the Elysia OpenAPI plugin.
+
+ - - meta
+ - property: 'og:description'
+ content: Elysia has first-class support and follows OpenAPI schema by default. Allowing any Elysia server to generate an API documentation page and serve as documentation automatically by using just 1 line of the Elysia OpenAPI plugin.
+---
+
+
+
+# OpenAPI
+
+Elysia has first-class support and follows OpenAPI schema by default.
+
+Elysia can automatically generate an API documentation page by using an OpenAPI plugin.
+
+To generate the Swagger page, install the plugin:
+
+```bash
+bun add @elysiajs/openapi
+```
+
+And register the plugin to the server:
+
+```typescript
+import { Elysia } from 'elysia'
+import { openapi } from '@elysiajs/openapi' // [!code ++]
+
+new Elysia()
+ .use(openapi()) // [!code ++]
+```
+
+By default, Elysia uses OpenAPI V3 schema and [Scalar UI](http://scalar.com)
+
+For OpenAPI plugin configuration, see the [OpenAPI plugin page](/plugins/openapi).
+
+## OpenAPI from types
+
+> This is optional, but we highly recommend it for much better documentation experience.
+
+By default, Elysia relies on runtime schema to generate OpenAPI documentation.
+
+However, you can also generate OpenAPI documentation from types by using a generator from OpenAPI plugin as follows:
+
+1. Specify your Elysia root file (if not specified, Elysia will use `src/index.ts`), and export an instance
+
+2. Import a generator and provide a **file path from project root** to type generator
+```ts
+import { Elysia, t } from 'elysia'
+import { openapi, fromTypes } from '@elysiajs/openapi' // [!code ++]
+
+export const app = new Elysia() // [!code ++]
+ .use(
+ openapi({
+ references: fromTypes() // [!code ++]
+ })
+ )
+ .get('/', { test: 'hello' as const })
+ .post('/json', ({ body, status }) => body, {
+ body: t.Object({
+ hello: t.String()
+ })
+ })
+ .listen(3000)
+```
+
+Elysia will attempt to generate OpenAPI documentation by reading the type of an exported instance to generate OpenAPI documentation.
+
+This will co-exists with the runtime schema, and the runtime schema will take precedence over the type definition.
+
+### Production
+In production environment, it's likely that you might compile Elysia to a [single executable with Bun](/patterns/deploy.html) or [bundle into a single JavaScript file](https://elysiajs.com/patterns/deploy.html#compile-to-javascript).
+
+It's recommended that you should pre-generate the declaration file (**.d.ts**) to provide type declaration to the generator.
+
+```ts
+import { Elysia, t } from 'elysia'
+import { openapi, fromTypes } from '@elysiajs/openapi'
+
+const app = new Elysia()
+ .use(
+ openapi({
+ references: fromTypes(
+ process.env.NODE_ENV === 'production' // [!code ++]
+ ? 'dist/index.d.ts' // [!code ++]
+ : 'src/index.ts' // [!code ++]
+ )
+ })
+ )
+```
+
+
+
+Having issues with type generation?
+
+### Caveats: Root path
+As it's unreliable to guess to root of the project, it's recommended to provide the path to the project root to allow generator to run correctly, especially when using monorepo.
+
+```ts
+import { Elysia, t } from 'elysia'
+import { openapi, fromTypes } from '@elysiajs/openapi'
+
+export const app = new Elysia()
+ .use(
+ openapi({
+ references: fromTypes('src/index.ts', {
+ projectRoot: path.join('..', import.meta.dir) // [!code ++]
+ })
+ })
+ )
+ .get('/', { test: 'hello' as const })
+ .post('/json', ({ body, status }) => body, {
+ body: t.Object({
+ hello: t.String()
+ })
+ })
+ .listen(3000)
+```
+
+### Custom tsconfig.json
+If you have multiple `tsconfig.json` files, it's important that you must specify a correct `tsconfig.json` file to be used for type generation.
+
+```ts
+import { Elysia, t } from 'elysia'
+import { openapi, fromTypes } from '@elysiajs/openapi'
+
+export const app = new Elysia()
+ .use(
+ openapi({
+ references: fromTypes('src/index.ts', {
+ // This is reference from root of the project
+ tsconfigPath: 'tsconfig.dts.json' // [!code ++]
+ })
+ })
+ )
+ .get('/', { test: 'hello' as const })
+ .post('/json', ({ body, status }) => body, {
+ body: t.Object({
+ hello: t.String()
+ })
+ })
+ .listen(3000)
+```
+
+
+
+## Standard Schema with OpenAPI
+Elysia will try to use a native method from each schema to convert to OpenAPI schema.
+
+However, if the schema doesn't provide a native method, you can provide a custom schema to OpenAPI by providing a `mapJsonSchema` as follows:
+
+
+
+
+
+### Zod OpenAPI
+As Zod doesn't have a `toJSONSchema` method on the schema, we need to provide a custom mapper to convert Zod schema to OpenAPI schema.
+
+::: code-group
+
+```typescript [Zod 4]
+import openapi from '@elysiajs/openapi'
+import * as z from 'zod'
+
+openapi({
+ mapJsonSchema: {
+ zod: z.toJSONSchema
+ }
+})
+```
+
+```typescript [Zod 3]
+import openapi from '@elysiajs/openapi'
+import { zodToJsonSchema } from 'zod-to-json-schema'
+
+openapi({
+ mapJsonSchema: {
+ zod: zodToJsonSchema
+ }
+})
+```
+
+:::
+
+
+
+
+
+### Valibot OpenAPI
+Valibot use a separate package (`@valibot/to-json-schema`) to convert Valibot schema to JSON Schema.
+
+```typescript
+import openapi from '@elysiajs/openapi'
+import { toJsonSchema } from '@valibot/to-json-schema'
+
+openapi({
+ mapJsonSchema: {
+ valibot: toJsonSchema
+ }
+})
+```
+
+
+
+
+
+### Effect OpenAPI
+As Effect doesn't have a `toJSONSchema` method on the schema, we need to provide a custom mapper to convert Effect schema to OpenAPI schema.
+
+```typescript
+import openapi from '@elysiajs/openapi'
+import { JSONSchema } from 'effect'
+
+openapi({
+ mapJsonSchema: {
+ effect: JSONSchema.make
+ }
+})
+```
+
+
+
+
+
+## Describing route
+
+We can add route information by providing a schema type.
+
+However, sometimes defining only a type does not make it clear what the route might do. You can use [detail](/plugins/openapi#detail) fields to explicitly describe the route.
+
+```typescript
+import { Elysia, t } from 'elysia'
+import { openapi } from '@elysiajs/openapi'
+
+new Elysia()
+ .use(openapi())
+ .post(
+ '/sign-in',
+ ({ body }) => body, {
+ body: t.Object(
+ {
+ username: t.String(),
+ password: t.String({
+ minLength: 8,
+ description: 'User password (at least 8 characters)' // [!code ++]
+ })
+ },
+ { // [!code ++]
+ description: 'Expected a username and password' // [!code ++]
+ } // [!code ++]
+ ),
+ detail: { // [!code ++]
+ summary: 'Sign in the user', // [!code ++]
+ tags: ['authentication'] // [!code ++]
+ } // [!code ++]
+ })
+```
+
+The detail fields follows an OpenAPI V3 definition with auto-completion and type-safety by default.
+
+Detail is then passed to OpenAPI to put the description to OpenAPI route.
+
+## Response headers
+We can add a response headers by wrapping a schema with `withHeader`:
+
+```typescript
+import { Elysia, t } from 'elysia'
+import { openapi, withHeader } from '@elysiajs/openapi' // [!code ++]
+
+new Elysia()
+ .use(openapi())
+ .get(
+ '/thing',
+ ({ body, set }) => {
+ set.headers['x-powered-by'] = 'Elysia'
+
+ return body
+ },
+ {
+ response: withHeader( // [!code ++]
+ t.Literal('Hi'), // [!code ++]
+ { // [!code ++]
+ 'x-powered-by': t.Literal('Elysia') // [!code ++]
+ } // [!code ++]
+ ) // [!code ++]
+ }
+ )
+```
+
+Note that `withHeader` is an annotation only, and does not enforce or validate the actual response headers. You need to set the headers manually.
+
+### Hide route
+
+You can hide the route from the Swagger page by setting `detail.hide` to `true`
+
+```typescript
+import { Elysia, t } from 'elysia'
+import { openapi } from '@elysiajs/openapi'
+
+new Elysia()
+ .use(openapi())
+ .post(
+ '/sign-in',
+ ({ body }) => body,
+ {
+ body: t.Object(
+ {
+ username: t.String(),
+ password: t.String()
+ },
+ {
+ description: 'Expected a username and password'
+ }
+ ),
+ detail: { // [!code ++]
+ hide: true // [!code ++]
+ } // [!code ++]
+ }
+ )
+```
+
+## Tags
+
+Elysia can separate the endpoints into groups by using the Swaggers tag system
+
+Firstly define the available tags in the swagger config object
+
+```typescript
+new Elysia().use(
+ openapi({
+ documentation: {
+ tags: [
+ { name: 'App', description: 'General endpoints' },
+ { name: 'Auth', description: 'Authentication endpoints' }
+ ]
+ }
+ })
+)
+```
+
+Then use the details property of the endpoint configuration section to assign that endpoint to the group
+
+```typescript
+new Elysia()
+ .get('/', () => 'Hello Elysia', {
+ detail: {
+ tags: ['App']
+ }
+ })
+ .group('/auth', (app) =>
+ app.post(
+ '/sign-up',
+ ({ body }) =>
+ db.user.create({
+ data: body,
+ select: {
+ id: true,
+ username: true
+ }
+ }),
+ {
+ detail: {
+ tags: ['Auth']
+ }
+ }
+ )
+ )
+```
+
+Which will produce a swagger page like the following
+
+
+### Tags group
+
+Elysia may accept tags to add an entire instance or group of routes to a specific tag.
+
+```typescript
+import { Elysia, t } from 'elysia'
+
+new Elysia({
+ tags: ['user']
+})
+ .get('/user', 'user')
+ .get('/admin', 'admin')
+```
+
+## Models
+
+By using [reference model](/essential/validation.html#reference-model), Elysia will handle the schema generation automatically.
+
+By separating models into a dedicated section and linked by reference.
+
+```typescript
+new Elysia()
+ .model({
+ User: t.Object({
+ id: t.Number(),
+ username: t.String()
+ })
+ })
+ .get('/user', () => ({ id: 1, username: 'saltyaom' }), {
+ response: {
+ 200: 'User'
+ },
+ detail: {
+ tags: ['User']
+ }
+ })
+```
+
+## Guard
+
+Alternatively, Elysia may accept guards to add an entire instance or group of routes to a specific guard.
+
+```typescript
+import { Elysia, t } from 'elysia'
+
+new Elysia()
+ .guard({
+ detail: {
+ description: 'Require user to be logged in'
+ }
+ })
+ .get('/user', 'user')
+ .get('/admin', 'admin')
+```
+
+## Change OpenAPI Endpoint
+
+You can change the OpenAPI endpoint by setting [path](#path) in the plugin config.
+
+```typescript twoslash
+import { Elysia } from 'elysia'
+import { openapi } from '@elysiajs/openapi'
+
+new Elysia()
+ .use(
+ openapi({
+ path: '/v2/openapi'
+ })
+ )
+ .listen(3000)
+```
+
+## Customize OpenAPI info
+
+We can customize the OpenAPI information by setting [documentation.info](#documentationinfo) in the plugin config.
+
+```typescript twoslash
+import { Elysia } from 'elysia'
+import { openapi } from '@elysiajs/openapi'
+
+new Elysia()
+ .use(
+ openapi({
+ documentation: {
+ info: {
+ title: 'Elysia Documentation',
+ version: '1.0.0'
+ }
+ }
+ })
+ )
+ .listen(3000)
+```
+
+This can be useful for
+
+- adding a title
+- settings an API version
+- adding a description explaining what our API is about
+- explaining what tags are available, what each tag means
+
+## Security Configuration
+
+To secure your API endpoints, you can define security schemes in the Swagger configuration. The example below demonstrates how to use Bearer Authentication (JWT) to protect your endpoints:
+
+```typescript
+new Elysia().use(
+ openapi({
+ documentation: {
+ components: {
+ securitySchemes: {
+ bearerAuth: {
+ type: 'http',
+ scheme: 'bearer',
+ bearerFormat: 'JWT'
+ }
+ }
+ }
+ }
+ })
+)
+
+export const addressController = new Elysia({
+ prefix: '/address',
+ detail: {
+ tags: ['Address'],
+ security: [
+ {
+ bearerAuth: []
+ }
+ ]
+ }
+})
+```
+
+This will ensures that all endpoints under the `/address` prefix require a valid JWT token for access.
diff --git a/docs/patterns/trace.md b/docs/patterns/trace.md
index 164356a7..68e45e21 100644
--- a/docs/patterns/trace.md
+++ b/docs/patterns/trace.md
@@ -7,11 +7,11 @@ head:
- - 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.
+ content: Trace is an API to measure the performance of your server. It allows you to interact with the duration span of each life-cycle event 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.
+ content: Trace is an API to measure the performance of your server. It allows you to interact with the duration span of each life-cycle event and measure the performance of each function to identify performance bottlenecks of the server.
---
# Trace
@@ -20,16 +20,20 @@ Performance is an important aspect for Elysia.
We don't want to be fast for benchmarking purposes, we want you to have a real fast server in real-world scenario.
-There are many factors that can slow down our app - and it's hard to identify them, but **trace** can helps solve that problem by injecting start and stop code to each life-cycle.
+There are many factors that can slow down our app - and it's hard to identify them, but **trace** can help solve that problem by injecting start and stop code to each life-cycle.
Trace allows us to inject code to before and after of each life-cycle event, block and interact with the execution of the function.
+::: warning
+trace doesn't work with dynamic mode `aot: false`, as it requires the function to be static and known at compile time otherwise it will have a large performance impact.
+:::
+
## Trace
Trace use a callback listener to ensure that callback function is finished before moving on to the next lifecycle event.
To use `trace`, you need to call `trace` method on the Elysia instance, and pass a callback function that will be executed for each life-cycle event.
-You may listen to each lifecycle by adding `on` prefix follows by life-cycle name, for example `onHandle` to listen to `handle` event.
+You may listen to each lifecycle by adding `on` prefix followed by the lifecycle name, for example `onHandle` to listen to the `handle` event.
```ts twoslash
import { Elysia } from 'elysia'
@@ -51,7 +55,7 @@ Please refer to [Life Cycle Events](/essential/life-cycle#events) for more infor

## Children
-Every events except `handle` have a children, which is an array of events that are executed inside for each life-cycle event.
+Every event except `handle` has children, which is an array of events that are executed inside for each lifecycle event.
You can use `onEvent` to listen to each child event in order
diff --git a/docs/patterns/type.md b/docs/patterns/type.md
index b190f459..61f8a9d8 100644
--- a/docs/patterns/type.md
+++ b/docs/patterns/type.md
@@ -21,7 +21,7 @@ import Deck from '../components/nearl/card-deck.vue'
# Type
-Here's a common patterns for writing validation types in Elysia.
+Here's a common patterns for writing validation types using `Elysia.t`.
@@ -471,7 +471,7 @@ You can find all the source code for Elysia types in `elysia/type-system`.
The following are types provided by Elysia:
-
+
`UnionEnum` allows the value to be one of the specified values.
@@ -490,9 +490,21 @@ The following are types provided by Elysia:
Accepts empty string or null value
- Validate type for FormData as a return value
+ Enforce and validate type for FormData body as a return value
-
+
+ Accepts a buffer that can be parsed into a `Uint8Array`
+
+
+ Accepts a buffer that can be parsed into a `ArrayBuffer`
+
+
+ Accepts a string that can be parsed into an object
+
+
+ Accepts a string that can be parsed into an boolean
+
+
Accepts a numeric string or number and then transforms the value into a number
@@ -505,8 +517,6 @@ The following are types provided by Elysia:
t.UnionEnum(['rapi', 'anis', 1, true, false])
```
-By default, these value will not automatically
-
### File
A singular file, often useful for **file upload** validation.
@@ -615,11 +625,43 @@ t.FormData({
})
```
-### Numeric (legacy)
-::: warning
-This is not need as Elysia type already transforms Number to Numeric automatically since 1.0
-:::
+### UInt8Array
+Accepts a buffer that can be parsed into a `Uint8Array`.
+
+```typescript
+t.UInt8Array()
+```
+
+This is useful when you want to accept a buffer that can be parsed into a `Uint8Array`, such as in a binary file upload. It's designed to use for the validation of body with `arrayBuffer` parser to enforce the body type.
+
+### ArrayBuffer
+Accepts a buffer that can be parsed into a `ArrayBuffer`.
+
+```typescript
+t.ArrayBuffer()
+```
+
+This is useful when you want to accept a buffer that can be parsed into a `Uint8Array`, such as in a binary file upload. It's designed to use for the validation of body with `arrayBuffer` parser to enforce the body type.
+
+### ObjectString
+Accepts a string that can be parsed into an object.
+
+```typescript
+t.ObjectString()
+```
+
+This is useful when you want to accept a string that can be parsed into an object but the environment does not allow it explicitly, such as in a query string, header, or FormData body.
+
+### BooleanString
+Accepts a string that can be parsed into a boolean.
+
+Similar to [ObjectString](#objectstring), this is useful when you want to accept a string that can be parsed into a boolean but the environment does not allow it explicitly.
+
+```typescript
+t.BooleanString()
+```
+### Numeric
Numeric accepts a numeric string or number and then transforms the value into a number.
```typescript
@@ -628,7 +670,7 @@ t.Numeric()
This is useful when an incoming value is a numeric string, for example, a path parameter or query string.
-Numeric accepts the same attributes as [Numeric Instance](https://json-schema.org/draft/2020-12/json-schema-validation#name-validation-keywords-for-num)
+Numeric accepts the same attributes as [Numeric Instance](https://json-schema.org/draft/2020-12/json-schema-validation#name-validation-keywords-for-num).
## Elysia behavior
@@ -662,7 +704,7 @@ new Elysia()
```
## Number to Numeric
-By default, Elysia will convert a `t.Number` to [t.Numeric](#numeric-legacy) when provided as route schema.
+By default, Elysia will convert a `t.Number` to [t.Numeric](#numeric) when provided as route schema.
Because parsed HTTP headers, query, url parameter is always a string. This means that even if a value is number, it will be treated as string.
diff --git a/docs/patterns/unit-test.md b/docs/patterns/unit-test.md
index 46d0c0b3..bc5994a4 100644
--- a/docs/patterns/unit-test.md
+++ b/docs/patterns/unit-test.md
@@ -30,7 +30,7 @@ import { describe, expect, it } from 'bun:test'
import { Elysia } from 'elysia'
describe('Elysia', () => {
- it('return a response', async () => {
+ it('returns a response', async () => {
const app = new Elysia().get('/', () => 'hi')
const response = await app
@@ -74,7 +74,7 @@ const app = new Elysia().get('/hello', 'hi')
const api = treaty(app)
describe('Elysia', () => {
- it('return a response', async () => {
+ it('returns a response', async () => {
const { data, error } = await api.hello.get()
expect(data).toBe('hi')
diff --git a/docs/patterns/websocket.md b/docs/patterns/websocket.md
index 8dfc9b39..d4a0b2bb 100644
--- a/docs/patterns/websocket.md
+++ b/docs/patterns/websocket.md
@@ -18,11 +18,11 @@ head:
WebSocket is a realtime protocol for communication between your client and server.
-Unlike HTTP where our client repeatedly asking the website for information and waiting for a reply each time, WebSocket sets up a direct line where our client and server can send messages back and forth directly, making the conversation quicker and smoother without having to start over each message.
+Unlike HTTP where our client repeatedly asks the website for information and waits for a reply each time, WebSocket sets up a direct line where our client and server can send messages back and forth directly, making the conversation quicker and smoother without having to start over with each message.
SocketIO is a popular library for WebSocket, but it is not the only one. Elysia uses [uWebSocket](https://github.com/uNetworking/uWebSockets) which Bun uses under the hood with the same API.
-To use websocket, simply call `Elysia.ws()`:
+To use WebSocket, simply call `Elysia.ws()`:
```typescript
import { Elysia } from 'elysia'
@@ -38,7 +38,7 @@ new Elysia()
## WebSocket message validation:
-Same as normal route, WebSockets also accepts a **schema** object to strictly type and validate requests.
+Same as normal routes, WebSockets also accept a **schema** object to strictly type and validate requests.
```typescript
import { Elysia, t } from 'elysia'
diff --git a/docs/plugins/cron.md b/docs/plugins/cron.md
index 31bf4f3d..912daec9 100644
--- a/docs/plugins/cron.md
+++ b/docs/plugins/cron.md
@@ -16,7 +16,7 @@ head:
# Cron Plugin
-This plugin adds support for running cronjob in the Elysia server.
+This plugin adds support for running cronjobs in the Elysia server.
Install with:
diff --git a/docs/plugins/graphql-apollo.md b/docs/plugins/graphql-apollo.md
index 6b740473..a5750203 100644
--- a/docs/plugins/graphql-apollo.md
+++ b/docs/plugins/graphql-apollo.md
@@ -26,7 +26,7 @@ bun add graphql @elysiajs/apollo @apollo/server
Then use it:
-```typescript twoslash
+```typescript
import { Elysia } from 'elysia'
import { apollo, gql } from '@elysiajs/apollo'
diff --git a/docs/plugins/graphql-yoga.md b/docs/plugins/graphql-yoga.md
index 791753fa..8f615947 100644
--- a/docs/plugins/graphql-yoga.md
+++ b/docs/plugins/graphql-yoga.md
@@ -26,7 +26,7 @@ bun add @elysiajs/graphql-yoga
Then use it:
-```typescript twoslash
+```typescript
import { Elysia } from 'elysia'
import { yoga } from '@elysiajs/graphql-yoga'
diff --git a/docs/plugins/jwt.md b/docs/plugins/jwt.md
index ac435d4a..f64e07d2 100644
--- a/docs/plugins/jwt.md
+++ b/docs/plugins/jwt.md
@@ -15,7 +15,7 @@ head:
---
# JWT Plugin
-This plugin adds support for using JWT in Elysia handler
+This plugin adds support for using JWT in Elysia handlers.
Install with:
```bash
diff --git a/docs/plugins/openapi.md b/docs/plugins/openapi.md
new file mode 100644
index 00000000..deff24f1
--- /dev/null
+++ b/docs/plugins/openapi.md
@@ -0,0 +1,179 @@
+---
+title: OpenAPI Plugin - ElysiaJS
+head:
+ - - meta
+ - property: 'og:title'
+ content: OpenAPI Plugin - ElysiaJS
+
+ - - meta
+ - name: 'description'
+ content: Plugin for Elysia that adds support for generating Swagger API documentation for Elysia Server. Start by installing the plugin with "bun add @elysiajs/swagger".
+
+ - - meta
+ - name: 'og:description'
+ content: Plugin for Elysia that adds support for generating Swagger API documentation for Elysia Server. Start by installing the plugin with "bun add @elysiajs/swagger".
+---
+
+# OpenAPI Plugin
+
+Plugin for [elysia](https://github.com/elysiajs/elysia) to auto-generate API documentation page.
+
+Install with:
+
+```bash
+bun add @elysiajs/openapi
+```
+
+Then use it:
+
+```typescript twoslash
+import { Elysia } from 'elysia'
+import { openapi } from '@elysiajs/openapi'
+
+new Elysia()
+ .use(openapi())
+ .get('/', () => 'hello')
+ .post('/hello', () => 'OpenAPI')
+ .listen(3000)
+```
+
+Accessing `/openapi` would show you a Scalar UI with the generated endpoint documentation from the Elysia server. You can also access the raw OpenAPI spec at `/openapi/json`.
+
+::: tip
+This page is the plugin configuration reference.
+
+If you're looking for a common patterns or an advanced usage of OpenAPI, check out [Patterns: OpenAPI](/patterns/openapi)
+:::
+
+## Detail
+
+`detail` extends the [OpenAPI Operation Object](https://spec.openapis.org/oas/v3.0.3.html#operation-object)
+
+The detail field is an object that can be used to describe information about the route for API documentation.
+
+It may contain the following fields:
+
+## detail.hide
+
+You can hide the route from the Swagger page by setting `detail.hide` to `true`
+
+```typescript
+import { Elysia, t } from 'elysia'
+import { openapi } from '@elysiajs/openapi'
+
+new Elysia().use(openapi()).post('/sign-in', ({ body }) => body, {
+ body: t.Object(
+ {
+ username: t.String(),
+ password: t.String()
+ },
+ {
+ description: 'Expected a username and password'
+ }
+ ),
+ detail: {
+ // [!code ++]
+ hide: true // [!code ++]
+ } // [!code ++]
+})
+```
+
+### detail.deprecated
+
+Declares this operation to be deprecated. Consumers SHOULD refrain from usage of the declared operation. Default value is `false`.
+
+### detail.description
+
+A verbose explanation of the operation behavior.
+
+### detail.summary
+
+A short summary of what the operation does.
+
+## Config
+
+Below is a config which is accepted by the plugin
+
+## enabled
+
+@default true
+Enable/Disable the plugin
+
+## documentation
+
+OpenAPI documentation information
+
+@see https://spec.openapis.org/oas/v3.0.3.html
+
+## exclude
+
+Configuration to exclude paths or methods from documentation
+
+## exclude.methods
+
+List of methods to exclude from documentation
+
+## exclude.paths
+
+List of paths to exclude from documentation
+
+## exclude.staticFile
+
+@default true
+
+Exclude static file routes from documentation
+
+## exclude.tags
+
+List of tags to exclude from documentation
+
+## mapJsonSchema
+A custom mapping function from Standard schema to OpenAPI schema
+
+### Example
+```typescript
+import { openapi } from '@elysiajs/openapi'
+import { toJsonSchema } from '@valibot/to-json-schema'
+
+openapi({
+ mapJsonSchema: {
+ valibot: toJsonSchema
+ }
+})
+```
+
+## path
+
+@default '/openapi'
+
+The endpoint to expose OpenAPI documentation frontend
+
+## provider
+
+@default 'scalar'
+
+OpenAPI documentation frontend between:
+
+- [Scalar](https://github.com/scalar/scalar)
+- [SwaggerUI](https://github.com/swagger-api/swagger-ui)
+- null: disable frontend
+
+## references
+
+Additional OpenAPI reference for each endpoint
+
+## scalar
+
+Scalar configuration, refers to [Scalar config](https://github.com/scalar/scalar/blob/main/documentation/configuration.md)
+
+## specPath
+
+@default '/${path}/json'
+
+The endpoint to expose OpenAPI specification in JSON format
+
+## swagger
+
+Swagger config, refers to [Swagger config](https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/)
+
+Below you can find the common patterns to use the plugin.
diff --git a/docs/plugins/overview.md b/docs/plugins/overview.md
index 2305d572..472bcc37 100644
--- a/docs/plugins/overview.md
+++ b/docs/plugins/overview.md
@@ -3,7 +3,7 @@ title: Plugin Overview - ElysiaJS
head:
- - meta
- property: 'og:title'
- content: Swagger Plugin - ElysiaJS
+ content: Plugin Overview - ElysiaJS
- - meta
- name: 'description'
@@ -34,27 +34,26 @@ This is to ensure developers end up with a performant web server they intend to
- [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/)
+- [OpenAPI](/plugins/openapi) - generate an [OpenAPI](https://swagger.io/specification/) documentation
- [OpenTelemetry](/plugins/opentelemetry) - add support for OpenTelemetry
- [Server Timing](/plugins/server-timing) - audit performance bottlenecks with the [Server-Timing API](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing)
- [Static](/plugins/static) - serve static files/folders
- [Stream](/plugins/stream) - integrate response streaming and [server-sent events (SSEs)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
-- [Swagger](/plugins/swagger) - generate [Swagger](https://swagger.io/) documentation
-- [tRPC](/plugins/trpc) - support [tRPC](https://trpc.io/)
- [WebSocket](/patterns/websocket) - support [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
## Community plugins:
-- [BunSai](https://github.com/nikiskaarup/bunsai2) - full-stack agnostic framework for the web, built upon Bun and Elysia
-- [Create ElysiaJS](https://github.com/kravetsone/create-elysiajs) - scaffolding your Elysia project with the environment with easy (help with ORM, Linters and Plugins)!
+- [Create ElysiaJS](https://github.com/kravetsone/create-elysiajs) - scaffold your Elysia project with the environment easily (help with ORM, Linters and Plugins)!
- [Lucia Auth](https://github.com/pilcrowOnPaper/lucia) - authentication, simple and clean
- [Elysia Clerk](https://github.com/wobsoriano/elysia-clerk) - unofficial Clerk authentication plugin
- [Elysia Polyfills](https://github.com/bogeychan/elysia-polyfills) - run Elysia ecosystem on Node.js and Deno
-- [Vite server](https://github.com/kravetsone/elysia-vite-server) - plugin which start and decorate [`vite`](https://vitejs.dev/) dev server in `development` and in `production` mode serve static (if it needed)
+- [Vite server](https://github.com/kravetsone/elysia-vite-server) - plugin which starts and decorates [`vite`](https://vitejs.dev/) dev server in `development` and in `production` mode serves static files (if needed)
- [Vite](https://github.com/timnghg/elysia-vite) - serve entry HTML file with Vite's scripts injected
-- [Nuxt](https://github.com/trylovetom/elysiajs-nuxt) - easily integrate elysia with nuxt!
+- [Nuxt](https://github.com/trylovetom/elysiajs-nuxt) - easily integrate Elysia with Nuxt!
- [Remix](https://github.com/kravetsone/elysia-remix) - use [Remix](https://remix.run/) with `HMR` support (powered by [`vite`](https://vitejs.dev/))! Close a really long-standing plugin request [#12](https://github.com/elysiajs/elysia/issues/12)
- [Sync](https://github.com/johnny-woodtke/elysiajs-sync) - a lightweight offline-first data synchronization framework powered by [Dexie.js](https://dexie.org/)
- [Connect middleware](https://github.com/kravetsone/elysia-connect-middleware) - plugin which allows you to use [`express`](https://www.npmjs.com/package/express)/[`connect`](https://www.npmjs.com/package/connect) middleware directly in Elysia!
+- [Elysia HTTP Exception](https://github.com/codev911/elysia-http-exception) - Elysia plugin for HTTP 4xx/5xx error handling with structured exception classes
- [Elysia Helmet](https://github.com/DevTobias/elysia-helmet) - secure Elysia apps with various HTTP headers
- [Vite Plugin SSR](https://github.com/timnghg/elysia-vite-plugin-ssr) - Vite SSR plugin using Elysia server
- [OAuth 2.0](https://github.com/kravetsone/elysia-oauth2) - a plugin for [OAuth 2.0](https://en.wikipedia.org/wiki/OAuth) Authorization Flow with more than **42** providers and **type-safety**!
@@ -96,7 +95,7 @@ This is to ensure developers end up with a performant web server they intend to
- [Elysia AuthKit](https://github.com/gtramontina/elysia-authkit) - unnoficial [WorkOS' AuthKit](https://www.authkit.com/) authentication
- [Elysia Error Handler](https://github.com/gtramontina/elysia-error-handler) - simpler error handling
- [Elysia env](https://github.com/yolk-oss/elysia-env) - typesafe environment variables with typebox
-- [Elysia Drizzle Schema](https://github.com/Edsol/elysia-drizzle-schema) - helps to use Drizzle ORM schema inside elysia swagger model.
+- [Elysia Drizzle Schema](https://github.com/Edsol/elysia-drizzle-schema) - helps to use Drizzle ORM schema inside Elysia OpenAPI model.
- [Unify-Elysia](https://github.com/qlaffont/unify-elysia) - unify error code for Elysia
- [Unify-Elysia-GQL](https://github.com/qlaffont/unify-elysia-gql) - unify error code for Elysia GraphQL Server (Yoga & Apollo)
- [Elysia Auth Drizzle](https://github.com/qlaffont/elysia-auth-drizzle) - library who handle authentification with JWT (Header/Cookie/QueryParam).
@@ -117,6 +116,9 @@ This is to ensure developers end up with a performant web server they intend to
- [Elysia Prometheus](https://github.com/m1handr/elysia-prometheus) - Elysia plugin for exposing HTTP metrics for Prometheus.
- [Elysia Remote DTS](https://github.com/rayriffy/elysia-remote-dts) - A plugin that provide .d.ts types remotely for Eden Treaty to consume.
- [Cap Checkpoint plugin for Elysia](https://capjs.js.org/guide/middleware/elysia.html) - Cloudflare-like middleware for Cap, a lightweight, modern open-source CAPTCHA alternative designed using SHA-256 PoW.
+- [Elysia Background](https://github.com/staciax/elysia-background) - A background task processing plugin for Elysia.js
+- [@fedify/elysia](https://github.com/fedify-dev/fedify/tree/main/packages/elysia) - A plugin that provides seamless integration with [Fedify](https://fedify.dev/), the ActivityPub server framework.
+- [elysia-healthcheck](https://github.com/iam-medvedev/elysia-healthcheck) - Healthcheck plugin for Elysia.js
## Complementary projects:
diff --git a/docs/plugins/static.md b/docs/plugins/static.md
index 2077befc..4b4e9113 100644
--- a/docs/plugins/static.md
+++ b/docs/plugins/static.md
@@ -94,7 +94,7 @@ Below you can find the common patterns to use the plugin.
## Single file
Suppose you want to return just a single file, you can use `file` instead of using the static plugin
-```typescript twoslash
+```typescript
import { Elysia, file } from 'elysia'
new Elysia()
diff --git a/docs/plugins/swagger.md b/docs/plugins/swagger.md
index 0547ec90..c94973cf 100644
--- a/docs/plugins/swagger.md
+++ b/docs/plugins/swagger.md
@@ -1,5 +1,6 @@
---
title: Swagger Plugin - ElysiaJS
+search: false
head:
- - meta
- property: 'og:title'
@@ -14,6 +15,10 @@ head:
content: Plugin for Elysia that adds support for generating Swagger API documentation for Elysia Server. Start by installing the plugin with "bun add @elysiajs/swagger".
---
+::: warning
+Swagger plugin is deprecated and is no longer be maintained. Please use [OpenAPI plugin](/plugins/openapi) instead.
+:::
+
# Swagger Plugin
This plugin generates a Swagger endpoint for an Elysia server
@@ -26,7 +31,7 @@ bun add @elysiajs/swagger
Then use it:
-```typescript twoslash
+```typescript
import { Elysia } from 'elysia'
import { swagger } from '@elysiajs/swagger'
@@ -91,7 +96,7 @@ Below you can find the common patterns to use the plugin.
You can change the swagger endpoint by setting [path](#path) in the plugin config.
-```typescript twoslash
+```typescript
import { Elysia } from 'elysia'
import { swagger } from '@elysiajs/swagger'
@@ -106,7 +111,7 @@ new Elysia()
## Customize Swagger info
-```typescript twoslash
+```typescript
import { Elysia } from 'elysia'
import { swagger } from '@elysiajs/swagger'
diff --git a/docs/plugins/trpc.md b/docs/plugins/trpc.md
deleted file mode 100644
index b6194ee0..00000000
--- a/docs/plugins/trpc.md
+++ /dev/null
@@ -1,70 +0,0 @@
----
-title: tRPC Plugin - ElysiaJS
-head:
- - - meta
- - property: 'og:title'
- content: tRPC Plugin - ElysiaJS
-
- - - meta
- - name: 'description'
- content: Plugin for Elysia that adds support for using tRPC on Bun with Elysia Server. Start by installing the plugin with "bun add @elysiajs/trpc".
-
- - - meta
- - name: 'og:description'
- content: Plugin for Elysia that adds support for using tRPC on Bun with Elysia Server. Start by installing the plugin with "bun add @elysiajs/trpc".
----
-
-# tRPC Plugin
-
-This plugin adds support for using [tRPC](https://trpc.io/)
-
-Install with:
-
-```bash
-bun add @elysiajs/trpc @trpc/server @elysiajs/websocket
-```
-
-Then use it:
-
-```typescript
-import { Elysia, t as T } from 'elysia'
-
-import { initTRPC } from '@trpc/server'
-import { compile as c, trpc } from '@elysiajs/trpc'
-
-const t = initTRPC.create()
-const p = t.procedure
-
-const router = t.router({
- greet: p
- // 💡 Using Zod
- //.input(z.string())
- // 💡 Using Elysia's T
- .input(c(T.String()))
- .query(({ input }) => input)
-})
-
-export type Router = typeof router
-
-const app = new Elysia().use(trpc(router)).listen(3000)
-```
-
-## trpc
-
-Accept the tRPC router and register to Elysia's handler.
-
-````ts
-trpc(
- router: Router,
- option?: {
- endpoint?: string
- }
-): this
-```
-
-`Router` is the TRPC Router instance.
-
-### endpoint
-
-The path to the exposed TRPC endpoint.
-````
diff --git a/docs/public/assets/blocks.svg b/docs/public/assets/blocks.svg
new file mode 100644
index 00000000..a07fdbbe
--- /dev/null
+++ b/docs/public/assets/blocks.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/public/assets/cable.svg b/docs/public/assets/cable.svg
new file mode 100644
index 00000000..a6e094c2
--- /dev/null
+++ b/docs/public/assets/cable.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/public/assets/scalar-preview-dark.webp b/docs/public/assets/scalar-preview-dark.webp
deleted file mode 100644
index 399841aa..00000000
Binary files a/docs/public/assets/scalar-preview-dark.webp and /dev/null differ
diff --git a/docs/public/assets/scalar-preview.webp b/docs/public/assets/scalar-preview.webp
deleted file mode 100644
index 47f5bb99..00000000
Binary files a/docs/public/assets/scalar-preview.webp and /dev/null differ
diff --git a/docs/public/assets/scalar/scalar-body-dark.webp b/docs/public/assets/scalar/scalar-body-dark.webp
new file mode 100644
index 00000000..e8a4a197
Binary files /dev/null and b/docs/public/assets/scalar/scalar-body-dark.webp differ
diff --git a/docs/public/assets/scalar/scalar-body-light.webp b/docs/public/assets/scalar/scalar-body-light.webp
new file mode 100644
index 00000000..0a34981e
Binary files /dev/null and b/docs/public/assets/scalar/scalar-body-light.webp differ
diff --git a/docs/public/assets/scalar/scalar-editor-dark.webp b/docs/public/assets/scalar/scalar-editor-dark.webp
new file mode 100644
index 00000000..a7a5793a
Binary files /dev/null and b/docs/public/assets/scalar/scalar-editor-dark.webp differ
diff --git a/docs/public/assets/scalar/scalar-editor-light.webp b/docs/public/assets/scalar/scalar-editor-light.webp
new file mode 100644
index 00000000..2a204540
Binary files /dev/null and b/docs/public/assets/scalar/scalar-editor-light.webp differ
diff --git a/docs/public/assets/scalar/scalar-landing-dark.webp b/docs/public/assets/scalar/scalar-landing-dark.webp
new file mode 100644
index 00000000..ad2fb631
Binary files /dev/null and b/docs/public/assets/scalar/scalar-landing-dark.webp differ
diff --git a/docs/public/assets/scalar/scalar-landing-light.webp b/docs/public/assets/scalar/scalar-landing-light.webp
new file mode 100644
index 00000000..963962f4
Binary files /dev/null and b/docs/public/assets/scalar/scalar-landing-light.webp differ
diff --git a/docs/public/assets/scalar/scalar-model-dark.webp b/docs/public/assets/scalar/scalar-model-dark.webp
new file mode 100644
index 00000000..05016059
Binary files /dev/null and b/docs/public/assets/scalar/scalar-model-dark.webp differ
diff --git a/docs/public/assets/scalar/scalar-model-light.webp b/docs/public/assets/scalar/scalar-model-light.webp
new file mode 100644
index 00000000..ecb886e0
Binary files /dev/null and b/docs/public/assets/scalar/scalar-model-light.webp differ
diff --git a/docs/public/assets/scalar/scalar-search-dark.webp b/docs/public/assets/scalar/scalar-search-dark.webp
new file mode 100644
index 00000000..5275fb4f
Binary files /dev/null and b/docs/public/assets/scalar/scalar-search-dark.webp differ
diff --git a/docs/public/assets/scalar/scalar-search-light.webp b/docs/public/assets/scalar/scalar-search-light.webp
new file mode 100644
index 00000000..01b6449f
Binary files /dev/null and b/docs/public/assets/scalar/scalar-search-light.webp differ
diff --git a/docs/public/assets/scalar/scalar-status-dark.webp b/docs/public/assets/scalar/scalar-status-dark.webp
new file mode 100644
index 00000000..e991d26f
Binary files /dev/null and b/docs/public/assets/scalar/scalar-status-dark.webp differ
diff --git a/docs/public/assets/scalar/scalar-status-light.webp b/docs/public/assets/scalar/scalar-status-light.webp
new file mode 100644
index 00000000..90f5b63a
Binary files /dev/null and b/docs/public/assets/scalar/scalar-status-light.webp differ
diff --git a/docs/public/assets/search.svg b/docs/public/assets/search.svg
new file mode 100644
index 00000000..2fa54163
--- /dev/null
+++ b/docs/public/assets/search.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/public/assets/sequoia-forest.webp b/docs/public/assets/sequoia-forest.webp
new file mode 100644
index 00000000..e87aa2a7
Binary files /dev/null and b/docs/public/assets/sequoia-forest.webp differ
diff --git a/docs/public/assets/tahoe-day.webp b/docs/public/assets/tahoe-day.webp
new file mode 100644
index 00000000..597021d5
Binary files /dev/null and b/docs/public/assets/tahoe-day.webp differ
diff --git a/docs/public/assets/tahoe-dusk.webp b/docs/public/assets/tahoe-dusk.webp
new file mode 100644
index 00000000..85110b64
Binary files /dev/null and b/docs/public/assets/tahoe-dusk.webp differ
diff --git a/docs/public/blog/authors/lilith-happy.webp b/docs/public/blog/authors/lilith-happy.webp
new file mode 100644
index 00000000..3dee31e1
Binary files /dev/null and b/docs/public/blog/authors/lilith-happy.webp differ
diff --git a/docs/public/blog/elysia-14/elysia-14.webp b/docs/public/blog/elysia-14/elysia-14.webp
new file mode 100644
index 00000000..f34913c4
Binary files /dev/null and b/docs/public/blog/elysia-14/elysia-14.webp differ
diff --git a/docs/public/blog/elysia-14/elysia-supersymmetry.webp b/docs/public/blog/elysia-14/elysia-supersymmetry.webp
new file mode 100644
index 00000000..2bf1b443
Binary files /dev/null and b/docs/public/blog/elysia-14/elysia-supersymmetry.webp differ
diff --git a/docs/public/blog/elysia-14/group-standalone.webp b/docs/public/blog/elysia-14/group-standalone.webp
new file mode 100644
index 00000000..cc9727cd
Binary files /dev/null and b/docs/public/blog/elysia-14/group-standalone.webp differ
diff --git a/docs/public/blog/elysia-14/macro-extension.webp b/docs/public/blog/elysia-14/macro-extension.webp
new file mode 100644
index 00000000..e7dfa515
Binary files /dev/null and b/docs/public/blog/elysia-14/macro-extension.webp differ
diff --git a/docs/public/blog/elysia-14/macro-schema-lifecycle.webp b/docs/public/blog/elysia-14/macro-schema-lifecycle.webp
new file mode 100644
index 00000000..e61b2054
Binary files /dev/null and b/docs/public/blog/elysia-14/macro-schema-lifecycle.webp differ
diff --git a/docs/public/blog/elysia-14/macro-schema.webp b/docs/public/blog/elysia-14/macro-schema.webp
new file mode 100644
index 00000000..cad3458e
Binary files /dev/null and b/docs/public/blog/elysia-14/macro-schema.webp differ
diff --git a/docs/public/blog/elysia-14/openapi-valibot.webp b/docs/public/blog/elysia-14/openapi-valibot.webp
new file mode 100644
index 00000000..8b00b5eb
Binary files /dev/null and b/docs/public/blog/elysia-14/openapi-valibot.webp differ
diff --git a/docs/public/blog/elysia-14/openapi-zod.webp b/docs/public/blog/elysia-14/openapi-zod.webp
new file mode 100644
index 00000000..2ba7df21
Binary files /dev/null and b/docs/public/blog/elysia-14/openapi-zod.webp differ
diff --git a/docs/public/blog/elysia-14/standard-schema.webp b/docs/public/blog/elysia-14/standard-schema.webp
new file mode 100644
index 00000000..f86d19d2
Binary files /dev/null and b/docs/public/blog/elysia-14/standard-schema.webp differ
diff --git a/docs/public/blog/elysia-14/type-inference.webp b/docs/public/blog/elysia-14/type-inference.webp
new file mode 100644
index 00000000..576a69ec
Binary files /dev/null and b/docs/public/blog/elysia-14/type-inference.webp differ
diff --git a/docs/public/blog/elysia-14/type-soundness.webp b/docs/public/blog/elysia-14/type-soundness.webp
new file mode 100644
index 00000000..9e893554
Binary files /dev/null and b/docs/public/blog/elysia-14/type-soundness.webp differ
diff --git a/docs/public/blog/openapi-type-gen/cover.webp b/docs/public/blog/openapi-type-gen/cover.webp
new file mode 100644
index 00000000..6cf42dec
Binary files /dev/null and b/docs/public/blog/openapi-type-gen/cover.webp differ
diff --git a/docs/public/blog/openapi-type-gen/drizzle-typegen.webp b/docs/public/blog/openapi-type-gen/drizzle-typegen.webp
new file mode 100644
index 00000000..f0b86dfe
Binary files /dev/null and b/docs/public/blog/openapi-type-gen/drizzle-typegen.webp differ
diff --git a/docs/public/assets/scalar-preview-light.webp b/docs/public/blog/openapi-type-gen/scalar-preview-light.webp
similarity index 100%
rename from docs/public/assets/scalar-preview-light.webp
rename to docs/public/blog/openapi-type-gen/scalar-preview-light.webp
diff --git a/docs/public/blog/openapi-type-gen/type-gen.webp b/docs/public/blog/openapi-type-gen/type-gen.webp
new file mode 100644
index 00000000..1e8e5194
Binary files /dev/null and b/docs/public/blog/openapi-type-gen/type-gen.webp differ
diff --git a/docs/public/blog/openapi-type-gen/union.webp b/docs/public/blog/openapi-type-gen/union.webp
new file mode 100644
index 00000000..69fcdb11
Binary files /dev/null and b/docs/public/blog/openapi-type-gen/union.webp differ
diff --git a/docs/public/logo/abacate-pay-dark.webp b/docs/public/logo/abacate-pay-dark.webp
new file mode 100644
index 00000000..322611d9
Binary files /dev/null and b/docs/public/logo/abacate-pay-dark.webp differ
diff --git a/docs/public/logo/abacate-pay.webp b/docs/public/logo/abacate-pay.webp
new file mode 100644
index 00000000..9eb17740
Binary files /dev/null and b/docs/public/logo/abacate-pay.webp differ
diff --git a/docs/public/logo/baac.webp b/docs/public/logo/baac.webp
new file mode 100644
index 00000000..f851b5ac
Binary files /dev/null and b/docs/public/logo/baac.webp differ
diff --git a/docs/public/logo/bun.svg b/docs/public/logo/bun.svg
new file mode 100644
index 00000000..7ef15001
--- /dev/null
+++ b/docs/public/logo/bun.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/public/logo/connex-dark.webp b/docs/public/logo/connex-dark.webp
new file mode 100644
index 00000000..db77e98b
Binary files /dev/null and b/docs/public/logo/connex-dark.webp differ
diff --git a/docs/public/logo/connex.webp b/docs/public/logo/connex.webp
new file mode 100644
index 00000000..7d4ed379
Binary files /dev/null and b/docs/public/logo/connex.webp differ
diff --git a/docs/public/logo/csmoney-dark.webp b/docs/public/logo/csmoney-dark.webp
new file mode 100644
index 00000000..a0b2d591
Binary files /dev/null and b/docs/public/logo/csmoney-dark.webp differ
diff --git a/docs/public/logo/csmoney.webp b/docs/public/logo/csmoney.webp
new file mode 100644
index 00000000..dc9e83b6
Binary files /dev/null and b/docs/public/logo/csmoney.webp differ
diff --git a/docs/public/logo/decidable-dark.webp b/docs/public/logo/decidable-dark.webp
new file mode 100644
index 00000000..69715b24
Binary files /dev/null and b/docs/public/logo/decidable-dark.webp differ
diff --git a/docs/public/logo/decidable.webp b/docs/public/logo/decidable.webp
new file mode 100644
index 00000000..0ca2fa35
Binary files /dev/null and b/docs/public/logo/decidable.webp differ
diff --git a/docs/public/logo/tiptap-dark.webp b/docs/public/logo/tiptap-dark.webp
new file mode 100644
index 00000000..7f3608e0
Binary files /dev/null and b/docs/public/logo/tiptap-dark.webp differ
diff --git a/docs/public/logo/tiptap.webp b/docs/public/logo/tiptap.webp
new file mode 100644
index 00000000..e06a85b6
Binary files /dev/null and b/docs/public/logo/tiptap.webp differ
diff --git a/docs/public/logo/x-dark.svg b/docs/public/logo/x-dark.svg
new file mode 100644
index 00000000..60b52d8e
--- /dev/null
+++ b/docs/public/logo/x-dark.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/docs/public/logo/x.svg b/docs/public/logo/x.svg
new file mode 100644
index 00000000..2b83afac
--- /dev/null
+++ b/docs/public/logo/x.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/docs/public/tweets/bewinxed.webp b/docs/public/tweets/bewinxed.webp
new file mode 100644
index 00000000..62b0ba2a
Binary files /dev/null and b/docs/public/tweets/bewinxed.webp differ
diff --git a/docs/quick-start.md b/docs/quick-start.md
index a89281ec..f46d0023 100644
--- a/docs/quick-start.md
+++ b/docs/quick-start.md
@@ -7,11 +7,11 @@ head:
- - meta
- name: 'description'
- content: Elysia is a library built for Bun and the only prerequisite. To start, bootstrap a new project with "bun create elysia hi-elysia" and start the development server with "bun dev". This is all it needs to do a quick start or get started with ElysiaJS.
+ content: Elysia is a library built for Bun and the only prerequisite. To start, bootstrap a new project with "bun create elysia hi-elysia" and start the development server with "bun dev". This is all you need to do a quick start or get started with ElysiaJS.
- - meta
- property: 'og:description'
- content: Elysia is a library built for Bun and the only prerequisite. To start, bootstrap a new project with "bun create elysia hi-elysia" and start the development server with "bun dev". This is all it needs to do a quick start or get started with ElysiaJS.
+ content: Elysia is a library built for Bun and the only prerequisite. To start, bootstrap a new project with "bun create elysia hi-elysia" and start the development server with "bun dev". This is all you need to do a quick start or get started with ElysiaJS.
---