@krisanalfa/bunest-adapter 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +866 -0
- package/dist/bun.adapter.d.ts +93 -0
- package/dist/bun.file.interceptor.d.ts +9 -0
- package/dist/bun.request.d.ts +339 -0
- package/dist/bun.response.d.ts +251 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1089 -0
- package/dist/index.js.map +17 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
# Native Bun Adapter for NestJS
|
|
2
|
+
|
|
3
|
+
This project provides a native Bun adapter for NestJS, allowing developers to leverage the performance benefits of the Bun runtime while using the powerful features of the NestJS framework.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
### Native Bun adapter for NestJS
|
|
8
|
+
|
|
9
|
+
Easy to set up and use Bun as the underlying HTTP server for your NestJS applications.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { BunAdapter } from "@krisanalfa/bunest-adapter";
|
|
13
|
+
import { Logger } from "@nestjs/common";
|
|
14
|
+
import { NestFactory } from "@nestjs/core";
|
|
15
|
+
import { Server } from "bun";
|
|
16
|
+
|
|
17
|
+
import { AppModule } from "./app.module.js";
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
const app = await NestFactory.create(AppModule, new BunAdapter());
|
|
21
|
+
await app.listen(3000);
|
|
22
|
+
const server = app.getHttpAdapter().getHttpServer() as Server<unknown>;
|
|
23
|
+
Logger.log(`Server started on ${server.url.toString()}`, "NestApplication");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
await main();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
You can also listen on Unix sockets:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
await app.listen("/tmp/nestjs-bun.sock");
|
|
33
|
+
|
|
34
|
+
// Using Bun's fetch API to make requests over the Unix socket
|
|
35
|
+
fetch("http://localhost/", {
|
|
36
|
+
unix: "/tmp/nestjs-bun.sock",
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or even an abstract namespace socket on Linux:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
await app.listen("\0nestjs-bun-abstract-socket");
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
To configure the underlying Bun server options, you can pass them to the `BunAdapter` constructor:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
new BunAdapter({
|
|
50
|
+
id: "my-nestjs-bun-server",
|
|
51
|
+
development: true,
|
|
52
|
+
maxRequestBodySize: 10 * 1024 * 1024, // 10 MB
|
|
53
|
+
idleTimeout: 120_000, // 2 minutes
|
|
54
|
+
tls: {}, // TLS options here (read more about TLS options in the HTTPS section)
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Since Bun only supports these HTTP methods:
|
|
59
|
+
|
|
60
|
+
- GET
|
|
61
|
+
- POST
|
|
62
|
+
- PUT
|
|
63
|
+
- DELETE
|
|
64
|
+
- PATCH
|
|
65
|
+
- OPTIONS
|
|
66
|
+
- HEAD
|
|
67
|
+
|
|
68
|
+
... the Bun adapter will throw an error if you try to use unsupported methods like ALL, COPY or SEARCH.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
@Controller("example")
|
|
72
|
+
class ExampleController {
|
|
73
|
+
@All() // This won't work with Bun adapter
|
|
74
|
+
handleAllMethods() {
|
|
75
|
+
//
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
However, you can still define consumer middleware to use these methods as needed.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
@Controller("example")
|
|
84
|
+
class ExampleController {
|
|
85
|
+
@Get()
|
|
86
|
+
getExample() {
|
|
87
|
+
return { message: "This works!" };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class ExampleMiddleware implements NestMiddleware {
|
|
92
|
+
use(req: BunRequest, res: BunResponse, next: () => void) {
|
|
93
|
+
// Middleware logic here
|
|
94
|
+
// You may send response directly if needed
|
|
95
|
+
res.end("Middleware response");
|
|
96
|
+
next();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@Module({
|
|
101
|
+
controllers: [ExampleController],
|
|
102
|
+
providers: [ExampleMiddleware],
|
|
103
|
+
})
|
|
104
|
+
class ExampleModule implements NestModule {
|
|
105
|
+
configure(consumer: MiddlewareConsumer) {
|
|
106
|
+
consumer.apply(ExampleMiddleware).forRoutes({
|
|
107
|
+
path: "example",
|
|
108
|
+
method: RequestMethod.ALL,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const response = fetch("http://localhost:3000/example", {
|
|
114
|
+
method: "TRACE", // This will be handled by the middleware
|
|
115
|
+
});
|
|
116
|
+
console.log(await response.text()); // Outputs: "Middleware response"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Full NestJS Feature Support
|
|
120
|
+
|
|
121
|
+
The Bun adapter supports all major NestJS features including:
|
|
122
|
+
|
|
123
|
+
#### Controllers & HTTP Methods
|
|
124
|
+
|
|
125
|
+
Full support for all HTTP methods and decorators:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
@Controller("users", { host: ":account.example.com" } /* Works too */)
|
|
129
|
+
class UsersController {
|
|
130
|
+
@Get()
|
|
131
|
+
findAll(@Query("search") search?: string) {
|
|
132
|
+
return { users: [] };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@Post()
|
|
136
|
+
create(@Body() data: CreateUserDto) {
|
|
137
|
+
return { created: data };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@Get(":id")
|
|
141
|
+
findOne(@Param("id", ParseIntPipe) id: number) {
|
|
142
|
+
return { user: { id } };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@Put(":id")
|
|
146
|
+
update(@Param("id") id: string, @Body() data: UpdateUserDto) {
|
|
147
|
+
return { updated: data };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@Delete(":id")
|
|
151
|
+
remove(@Param("id") id: string) {
|
|
152
|
+
return { deleted: id };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Patch(":id")
|
|
156
|
+
partialUpdate(
|
|
157
|
+
@HostParam("account") account: string, // Works too
|
|
158
|
+
@Param("id") id: string,
|
|
159
|
+
@Body() data: Partial<UpdateUserDto>,
|
|
160
|
+
) {
|
|
161
|
+
return { updated: data };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
##### Notes on `@HostParam()` and `host` Controller option
|
|
167
|
+
|
|
168
|
+
The BunRequest's hostname always trusts the proxy headers (like `X-Forwarded-Host`) since Bun itself does not provide direct access to the raw socket information. When this header is not present, it falls back to the `Host` header. Ensure that your application is behind a trusted proxy when using `X-Forwarded-Host` to avoid host header injection attacks.
|
|
169
|
+
|
|
170
|
+
In express.js, you would typically configure trusted proxies using `app.set('trust proxy', true)`. See the docs [here](https://expressjs.com/en/guide/behind-proxies.html). But in Bun, this is not applicable. In the future, we may provide a way to configure trusted proxies directly in the Bun adapter. But for now, be cautious when using `@HostParam()` in untrusted environments.
|
|
171
|
+
|
|
172
|
+
#### Middleware
|
|
173
|
+
|
|
174
|
+
Full middleware support with route exclusion and global middleware:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
@Module({
|
|
178
|
+
controllers: [DummyController, AnotherDummyController],
|
|
179
|
+
providers: [DummyMiddleware],
|
|
180
|
+
})
|
|
181
|
+
class DummyModule implements NestModule {
|
|
182
|
+
configure(consumer: MiddlewareConsumer) {
|
|
183
|
+
consumer
|
|
184
|
+
.apply(DummyMiddleware)
|
|
185
|
+
.exclude({
|
|
186
|
+
path: "/dummy/skip",
|
|
187
|
+
method: RequestMethod.GET,
|
|
188
|
+
})
|
|
189
|
+
.forRoutes(DummyController);
|
|
190
|
+
consumer.apply(GlobalMiddleware).forRoutes("*");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### Guards
|
|
196
|
+
|
|
197
|
+
Protect routes with guards:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
@Injectable()
|
|
201
|
+
class AuthGuard implements CanActivate {
|
|
202
|
+
canActivate(context: ExecutionContext): boolean {
|
|
203
|
+
const request = context.switchToHttp().getRequest<BunRequest>();
|
|
204
|
+
return request.headers.get("authorization") !== null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@Controller("protected")
|
|
209
|
+
class ProtectedController {
|
|
210
|
+
@Get()
|
|
211
|
+
@UseGuards(AuthGuard)
|
|
212
|
+
getData() {
|
|
213
|
+
return { data: "secret" };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### Interceptors
|
|
219
|
+
|
|
220
|
+
Including support for cancellable requests:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
@Injectable()
|
|
224
|
+
class CancellableInterceptor implements NestInterceptor {
|
|
225
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
|
226
|
+
const request = context.switchToHttp().getRequest<BunRequest>();
|
|
227
|
+
const signal = request.signal;
|
|
228
|
+
const close$ = fromEvent(signal, "abort");
|
|
229
|
+
|
|
230
|
+
return next.handle().pipe(
|
|
231
|
+
takeUntil(
|
|
232
|
+
close$.pipe(
|
|
233
|
+
mergeMap(async () => {
|
|
234
|
+
// Cleanup on request cancellation
|
|
235
|
+
}),
|
|
236
|
+
),
|
|
237
|
+
),
|
|
238
|
+
defaultIfEmpty(null),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### Exception Filters
|
|
245
|
+
|
|
246
|
+
Custom exception handling:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
@Catch(HttpException)
|
|
250
|
+
class HttpExceptionFilter implements ExceptionFilter {
|
|
251
|
+
catch(exception: HttpException, host: ArgumentsHost) {
|
|
252
|
+
const ctx = host.switchToHttp();
|
|
253
|
+
const response = ctx.getResponse<BunResponse>();
|
|
254
|
+
const request = ctx.getRequest<BunRequest>();
|
|
255
|
+
const status = exception.getStatus();
|
|
256
|
+
|
|
257
|
+
response.setStatus(status);
|
|
258
|
+
response.end({
|
|
259
|
+
statusCode: status,
|
|
260
|
+
timestamp: new Date().toISOString(),
|
|
261
|
+
path: request.pathname,
|
|
262
|
+
message: exception.message,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Usage
|
|
268
|
+
app.useGlobalFilters(new HttpExceptionFilter());
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### Validation
|
|
272
|
+
|
|
273
|
+
Full support for class-validator and pipes:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
class CreateUserDto {
|
|
277
|
+
@IsString()
|
|
278
|
+
name!: string;
|
|
279
|
+
|
|
280
|
+
@IsNumber()
|
|
281
|
+
@Min(0)
|
|
282
|
+
age!: number;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
@Controller("users")
|
|
286
|
+
class UsersController {
|
|
287
|
+
@Post()
|
|
288
|
+
create(@Body() dto: CreateUserDto) {
|
|
289
|
+
return { created: dto };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Enable validation globally
|
|
294
|
+
app.useGlobalPipes(new ValidationPipe());
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
#### File Uploads
|
|
298
|
+
|
|
299
|
+
Native file upload support using File API:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
@Controller("upload")
|
|
303
|
+
class UploadController {
|
|
304
|
+
@Post("single")
|
|
305
|
+
uploadFile(@UploadedFile("file") file: File) {
|
|
306
|
+
return { filename: file.name, size: file.size };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@Post("multiple")
|
|
310
|
+
uploadFiles(@UploadedFiles() files: File[]) {
|
|
311
|
+
return files.map((f) => ({ filename: f.name, size: f.size }));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
To work with Bun's native `BunFile` API for uploads, consider using the `BunFileInterceptor` provided by this package (see the Bun File API Support section below).
|
|
317
|
+
|
|
318
|
+
#### Streaming Responses
|
|
319
|
+
|
|
320
|
+
Support for [`StreamableFile`](https://docs.nestjs.com/techniques/streaming-files#streamable-file-class):
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
@Controller("files")
|
|
324
|
+
class FilesController {
|
|
325
|
+
@Get("download")
|
|
326
|
+
@Header("Content-Type", "text/plain")
|
|
327
|
+
download() {
|
|
328
|
+
const buffer = new TextEncoder().encode("File content");
|
|
329
|
+
return new StreamableFile(buffer, {
|
|
330
|
+
type: "text/plain",
|
|
331
|
+
disposition: 'attachment; filename="file.txt"',
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
#### Versioning
|
|
338
|
+
|
|
339
|
+
Full API [versioning](https://docs.nestjs.com/techniques/versioning) support (URI, Header, Media Type, Custom):
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
// URI Versioning
|
|
343
|
+
app.enableVersioning({
|
|
344
|
+
type: VersioningType.URI,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
@Controller("cats")
|
|
348
|
+
@Version("1")
|
|
349
|
+
class CatsControllerV1 {
|
|
350
|
+
@Get()
|
|
351
|
+
findAll() {
|
|
352
|
+
return { version: "1", cats: [] };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@Controller("cats")
|
|
357
|
+
@Version("2")
|
|
358
|
+
class CatsControllerV2 {
|
|
359
|
+
@Get()
|
|
360
|
+
findAll() {
|
|
361
|
+
return { version: "2", cats: [], pagination: true };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Access via: /v1/cats and /v2/cats
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
#### CORS
|
|
369
|
+
|
|
370
|
+
Built-in CORS support with dynamic configuration:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
// Simple CORS
|
|
374
|
+
app.enableCors();
|
|
375
|
+
|
|
376
|
+
// Advanced CORS with dynamic options
|
|
377
|
+
app.enableCors((req: BunRequest, callback) => {
|
|
378
|
+
const origin = req.headers.get("origin");
|
|
379
|
+
|
|
380
|
+
if (origin === "https://allowed.example.com") {
|
|
381
|
+
callback(null, {
|
|
382
|
+
origin: true,
|
|
383
|
+
credentials: true,
|
|
384
|
+
methods: ["GET", "POST"],
|
|
385
|
+
});
|
|
386
|
+
} else {
|
|
387
|
+
callback(null, { origin: false });
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
You can also use NestJS's `CorsOptions` type for static configuration.
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
const app = await NestFactory.create(AppModule, new BunAdapter(), {
|
|
396
|
+
cors: {
|
|
397
|
+
origin: "https://example.com",
|
|
398
|
+
methods: ["GET", "POST", "PUT"],
|
|
399
|
+
credentials: true,
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
#### Cookies
|
|
405
|
+
|
|
406
|
+
Full cookie support:
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
@Controller()
|
|
410
|
+
class CookiesController {
|
|
411
|
+
@Get("set")
|
|
412
|
+
setCookie(@Res({ passthrough: true }) response: BunResponse) {
|
|
413
|
+
response.cookie("session", "abc123", {
|
|
414
|
+
httpOnly: true,
|
|
415
|
+
maxAge: 3600000,
|
|
416
|
+
});
|
|
417
|
+
return { message: "Cookie set" };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
@Get("get")
|
|
421
|
+
getCookie(@Req() request: BunRequest) {
|
|
422
|
+
const session = request.cookies.get("session");
|
|
423
|
+
return { session };
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
#### Popular Express Middleware
|
|
429
|
+
|
|
430
|
+
Compatible with popular Express middleware:
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
import helmet from "helmet";
|
|
434
|
+
|
|
435
|
+
const app = await NestFactory.create(AppModule, new BunAdapter());
|
|
436
|
+
app.use(helmet());
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Tested and working with:
|
|
440
|
+
|
|
441
|
+
- `helmet` - Security headers
|
|
442
|
+
- `cors` - CORS handling
|
|
443
|
+
- And most other Express-compatible middleware
|
|
444
|
+
|
|
445
|
+
### Bun File API Support
|
|
446
|
+
|
|
447
|
+
This package provides first-class support for Bun's native [`BunFile`](https://bun.com/docs/runtime/file-io) API, enabling seamless file uploads and downloads using Bun's efficient file handling capabilities.
|
|
448
|
+
|
|
449
|
+
#### BunFileInterceptor
|
|
450
|
+
|
|
451
|
+
The `BunFileInterceptor` is a NestJS interceptor that processes file uploads using Bun's native `BunFile` API. It automatically saves uploaded files to a temporary directory and attaches them to the request object, making them available in your controller methods via the `@UploadedFile()` or `@UploadedFiles()` decorators.
|
|
452
|
+
|
|
453
|
+
**How it works:**
|
|
454
|
+
|
|
455
|
+
- When a request with file uploads is received, the interceptor:
|
|
456
|
+
|
|
457
|
+
- Saves each uploaded file to a directory set in `BUN_UPLOAD_DIR` environment variables. If not set, it falls back to a unique temporary directory (using Bun's `Bun.write` and `randomUUIDv7()` for isolation). Here's how we determine the upload path if `BUN_UPLOAD_DIR` is not set:
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
import { tempdir } from "os";
|
|
461
|
+
import { join } from "path";
|
|
462
|
+
import { randomUUIDv7 } from "bun";
|
|
463
|
+
const uploadPath = join(tempdir(), "uploads", SERVER_ID, randomUUIDv7());
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
`SERVER_ID` is a unique identifier for the Bun server instance, ensuring that uploads from different server instances do not conflict. See the [Bun documentation](https://bun.com/docs/runtime/http/server#reference) for more details on server id.
|
|
467
|
+
|
|
468
|
+
- Replaces the uploaded file(s) in the request with Bun's `BunFile` objects pointing to the saved files.
|
|
469
|
+
|
|
470
|
+
#### Usage Example
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
@Controller()
|
|
474
|
+
class UploadController {
|
|
475
|
+
@Post("single")
|
|
476
|
+
@UseInterceptors(BunFileInterceptor)
|
|
477
|
+
uploadSingle(
|
|
478
|
+
@Res({ passthrough: true }) res: BunResponse,
|
|
479
|
+
@UploadedFile() file?: BunFile,
|
|
480
|
+
) {
|
|
481
|
+
res.end(file); // Optimized to send BunFile directly
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
@Post("multiple")
|
|
485
|
+
@UseInterceptors(BunFileInterceptor)
|
|
486
|
+
uploadMultiple(
|
|
487
|
+
@UploadedFiles() files: BunFile[],
|
|
488
|
+
@Res({ passthrough: true }) res: BunResponse,
|
|
489
|
+
) {
|
|
490
|
+
// Return the last file for demonstration
|
|
491
|
+
res.end(files[files.length - 1]);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**Best Practices:**
|
|
497
|
+
You should never use the `BunFileInterceptor` globally, as it incurs overhead for all requests. Instead, apply it only to specific routes that handle file uploads.
|
|
498
|
+
|
|
499
|
+
### HTTPS
|
|
500
|
+
|
|
501
|
+
You can run your NestJS application with HTTPS using two approaches:
|
|
502
|
+
|
|
503
|
+
#### Using Bun's built-in HTTPS support (recommended)
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
const app = await NestFactory.create(
|
|
507
|
+
AppModule,
|
|
508
|
+
new BunAdapter({
|
|
509
|
+
tls: {
|
|
510
|
+
cert: Bun.file("/path/to/cert.pem"),
|
|
511
|
+
key: Bun.file("/path/to/key.pem"),
|
|
512
|
+
},
|
|
513
|
+
}),
|
|
514
|
+
);
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
#### Using NestJS App Factory HTTPS options
|
|
518
|
+
|
|
519
|
+
```ts
|
|
520
|
+
const app = await NestFactory.create(
|
|
521
|
+
AppModule,
|
|
522
|
+
new BunAdapter(/* leave it empty */),
|
|
523
|
+
{
|
|
524
|
+
httpsOptions: {
|
|
525
|
+
cert: fs.readFileSync("/path/to/cert.pem"), // works with Bun too
|
|
526
|
+
key: Bun.file("/path/to/key.pem"), // you can use Bun.file here as well
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
);
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
##### Limitations
|
|
533
|
+
|
|
534
|
+
If you're using NestJS's built-in HTTPS options, only these options will be passed to Bun:
|
|
535
|
+
|
|
536
|
+
- `key`
|
|
537
|
+
- `cert`
|
|
538
|
+
- `ca`
|
|
539
|
+
- `passphrase`
|
|
540
|
+
- `ciphers`
|
|
541
|
+
- `secureOptions`
|
|
542
|
+
- `requestCert`
|
|
543
|
+
|
|
544
|
+
Other options will be ignored.
|
|
545
|
+
|
|
546
|
+
### Code Quality
|
|
547
|
+
|
|
548
|
+
The Bun adapter is developed with high code quality standards, including:
|
|
549
|
+
|
|
550
|
+
- Strict TypeScript typings
|
|
551
|
+
- Strict linting rules (ESLint)
|
|
552
|
+
- Comprehensive unit and integration tests
|
|
553
|
+
- Coverage reports (>98% line coverage, >85% function coverage)
|
|
554
|
+
|
|
555
|
+
## Request / Response Objects
|
|
556
|
+
|
|
557
|
+
The Bun adapter provides `BunRequest` and `BunResponse` classes that wrap Bun's native `Request` and `Response` objects, adding NestJS-specific functionality with performance optimizations through lazy parsing and caching.
|
|
558
|
+
|
|
559
|
+
### BunRequest
|
|
560
|
+
|
|
561
|
+
A high-performance request wrapper that provides lazy parsing and caching for optimal performance. Properties like `pathname`, `hostname`, `query`, and `headers` are only parsed when first accessed.
|
|
562
|
+
|
|
563
|
+
#### Properties
|
|
564
|
+
|
|
565
|
+
- **`url`** - The complete request URL
|
|
566
|
+
- **`method`** - HTTP method (GET, POST, etc.)
|
|
567
|
+
- **`pathname`** - URL path (lazily parsed)
|
|
568
|
+
- **`hostname`** - Host name without port (lazily parsed)
|
|
569
|
+
- **`query`** - Parsed query parameters (lazily parsed)
|
|
570
|
+
- **`headers`** - Request headers object with `.get()` method (lazily materialized)
|
|
571
|
+
- **`params`** - Route parameters from URL patterns
|
|
572
|
+
- **`body`** - Parsed request body (set by body parser middleware)
|
|
573
|
+
- **`rawBody`** - Raw request body as ArrayBuffer
|
|
574
|
+
- **`file`** - Single uploaded file (for single file uploads)
|
|
575
|
+
- **`files`** - Multiple uploaded files (for multi-file uploads)
|
|
576
|
+
- **`signal`** - AbortSignal for request cancellation
|
|
577
|
+
- **`cookies`** - Cookie map for accessing request cookies
|
|
578
|
+
|
|
579
|
+
#### Methods
|
|
580
|
+
|
|
581
|
+
- **`get(key)`** - Get custom property stored in request
|
|
582
|
+
- **`set(key, value)`** - Set custom property in request (useful for middleware)
|
|
583
|
+
- **`json()`** - Parse body as JSON
|
|
584
|
+
- **`text()`** - Read body as text
|
|
585
|
+
- **`formData()`** - Parse body as FormData
|
|
586
|
+
- **`arrayBuffer()`** - Read body as ArrayBuffer
|
|
587
|
+
- **`blob()`** - Read body as Blob
|
|
588
|
+
- **`bytes()`** - Read body as Uint8Array
|
|
589
|
+
- **`clone()`** - Create a deep clone of the request
|
|
590
|
+
|
|
591
|
+
#### Usage Examples
|
|
592
|
+
|
|
593
|
+
```ts
|
|
594
|
+
@Controller("users")
|
|
595
|
+
class UsersController {
|
|
596
|
+
@Get()
|
|
597
|
+
findAll(@Req() req: BunRequest) {
|
|
598
|
+
// Access query parameters
|
|
599
|
+
const page = req.query.page; // Lazily parsed
|
|
600
|
+
const limit = req.query.limit;
|
|
601
|
+
|
|
602
|
+
// Access headers
|
|
603
|
+
const auth = req.headers.get("authorization");
|
|
604
|
+
const contentType = req.headers["content-type"];
|
|
605
|
+
|
|
606
|
+
return { page, limit, auth };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
@Post()
|
|
610
|
+
create(@Req() req: BunRequest, @Body() dto: CreateUserDto) {
|
|
611
|
+
// Access parsed body
|
|
612
|
+
console.log(req.body); // Same as dto
|
|
613
|
+
|
|
614
|
+
// Access request info
|
|
615
|
+
console.log(req.pathname); // "/users"
|
|
616
|
+
console.log(req.hostname); // "localhost"
|
|
617
|
+
console.log(req.method); // "POST"
|
|
618
|
+
|
|
619
|
+
return { created: dto };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
@Post("upload")
|
|
623
|
+
upload(@Req() req: BunRequest, @UploadedFile() file: File) {
|
|
624
|
+
// Access uploaded file
|
|
625
|
+
console.log(req.file?.name);
|
|
626
|
+
console.log(req.file?.size);
|
|
627
|
+
|
|
628
|
+
return { filename: file.name, size: file.size };
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
**Storing custom data between middleware and handlers:**
|
|
634
|
+
|
|
635
|
+
```ts
|
|
636
|
+
@Injectable()
|
|
637
|
+
class AuthMiddleware implements NestMiddleware {
|
|
638
|
+
use(req: BunRequest, res: BunResponse, next: () => void) {
|
|
639
|
+
// Store user data in request
|
|
640
|
+
req.set("user", { id: 1, name: "John" });
|
|
641
|
+
req.set("requestTime", Date.now());
|
|
642
|
+
next();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
@Controller("api")
|
|
647
|
+
class ApiController {
|
|
648
|
+
@Get("profile")
|
|
649
|
+
getProfile(@Req() req: BunRequest) {
|
|
650
|
+
// Retrieve stored data
|
|
651
|
+
const user = req.get("user");
|
|
652
|
+
const requestTime = req.get("requestTime");
|
|
653
|
+
|
|
654
|
+
return { user, requestTime };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### BunResponse
|
|
660
|
+
|
|
661
|
+
A high-performance response builder that provides methods to construct responses with headers, cookies, and various body types. Uses lazy initialization and optimized response building for maximum performance.
|
|
662
|
+
|
|
663
|
+
#### Methods
|
|
664
|
+
|
|
665
|
+
- **`setStatus(code)`** - Set HTTP status code
|
|
666
|
+
- **`getStatus()`** - Get current status code
|
|
667
|
+
- **`setHeader(name, value)`** - Set a response header
|
|
668
|
+
- **`getHeader(name)`** - Get a response header value
|
|
669
|
+
- **`appendHeader(name, value)`** - Append value to existing header (comma-separated per RFC 9110)
|
|
670
|
+
- **`removeHeader(name)`** - Remove a response header
|
|
671
|
+
- **`cookie(name, value)`** - Set a cookie with name and value
|
|
672
|
+
- **`cookie(options)`** - Set a cookie with detailed options
|
|
673
|
+
- **`deleteCookie(name)`** - Delete a cookie by name
|
|
674
|
+
- **`deleteCookie(options)`** - Delete a cookie with path/domain options
|
|
675
|
+
- **`redirect(url, statusCode?)`** - Send redirect response (default 302)
|
|
676
|
+
- **`end(body?)`** - End response and send body (auto-handles JSON, streams, binary, or even [`BunFile`](https://bun.com/docs/runtime/file-io))
|
|
677
|
+
- **`res()`** - Get the native Response promise
|
|
678
|
+
- **`isEnded()`** - Check if response has been ended
|
|
679
|
+
|
|
680
|
+
#### Usage Examples
|
|
681
|
+
|
|
682
|
+
**Setting status and headers:**
|
|
683
|
+
|
|
684
|
+
```ts
|
|
685
|
+
@Controller("api")
|
|
686
|
+
class ApiController {
|
|
687
|
+
@Get("custom")
|
|
688
|
+
custom(@Res() res: BunResponse) {
|
|
689
|
+
res.setStatus(201);
|
|
690
|
+
res.setHeader("X-Custom-Header", "custom-value");
|
|
691
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
692
|
+
res.end({ message: "Success" });
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
@Get("append")
|
|
696
|
+
appendHeaders(@Res() res: BunResponse) {
|
|
697
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
698
|
+
res.appendHeader("Cache-Control", "no-store");
|
|
699
|
+
// Results in: "Cache-Control: no-cache, no-store"
|
|
700
|
+
res.end({ message: "Done" });
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
**Working with cookies:**
|
|
706
|
+
|
|
707
|
+
```ts
|
|
708
|
+
@Controller("auth")
|
|
709
|
+
class AuthController {
|
|
710
|
+
@Post("login")
|
|
711
|
+
login(@Res() res: BunResponse) {
|
|
712
|
+
// Simple cookie
|
|
713
|
+
res.cookie("session", "abc123");
|
|
714
|
+
|
|
715
|
+
// Cookie with options
|
|
716
|
+
res.cookie({
|
|
717
|
+
name: "auth",
|
|
718
|
+
value: "token123",
|
|
719
|
+
httpOnly: true,
|
|
720
|
+
secure: true,
|
|
721
|
+
maxAge: 3600000, // 1 hour
|
|
722
|
+
path: "/",
|
|
723
|
+
sameSite: "strict",
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
res.end({ message: "Logged in" });
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
@Post("logout")
|
|
730
|
+
logout(@Res() res: BunResponse) {
|
|
731
|
+
res.deleteCookie("session");
|
|
732
|
+
res.deleteCookie("auth", { path: "/", domain: "example.com" });
|
|
733
|
+
res.end({ message: "Logged out" });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
**Redirects:**
|
|
739
|
+
|
|
740
|
+
```ts
|
|
741
|
+
@Controller("redirect")
|
|
742
|
+
class RedirectController {
|
|
743
|
+
@Get("temporary")
|
|
744
|
+
temporaryRedirect(@Res() res: BunResponse) {
|
|
745
|
+
res.redirect("/new-location"); // 302
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
@Get("permanent")
|
|
749
|
+
@Redirect("https://example.com", 301) // Works too
|
|
750
|
+
permanentRedirect(@Res() res: BunResponse) {
|
|
751
|
+
//
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
**Different response types:**
|
|
757
|
+
|
|
758
|
+
```ts
|
|
759
|
+
@Controller("files")
|
|
760
|
+
class FilesController {
|
|
761
|
+
@Get("json")
|
|
762
|
+
sendJson(@Res() res: BunResponse) {
|
|
763
|
+
res.end({ data: [1, 2, 3] }); // Auto-serialized as JSON
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
@Get("empty")
|
|
767
|
+
sendEmpty(@Res() res: BunResponse) {
|
|
768
|
+
res.setStatus(204);
|
|
769
|
+
res.end(); // No body
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
@Get("binary")
|
|
773
|
+
sendBinary(@Res() res: BunResponse) {
|
|
774
|
+
const buffer = new Uint8Array([1, 2, 3, 4, 5]);
|
|
775
|
+
res.setHeader("Content-Type", "application/octet-stream");
|
|
776
|
+
res.end(buffer);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
@Get("stream")
|
|
780
|
+
sendStream(@Res() res: BunResponse) {
|
|
781
|
+
const content = new TextEncoder().encode("File content");
|
|
782
|
+
const file = new StreamableFile(content, {
|
|
783
|
+
type: "text/plain",
|
|
784
|
+
disposition: 'attachment; filename="file.txt"',
|
|
785
|
+
});
|
|
786
|
+
res.end(file);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
**Using with passthrough mode:**
|
|
792
|
+
|
|
793
|
+
```ts
|
|
794
|
+
@Controller("hybrid")
|
|
795
|
+
class HybridController {
|
|
796
|
+
@Get("passthrough")
|
|
797
|
+
withPassthrough(@Res({ passthrough: true }) res: BunResponse) {
|
|
798
|
+
// Set headers/cookies but let NestJS handle the response
|
|
799
|
+
res.setStatus(201);
|
|
800
|
+
res.setHeader("X-Custom", "value");
|
|
801
|
+
res.cookie("session", "abc123");
|
|
802
|
+
|
|
803
|
+
// Return value will be serialized by NestJS
|
|
804
|
+
return { message: "Created" };
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
@Get("manual")
|
|
808
|
+
manualResponse(@Res() res: BunResponse) {
|
|
809
|
+
// Full control - must call end()
|
|
810
|
+
res.setStatus(200);
|
|
811
|
+
res.end({ message: "Manual response" });
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
## Benchmark Results
|
|
817
|
+
|
|
818
|
+
Tested on MacOS Sequoia (15.6.1), Apple M1 Max (64GB RAM), Bun 1.3.5, Node.js 20.10.0
|
|
819
|
+
|
|
820
|
+
| Configuration | Requests/sec | Compared to Pure Bun |
|
|
821
|
+
| --------------------------------------------------------------------------------- | -----------: | -------------------: |
|
|
822
|
+
| Pure Bun | 80,742.72 | 100.00% |
|
|
823
|
+
| Nest + Bun + Native Bun Adapter | 69,665.59 | 86.32% |
|
|
824
|
+
| Nest + Bun + Express Adapter | 43,375.97 | 53.72% |
|
|
825
|
+
| Nest + Bun + [Hono Adapter](https://www.npmjs.com/package/@kiyasov/platform-hono) | 19,194.78 | 23.77% |
|
|
826
|
+
| Nest + Node + Express | 14,019.88 | 17.36% |
|
|
827
|
+
|
|
828
|
+
> **Pure Bun** is the fastest at **80,743 req/s**. **Nest + Bun + Native Bun Adapter** achieves **~86%** of Pure Bun's performance while providing full NestJS features, and is **~5x faster** than Nest + Node + Express. Compared to Bun with Express adapter, the native Bun adapter is **~1.6x faster**.
|
|
829
|
+
|
|
830
|
+
### Running Benchmarks
|
|
831
|
+
|
|
832
|
+
This project includes benchmark configurations in the `benchmarks` directory.
|
|
833
|
+
To run the specific server benchmark, you can use predefined scripts in `package.json`. For example:
|
|
834
|
+
|
|
835
|
+
```bash
|
|
836
|
+
# Running native bun server benchmark
|
|
837
|
+
bun run native
|
|
838
|
+
|
|
839
|
+
# Running NestJS with Bun adapter benchmark
|
|
840
|
+
bun run bun
|
|
841
|
+
|
|
842
|
+
# Running NestJS with Hono adapter benchmark
|
|
843
|
+
bun run hono
|
|
844
|
+
|
|
845
|
+
# Running NestJS with Express adapter benchmark
|
|
846
|
+
bun run express
|
|
847
|
+
|
|
848
|
+
# Running NestJS with Node and Express benchmark
|
|
849
|
+
bun run node
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
All benchmarks use port `3000` by default. You can adjust the port in the respective benchmark files if needed.
|
|
853
|
+
|
|
854
|
+
## Contributing
|
|
855
|
+
|
|
856
|
+
Contributions are welcome! Please open issues or submit pull requests for bug fixes, improvements, or new features.
|
|
857
|
+
|
|
858
|
+
## Future Plans
|
|
859
|
+
|
|
860
|
+
- Support for WebSocket integration with Bun
|
|
861
|
+
- Enhanced trusted proxy configuration for host header handling
|
|
862
|
+
- Additional performance optimizations and benchmarks
|
|
863
|
+
|
|
864
|
+
## License
|
|
865
|
+
|
|
866
|
+
MIT License. See the [LICENSE](./LICENSE) file for details.
|