CodeWorlds
Powrót do kolekcji
Przewodnik13 min czytaniaZespół CodeWorlds

Elysia, framework dla Buna z typami end-to-end

Elysia wyprowadza typ trasy od serwera aż do klienta przez Eden i zwykły HTTP. Wersja 1.4.29, licencja MIT, praca na Node i cena przywiązania do Buna.

Elysia, framework dla Buna z typami end-to-end

Elysia to framework HTTP napisany w TypeScripcie, zbudowany wokół środowiska uruchomieniowego Bun. Obietnica jest wąska i konkretna: typ trasy zdefiniowany na serwerze trafia do klienta bez generowania kodu i bez ręcznie utrzymywanych interfejsów. Bieżąca wersja stabilna to 1.4.29, licencja MIT, repozytorium elysiajs/elysia.

Co Elysia właściwie robi

Elysia jest routerem HTTP z wbudowaną walidacją danych wejściowych, cyklem życia żądania i generowaniem dokumentacji. Interfejs jest łańcuchowy, a to nie jest kwestia estetyki. Każde wywołanie .get(), .post() czy .use() zwraca nowy typ instancji, wzbogacony o informację o dodanej trasie. Po zbudowaniu aplikacji typeof app zawiera pełną mapę ścieżek razem z kształtem ciała żądania, parametrów zapytania i odpowiedzi. To jest fundament, na którym stoi cała reszta: bez łańcucha nie ma typu, a bez typu nie ma klienta Eden.

Pod spodem działa kompilator, który przy starcie generuje kod obsługi każdej trasy zamiast interpretować konfigurację przy każdym żądaniu. Odpowiada za to opcja aot, domyślnie włączona, wsparta analizą statyczną kodu obsługi o nazwie Sucrose. Jeśli funkcja obsługi nigdy nie sięga po body, wygenerowany kod pomija parsowanie ciała żądania. Ta sama warstwa odpowiada za nativeStaticResponse, czyli oddanie stałych odpowiedzi bezpośrednio serwerowi Buna.

Walidacja opiera się na TypeBoksie, udostępnionym pod eksportem t. Schemat opisany przez t.Object pełni trzy funkcje naraz: sprawdza dane w czasie działania, wyprowadza typ TypeScriptu dla funkcji obsługi i staje się wpisem w dokumencie OpenAPI. Elysia dokłada do TypeBoksa własne typy pomocnicze, między innymi t.Numeric do wartości liczbowych przychodzących jako tekst, t.File i t.Files dla przesyłanych plików, t.Form, t.UnionEnum, t.Nullable oraz t.ObjectString.

Czego Elysia nie robi, jest równie istotne. Nie ma warstwy dostępu do bazy danych, więc Drizzle ORM albo Prisma pozostają osobnym wyborem. Nie ma renderowania widoków po stronie serwera w rozumieniu Next.js. Nie ma wbudowanego uwierzytelniania poza wtyczką odczytującą nagłówek Bearer, więc logikę sesji piszesz sam albo bierzesz bibliotekę zewnętrzną.

Wersja, licencja i dwa zakresy nazw na npm

Wersja z etykietą latest to 1.4.29, opublikowana 16 czerwca 2026 roku. Pakiet ma za sobą 749 wydań, co przy repozytorium założonym w grudniu 2022 roku daje bardzo gęsty rytm. Równolegle trwa praca nad wersją drugą: etykieta next wskazuje na 2.0.0-beta.6 z 19 sierpnia 2026 roku, a etykieta experimental na 2.0.0-exp.64. Oznacza to, że linia stabilna nie dostała wydania od czerwca, podczas gdy gałąź główna żyje bardzo aktywnie. Jeśli planujesz wdrożenie na kilka lat, policz się z migracją do wersji drugiej.

