İçeriğe geç
Sedat Demir
Geri dön

Elysia.js ile End-to-End Tip Güvenliği: Full-Stack TypeScript'in Geleceği

Elysia.js ile End-to-End Tip Güvenliği: Full-Stack TypeScript'in Geleceği

Modern web geliştirmede en sinir bozucu hatalardan biri, backend'in döndüğü verinin frontend'de beklediğimiz yapıyla uyuşmamasıdır. API'den gelen response'un tipini elle tanımlamak, zamanla senkronizasyon sorunlarına ve runtime hatalarına yol açar. Peki ya backend'de tanımladığınız tipler otomatik olarak frontend'e taşınabilseydi?

İşte tam bu noktada Elysia.js devreye giriyor. Bun runtime üzerinde çalışan bu framework, TypeScript'in tip sistemini sonuna kadar kullanarak end-to-end (uçtan uca) tip güvenliği sağlar. Bu yazıda Elysia.js'in ne olduğunu, nasıl çalıştığını ve Eden Treaty ile frontend-backend arasında nasıl sıfır boşluklu bir tip köprüsü kurabileceğinizi derinlemesine inceleyeceğiz.


Elysia.js Nedir?

Elysia.js, Bun runtime üzerinde çalışmak üzere tasarlanmış, yüksek performanslı bir TypeScript web framework'üdür. Express.js veya Fastify'a alternatif olarak konumlanır ancak onlardan çok farklı bir yaklaşım benimser: tip güvenliğini birinci sınıf vatandaş olarak ele alır.

Neden Elysia.js?


Kurulum ve İlk Adımlar

Öncelikle Bun'un sisteminizde kurulu olduğundan emin olun:

curl -fsSL https://bun.sh/install | bash

Yeni bir Elysia projesi oluşturalım:

bun create elysia my-app
cd my-app
bun install

İlk Elysia sunucunuz şu kadar basit:

// src/index.ts
import { Elysia } from 'elysia'

const app = new Elysia()
  .get('/', () => 'Merhaba Dünya!')
  .get('/health', () => ({ status: 'ok', timestamp: Date.now() }))
  .listen(3000)

console.log(`🦊 Elysia sunucusu http://localhost:${app.server?.port} adresinde çalışıyor`)
bun run src/index.ts

Bu basit örnekte bile Elysia, döndürdüğünüz değerlerin tiplerini otomatik olarak çıkarsıyor. '/health' endpoint'i { status: string, timestamp: number } tipinde bir response döndüğünü biliyor.


Tip Güvenliğinin Temeli: Route Şemaları

Elysia.js'in gerçek gücü, route'lara şema tanımlamaları eklediğinizde ortaya çıkar. Bu şemalar hem runtime doğrulaması hem de derleme zamanı tip güvenliği sağlar.

Request Body Doğrulama

import { Elysia, t } from 'elysia'

const app = new Elysia()
  .post('/users', ({ body }) => {
    // body otomatik olarak { name: string, email: string, age: number } tipindedir
    return {
      id: crypto.randomUUID(),
      name: body.name,
      email: body.email,
      age: body.age,
      createdAt: new Date().toISOString()
    }
  }, {
    body: t.Object({
      name: t.String({ minLength: 2 }),
      email: t.String({ format: 'email' }),
      age: t.Number({ minimum: 18 })
    })
  })
  .listen(3000)

Burada t (TypeBox) ile tanımladığınız şema iki işlevi aynı anda yerine getirir:

  1. Runtime: Gelen isteğin gövdesini doğrular; uyumsuzlukta otomatik 422 hatası döner
  2. Compile-time: body parametresinin tipini TypeScript'e bildirir; IDE'nizde otomatik tamamlama çalışır

Query ve Params Doğrulama

const app = new Elysia()
  .get('/users/:id', ({ params, query }) => {
    // params.id -> string
    // query.includeProfile -> boolean (opsiyonel)
    return {
      userId: params.id,
      includeProfile: query.includeProfile
    }
  }, {
    params: t.Object({
      id: t.String({ format: 'uuid' })
    }),
    query: t.Object({
      includeProfile: t.Optional(t.Boolean({ default: false }))
    })
  })

Response Tipini Açıkça Tanımlama

Response tipini de şema ile kısıtlayabilirsiniz. Bu, API kontratınızı daha da sağlam hale getirir:

const app = new Elysia()
  .post('/login', ({ body }) => {
    // Eğer burada response şemasına uymayan bir değer dönerseniz
    // TypeScript derleme zamanında hata verir!
    return {
      token: 'jwt-token-here',
      expiresIn: 3600
    }
  }, {
    body: t.Object({
      email: t.String({ format: 'email' }),
      password: t.String({ minLength: 8 })
    }),
    response: t.Object({
      token: t.String(),
      expiresIn: t.Number()
    })
  })

