Eden installation
Start by installing Eden on your frontend:
bun add @elysia/eden@next# Need Elysia to infer utility typesbun add -d elysia@rcFirst, export your existing Elysia server type:
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 appThen consume the Elysia API on the client side:
client.ts
import { treaty } from '@elysia/eden'import type { App } from './server'
const client = treaty<App>('localhost:3000')
// response: Hi Elysiaconst { data: index } = await client.get()
// response: 1895const { data: id } = await client.id({ id: 1895 }).get()
// response: { id: 1895, name: 'Skadi' }const { data: nendoroid } = await client.mirror.post({ id: 1895, name: 'Skadi'})Gotcha
Section titled “Gotcha”Sometimes, Eden may not infer types from Elysia correctly. The following are the most common workarounds to fix Eden type inference.
Type strict
Section titled “Type strict”Make sure to enable strict mode in tsconfig.json.
{ "compilerOptions": { "strict": true }}Mismatched Elysia version
Section titled “Mismatched Elysia version”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:
npm why elysiaCheck 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.
TypeScript version
Section titled “TypeScript version”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.
Method chaining
Section titled “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.
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)Type definitions
Section titled “Type definitions”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.
bun add -d @types/bunPath alias (monorepo)
Section titled “Path alias (monorepo)”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 appYou 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'Namespace
Section titled “Namespace”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.