Files
agent/.agent/skills/tech-stack/elysiajs/references/eden.md

3.5 KiB

Eden Treaty

e2e type safe RPC client for share type from backend to frontend.

What It Is

Type-safe object representation for Elysia server. Auto-completion + error handling.

Installation

bun add @elysiajs/eden
bun add -d elysia

Export Elysia server type:

const app = new Elysia()
    .get('/', () => 'Hi Elysia')
    .get('/id/:id', ({ params: { id } }) => id)
    .post('/mirror', ({ body }) => body, {
        body: t.Object({
            id: t.Number(),
            name: t.String()
        })
    })
    .listen(3000)

export type App = typeof app

Consume on client side:

import { treaty } from '@elysiajs/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'
})

Common Errors & Fixes

  • Strict mode: Enable in tsconfig
  • Version mismatch: npm why elysia - must match server/client
  • TypeScript: Min 5.0
  • Method chaining: Required on server
  • Bun types: bun add -d @types/bun if using Bun APIs
  • Path alias: Must resolve same on frontend/backend

Monorepo Path Alias

Must resolve to same file on frontend/backend

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

Syntax Mapping

Path Method Treaty
/ GET .get()
/hi GET .hi.get()
/deep/nested POST .deep.nested.post()
/item/:name GET .item({ name: 'x' }).get()

Parameters

With body (POST/PUT/PATCH/DELETE):

.user.post(
  { name: 'Elysia' },              // body
  { headers: {}, query: {}, fetch: {} } // optional
)

No body (GET/HEAD):

.hello.get({ headers: {}, query: {}, fetch: {} })

Empty body with query/headers:

.user.post(null, { query: { name: 'Ely' } })

Fetch options:

.hello.get({ fetch: { signal: controller.signal } })

File upload:

// Accepts: File | File[] | FileList | Blob
.image.post({
  title: 'Title',
  image: fileInput.files!
})

Response

const { data, error, response, status, headers } = await api.user.post({ name: 'x' })

if (error) {
  switch (error.status) {
    case 400: throw error.value
    default: throw error.value
  }
}
// data unwrapped after error handling
return data

status >= 300 → data = null, error has value

Stream/SSE

Interpreted as AsyncGenerator:

const { data, error } = await treaty(app).ok.get()
if (error) throw error

for await (const chunk of data) console.log(chunk)

Utility Types

import { Treaty } from '@elysiajs/eden'

type UserData = Treaty.Data<typeof api.user.post>
type UserError = Treaty.Error<typeof api.user.post>

WebSocket

const chat = api.chat.subscribe()

chat.subscribe((message) => console.log('got', message))
chat.on('open', () => chat.send('hello'))

// Native access: chat.raw

.subscribe() accepts same params as get/head