Eden Treaty: Frontend ile Tip Köprüsü

Elysia.js'in en devrimci özelliği Eden Treaty isimli istemci kütüphanesidir. Eden, backend'deki Elysia uygulamanızın tip bilgisini frontend'e otomatik olarak taşır. Hiçbir kod üretimi (code generation) veya ek adım gerekmez.

Nasıl Çalışır?

Eden Treaty, TypeScript'in tip çıkarımı (type inference) mekanizmasını kullanır. Elysia uygulamanızın tipini export edersiniz ve Eden bu tipi alarak tüm endpoint'leriniz için tip güvenli bir istemci oluşturur.

Adım 1: Backend'de Tip Export Etme

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

const app = new Elysia()
  .get('/posts', () => {
    return [
      { id: 1, title: 'Elysia.js Rehberi', views: 1500 },
      { id: 2, title: 'Bun ile Performans', views: 2300 }
    ]
  })
  .get('/posts/:id', ({ params }) => {
    return {
      id: parseInt(params.id),
      title: 'Elysia.js Rehberi',
      content: 'Detaylı içerik burada...',
      views: 1500,
      author: { name: 'Ahmet', avatar: '/avatars/ahmet.png' }
    }
  }, {
    params: t.Object({
      id: t.String()
    })
  })
  .post('/posts', ({ body }) => {
    return {
      id: 3,
      ...body,
      views: 0,
      createdAt: new Date().toISOString()
    }
  }, {
    body: t.Object({
      title: t.String({ minLength: 5 }),
      content: t.String({ minLength: 20 })
    })
  })
  .listen(3000)

// ✅ Uygulamanın tipini export edin
export type App = typeof app

Adım 2: Frontend'de Eden Treaty Kurulumu

bun add @elysiajs/eden

Adım 3: Tip Güvenli API Çağrıları

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

// Tip güvenli istemci oluştur
const api = treaty<App>('http://localhost:3000')

// ✅ Tüm endpoint'ler otomatik tamamlama ile gelir
async function fetchPosts() {
  const { data, error } = await api.posts.get()
  
  if (error) {
    console.error('Hata:', error)
    return
  }

  // data tipi otomatik olarak:
  // { id: number, title: string, views: number }[]
  data.forEach(post => {
    console.log(post.title)  // ✅ Otomatik tamamlama çalışır
    console.log(post.views)  // ✅ number olarak tanınır
    // console.log(post.foo) // ❌ TypeScript hatası verir!
  })
}

async function fetchPostById(id: string) {
  const { data, error } = await api.posts({ id }).get()
  
  if (data) {
    // data.author.name -> string ✅
    // data.content -> string ✅
    console.log(`${data.title} - ${data.author.name}`)
  }
}

async function createPost() {
  const { data, error } = await api.posts.post({
    title: 'Yeni Yazı Başlığı',
    content: 'Bu yazının içeriği en az 20 karakter olmalı.'
    // age: 25  // ❌ TypeScript hatası: 'age' şemada yok
  })
  
  if (data) {
    console.log(`Post oluşturuldu: ${data.id} - ${data.createdAt}`)
  }
}

Bu yaklaşımın güzelliği: backend'de bir şeyi değiştirdiğinizde, frontend'de anında TypeScript hatası alırsınız. Artık "API response yapısı değişmiş ama frontend hâlâ eski yapıyı bekliyor" gibi bug'lar tarihe karışıyor.


React ile Elysia Eden Entegrasyonu

Eden Treaty'yi bir React uygulamasında nasıl kullanacağınıza bakalım:

// client/src/hooks/useApi.ts
import { treaty } from '@elysiajs/eden'
import type { App } from '../../../server/src/index'
import { useState, useEffect } from 'react'

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

// Tip güvenli custom hook
export function usePosts() {
  const [posts, setPosts] = useState<Awaited<ReturnType<typeof api.posts.get>>['data']>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    async function load() {
      const { data, error } = await api.posts.get()
      if (error) {
        setError('Postlar yüklenirken hata oluştu')
      } else {
        setPosts(data)
      }
      setLoading(false)
    }
    load()
  }, [])

  return { posts, loading, error }
}
// client/src/components/PostList.tsx
import { usePosts } from '../hooks/useApi'

export function PostList() {
  const { posts, loading, error } = usePosts()

  if (loading) return <div>Yükleniyor...</div>
  if (error) return <div className="error">{error}</div>

  return (
    <ul>
      {posts?.map(post => (
        <li key={post.id}>
          <h3>{post.title}</h3>
          <span>{post.views} görüntülenme</span>
        </li>
      ))}
    </ul>
  )
}