Licencja jest zgodna we wszystkich trzech miejscach, w których zwykle się rozjeżdża. Plik LICENSE w gałęzi głównej repozytorium zawiera tekst MIT z notą „Copyright 2022 saltyAom”. Pole license w rejestrze npm ma wartość MIT. Opublikowana paczka elysia-1.4.29.tgz zawiera plik package/LICENSE bajt w bajt identyczny z tym z repozytorium. Wykrywacz licencji GitHuba również raportuje MIT. To rzadki przypadek pełnej zgodności i przy audycie zależności nie ma tu nic do wyjaśniania.

Znacznie więcej zamieszania robi zakres nazw. Wtyczki istnieją na npm w dwóch wariantach: starszym @elysiajs/ i nowszym @elysia/, publikowanych przez tego samego opiekuna. Adapter dla Node to @elysiajs/node w wersji 1.4.5 oraz @elysia/node w wersji 1.4.6. Klient to @elysiajs/eden w wersji 1.4.9 ze 110 wydaniami za sobą oraz @elysia/eden w wersji 1.4.10 z ośmioma wydaniami. Dokumentacja na stronie projektu instruuje instalację z nowego zakresu, natomiast plik README w opublikowanej paczce Eden pokazuje stary zakres i przy okazji stary interfejs, z funkcją edenTreaty i nieistniejącym już opakowaniem schema. Przy pierwszym kontakcie z projektem łatwo zainstalować pakiet z jednego zakresu, a przepisać przykład z drugiego.

Wariantu płatnego nie ma. Finansowanie idzie przez GitHub Sponsors, a projekt jest w praktyce dziełem jednego autora publikującego pod kontem aomkirby123. To niesie znane ryzyko: brak umowy wsparcia, którą można wyegzekwować, i zależność rozwoju od dostępności jednej osoby.

Trasy, walidacja i wyprowadzanie typów

Instalacja i uruchomienie zajmują kilka poleceń.

Code
Bash
# nowy projekt ze szkieletem
bun create elysia app

# albo dodanie do istniejącego projektu
bun add elysia

# klient i dokumentacja
bun add @elysia/eden @elysia/openapi

# uruchomienie z przeładowaniem
bun --watch src/index.ts

# kompilacja do pojedynczego pliku wykonywalnego
bun build --compile --minify --sourcemap src/index.ts --outfile server

Sam serwer wygląda tak. Poniższy przykład używa wyłącznie nazw obecnych w wersji 1.4.29.

Code
TypeScript
import { Elysia, t, status } from 'elysia'
import { openapi } from '@elysia/openapi'

const app = new Elysia({
  prefix: '/api',
  aot: true,
  normalize: 'exactMirror',
  strictPath: false,
  nativeStaticResponse: true
})
  .use(openapi())
  .model({
    user: t.Object({
      id: t.Numeric(),
      email: t.String({ format: 'email' }),
      displayName: t.String({ minLength: 2, maxLength: 40 })
    })
  })
  .state('requestCount', 0)
  .derive(({ headers }) => ({
    traceId: headers['x-trace-id'] ?? crypto.randomUUID()
  }))
  .get(
    '/users',
    ({ query, store }) => {
      store.requestCount += 1
      return { items: [], page: query.page }
    },
    {
      query: t.Object({
        page: t.Numeric({ default: 1 }),
        search: t.Optional(t.String())
      }),
      detail: { summary: 'Lista użytkowników', tags: ['users'] }
    }
  )
  .post('/users', ({ body }) => body, {
    body: 'user',
    response: {
      200: 'user',
      409: t.Object({ message: t.String() })
    }
  })
  .get('/users/:id', ({ params: { id } }) => {
    if (id > 1000) return status(404, { message: 'Nie znaleziono' })
    return { id, email: 'a@b.pl', displayName: 'Ada' }
  }, {
    params: t.Object({ id: t.Numeric() })
  })
  .onError(({ code, error, set }) => {
    if (code === 'VALIDATION') {
      set.status = 422
      return { message: error.message }
    }
  })
  .listen(3000)

export type App = typeof app

