Bun production'da: Node.js'ten geçiş rehberi
Bir Node.js projesini Bun'a taşımayı ilk denediğimde her şeyin "drop-in replacement" olacağını düşünüyordum. bun run start yazdım, sunucu ayağa kalktı, ilk birkaç endpoint çalıştı. Sonra native addon kullanan bir bağımlılık patladı, node:dns resolver davranışı farklı çıktı, bir CI pipeline'ı sessizce yanlış çıktı üretti. Bun hızlı, evet. Ama geçiş süreci "runtime'ı değiştir, keyfine bak" kadar basit değil.
Bu yazıda Bun v1.2+ sürümünü temel alıyorum. Her sürümde uyumluluk iyileşiyor; burada anlattığım bazı kısıtlamalar ileride ortadan kalkabilir.
Node.js uyumluluğu: nerede duruyor?
Bun, Node.js API'lerinin büyük bölümünü implemente ediyor. fs, path, crypto, http, https, stream, buffer, child_process, worker_threads gibi modüller çalışır durumda. Bun'ın kendi dokümantasyonundaki uyumluluk tablosuna göre (bun.sh/docs/runtime/nodejs-apis) node: prefix'li import'ların çoğu destekleniyor.
Ama "çoğu" ile "tamamı" arasındaki fark, production'da sizi yakalayan fark.
Bilinen eksikler ve farklılıklar şunlar:
node:vmmodülü kısıtlı.vm.createContextvevm.runInContexttam izolasyon sağlamıyor. Eğer bir bağımlılığınız sandbox içinvmkullanıyorsa (bazı template engine'ler bunu yapar), beklenmedik davranışlar görebilirsiniz.- Native N-API addon'ları (
.nodedosyaları) Bun v1.2'den itibaren destekleniyor, ama her addon çalışacak diye bir garanti yok.bcrypt,sharpgibi yaygın paketlerin native binding'leri genelde sorunsuz. Daha niş addon'larda test etmeden geçmeyin. node:dnsresolver'ı farklı sırada çözümleme yapabiliyor. Node.js varsayılan olarakdns.lookupile OS resolver'ını kullanırken, Bun bazı durumlarda kendi resolver'ını tercih ediyor. Bu, Docker container'larında/etc/hostsgirdilerinin farklı çözümlenmesine yol açabiliyor.node:clustermodülü Bun'da yok. Çoklu process modeli istiyorsanızworker_threadsveya Bun'ın kendiBun.servemulti-threading desteğini kullanmanız gerekiyor.
Geçiş adımları
Package manager olarak bun install
En az riskli başlangıç noktası, runtime'ı değiştirmeden sadece package manager'ı değiştirmek. bun install, npm ve yarn'a kıyasla tutarlı biçimde daha hızlı.
Bir projede denedim: 1.847 paketlik bir node_modules kurulumu npm'de 38 saniye, bun install'da 4.2 saniye sürdü (lokal cache sıcak, aynı makine, M2 MacBook Pro). Soğuk cache'te bile fark belirgin: npm 52 saniye, Bun 11 saniye.
# Mevcut lockfile'dan geçiş
rm -rf node_modules
bun installBun, bun.lockb adında binary bir lockfile üretiyor. Eğer ekipte npm veya yarn kullanan insanlar da varsa bu bir sorun. v1.2'den itibaren bun install --lockfile-format=text ile TOML tabanlı bun.lock dosyası üretebilirsiniz:
bun install --lockfile-format=textBu dosya git diff'te okunabilir, code review'da ne değiştiğini görebilirsiniz.
Bir uyarı: package-lock.json veya yarn.lock ile bun.lockb arasında çözümleme farkları olabilir. Özellikle overrides ve resolutions alanları farklı yorumlanabiliyor. Geçişten sonra bun install çıktısını dikkatle kontrol edin, beklenmedik sürüm değişiklikleri var mı diye.
Test runner olarak bun test
bun test, Jest-uyumlu bir API sunuyor. describe, it, expect, beforeEach, afterEach, mock gibi temel yapılar çalışıyor.
// math.test.ts
import { describe, it, expect } from "bun:test";
import { add } from "./math";
describe("add", () => {
it("iki sayıyı toplar", () => {
expect(add(2, 3)).toBe(5);
});
it("negatif sayılarla çalışır", () => {
expect(add(-1, -4)).toBe(-5);
});
});bun testJest'ten geçiş yapıyorsanız dikkat edilecek noktalar var. jest.mock() yerine mock.module() kullanılıyor:
import { mock } from "bun:test";
mock.module("./db", () => ({
query: () => Promise.resolve([{ id: 1 }]),
}));Jest'in moduleNameMapper, transformIgnorePatterns gibi config seçenekleri Bun'da yok. TypeScript ve JSX desteği zaten built-in olduğu için çoğu transform config'ine ihtiyaç duymazsınız, ama eğer jest.config.js'te özel transform'larınız varsa bunları port etmeniz gerekecek.
Hız farkı somut: 340 test içeren bir projede Jest (ts-jest ile) 14 saniye, bun test 2.1 saniye sürdü. Vitest de bu aralıkta hızlı bir alternatif; Bun'ın avantajı ayrı bir test runner kurmaya gerek kalmaması.
HTTP sunucusu: Bun.serve
Express veya Fastify projenizi olduğu gibi bun run server.ts ile çalıştırabilirsiniz. Node.js'in http modülü Bun'da implemente edilmiş durumda. Ama Bun'ın kendi HTTP API'si (Bun.serve) farklı bir performans profili sunuyor.
// server.ts
Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/api/health") {
return new Response("ok");
}
if (url.pathname === "/api/data") {
return Response.json({ ts: Date.now(), status: "running" });
}
return new Response("Not Found", { status: 404 });
},
});
console.log("Server listening on :3000");Bun.serve Web Standard API'leri kullanıyor: Request, Response, URL. Bu API Hono.js gibi framework'lerle doğal uyum sağlıyor; Hono zaten Bun adapter'ı ile birlikte geliyor.
Eğer Elysia.js kullanıyorsanız zaten Bun-native bir framework seçmiş oluyorsunuz, geçiş maliyeti sıfır.
Gerçek performans sayıları
Mikro-benchmark'lar yanıltıcı olabiliyor. "Saniyede 1 milyon request" rakamları genelde boş bir response döndüren, hiçbir iş yapmayan sunucularda ölçülüyor. Gerçek bir uygulamada I/O, veritabanı sorguları, JSON serialization gibi işler darboğaz oluşturuyor.
Aşağıdaki ölçümleri kendim yaptım. Ortam: M2 MacBook Pro, 16 GB RAM, Bun 1.2.14, Node.js 22.12. Test aracı: bombardier -c 50 -d 10s.
Senaryo 1: Statik JSON response (framework yok, sadece runtime HTTP server'ı).
| Runtime | req/s (ortalama) | p99 latency |
|---|---|---|
| Node.js (http module) | 48.200 | 2.1 ms |
| Bun.serve | 112.400 | 0.8 ms |
Senaryo 2: Express.js uygulaması, PostgreSQL'den tek satır okuma (pg paketi), JSON response.
| Runtime | req/s (ortalama) | p99 latency |
|---|---|---|
| Node.js 22 + Express 4 | 4.850 | 18 ms |
| Bun + Express 4 | 5.320 | 15 ms |
İkinci senaryoda fark dramatik değil. Darboğaz veritabanı round-trip'i. Runtime değişikliğinin tek başına bir uygulamayı "5 kat hızlı" yapacağını düşünmek hata. Kazancın büyük kısmı CPU-bound işlerde, startup süresinde, script çalıştırma hızında ortaya çıkıyor.
Startup süresi ise gerçekten farklı bir dünya:
# Node.js
time node server.js
# 0.31s
# Bun
time bun server.ts
# 0.02sServerless ve edge ortamlarında bu fark anlamlı. Cloudflare Workers gibi cold start'ın kritik olduğu yerlerde Bun'ın hızlı başlaması gerçek bir avantaj.
Tuzaklar ve dikkat edilecekler
TypeScript çalıştırma ayrımı
Bun, .ts dosyalarını doğrudan çalıştırıyor, ayrı bir derleme adımı yok. Bu geliştirme sırasında harika. Ama Bun, tip kontrolü yapmıyor. tsc --noEmit adımını CI pipeline'ınızdan çıkarmayın:
# CI'da ikisini birlikte kullanın
tsc --noEmit
bun testTip hatası olan kod Bun'da sorunsuz çalışır, runtime'da patlayıncaya kadar fark etmezsiniz.
Ecosystem uyumsuzlukları
Bazı paketler Node.js-specific API'lere sıkı bağımlı. Karşılaştığım örnekler:
node-cronçalışıyor amacronpaketinin eski sürümlerinode:vmkullanımı yüzünden sorun çıkarabiliyor.winstonv3 çalışıyor, ama custom transport'larda stream davranışı farklılıkları görebilirsiniz.- Prisma Client, Bun'da çalışıyor (Prisma v5+). Drizzle ORM Bun ile sorunsuz çalışıyor, çünkü native addon'a bağımlılığı yok.
Geçiş yapmadan önce projenizin bağımlılık ağacını tarayın. Şu komut, native addon kullanan paketleri bulmanıza yardımcı olur:
find node_modules -name "*.node" -o -name "binding.gyp" | head -20Bu listede çıkan her paket potansiyel bir uyumsuzluk noktası.
Docker ve production deployment
Bun'ın resmi Docker image'ı oven/bun olarak Docker Hub'da mevcut:
FROM oven/bun:1.2 AS base
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production
COPY . .
EXPOSE 3000
CMD ["bun", "run", "server.ts"]--frozen-lockfile flag'i lockfile'ın değişmediğini garanti eder; CI ortamında mutlaka kullanın.
Image boyutu karşılaştırması: node:22-slim yaklaşık 180 MB, oven/bun:1.2-slim yaklaşık 100 MB. Alpine varyantları daha küçük ama Bun'ın Alpine desteği Node.js kadar olgun değil, glibc bağımlılıkları sorun çıkarabiliyor.
Gradual migration stratejisi
Her şeyi bir günde taşımak yerine şu sırayı öneriyorum:
bun installile başlayın. En az riskli adım. Lockfile'ı commit edin, CI'da çalıştığını doğrulayın.- Script'leri taşıyın.
bun run dev,bun run buildgibi package.json script'leri genelde sorunsuz çalışır. - Test runner'ı taşıyın. Mevcut test'lerinizi
bun testile çalıştırın, başarısız olanları inceleyin. - Runtime'ı production'da taşıyın. Önce staging'de. Monitoring'inizi (error rate, latency percentile'lar) yakından izleyin.
Ne zaman geçmeli, ne zaman geçmemeli?
Bun'a geçiş mantıklı olan durumlar: TypeScript-first projeler, hızlı startup gerektiren serverless uygulamalar, monorepo'larda script çalıştırma süresinin acı verdiği ortamlar, greenfield projeler.
Geçişin riskli olduğu durumlar: native addon'lara ağır bağımlılık (örneğin canvas, sqlite3 gibi C++ binding'leri yoğun kullanan projeler), node:cluster ile multi-process mimari kullanan uygulamalar, Node.js'e özgü APM/profiling araçlarına (New Relic, Datadog Node.js agent) bağımlılık.
Bir runtime değişikliği, framework değiştirmekten daha derin bir karar. Test suite'iniz güçlüyse ve staging ortamınızda yeterince trafik simüle edebiliyorsanız, geçiş genelde sorunsuz oluyor. Test coverage'ı düşük bir projede runtime değiştirmek ise, karanlıkta koşmaya benzer.