@trishchuk/bun-nestjs-adapter
v0.2.0
Published
Native Bun.serve adapter for NestJS — HTTP, WebSocket, SSE, streaming, file uploads.
Maintainers
Readme
@trishchuk/bun-nestjs-adapter
platform-bun runs a NestJS application directly on top of Bun.serve — the
Web-Fetch API surface — instead of going through node:http.
It implements AbstractHttpAdapter and AbstractWsAdapter end-to-end, with a
fast path that hands routing to Bun's native C++ matcher (Bun.serve({ routes }))
whenever no middleware/static/CORS feature is active.
What's in the box
- HTTP: all verbs, JSON / urlencoded / text body parsing, multi-match
dispatch (header / media-type / custom versioning),
applyVersionFilterfor URI / HEADER / MEDIA_TYPE / CUSTOM, CORS, Express-compatiblereq/resshim methods. - Static assets via
Bun.file()(useStaticAssets(root, { prefix, index })). - Streaming:
StreamableFileand bare NodeReadablereturns are converted to a WebReadableStreamand piped throughBun.serve. - Server-Sent Events (
@Sse()) — the response carries a Node-Writableshim and the request bridgesAbortSignalto'close'so Nest's coreRouterResponseController.sse()works unchanged. - File uploads:
BunFileInterceptor,BunFilesInterceptor,BunAnyFilesInterceptor— read multipart bodies via the nativeRequest.formData()and populatereq.file/req.files. Optionallimits(fileSize,files) reject oversized / too-many-file uploads. - WebSocket:
BunWsAdapteron top ofBun.serve({ websocket })with shim EventEmitters that map Bun callbacks to Node-wsevents; native pub/sub (client.subscribe / unsubscribe / publish,server.publish). A@WebSocketGateway()with no port (or the HTTP port) shares the mainBun.servevia HTTP upgrade — no second listener; an explicit different port still spins up its own server.
Documentation
Full guides live in docs/:
Getting started ·
HTTP ·
Streaming & SSE ·
File uploads ·
WebSockets ·
GraphQL ·
Benchmarks ·
Architecture.
Install
bun add @trishchuk/bun-nestjs-adapter
# peer deps if not already in your tree:
bun add @nestjs/common @nestjs/core @nestjs/websockets rxjs reflect-metadataQuick start
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { BunHttpAdapter, BunWsAdapter } from '@trishchuk/bun-nestjs-adapter';
import { AppModule } from './app.module';
const app = await NestFactory.create(AppModule, new BunHttpAdapter());
app.useWebSocketAdapter(new BunWsAdapter(app)); // optional
await app.listen(3000);File uploads (with Safety Limits)
Upload interceptors mirror the behavior of @nestjs/platform-express but read multipart bodies asynchronously via Bun's native Request.formData() without importing Multer.
You can configure limits (fileSize in bytes, files count) to prevent out-of-memory errors from oversized uploads:
import {
BunFileInterceptor,
type BunUploadedFile,
} from '@trishchuk/bun-nestjs-adapter';
import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
@Controller('uploads')
export class UploadsController {
@Post()
@UseInterceptors(
BunFileInterceptor('avatar', {
limits: {
fileSize: 10 * 1024 * 1024, // 10MB limit
files: 1, // max 1 file
},
}),
)
upload(@UploadedFile() file: BunUploadedFile) {
return { name: file.originalname, size: file.size };
}
}If limits are exceeded, a standard NestJS exception is thrown:
PayloadTooLargeException(HTTP 413) iffileSizeis exceeded.BadRequestException(HTTP 400) iffilescount is exceeded.
There are also BunFilesInterceptor(fieldName, maxCount, options) and BunAnyFilesInterceptor(options) for multiple fields/files.
WebSockets (Shared Port or Explicit Listener)
When you use BunWsAdapter, @WebSocketGateway() gateways can share the main HTTP server port (no separate listener required) or spin up a dedicated server on another port.
To share the HTTP port, configure the gateway without a port (or set it to 0 or the HTTP port):
import { WebSocketGateway, WebSocketServer, SubscribeMessage, MessageBody } from '@nestjs/websockets';
@WebSocketGateway({ path: '/ws' })
export class ChatGateway {
@WebSocketServer()
server: any;
@SubscribeMessage('ping')
handlePing(@MessageBody() data: any) {
return { event: 'pong', data };
}
}Under the hood, BunHttpAdapter intercepts WebSocket upgrade requests and upgrades them using Bun's native server.upgrade callback, passing client connection data to the ws adapter server instance.
Dynamic Route Reloading (Lazy Modules)
If you load routes dynamically at runtime (for example, using Nest's LazyModuleLoader), BunHttpAdapter automatically intercepts new route/middleware additions and calls server.reload({ routes }) to refresh the native Bun.serve routing tables. No manual action or server restart is required.
SSE
import { Controller, Sse } from '@nestjs/common';
import { interval, map } from 'rxjs';
@Controller('events')
export class EventsController {
@Sse()
stream() {
return interval(1000).pipe(map(t => ({ data: { tick: t } })));
}
}How it stacks up
Single-route GET / benchmark on M-series Mac, 50 concurrent connections,
30 seconds, measured under a Bun-native fetch client (autocannon's Node
HTTP client systematically under-counts Bun.serve — see
tests/bench/bun-bench.ts):
| Target | RPS | p50 / p99 (ms) |
| --- | ---: | ---: |
| bun (raw) | 130 308 | 0.42 / 0.91 |
| nest-bun (this adapter) | 102 319 | 0.42 / 1.00 |
| nest-fastify | 84 936 | 0.53 / 1.29 |
| nest-express | 69 011 | 0.66 / 1.61 |
So Nest-Bun is +22 % vs nest-fastify and +48 % vs nest-express in RPS, with p99 latency 23–38 % lower. Numbers fluctuate ±15 % across runs due to thermal throttling — re-measure on your hardware:
bun run bench
# or:
bun run bench:quick # 5 s × 50 connectionsFor a fuller picture — a GraphQL-vs-REST matrix across bun / express / fastify
(driven by external k6 so the load generator never shares a runtime with
the server under test), plus an Apollo-vs-Yoga-on-bun comparison — see
BENCHMARK.md (bun run bench:matrix).
Compatibility test against upstream nestjs/nest fixtures
tests/integration/upstream-fixtures.spec.ts boots BunHttpAdapter against
the real integration/hello-world, integration/versioning, integration/cors
and friends from the upstream nestjs/nest repository (loaded as a git
submodule under fixtures/nestjs-nest/):
bun run fixtures:install # one-time: clone submodule + build nest packages
bun run test:fixtures # run our compatibility suite against themThis means every regression in upstream Nest core is automatically picked up
by our CI. Update with bun run fixtures:update.
Development
bun install
bun run typecheck
bun run lint
bun run test
bun run build # tsup → dist/{index.js, index.cjs, index.d.ts}Repository layout
src/ # source — published as @trishchuk/bun-nestjs-adapter
adapters/
bun-http-adapter.ts # BunHttpAdapter (~1.6k LOC)
bun-ws-adapter.ts # BunWsAdapter
interceptors/
bun-file-interceptor.ts # BunFileInterceptor & friends
index.ts
tests/
preload.ts # reflect-metadata + Mocha-shim
integration/ # our own e2e specs (fetch client)
fixtures/own/ # local AppModules / controllers
upstream-fixtures.spec.ts # smoke-tests vs upstream nestjs/nest fixtures
bench/
bun-bench.ts # parallel-fetch benchmark runner
frameworks/ # express / fastify / bun / nest-* targets
fixtures/
nestjs-nest/ # git submodule, pinned to a known-good SHA
scripts/
fixtures-install.sh # clone submodule + build upstream nest
fixtures-update.sh # bump submodule SHARoadmap
- [x] Shared-port WebSocket upgrade —
@WebSocketGateway()with no port shares the HTTP server'sBun.serve. - [~] GraphQL Yoga driver (
BunYogaDriver) — fetch-native driver prototyped and benchmarked (bench-only, ~+40 % RPS vs Apollo on bun); not yet a published export. - [x]
setViewEngine+render()—ejs/pug/handlebarsvia optional peer deps (install the engine you use; not bundled). - [x] Cookie helper —
res.cookie/res.clearCookie, signed cookies (enableCookieParser(secret)) andreq.cookies/req.signedCookies. - [x] Unix-socket binding —
app.listen('/path/to.sock')(Bun native).
Prior art
@krisanalfa/bunest-adapter— the most mature community native-Bun adapter; shipsBunYogaDriver, file uploads, SSE, streaming. Different routing strategy (own middleware engine rather thanBun.serve({ routes })).@kiyasov/platform-hono— wraps Hono, runs under Bun / Deno / Node. Not a native-Bun adapter (uses Hono's router on top).- Upstream tickets: Bun #1641, Nest #13073, Nest #13881.