Kilka rzeczy zasługuje na komentarz. Pole body: 'user' odwołuje się do schematu zarejestrowanego przez .model(), dzięki czemu ten sam kształt trafia do walidacji, do typów i do dokumentu OpenAPI w jednym miejscu. Funkcja status zastąpiła wcześniejsze error i zwraca odpowiedź z konkretnym kodem, zachowując ten kod w typie trasy. Opcja normalize ustawiona na exactMirror odcina z odpowiedzi pola nieopisane w schemacie, co przy zwracaniu wierszy z bazy chroni przed wypuszczeniem kolumny z hasłem. Ostatnia linia, export type App = typeof app, to cały mechanizm eksportu kontraktu.

Powtarzalną logikę opakowuje się w makro, które dokłada do trasy zdarzenia cyklu życia razem z typami.

Code
TypeScript
import { Elysia, status } from 'elysia'

const auth = new Elysia({ name: 'auth' }).macro({
  requireUser: {
    resolve({ headers }) {
      const token = headers.authorization?.slice(7)
      if (!token) return status(401, { message: 'Brak tokenu' })
      return { user: { id: 1, role: 'admin' as const } }
    }
  }
})

const routes = new Elysia()
  .use(auth)
  .get('/me', ({ user }) => user, { requireUser: true })

Pole user w funkcji obsługi jest znane kompilatorowi, ponieważ pochodzi z resolve w makrze. Nie ma tu ani rzutowania, ani deklaracji poszerzającej typ kontekstu.

Eden, czyli te same typy po stronie klienta

Eden to klient, który przyjmuje typ serwera jako parametr generyczny i buduje z niego obiekt odpowiadający drzewu ścieżek. Ścieżka /users/:id staje się wywołaniem api.users({ id: 5 }).get(), a metoda HTTP jest ostatnim wywołaniem w łańcuchu.

Code
TypeScript
import { treaty } from '@elysia/eden'
import type { App } from '../server/index'

const api = treaty<App>('localhost:3000', {
  headers: { 'x-trace-id': crypto.randomUUID() }
})

const { data, error } = await api.api.users.get({
  query: { page: 2, search: 'ada' }
})

if (error) {
  switch (error.status) {
    case 422:
      console.error(error.value.message)
      break
    default:
      throw error
  }
}

console.log(data?.items)

const created = await api.api.users.post({
  id: 7,
  email: 'ada@example.com',
  displayName: 'Ada'
})

Efekt jest ten sam, co przy tRPC: zmiana kształtu odpowiedzi na serwerze psuje kompilację klienta, zanim ktokolwiek uruchomi aplikację. Różnica leży w warstwie transportu. tRPC opakowuje wywołania we własny format i wymaga swojego klienta po drugiej stronie. Elysia wystawia zwykłe trasy HTTP, które można wywołać z curl, z aplikacji mobilnej albo z usługi napisanej w innym języku, a Eden jest tylko dodatkiem dla klientów w TypeScripcie. Do tego wtyczka OpenAPI generuje dokument opisujący te same trasy, więc konsument spoza ekosystemu też ma kontrakt.

Ograniczenie jest istotne: rozdzielenie serwera i klienta na dwa repozytoria psuje ten układ, bo typeof app musi być dostępne dla kompilatora po stronie klienta. Działa to w monorepozytorium albo przy publikowaniu paczki z typami. Drugie ograniczenie to koszt kompilacji. Typ instancji rośnie z każdą trasą, a przy kilkuset trasach serwer języka TypeScript zaczyna wyraźnie zwalniać. Sposobem na to jest podział aplikacji na mniejsze instancje łączone przez .use().

Bun, Node i inne środowiska uruchomieniowe

Elysia powstała dla Buna i tam ma najkrótszą ścieżkę: adapter elysia/adapter/bun jest domyślny, listen mapuje się na Bun.serve, a gniazda sieciowe działają bez dodatkowej biblioteki. Paczka nie deklaruje jednak pola engines, a w katalogu dist/adapter leżą trzy warianty: bun, web-standard i cloudflare-worker.

Praca na Node jest realna i sprowadza się do jednej opcji konstruktora.

Code
TypeScript
import { Elysia } from 'elysia'
import { node } from '@elysia/node'

