Install @elysia/eden and Elysia
mainTo use Eden for fully type-safe Elysia client communication, install both elysia and @elysia/eden using bun:
bun add elysia @elysia/edenrepository·main·Indexed 43 days ago
https://github.com/elysiajs/edenA fully type-safe client for Elysia applications that allows developers to consume server routes with TypeScript autocompletion by sharing the server's type definition. It provides two primary clients: edenTreaty, a proxy-based client for seamless integration, and edenFetch, a lightweight fetch-based client. Version 1.4.10 includes support for WebSocket communication via EdenWS, automatic FormData/File upload handling, and customizable error handling using EdenFetchError.
To use Eden for fully type-safe Elysia client communication, install both elysia and @elysia/eden using bun:
bun add elysia @elysia/edenEdenTreaty automatically maps Elysia route parameters (prefixed with :) to function arguments.
/:id, the client will require an object containing id as a string or number./:id?, the parameter is treated as optional.When calling these routes, the path parameters are passed as the first argument to the generated function, followed by the body (if applicable) and options (for headers/fetch config).
// If Elysia route is: app.get('/user/:id', ...)
// EdenTreaty usage:
const { data } = await client.api.user[':id']({ id: '123' });
// If Elysia route is: app.get('/user/:id?', ...)
// EdenTreaty usage:
const { data } = await client.api.user[':id']({ id: '123' });
const { data } = await client.api.user[':id']({}); // id is optionalEdenTreaty automatically detects if your request body contains File or FileList objects (or is a plain object containing them). If it does, it automatically converts the request into a FormData object.
This works in both browser and server environments (using Blob on the server). It supports:
File objects.FileList objects.File objects.When FormData is used, the Content-Type header is managed automatically to ensure the boundary is correctly set.
// If the server expects multipart/form-data
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement
const file = fileInput.files?.[0]
if (file) {
await client.upload.post({
file, // Automatically converted to FormData
description: 'My profile picture'
})
}The edenFetch client automatically manages Content-Type headers and body serialization:
content-type is set, it automatically calls JSON.stringify(body) and sets the header to application/json.FormData or URLSearchParams, it is sent as-is without manual stringification.query object is automatically converted into a URL search string (e.g., ?key=value). Null or undefined values in the query object are filtered out before transmission.Eden Treaty supports WebSocket connections through the .subscribe() method. When .subscribe() is called on a path, it returns an EdenWS instance instead of a standard fetch promise. The protocol (WS vs WSS) is automatically determined based on the base domain's protocol.
// If your Elysia app has a websocket route at /ws
const ws = client.ws.subscribe()
ws.send('hello')
ws.addEventListener('message', (event) => {
console.log(event.data)
})Every request made via EdenTreaty returns a TreatyResponse. This object is a discriminated union that handles both successful and error responses based on the HTTP status code.
When the status code is one of the SuccessCodes (e.g., 200, 201, 204), the object contains:
data: The parsed response body (automatically unwrapped from ELYSIA_FORM_DATA if applicable).error: null.response: The raw Response object.status: The numeric HTTP status code.headers: The response headers.When the status code is not a success code, the object contains:
data: null.error: An object containing the error details. If the server defined specific error shapes for certain status codes, error will contain those shapes (e.g., { status: 404, value: 'Not Found' }).response: The raw Response object.status: The numeric HTTP status code.headers: The response headers.const { data, error, status } = await client.api.user.get();
if (error) {
console.error(`Error ${status}:`, error);
return;
}
console.log(data);Every request made via the edenFetch client returns a promise that resolves to a response object. This object follows a consistent shape regardless of whether the request succeeded or failed.
Response Shape:
data: The parsed response body (e.g., JSON, ArrayBuffer, or FormData). Returns null if an error occurred.error: An EdenFetchError instance if the request failed, otherwise null.status: The HTTP status code returned by the server.headers: The Headers object from the response.retry: A function that, when called, re-executes the exact same request. This is useful for implementing retry logic.const { data, error, status, retry } = await client.endpoint.get()
if (error) {
console.error(`Request failed with status ${status}`)
// You can attempt to retry the request
const retryData = await retry()
}When calling a route via the EdenTreaty proxy, you can pass a special object as the first argument to modify the request. This object can contain both your route's expected body and special internal keys (prefixed with $).
Body Modifiers (Internal Keys):
$fetch: Merges directly into the RequestInit object passed to fetch.$headers: Merges into the request headers.$query: Merges into the URL query parameters.$transform: A local transform function applied to the response for this specific call.getRaw: A boolean flag. If true, the function returns the raw Response object instead of the parsed data/error object.Execution Options:
The proxy call follows the pattern: client.path.method({ body, $query, ... }, { fetch, transform, ... })
// Example of using modifiers to add query params and headers
const { data, error } = await client.user.get({
$query: { id: '123' },
$headers: {
Authorization: 'Bearer token'
}
})
// Example of getting the raw response
const response = await client.user.get({ getRaw: true })Eden provides a type-safe client via edenTreaty. To use it, you must export the type of your Elysia application instance from your server file and pass it as a generic type to edenTreaty in your client file. This allows the client to mirror the exact routes, parameters, and body schemas defined on the server.
// server.ts
import { Elysia, t } from 'elysia'
const app = new Elysia()
.get('/', () => 'Hi Elysia')
.get('/id/:id', ({ params: { id } }) => id)
.post('/mirror', ({ body }) => body, {
schema: {
body: t.Object({
id: t.Number(),
name: t.String()
})
}
})
.listen(8080)
export type App = typeof app
// client.ts
import { edenTreaty } from '@elysia/eden'
import type { App } from './server'
const app = edenTreaty<App>('http://localhost:8080')
// GET / -> returns 'Hi Elysia'
const { data: pong } = app.index.get()
// GET /id/1895 -> returns 1895
const { data: id } = app.id['1895'].get()
// POST /mirror -> returns the body
const { data: nendoroid } = app.mirror.post({
id: 1895,
name: 'Skadi'
})When initializing an EdenTreaty client, you can provide a Config object to customize how requests are handled. This allows you to set default fetch options, provide a custom fetch implementation, or define global response transformations.
Key configuration options:
$fetch: Default options to pass to the standard fetch API (RequestInit).fetcher: A custom function to replace the default fetch implementation.transform: A Transform function (or array of functions) applied to responses globally.const config: EdenTreaty.Config = {
$fetch: {
mode: 'cors'
},
fetcher: customFetcher,
transform: (response) => {
// transform logic
return response;
}
};When initializing treaty, you can provide a config object to customize request behavior. Key configuration options include:
headers: Global headers applied to every request. Can be an object, a Headers instance, or a function that returns headers.fetcher: A custom fetch implementation (defaults to the standard fetch).onRequest: A lifecycle hook (or array of hooks) called before a request is sent. It can be used to modify the fetchInit object.onResponse: A lifecycle hook (or array of hooks) called after a response is received. It can be used to transform the response data.throwHttpError: Determines if the client should throw an EdenFetchError on non-2xx status codes. Can be set to true or a custom function (error) => boolean.parseDate: A boolean flag to enable/disable automatic parsing of stringified dates in JSON responses.const client = treaty('http://localhost:3000', {
headers: { Authorization: 'Bearer token' },
throwHttpError: true,
parseDate: true,
onRequest: async (path, init) => {
// Modify request before it's sent
return { headers: { 'X-Custom-Header': 'value' } }
}
})When initializing edenFetch, you can provide a configuration object to customize behavior globally for all requests made by that client instance.
Supported configuration options:
fetcher: A custom fetch implementation (defaults to globalThis.fetch).throwHttpError: A global setting to determine if HTTP error statuses should throw an EdenFetchError or return an error object.const client = edenFetch<App>('http://localhost:3000', {
fetcher: myCustomFetcher,
throwHttpError: true
})