Gruplandırma ve Modüler Yapı

Gerçek projelerde tüm route'ları tek bir dosyaya yazmak sürdürülebilir değildir. Elysia, plugin sistemi ile modüler yapıyı destekler ve tip güvenliğini korur:

// server/src/modules/auth.ts
import { Elysia, t } from 'elysia'

export const authModule = new Elysia({ prefix: '/auth' })
  .post('/register', ({ body }) => {
    return { id: crypto.randomUUID(), email: body.email, role: 'user' as const }
  }, {
    body: t.Object({
      email: t.String({ format: 'email' }),
      password: t.String({ minLength: 8 }),
      name: t.String()
    })
  })
  .post('/login', ({ body }) => {
    return { token: 'jwt-token', refreshToken: 'refresh-token' }
  }, {
    body: t.Object({
      email: t.String({ format: 'email' }),
      password: t.String()
    })
  })
// server/src/modules/posts.ts
import { Elysia, t } from 'elysia'

export const postsModule = new Elysia({ prefix: '/posts' })
  .get('/', () => {
    return [{ id: 1, title: 'İlk Yazı', slug: 'ilk-yazi' }]
  })
  .post('/', ({ body }) => {
    return { id: 2, ...body, createdAt: new Date() }
  }, {
    body: t.Object({
      title: t.String(),
      content: t.String()
    })
  })
// server/src/index.ts
import { Elysia } from 'elysia'
import { authModule } from './modules/auth'
import { postsModule } from './modules/posts'

const app = new Elysia()
  .use(authModule)
  .use(postsModule)
  .listen(3000)

// Tüm modüllerin tipleri birleştirilir
export type App = typeof app

Frontend tarafında artık api.auth.register.post(...) ve api.posts.index.get() gibi çağrıların hepsi tip güvenlidir.


tRPC ile Karşılaştırma

End-to-end tip güvenliği denince akla ilk gelen tRPC'dir. İkisini kısaca karşılaştıralım:

Özellik Elysia.js + Eden tRPC
Runtime Bun (öncelikli) Node.js / Bun
Protokol Standart REST RPC (özel protokol)
Performans Çok yüksek İyi
Öğrenme eğrisi Düşük Orta
REST uyumluluğu Doğal Yok (adapter gerekir)
Swagger/OpenAPI Built-in plugin Ek kütüphane
Frontend agnostik Evet React/Next.js odaklı

Elysia.js'in en büyük avantajı standart REST endpoint'leri kullanmasıdır. Bu, API'nizi sadece TypeScript istemcileri değil, mobil uygulamalar veya üçüncü parti servisler de tüketebilir demektir. Aynı zamanda Swagger dokümantasyonu otomatik üretilebilir.


Hata Yönetimi ve Tip Güvenliği

Elysia, hata durumlarını da tipli bir şekilde yönetmenize olanak tanır:

import { Elysia, t, error } from 'elysia'

const app = new Elysia()
  .get('/users/:id', ({ params }) => {
    const user = findUser(params.id)
    
    if (!user) {
      return error(404, {
        message: 'Kullanıcı bulunamadı',
        code: 'USER_NOT_FOUND'
      })
    }
    
    return user
  }, {
    params: t.Object({ id: t.String() }),
    response: {
      200: t.Object({
        id: t.String(),
        name: t.String(),
        email: t.String()
      }),
      404: t.Object({
        message: t.String(),
        code: t.String()
      })
    }
  })

Eden tarafında bu hata tipleri de korunur:

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

if (error) {
  // error.value tipi: { message: string, code: string }
  switch (error.status) {
    case 404:
      console.log(error.value.code) // 'USER_NOT_FOUND'
      break
  }
}

Sonuç

Elysia.js, TypeScript ekosisteminde end-to-end tip güvenliği konusunda en ergonomik çözümlerden birini sunuyor. Özetlemek gerekirse:

Eğer yeni bir full-stack TypeScript projesi başlıyorsanız veya mevcut API'nizde tip güvenliği sorunları yaşıyorsanız, Elysia.js kesinlikle değerlendirmeniz gereken bir framework. Backend'den frontend'e uzanan kesintisiz tip zinciri, geliştirici deneyiminizi ve uygulamanızın güvenilirliğini bir üst seviyeye taşıyacaktır.


Share this post on:

Önceki Yazı
Prisma ORM vs Drizzle ORM: TypeScript Veritabanı Araçlarının Kapsamlı Karşılaştırması
Sonraki Yazı
useOptimistic Hook ile Anlık UI Güncellemeleri: React'te Kullanıcı Deneyimini Zirveye Taşıyın