const app = new Elysia({ adapter: node() })
  .get('/', () => 'Hello Node')
  .listen(3000)

// wariant dla Deno: bez listen, przez Web Standard
// Deno.serve(app.fetch)

// wariant dla testów jednostkowych, bez otwierania portu
const response = await app.handle(
  new Request('http://localhost/', { method: 'GET' })
)
console.log(response.status, await response.text())

Adapter dla Node opiera się na pakietach srvx i crossws, które mapują interfejs Web Standard na serwer HTTP Node i na gniazda sieciowe. Deno nie potrzebuje adaptera, wystarczy przekazać app.fetch do Deno.serve. Cloudflare Workers mają własny adapter w paczce.

Warstwa adapterów nie zdejmuje jednak głównego zastrzeżenia. Dokumentacja, przykłady, wtyczki i odpowiedzi w społeczności zakładają Buna. Wtyczki sięgają po API Buna, kompilacja do pojedynczego pliku wykonywalnego to funkcja Buna, a tryb wielordzeniowy opisany w dokumentacji wdrożeniowej korzysta z SO_REUSEPORT, które działa wyłącznie na Linuksie i tylko pod Bunem. Poza Bunem dostajesz działający framework, ale bez części tego, dla czego się go wybiera. Jeśli Twoja platforma wdrożeniowa nie ma obrazu z Bunem albo zespół nie chce wprowadzać drugiego środowiska uruchomieniowego obok Node, to jest argument rozstrzygający, a nie drobiazg.

Warto zauważyć, że materiały projektu podają liczbę osiemnastu razy szybciej od Express, opartą na zestawie TechEmpower. Ten fragment README jest w opublikowanej paczce zakomentowany, a każdy taki wynik pochodzi z testu syntetycznego, w którym większość czasu żądania zajmuje warstwa transportu, a nie kod aplikacji. W usłudze odpytującej bazę danych różnica między frameworkami zwykle znika w szumie.

Elysia na tle alternatyw

CechaElysiaHonotRPCFastify
Domyślne środowiskoBundowolne zgodne z Web Standardwarstwa nad frameworkiemNode
Typy po stronie klientaEden treatyklient hcrdzeń bibliotekibrak
Protokółzwykły HTTP i OpenAPIzwykły HTTPwłasny format nad HTTPzwykły HTTP
WalidacjaTypeBox przez eksport tdowolna przez pośrednikadowolna, na przykład ZodJSON Schema przez Ajv
Bieżąca wersja1.4.294.13.311.18.05.12.1
LicencjaMITMITMITMIT

Wybór rozstrzyga się na trzech pytaniach. Jeśli już stoisz na Bunie i piszesz interfejs programistyczny konsumowany przez własny frontend w TypeScripcie, Elysia daje najkrótszą drogę od trasy do wywołania w przeglądarce. Jeśli musisz wdrażać na wiele platform albo na środowisko brzegowe, Hono robi to samo w kwestii typów przez klienta hc, a przy tym działa wszędzie tam, gdzie działa Web Standard. Jeśli klient i serwer są w jednym repozytorium i nikt spoza zespołu nie będzie wywoływał tych tras, tRPC pozostaje najbardziej dopracowaną opcją, choć płacisz za to własnym protokołem.

Typowe błędy

Pierwszy to rozbicie łańcucha na osobne instrukcje. Zapis app.get(...) w oddzielnej linii, bez przypisania wyniku, wykonuje się poprawnie w czasie działania, ale nie dokłada trasy do typu instancji. Efekt jest mylący: serwer odpowiada, a Eden twierdzi, że takiej ścieżki nie ma. Łańcuch musi pozostać jednym wyrażeniem albo wynik trzeba przypisać ponownie.

Drugi to pomieszanie zakresów @elysia/ i @elysiajs/ w jednym projekcie. Obie wersje pakietu potrafią wylądować w node_modules naraz, a wtedy dostajesz dwa różne moduły z tymi samymi nazwami typów i błędy kompilacji, których treść nie wskazuje przyczyny. Wybierz jeden zakres i trzymaj się go w całym repozytorium.

