Skip to content

Eden installation

Start by installing Eden on your frontend:

Terminal window
bun add @elysia/eden@next
# Need Elysia to infer utility types
bun add -d elysia@rc

First, export your existing Elysia server type:

server.ts
import { Elysia, t } from 'elysia'
const app = new Elysia()
.get('/', () => 'Hi Elysia')
.get('/id/:id', ({ params: { id } }) => id)
.post('/mirror', {
body: t.Object({
id: t.Number(),
name: t.String()
})
}, ({ body }) => body)
.listen(3000)
export type App = typeof app

Then consume the Elysia API on the client side:

client.ts

client.ts
import { treaty } from '@elysia/eden'
import type { App } from './server'
const client = treaty<App>('localhost:3000')
// response: Hi Elysia
const { data: index } = await client.get()
// response: 1895
const { data: id } = await client.id({ id: 1895 }).get()
// response: { id: 1895, name: 'Skadi' }
const { data: nendoroid } = await client.mirror.post({
id: 1895,
name: 'Skadi'
})

Sometimes, Eden may not infer types from Elysia correctly. The following are the most common workarounds to fix Eden type inference.

Make sure to enable strict mode in tsconfig.json.

{
"compilerOptions": {
"strict": true
}
}

Eden depends on the Elysia class to import the Elysia instance and infer types correctly.

Make sure that both the client and server have matching Elysia versions.

You can check it with the npm why command:

Terminal window
npm why elysia

Check that client, server, and plugin dependencies agree on the same Elysia v2 release.

The exact output depends on your dependency tree. Look for duplicate Elysia versions, incompatible peer ranges, or a frontend version that differs from the server.

Elysia uses newer features and syntax of TypeScript to infer types in the most performant way. Features like Const Generics and Template Literals are heavily used.

Use a TypeScript version supported by your Elysia v2 release. The local v2 packages use modern TypeScript features; use TypeScript 5.9 or newer for these examples.

To make Eden work, Elysia must use method chaining

Elysia’s type system is complex. Methods usually introduce a new type to the instance.

Using method chaining will help save that new type reference.

For example:

import { Elysia } from 'elysia'
new Elysia()
.state('build', 1)
// Store is strictly typed
.get('/', ({ store: { build } }) => build)
.listen(3000)

Using this, state now returns a new Elysia type, introducing build into the store and replacing the current type.

Without method chaining, Elysia doesn’t save the new type when introduced, leading to no type inference.

// Expected type error: the earlier call did not retain its new type.
import { Elysia } from 'elysia'
const app = new Elysia()
app.state('build', 1)
app.get('/', ({ store: { build } }) => build)
app.listen(3000)

If you are using a Bun-specific feature, like Bun.file or a similar API, and return it from a handler, you may need to install Bun type definitions for the client as well.

Terminal window
bun add -d @types/bun

If you are using path aliases in your monorepo, make sure that the frontend is able to resolve the path the same way as the backend.

For example, if you have the following path alias for your backend in tsconfig.json:

{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

And your backend code is like this:

import { Elysia } from 'elysia'
import { a, b } from '@/controllers'
const app = new Elysia()
.use(a)
.use(b)
.listen(3000)
export type app = typeof app

You must make sure that your frontend code is able to resolve the same path alias. Otherwise, type inference will be resolved as any.

import { treaty } from '@elysia/eden'
import type { app } from '@/index'
const client = treaty<app>('localhost:3000')
// This should be able to resolve the same module in both frontend and backend, and not `any`
import { a, b } from '@/controllers'

To fix this, you must make sure that the path alias is resolved to the same file in both the frontend and backend.

So, you must change the path alias in the frontend’s tsconfig.json (apps/frontend) to:

{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["../backend/src/*"]
}
}
}

If configured correctly, you should be able to resolve the same module in both the frontend and backend.

// This should be able to resolve the same module in both frontend and backend, and not `any`
import { a, b } from '@/controllers'

We recommend adding a namespace prefix for each module in your monorepo to avoid any confusion and conflicts that may occur.

{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@frontend/*": ["./apps/frontend/src/*"],
"@backend/*": ["./apps/backend/src/*"]
}
}
}

Then, you can import the module like this:

// Should work in both frontend and backend and not return `any`
import { a, b } from '@backend/controllers'

We recommend creating a single tsconfig.json that defines a baseUrl as the root of your repo, provides a path according to the module location, and creates a tsconfig.json for each module that inherits the root tsconfig.json which has the path alias.

You may find a working example in this path alias example repo or Kozeki Template.

Type to search the documentation.

    Up arrowDown arrow to navigateEnter to selectEscape to close