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:
| Solusi | Kekurangan |
|---|---|
| Manual typing | Error-prone, cepat outdated |
| Swagger/OpenAPI | Perlu generate ulang tiap ada perubahan |
| GraphQL | Complex setup, learning curve tinggi |
| tRPC | Bagus, 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
| Feature | Eden Treaty | tRPC | GraphQL |
|---|---|---|---|
| Code Generation | ❌ | ❌ | ✅ Required |
| Build Step | ❌ | ❌ | ✅ Required |
| REST Compatible | ✅ | ❌ | ❌ |
| Learning Curve | Low | Medium | High |
| Framework Lock-in | ElysiaJS | Any | Any |
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 Route | Eden Treaty Client |
|---|---|
GET /houses | api.houses.get() |
GET /houses?available=true | api.houses.get({ query: { available: 'true' } }) |
GET /houses/:id | api.houses({ id: '1' }).get() |
POST /houses/:id/book | api.houses({ id: '1' }).book.post(body) |
GET /bookings | api.bookings.get() |
Pattern-nya:
- Slash
/menjadi dot. - Path parameter
:idmenjadi 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
| Aspect | Eden Treaty | tRPC | REST + 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 Curve | Low | Medium | Low |
| 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:
export type App = typeof appdi servertreaty<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
- ElysiaJS Documentation: elysiajs.com
- Eden Treaty Guide: elysiajs.com/eden/overview
- TypeBox: github.com/sinclairzx81/typebox
Happy coding! 🏠✨