Trzeci to przepisywanie przykładów z README opublikowanej paczki Eden. Znajduje się tam funkcja edenTreaty z pierwszej generacji klienta oraz opakowanie schema przy definicji trasy, którego dzisiejsza wersja nie przyjmuje. Aktualne przykłady są w dokumentacji na stronie projektu, nie w paczce.

Czwarty to brak schematu odpowiedzi. Bez pola response Elysia zwróci to, co poda funkcja obsługi, razem z każdą kolumną wyciągniętą z bazy. Schemat odpowiedzi w połączeniu z normalize ustawionym na exactMirror odcina nadmiarowe pola i jednocześnie opisuje kontrakt w dokumencie OpenAPI.

Piąty to traktowanie parametrów zapytania jak liczb. Wszystko, co przychodzi w adresie, jest tekstem. Schemat t.Number() odrzuci wartość "2", a t.Numeric() przekształci ją na liczbę przed wywołaniem funkcji obsługi. Ta sama pułapka dotyczy parametrów ścieżki.

Szósty to zakładanie, że wtyczka napisana dla Buna zadziała po podłączeniu adaptera Node. Adapter tłumaczy warstwę serwera, a nie każde wywołanie API Buna użyte wewnątrz wtyczki. Przy pracy na Node każdą wtyczkę trzeba sprawdzić osobno.

Siódmy to jedna wielka instancja z kilkuset trasami. Kompilator TypeScriptu musi utrzymać jeden typ opisujący całość, a czas podpowiedzi w edytorze rośnie razem z nim. Podział na moduły domenowe łączone przez .use() rozwiązuje problem, zanim się pojawi.

FAQ

Czy Elysia działa na Node?

Tak, przez adapter instalowany osobno i przekazany do konstruktora jako new Elysia({ adapter: node() }). Adapter opiera się na pakietach srvx i crossws. Sam framework nie deklaruje pola engines, więc menedżer pakietów nie zablokuje instalacji na Node. Poza Bunem tracisz kompilację do pliku wykonywalnego i część wtyczek sięgających po API Buna.

Czym Eden różni się od tRPC?

Zakresem ingerencji w protokół. tRPC definiuje własny format wywołań i wymaga swojego klienta. Elysia wystawia zwykłe trasy HTTP, a Eden jest opcjonalną nakładką dla klientów w TypeScripcie. Trasę można wywołać z curl albo z usługi w innym języku, a wtyczka OpenAPI generuje dla niej dokument.

Który zakres nazw wybrać przy instalacji wtyczek?

Dokumentacja projektu instruuje instalację z zakresu @elysia/ i tam wersje są nowsze: adapter Node ma numer 1.4.6 wobec 1.4.5 w zakresie @elysiajs/, a Eden 1.4.10 wobec 1.4.9. Starszy zakres ma za sobą znacznie więcej wydań i nadal jest utrzymywany przez tego samego opiekuna. Mieszanie obu w jednym projekcie kończy się konfliktem typów.

Czy Elysia jest gotowa na produkcję?

Linia 1.4 jest stabilna i szeroko używana, ale ostatnie wydanie stabilne pochodzi z 16 czerwca 2026 roku, podczas gdy prace idą w stronę wersji drugiej wydawanej jako beta od sierpnia 2026 roku. Repozytorium ma około 18,9 tysiąca gwiazdek, 568 rozgałęzień i 373 otwarte zgłoszenia. Planując wdrożenie, uwzględnij przyszłą migrację.

Czy Elysia kosztuje?

Nie. Cały projekt jest na licencji MIT, bez wariantu płatnego i bez ograniczeń w użyciu komercyjnym. Finansowanie idzie przez GitHub Sponsors, a autorem jest w praktyce jedna osoba, co przekłada się na brak umowy wsparcia i na zależność rozwoju od jej dostępności.

Dokumentację znajdziesz na stronie projektu, opis klienta w dziale Eden, a kod źródłowy w repozytorium na GitHubie.

Czytaj dalej

Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie