Mengenal Eden Treaty Unique Selling Point Elysia JS

Mengenal Eden Treaty, fitur unik ElysiaJS yang memberikan end-to-end type safety antara backend dan frontend tanpa code generation. Dengan Eden Treaty, perubahan API di server langsung terdeteksi di client — IDE kamu akan menunjukkan error sebelum code dijalankan. Artikel ini menjelaskan konsep Eden Treaty dengan contoh praktis aplikasi sewa rumah.

Saya Angga Risky Setiawan, Founder dan CEO BuildWithAngga. Salah satu pertanyaan yang sering muncul dari students adalah: "Bagaimana cara menjaga frontend dan backend tetap sync?" Eden Treaty adalah jawaban paling elegan yang pernah saya temui.

Bagian 1: Apa itu Eden Treaty?

Problem yang Diselesaikan

Bayangkan skenario ini: kamu punya API endpoint /users yang return { data: users }. Suatu hari, backend developer mengubahnya menjadi { users: users }. Apa yang terjadi?

  • Frontend tidak tahu ada perubahan
  • Code tetap jalan tanpa error saat compile
  • Bug baru ketahuan saat production
  • User complain, tim panik

Ini adalah masalah klasik di full-stack development. Solusi yang ada sebelumnya:

SolusiKekurangan
Manual typingError-prone, cepat outdated
Swagger/OpenAPIPerlu generate ulang tiap ada perubahan
GraphQLComplex setup, learning curve tinggi
tRPCBagus, tapi butuh setup khusus

Eden Treaty: Solusi ElysiaJS

Eden Treaty adalah RPC-like client yang memberikan end-to-end type safety dengan cara yang sangat sederhana. Kuncinya: TypeScript type inference tanpa code generation.

Cara kerjanya:

// === SERVER ===
import { Elysia } from 'elysia'

const app = new Elysia()
  .get('/users', () => {
    return { data: [{ id: 1, name: 'John' }] }
  })
  .listen(3000)

// ⭐ Export type, bukan value
export type App = typeof app

// === CLIENT ===
import { treaty } from '@elysiajs/eden'
import type { App } from './server'  // Import TYPE only

const api = treaty<App>('<http://localhost:3000>')

// ✅ Fully typed! IDE tahu response shape
const { data, error } = await api.users.get()

console.log(data?.data[0].name)  // Autocomplete works!

Perhatikan import type { App } — ini hanya import TypeScript type, bukan actual code. Artinya:

  • Tidak ada server code yang masuk ke client bundle
  • Type checking terjadi saat development
  • Zero runtime overhead

Kenapa Ini Game-Changer?

1. Instant Feedback

Jika server mengubah response dari { data: users } ke { users: users }, client akan langsung error:

console.log(data?.data[0].name)
//                ^^^^ Property 'data' does not exist

Error muncul di IDE sebelum kamu menjalankan code. Bug tertangkap dalam hitungan detik, bukan hari.

2. Tidak Perlu Build Step

Berbeda dengan GraphQL yang butuh codegen, Eden Treaty langsung bekerja. Tidak ada:

  • npm run generate
  • File .generated.ts
  • Build step tambahan

Cukup save file server, type di client otomatis update.

3. Path URL Jadi Object Property

Eden Treaty mengubah URL path menjadi object yang bisa di-navigate:

GET  /houses          →  api.houses.get()
GET  /houses/123      →  api.houses({ id: '123' }).get()
POST /houses/123/book →  api.houses({ id: '123' }).book.post(body)

Setiap segment path menjadi property. Path parameter menjadi function call dengan object.

4. Response & Error Fully Typed

const { data, error } = await api.houses.get()

if (error) {
  // error.status: number
  // error.value: response body (typed berdasarkan server)
  console.log(error.status, error.value)
  return
}

// Setelah error check, TypeScript tahu data pasti ada
console.log(data.total)  // ✅ Safe access

Eden Treaty vs Alternatives

FeatureEden TreatytRPCGraphQL
Code Generation✅ Required
Build Step✅ Required
REST Compatible
Learning CurveLowMediumHigh
Framework Lock-inElysiaJSAnyAny

Eden Treaty punya trade-off: hanya bekerja dengan ElysiaJS. Tapi jika kamu sudah memilih ElysiaJS untuk backend, ini adalah bonus besar yang tidak dimiliki framework lain.

Di bagian selanjutnya, kita akan setup project sewa rumah dan lihat Eden Treaty beraksi dengan contoh nyata.

Bagian 2: Setup Project Sewa Rumah

Mari bangun aplikasi sewa rumah sederhana untuk melihat Eden Treaty beraksi. Project ini terdiri dari server ElysiaJS dan client yang menggunakan Eden Treaty.

Project Structure

sewa-rumah/
├── server/
│   ├── src/
│   │   └── index.ts      # Server + API routes
│   ├── package.json
│   └── tsconfig.json
├── client/
│   ├── src/
│   │   ├── api.ts        # Eden Treaty client
│   │   └── app.ts        # Usage examples
│   ├── package.json
│   └── tsconfig.json

Setup Server

mkdir sewa-rumah && cd sewa-rumah

# Setup server
mkdir -p server/src
cd server
bun init -y
bun add elysia @elysiajs/cors

Server Implementation

Buat file server/src/index.ts:

// server/src/index.ts
import { Elysia, t } from 'elysia'
import { cors } from '@elysiajs/cors'

// Data rumah (in-memory untuk demo)
const houses = [
  {
    id: '1',
    name: 'Villa Bali Modern',
    location: 'Seminyak, Bali',
    price: 5000000,      // per malam dalam Rupiah
    bedrooms: 3,
    bathrooms: 2,
    available: true,
    image: '/images/villa-bali.jpg'
  },
  {
    id: '2',
    name: 'Rumah Joglo Klasik',
    location: 'Yogyakarta',
    price: 3500000,
    bedrooms: 4,
    bathrooms: 3,
    available: true,
    image: '/images/joglo.jpg'
  },
  {
    id: '3',
    name: 'Apartemen Jakarta CBD',
    location: 'Sudirman, Jakarta',
    price: 8000000,
    bedrooms: 2,
    bathrooms: 1,
    available: false,
    image: '/images/apt-jakarta.jpg'
  },
  {
    id: '4',
    name: 'Villa Tepi Pantai',
    location: 'Lombok',
    price: 6500000,
    bedrooms: 3,
    bathrooms: 2,
    available: true,
    image: '/images/villa-lombok.jpg'
  }
]

// Bookings storage
const bookings: Array<{
  id: string
  houseId: string
  guestName: string
  guestEmail: string
  checkIn: string
  checkOut: string
  nights: number
  totalPrice: number
  createdAt: string
}> = []

let bookingId = 1

const app = new Elysia()
  .use(cors())

  // ============================================
  // GET /houses - List semua rumah dengan filter
  // ============================================
  .get('/houses', ({ query }) => {
    let result = [...houses]

    // Filter by availability
    if (query.available !== undefined) {
      const isAvailable = query.available === 'true'
      result = result.filter(h => h.available === isAvailable)
    }

    // Filter by max price
    if (query.maxPrice) {
      result = result.filter(h => h.price <= Number(query.maxPrice))
    }

    // Filter by location (partial match)
    if (query.location) {
      result = result.filter(h =>
        h.location.toLowerCase().includes(query.location!.toLowerCase())
      )
    }

    // Filter by minimum bedrooms
    if (query.minBedrooms) {
      result = result.filter(h => h.bedrooms >= Number(query.minBedrooms))
    }

    return {
      data: result,
      total: result.length,
      filters: {
        available: query.available,
        maxPrice: query.maxPrice,
        location: query.location
      }
    }
  }, {
    query: t.Object({
      available: t.Optional(t.String()),
      maxPrice: t.Optional(t.String()),
      location: t.Optional(t.String()),
      minBedrooms: t.Optional(t.String())
    })
  })

  // ============================================
  // GET /houses/:id - Detail satu rumah
  // ============================================
  .get('/houses/:id', ({ params, set }) => {
    const house = houses.find(h => h.id === params.id)

    if (!house) {
      set.status = 404
      return {
        error: 'NOT_FOUND',
        message: `Rumah dengan ID ${params.id} tidak ditemukan`
      }
    }

    return { data: house }
  }, {
    params: t.Object({
      id: t.String()
    })
  })

  // ============================================
  // POST /houses/:id/book - Booking rumah
  // ============================================
  .post('/houses/:id/book', ({ params, body, set }) => {
    const house = houses.find(h => h.id === params.id)

    // Validasi: rumah ada?
    if (!house) {
      set.status = 404
      return {
        error: 'NOT_FOUND',
        message: 'Rumah tidak ditemukan'
      }
    }

    // Validasi: rumah available?
    if (!house.available) {
      set.status = 400
      return {
        error: 'NOT_AVAILABLE',
        message: 'Maaf, rumah ini sedang tidak tersedia'
      }
    }

    // Validasi: minimal 1 malam
    if (body.nights < 1) {
      set.status = 400
      return {
        error: 'INVALID_NIGHTS',
        message: 'Minimal booking 1 malam'
      }
    }

    // Hitung total harga
    const totalPrice = house.price * body.nights

    // Buat booking
    const booking = {
      id: `BK${String(bookingId++).padStart(4, '0')}`,
      houseId: house.id,
      guestName: body.guestName,
      guestEmail: body.guestEmail,
      checkIn: body.checkIn,
      checkOut: body.checkOut,
      nights: body.nights,
      totalPrice,
      createdAt: new Date().toISOString()
    }

    bookings.push(booking)

    // Update availability
    house.available = false

    set.status = 201
    return {
      message: 'Booking berhasil!',
      booking: {
        ...booking,
        house: {
          name: house.name,
          location: house.location,
          pricePerNight: house.price
        }
      }
    }
  }, {
    params: t.Object({
      id: t.String()
    }),
    body: t.Object({
      guestName: t.String({ minLength: 2 }),
      guestEmail: t.String({ format: 'email' }),
      checkIn: t.String(),   // Format: YYYY-MM-DD
      checkOut: t.String(),  // Format: YYYY-MM-DD
      nights: t.Number({ minimum: 1, maximum: 30 })
    })
  })

  // ============================================
  // GET /bookings - List booking (untuk admin)
  // ============================================
  .get('/bookings', () => {
    return {
      data: bookings,
      total: bookings.length
    }
  })

  .listen(3000)

console.log('🏠 Sewa Rumah API running on <http://localhost:3000>')

// ⭐⭐⭐ PENTING: Export type untuk Eden Treaty ⭐⭐⭐
export type App = typeof app

Penjelasan Code

1. TypeBox Validation

Setiap route menggunakan TypeBox (t.Object, t.String, dll) untuk validasi. Ini penting karena:

  • Runtime validation (reject invalid requests)
  • Type inference (Eden Treaty tahu exact shape)

2. Response Patterns

Kita menggunakan pattern konsisten:

  • Success: { data: ... } atau { message: ..., data: ... }
  • Error: { error: 'CODE', message: '...' }

3. Export Type

Baris paling penting:

export type App = typeof app

Ini mengexport type dari Elysia instance. TypeScript akan infer:

  • Semua routes (/houses, /houses/:id, dll)
  • HTTP methods per route
  • Query/params/body schema per route
  • Response type per route

Test Server

Jalankan server:

cd server
bun run src/index.ts

Test dengan cURL:

# List semua rumah
curl <http://localhost:3000/houses> | jq

# Filter rumah available dengan max price 6jt
curl "<http://localhost:3000/houses?available=true&maxPrice=6000000>" | jq

# Detail rumah
curl <http://localhost:3000/houses/1> | jq

# Booking rumah
curl -X POST <http://localhost:3000/houses/1/book> \\
  -H "Content-Type: application/json" \\
  -d '{
    "guestName": "Budi Santoso",
    "guestEmail": "[email protected]",
    "checkIn": "2025-01-15",
    "checkOut": "2025-01-18",
    "nights": 3
  }' | jq

Server sudah siap. Di bagian selanjutnya, kita akan setup client dengan Eden Treaty dan lihat keajaiban type safety beraksi.

Bagian 3: Implementasi Eden Treaty di Client

Sekarang bagian seru — setup Eden Treaty di client dan lihat type safety beraksi.

Setup Client

# Dari root folder sewa-rumah
mkdir -p client/src
cd client
bun init -y
bun add @elysiajs/eden

Buat Eden Treaty Client

// client/src/api.ts
import { treaty } from '@elysiajs/eden'
import type { App } from '../../server/src/index'

// Buat client dengan type dari server
export const api = treaty<App>('<http://localhost:3000>')

Hanya 3 baris. Tapi di balik kesederhanaan ini, TypeScript sudah tahu semua routes, params, dan response types dari server.

Penggunaan dengan Full Type Safety

Buat file client/src/app.ts:

// client/src/app.ts
import { api } from './api'

async function main() {
  console.log('🏠 Sewa Rumah Client Demo\\n')

  // =============================================
  // 1. GET /houses - List dengan filter
  // =============================================
  console.log('--- List Rumah Available (Max 6jt) ---')

  const { data: houses, error: listError } = await api.houses.get({
    query: {
      available: 'true',
      maxPrice: '6000000'
    }
  })

  if (listError) {
    console.error('Error:', listError.value)
    return
  }

  // ✅ TypeScript tahu structure response!
  console.log(`Found ${houses.total} houses:`)
  houses.data.forEach(house => {
    // ✅ Autocomplete: house.name, house.price, house.location, dll
    console.log(`- ${house.name} (${house.location}) - Rp ${house.price.toLocaleString()}/malam`)
  })

  // =============================================
  // 2. GET /houses/:id - Detail rumah
  // =============================================
  console.log('\\n--- Detail Rumah ID 2 ---')

  // Path param menggunakan function call dengan object
  const { data: detail, error: detailError } = await api.houses({ id: '2' }).get()

  if (detailError) {
    // Type narrowing berdasarkan response
    if (detailError.status === 404) {
      console.log('Rumah tidak ditemukan')
    }
    return
  }

  // ✅ Setelah error check, detail.data pasti ada
  console.log('Nama:', detail.data.name)
  console.log('Lokasi:', detail.data.location)
  console.log('Kamar:', detail.data.bedrooms, 'bedrooms,', detail.data.bathrooms, 'bathrooms')
  console.log('Harga:', `Rp ${detail.data.price.toLocaleString()}/malam`)
  console.log('Status:', detail.data.available ? '✅ Available' : '❌ Booked')

  // =============================================
  // 3. POST /houses/:id/book - Booking
  // =============================================
  console.log('\\n--- Booking Rumah ---')

  const { data: booking, error: bookingError } = await api.houses({ id: '2' }).book.post({
    guestName: 'Andi Wijaya',
    guestEmail: '[email protected]',
    checkIn: '2025-02-01',
    checkOut: '2025-02-04',
    nights: 3
  })

  if (bookingError) {
    // ✅ Error juga typed!
    console.error('Booking gagal:', bookingError.value)
    return
  }

  // ✅ TypeScript tahu booking.booking.house.name exists
  console.log('🎉', booking.message)
  console.log('Booking ID:', booking.booking.id)
  console.log('Rumah:', booking.booking.house.name)
  console.log('Tamu:', booking.booking.guestName)
  console.log('Check-in:', booking.booking.checkIn)
  console.log('Check-out:', booking.booking.checkOut)
  console.log('Total:', `Rp ${booking.booking.totalPrice.toLocaleString()}`)

  // =============================================
  // 4. GET /bookings - List booking
  // =============================================
  console.log('\\n--- Semua Booking ---')

  const { data: allBookings } = await api.bookings.get()

  console.log(`Total bookings: ${allBookings?.total}`)
  allBookings?.data.forEach(b => {
    console.log(`- ${b.id}: ${b.guestName} (${b.nights} malam) - Rp ${b.totalPrice.toLocaleString()}`)
  })
}

main().catch(console.error)

Jalankan Client

Pastikan server running di terminal terpisah, lalu:

cd client
bun run src/app.ts

Output:

🏠 Sewa Rumah Client Demo

--- List Rumah Available (Max 6jt) ---
Found 3 houses:
- Villa Bali Modern (Seminyak, Bali) - Rp 5,000,000/malam
- Rumah Joglo Klasik (Yogyakarta) - Rp 3,500,000/malam
- Villa Tepi Pantai (Lombok) - Rp 6,500,000/malam

--- Detail Rumah ID 2 ---
Nama: Rumah Joglo Klasik
Lokasi: Yogyakarta
Kamar: 4 bedrooms, 3 bathrooms
Harga: Rp 3,500,000/malam
Status: ✅ Available

--- Booking Rumah ---
🎉 Booking berhasil!
Booking ID: BK0001
Rumah: Rumah Joglo Klasik
Tamu: Andi Wijaya
Check-in: 2025-02-01
Check-out: 2025-02-04
Total: Rp 10,500,000

--- Semua Booking ---
Total bookings: 1
- BK0001: Andi Wijaya (3 malam) - Rp 10,500,000

Path Mapping Reference

Ini adalah aturan mapping dari URL ke Eden Treaty syntax:

Server RouteEden Treaty Client
GET /housesapi.houses.get()
GET /houses?available=trueapi.houses.get({ query: { available: 'true' } })
GET /houses/:idapi.houses({ id: '1' }).get()
POST /houses/:id/bookapi.houses({ id: '1' }).book.post(body)
GET /bookingsapi.bookings.get()

Pattern-nya:

  • Slash / menjadi dot .
  • Path parameter :id menjadi function ({ id: '...' })
  • HTTP method menjadi method call .get(), .post(body)

Error Handling dengan Type Narrowing

Eden Treaty mengembalikan { data, error }. Keduanya typed:

const { data, error } = await api.houses({ id: '999' }).get()

if (error) {
  // error.status: number (400, 404, 500, dll)
  // error.value: response body dari server

  switch (error.status) {
    case 404:
      // TypeScript tahu error.value adalah { error: string, message: string }
      console.log(error.value.message)
      break
    case 400:
      console.log('Bad request:', error.value)
      break
    default:
      console.log('Unknown error')
  }
  return
}

// ✅ Setelah if (error), TypeScript tahu data PASTI ada
// Tidak perlu optional chaining lagi
console.log(data.data.name)  // Safe!

Demo: Ubah Server, Client Langsung Error

Ini keajaiban Eden Treaty. Coba ubah response di server:

// server/src/index.ts - SEBELUM
.get('/houses/:id', ({ params, set }) => {
  const house = houses.find(h => h.id === params.id)
  if (!house) { ... }
  return { data: house }  // ← return { data: house }
})

// server/src/index.ts - SESUDAH (ubah property name)
.get('/houses/:id', ({ params, set }) => {
  const house = houses.find(h => h.id === params.id)
  if (!house) { ... }
  return { house: house }  // ← Ubah jadi { house: house }
})

Sekarang buka client/src/app.ts. IDE langsung menunjukkan error:

console.log('Nama:', detail.data.name)
//                         ^^^^ Property 'data' does not exist on type '{ house: {...} }'

Tanpa menjalankan code, tanpa deploy, bug langsung tertangkap. Ini adalah kekuatan end-to-end type safety.

Bagian 4: Keunggulan, Best Practices & Penutup

Setelah hands-on dengan project sewa rumah, mari rangkum keunggulan Eden Treaty dan best practices untuk production.

Keunggulan Eden Treaty

AspectEden TreatytRPCREST + Manual Types
Code Generation❌ Tidak perlu❌ Tidak perlu❌ Tidak perlu
Build Step Tambahan
End-to-End Type Safety✅ Full✅ Full⚠️ Partial (manual)
REST Compatible✅ Yes❌ Custom protocol✅ Yes
Framework Agnostic❌ ElysiaJS only✅ Any✅ Any
Learning CurveLowMediumLow
WebSocket Support✅ Built-in⚠️ Separate setup❌ Manual
Error Type Narrowing✅ Automatic✅ Automatic❌ Manual

Best Practices

1. Selalu gunakan import type

// ✅ BENAR: Import type only
import type { App } from '../../server/src/index'

// ❌ SALAH: Import value (akan bundle server code!)
import { App } from '../../server/src/index'

Keyword type memastikan hanya TypeScript type yang di-import, bukan actual JavaScript code.

2. Centralize API client

// ✅ BENAR: Satu file untuk API client
// client/src/api.ts
import { treaty } from '@elysiajs/eden'
import type { App } from '../../server/src/index'

export const api = treaty<App>('<http://localhost:3000>')

// Bisa tambahkan default headers
export const apiWithAuth = treaty<App>('<http://localhost:3000>', {
  headers: {
    authorization: `Bearer ${getToken()}`
  }
})

// ❌ SALAH: Buat instance di setiap file
// components/Houses.tsx
const api = treaty<App>('...') // Duplikat!

// components/Booking.tsx
const api = treaty<App>('...') // Duplikat lagi!

3. Handle error dengan type narrowing

// ✅ BENAR: Check error sebelum akses data
const { data, error } = await api.houses.get()

if (error) {
  handleError(error)
  return
}

// Sekarang data pasti ada
console.log(data.total)

// ❌ SALAH: Abaikan error, langsung akses
const { data } = await api.houses.get()
console.log(data.total)  // Error: data bisa undefined!

4. Gunakan TypeBox validation di server

// ✅ BENAR: Explicit schema = better type inference
.post('/book', ({ body }) => { ... }, {
  body: t.Object({
    guestName: t.String({ minLength: 2 }),
    nights: t.Number({ minimum: 1 })
  })
})

// ⚠️ KURANG BAIK: Tanpa schema, type jadi 'unknown'
.post('/book', ({ body }) => {
  // body: unknown - tidak ada autocomplete
})

5. Konsisten dengan response pattern

// ✅ Konsisten: selalu { data: ... } untuk success
return { data: house }
return { data: houses, total: houses.length }

// ✅ Konsisten: selalu { error: ..., message: ... } untuk error
return { error: 'NOT_FOUND', message: 'Rumah tidak ditemukan' }

Kapan Gunakan Eden Treaty?

Cocok untuk:

  • Full-stack TypeScript project
  • Monorepo dengan shared types
  • Tim yang prioritaskan type safety
  • Rapid prototyping yang butuh feedback cepat
  • Project dengan banyak API endpoints

Kurang cocok untuk:

  • Backend bukan ElysiaJS
  • Client bukan TypeScript (vanilla JS, Python, dll)
  • Public API untuk third-party developers
  • Project yang butuh framework-agnostic solution

Integrasi dengan Frontend Framework

Eden Treaty bekerja dengan semua frontend framework:

React:

// hooks/useHouses.ts
import { api } from '../api'
import { useEffect, useState } from 'react'

export function useHouses() {
  const [houses, setHouses] = useState<typeof data>()

  useEffect(() => {
    api.houses.get({ query: { available: 'true' } })
      .then(({ data }) => setHouses(data))
  }, [])

  return houses
}

Vue:

// composables/useHouses.ts
import { api } from '../api'
import { ref, onMounted } from 'vue'

export function useHouses() {
  const houses = ref()

  onMounted(async () => {
    const { data } = await api.houses.get()
    houses.value = data
  })

  return { houses }
}

Svelte:

// +page.ts
import { api } from '$lib/api'

export async function load() {
  const { data } = await api.houses.get()
  return { houses: data }
}

Penutup

Eden Treaty adalah salah satu fitur yang membuat ElysiaJS stand out dari framework lain. Dengan hanya:

  1. export type App = typeof app di server
  2. treaty<App>(url) di client

Kamu mendapat end-to-end type safety yang biasanya butuh setup kompleks di stack lain.

Project sewa rumah yang kita buat mendemonstrasikan:

  • Type-safe API calls
  • Autocomplete untuk semua properties
  • Error detection saat development
  • Zero code generation

Ini bukan magic — ini TypeScript inference yang dimanfaatkan dengan cerdas.

Next Steps

Untuk memperdalam skill full-stack TypeScript, explore kelas gratis di BuildWithAngga. Ada track lengkap dari backend fundamentals dengan ElysiaJS sampai integrasi dengan React, Vue, dan framework frontend lainnya.

Butuh template UI untuk aplikasi property atau booking? Download HTML template gratis di shaynakit.com — tersedia berbagai template real estate, hotel booking, dan rental yang bisa langsung kamu customize untuk project client.

Resources

Happy coding! 🏠✨