@bunary/http 0.0.5 → 0.1.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,74 @@ All notable changes to `@bunary/http` will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.0] - 2026-01-31
9
+
10
+ ### Added
11
+
12
+ - First minor release — API stable for development use until 1.0.0
13
+
14
+ ## [0.0.11] - 2026-01-29
15
+
16
+ ### Removed
17
+
18
+ - Removed unused `@bunary/core` dependency (chore #21)
19
+ - Package now has zero runtime dependencies
20
+ - Updated README and documentation to reflect this change
21
+
22
+ ## [0.0.10] - 2026-01-29
23
+
24
+ ### Added
25
+
26
+ - Configurable error handlers in `createApp()` options (feature #20)
27
+ - `onNotFound` - Custom handler for 404 Not Found responses
28
+ - `onMethodNotAllowed` - Custom handler for 405 Method Not Allowed responses
29
+ - `onError` - Custom handler for 500 Internal Server Error responses
30
+ - All handlers receive `RequestContext` and can return `Response` or `HandlerResponse`
31
+ - Default behavior unchanged when handlers are not provided
32
+ - `Allow` header automatically added to 405 responses if custom handler doesn't set it
33
+
34
+ ## [0.0.9] - 2026-01-29
35
+
36
+ ### Fixed
37
+
38
+ - Proper HEAD and OPTIONS request handling (bug #16)
39
+ - HEAD requests to GET routes now return 200 with empty body (preserves headers and status)
40
+ - OPTIONS requests return 204 with `Allow` header listing permitted methods
41
+ - 405 Method Not Allowed responses now include `Allow` header
42
+ - Route constraints are respected when determining allowed methods
43
+
44
+ ## [0.0.8] - 2026-01-29
45
+
46
+ ### Added
47
+
48
+ - `createApp({ basePath })` option to prefix all routes (feature #18)
49
+ - Base path is automatically normalized (leading slash added, trailing slash removed)
50
+ - Composes correctly with route groups: `basePath + group prefix + route path`
51
+ - Included in `app.route()` URL generation for named routes
52
+ - Useful when mounting the app behind a reverse proxy
53
+
54
+ ## [0.0.7] - 2026-01-29
55
+
56
+ ### Added
57
+
58
+ - `app.listen({ port, hostname })` overload (feature #19)
59
+ - Object form: `app.listen({ port: 3000, hostname: 'localhost' })`
60
+ - Existing positional form still works: `app.listen(3000, 'localhost')`
61
+ - Exported `ListenOptions` type for options object
62
+
63
+ ## [0.0.6] - 2026-01-29
64
+
65
+ ### Fixed
66
+
67
+ - Aligned query parameter API with actual implementation (bug #17)
68
+ - Removed misleading `QueryParams` type export that didn't match runtime `URLSearchParams`
69
+ - Updated README examples to use `ctx.query.get()` and `ctx.query.getAll()` instead of destructuring
70
+ - Fixed `RequestContext` interface documentation to show `query: URLSearchParams`
71
+
72
+ ### Removed
73
+
74
+ - `QueryParams` type export (was inconsistent with actual `URLSearchParams` runtime type)
75
+
8
76
  ## [0.0.5] - 2026-01-28
9
77
 
10
78
  ### Added
package/README.md CHANGED
@@ -1,24 +1,6 @@
1
1
  # @bunary/http
2
2
 
3
- A lightweight, type-safe HTTP framework built exclusively for [Bun](https://bun.sh).
4
-
5
- Part of the [Bunary](https://github.com/bunary-dev) ecosystem - a Bun-first backend platform inspired by Laravel.
6
-
7
- ## Documentation
8
-
9
- Canonical documentation for this package lives in [`docs/index.md`](./docs/index.md).
10
-
11
- ## Features
12
-
13
- - 🚀 **Bun-native** - Uses `Bun.serve()` directly, no Node.js compatibility layer
14
- - 📦 **Zero dependencies** - Only depends on `@bunary/core`
15
- - 🔒 **Type-safe** - Full TypeScript support with strict types
16
- - ⚡ **Fast** - Minimal overhead, direct routing
17
- - 🧩 **Simple API** - Chainable route registration with automatic JSON serialization
18
- - 📂 **Route Groups** - Organize routes with shared prefixes, middleware, and name prefixes
19
- - 🏷️ **Named Routes** - URL generation with route names
20
- - ✅ **Route Constraints** - Validate parameters with regex patterns
21
- - ❓ **Optional Parameters** - Flexible routes with optional path segments
3
+ Lightweight, type-safe HTTP framework for [Bun](https://bun.sh). Routes, middleware, groups, named routes, constraints. Full reference: [docs/index.md](./docs/index.md).
22
4
 
23
5
  ## Installation
24
6
 
@@ -32,386 +14,11 @@ bun add @bunary/http
32
14
  import { createApp } from '@bunary/http';
33
15
 
34
16
  const app = createApp();
35
-
36
17
  app.get('/hello', () => ({ message: 'Hello, Bun!' }));
37
-
38
18
  app.listen({ port: 3000 });
39
19
  ```
40
20
 
41
- ## API
42
-
43
- ### `createApp()`
44
-
45
- Creates a new Bunary application instance.
46
-
47
- ```typescript
48
- import { createApp } from '@bunary/http';
49
-
50
- const app = createApp();
51
- ```
52
-
53
- ### Route Registration
54
-
55
- Register routes using chainable HTTP method helpers:
56
-
57
- ```typescript
58
- app
59
- .get('/users', () => ({ users: [] }))
60
- .post('/users', async (ctx) => {
61
- const body = await ctx.request.json();
62
- return { id: 1, ...body };
63
- })
64
- .put('/users/:id', (ctx) => {
65
- return { id: ctx.params.id, updated: true };
66
- })
67
- .delete('/users/:id', (ctx) => {
68
- return { deleted: ctx.params.id };
69
- })
70
- .patch('/users/:id', (ctx) => {
71
- return { patched: ctx.params.id };
72
- });
73
- ```
74
-
75
- ### Path Parameters
76
-
77
- Path parameters are extracted automatically:
78
-
79
- ```typescript
80
- app.get('/users/:id', (ctx) => {
81
- return { userId: ctx.params.id };
82
- });
83
-
84
- app.get('/posts/:postId/comments/:commentId', (ctx) => {
85
- const { postId, commentId } = ctx.params;
86
- return { postId, commentId };
87
- });
88
- ```
89
-
90
- ### Query Parameters
91
-
92
- Query parameters are parsed from the URL:
93
-
94
- ```typescript
95
- app.get('/search', (ctx) => {
96
- const { q, page, limit } = ctx.query;
97
- return { query: q, page, limit };
98
- });
99
- ```
100
-
101
- ### Request Context
102
-
103
- Route handlers receive a `RequestContext` object:
104
-
105
- ```typescript
106
- interface RequestContext {
107
- request: Request; // Original Bun Request object
108
- params: Record<string, string>; // Path parameters
109
- query: Record<string, string>; // Query parameters
110
- }
111
- ```
112
-
113
- ### Response Handling
114
-
115
- Handlers can return various types - they're automatically serialized:
116
-
117
- ```typescript
118
- // Objects/Arrays → JSON with Content-Type: application/json
119
- app.get('/json', () => ({ data: 'value' }));
120
-
121
- // Strings → text/plain
122
- app.get('/text', () => 'Hello, world!');
123
-
124
- // Response objects passed through unchanged
125
- app.get('/custom', () => new Response('Custom', { status: 201 }));
126
-
127
- // null/undefined → 204 No Content
128
- app.get('/empty', () => null);
129
- ```
130
-
131
- ### Starting the Server
132
-
133
- ```typescript
134
- const server = app.listen({ port: 3000, hostname: 'localhost' });
135
-
136
- console.log(`Server running on ${server.hostname}:${server.port}`);
137
-
138
- // Stop the server when done
139
- server.stop();
140
- ```
141
-
142
- ### Testing Without Server
143
-
144
- Use `app.fetch()` to test handlers directly:
145
-
146
- ```typescript
147
- const app = createApp();
148
- app.get('/hello', () => ({ message: 'hi' }));
149
-
150
- const response = await app.fetch(new Request('http://localhost/hello'));
151
- const data = await response.json();
152
- // { message: 'hi' }
153
- ```
154
-
155
- ## Middleware
156
-
157
- Add middleware to handle cross-cutting concerns like logging, authentication, and error handling.
158
-
159
- ### Basic Middleware
160
-
161
- ```typescript
162
- // Logging middleware
163
- app.use(async (ctx, next) => {
164
- const start = Date.now();
165
- const result = await next();
166
- console.log(`${ctx.request.method} ${new URL(ctx.request.url).pathname} - ${Date.now() - start}ms`);
167
- return result;
168
- });
169
- ```
170
-
171
- ### Middleware Chain
172
-
173
- Middleware executes in registration order. Each middleware can:
174
- - Run code before calling `next()`
175
- - Call `next()` to continue the chain
176
- - Run code after `next()` returns
177
- - Return early without calling `next()`
178
-
179
- ```typescript
180
- app
181
- .use(async (ctx, next) => {
182
- console.log('First - before');
183
- const result = await next();
184
- console.log('First - after');
185
- return result;
186
- })
187
- .use(async (ctx, next) => {
188
- console.log('Second - before');
189
- const result = await next();
190
- console.log('Second - after');
191
- return result;
192
- });
193
-
194
- // Output order: First-before, Second-before, handler, Second-after, First-after
195
- ```
196
-
197
- ### Error Handling Middleware
198
-
199
- ```typescript
200
- app.use(async (ctx, next) => {
201
- try {
202
- return await next();
203
- } catch (error) {
204
- console.error('Error:', error);
205
- return new Response(JSON.stringify({ error: error.message }), {
206
- status: 500,
207
- headers: { 'Content-Type': 'application/json' }
208
- });
209
- }
210
- });
211
- ```
212
-
213
- ### Auth Middleware (Example)
214
-
215
- ```typescript
216
- app.use(async (ctx, next) => {
217
- const token = ctx.request.headers.get('Authorization');
218
- if (!token) {
219
- return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 });
220
- }
221
- // Validate token...
222
- return await next();
223
- });
224
- ```
225
-
226
- ## Route Groups
227
-
228
- Group routes together with shared prefixes, middleware, and name prefixes.
229
-
230
- ### Basic Groups
231
-
232
- ```typescript
233
- // Simple prefix
234
- app.group('/api', (router) => {
235
- router.get('/users', () => ({ users: [] })); // /api/users
236
- router.get('/posts', () => ({ posts: [] })); // /api/posts
237
- });
238
- ```
239
-
240
- ### Groups with Options
241
-
242
- ```typescript
243
- // Auth middleware for protected routes
244
- const authMiddleware = async (ctx, next) => {
245
- const token = ctx.request.headers.get('Authorization');
246
- if (!token) return new Response('Unauthorized', { status: 401 });
247
- return await next();
248
- };
249
-
250
- app.group({
251
- prefix: '/admin',
252
- middleware: [authMiddleware],
253
- name: 'admin.'
254
- }, (router) => {
255
- router.get('/dashboard', () => ({})).name('dashboard'); // name: admin.dashboard
256
- router.get('/users', () => ({})).name('users'); // name: admin.users
257
- });
258
- ```
259
-
260
- ### Nested Groups
261
-
262
- ```typescript
263
- app.group('/api', (api) => {
264
- api.group('/v1', (v1) => {
265
- v1.get('/users', () => ({})); // /api/v1/users
266
- });
267
- api.group('/v2', (v2) => {
268
- v2.get('/users', () => ({})); // /api/v2/users
269
- });
270
- });
271
- ```
272
-
273
- ## Named Routes
274
-
275
- Assign names to routes for URL generation.
276
-
277
- ### Naming Routes
278
-
279
- ```typescript
280
- app.get('/users/:id', (ctx) => ({})).name('users.show');
281
- app.get('/posts/:slug', (ctx) => ({})).name('posts.show');
282
- ```
283
-
284
- ### Generating URLs
285
-
286
- ```typescript
287
- // Basic URL generation
288
- const url = app.route('users.show', { id: 42 });
289
- // "/users/42"
290
-
291
- // With query string
292
- const searchUrl = app.route('users.show', { id: 42, tab: 'profile' });
293
- // "/users/42?tab=profile"
294
-
295
- // Check if route exists
296
- if (app.hasRoute('users.show')) {
297
- // ...
298
- }
299
-
300
- // List all routes
301
- const routes = app.getRoutes();
302
- // [{ name: 'users.show', method: 'GET', path: '/users/:id' }, ...]
303
- ```
304
-
305
- ## Route Constraints
306
-
307
- Add regex constraints to validate route parameters.
308
-
309
- ### Basic Constraints
310
-
311
- ```typescript
312
- // Only match if :id is numeric
313
- app.get('/users/:id', (ctx) => ({}))
314
- .where('id', /^\d+$/);
315
-
316
- // Using string pattern
317
- app.get('/posts/:slug', (ctx) => ({}))
318
- .where('slug', '^[a-z0-9-]+$');
319
-
320
- // Multiple constraints
321
- app.get('/users/:id/posts/:postId', (ctx) => ({}))
322
- .where({ id: /^\d+$/, postId: /^\d+$/ });
323
- ```
324
-
325
- ### Helper Methods
326
-
327
- ```typescript
328
- // whereNumber - digits only
329
- app.get('/users/:id', () => ({})).whereNumber('id');
330
-
331
- // whereAlpha - letters only (a-zA-Z)
332
- app.get('/categories/:name', () => ({})).whereAlpha('name');
333
-
334
- // whereAlphaNumeric - letters and digits
335
- app.get('/codes/:code', () => ({})).whereAlphaNumeric('code');
336
-
337
- // whereUuid - UUID format
338
- app.get('/items/:uuid', () => ({})).whereUuid('uuid');
339
-
340
- // whereUlid - ULID format
341
- app.get('/records/:ulid', () => ({})).whereUlid('ulid');
342
-
343
- // whereIn - specific allowed values
344
- app.get('/status/:status', () => ({})).whereIn('status', ['active', 'pending', 'archived']);
345
- ```
346
-
347
- ### Chaining Constraints
348
-
349
- ```typescript
350
- app.get('/users/:id/posts/:slug', (ctx) => ({}))
351
- .whereNumber('id')
352
- .whereAlpha('slug')
353
- .name('users.posts');
354
- ```
355
-
356
- ## Optional Parameters
357
-
358
- Use `?` to mark route parameters as optional.
359
-
360
- ```typescript
361
- // :id is optional
362
- app.get('/users/:id?', (ctx) => {
363
- if (ctx.params.id) {
364
- return { user: ctx.params.id };
365
- }
366
- return { users: [] };
367
- });
368
-
369
- // Multiple optional params
370
- app.get('/archive/:year?/:month?', (ctx) => {
371
- const { year, month } = ctx.params;
372
- // year and month may be undefined
373
- return { year, month };
374
- });
375
-
376
- // Constraints work with optional params
377
- app.get('/posts/:id?', (ctx) => ({})).whereNumber('id');
378
- ```
379
-
380
- ## Error Handling
381
-
382
- Uncaught errors in handlers return a 500 response with the error message:
383
-
384
- ```typescript
385
- app.get('/error', () => {
386
- throw new Error('Something went wrong');
387
- });
388
-
389
- // Returns: 500 Internal Server Error
390
- // Body: { error: "Something went wrong" }
391
- ```
392
-
393
- ## Types
394
-
395
- All types are exported for TypeScript users:
396
-
397
- ```typescript
398
- import type {
399
- BunaryApp,
400
- BunaryServer,
401
- RequestContext,
402
- RouteHandler,
403
- Middleware,
404
- RouteBuilder,
405
- GroupOptions,
406
- GroupRouter,
407
- GroupCallback,
408
- RouteInfo
409
- } from '@bunary/http';
410
- ```
411
-
412
- ## Requirements
413
-
414
- - Bun ≥ 1.0.0
21
+ For createApp options, route groups, middleware, named routes, and types, see [docs/index.md](./docs/index.md).
415
22
 
416
23
  ## License
417
24
 
package/dist/app.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BunaryApp } from "./types/index.js";
1
+ import type { AppOptions, BunaryApp } from "./types/index.js";
2
2
  /**
3
3
  * Create a new Bunary HTTP application instance.
4
4
  *
@@ -32,6 +32,21 @@ import type { BunaryApp } from "./types/index.js";
32
32
  *
33
33
  * app.listen(3000);
34
34
  * ```
35
+ *
36
+ * @param options - Optional configuration
37
+ * @param options.basePath - Base path prefix for all routes (e.g., "/api")
38
+ * @returns BunaryApp instance
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * // Without basePath
43
+ * const app = createApp();
44
+ * app.get("/users", () => ({})); // Matches /users
45
+ *
46
+ * // With basePath
47
+ * const apiApp = createApp({ basePath: "/api" });
48
+ * apiApp.get("/users", () => ({})); // Matches /api/users
49
+ * ```
35
50
  */
36
- export declare function createApp(): BunaryApp;
51
+ export declare function createApp(options?: AppOptions): BunaryApp;
37
52
  //# sourceMappingURL=app.d.ts.map
package/dist/app.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACX,SAAS,EAYT,MAAM,kBAAkB,CAAC;AAE1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,SAAS,IAAI,SAAS,CAoOrC"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EACX,UAAU,EACV,SAAS,EAWT,MAAM,kBAAkB,CAAC;AAE1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,wBAAgB,SAAS,CAAC,OAAO,CAAC,EAAE,UAAU,GAAG,SAAS,CAiPzD"}
@@ -0,0 +1,7 @@
1
+ import type { AppOptions, RequestContext } from "../types/index.js";
2
+ /**
3
+ * Handle 500 Internal Server Error responses.
4
+ * Uses custom onError handler if provided, otherwise returns default JSON response.
5
+ */
6
+ export declare function handleError(ctx: RequestContext, error: unknown, options?: AppOptions): Promise<Response>;
7
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/handlers/error.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEpE;;;GAGG;AACH,wBAAsB,WAAW,CAChC,GAAG,EAAE,cAAc,EACnB,KAAK,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,QAAQ,CAAC,CAUnB"}
@@ -0,0 +1,11 @@
1
+ import type { HttpMethod, Route } from "../types/index.js";
2
+ /**
3
+ * Normalize HEAD requests to use GET route if no explicit HEAD route exists.
4
+ * Returns the method to use for route matching.
5
+ */
6
+ export declare function normalizeHeadMethod(method: HttpMethod, path: string, routes: Route[]): HttpMethod;
7
+ /**
8
+ * Convert a response to HEAD format (empty body, preserve headers and status).
9
+ */
10
+ export declare function toHeadResponse(response: Response): Response;
11
+ //# sourceMappingURL=head.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"head.d.ts","sourceRoot":"","sources":["../../src/handlers/head.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE3D;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,UAAU,CAejG;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CAM3D"}
@@ -0,0 +1,7 @@
1
+ export { handleError } from "./error.js";
2
+ export { normalizeHeadMethod, toHeadResponse } from "./head.js";
3
+ export { handleMethodNotAllowed } from "./methodNotAllowed.js";
4
+ export { handleNotFound } from "./notFound.js";
5
+ export { handleOptions } from "./options.js";
6
+ export { executeRoute } from "./route.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { AppOptions, Route } from "../types/index.js";
2
+ /**
3
+ * Handle 405 Method Not Allowed responses.
4
+ * Uses custom onMethodNotAllowed handler if provided, otherwise returns default JSON response.
5
+ * Ensures Allow header is always present.
6
+ */
7
+ export declare function handleMethodNotAllowed(request: Request, path: string, routes: Route[], options?: AppOptions): Promise<Response>;
8
+ //# sourceMappingURL=methodNotAllowed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"methodNotAllowed.d.ts","sourceRoot":"","sources":["../../src/handlers/methodNotAllowed.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAkB,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE3E;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC3C,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,KAAK,EAAE,EACf,OAAO,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,QAAQ,CAAC,CAgCnB"}
@@ -0,0 +1,7 @@
1
+ import type { AppOptions } from "../types/index.js";
2
+ /**
3
+ * Handle 404 Not Found responses.
4
+ * Uses custom onNotFound handler if provided, otherwise returns default JSON response.
5
+ */
6
+ export declare function handleNotFound(request: Request, _path: string, options?: AppOptions): Promise<Response>;
7
+ //# sourceMappingURL=notFound.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notFound.d.ts","sourceRoot":"","sources":["../../src/handlers/notFound.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,mBAAmB,CAAC;AAEpE;;;GAGG;AACH,wBAAsB,cAAc,CACnC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,QAAQ,CAAC,CAgBnB"}
@@ -0,0 +1,7 @@
1
+ import type { AppOptions, Route } from "../types/index.js";
2
+ /**
3
+ * Handle OPTIONS requests.
4
+ * Returns 204 with Allow header if path exists, otherwise delegates to 404 handler.
5
+ */
6
+ export declare function handleOptions(request: Request, path: string, routes: Route[], options?: AppOptions): Promise<Response>;
7
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/handlers/options.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAG3D;;;GAGG;AACH,wBAAsB,aAAa,CAClC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,KAAK,EAAE,EACf,OAAO,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,QAAQ,CAAC,CAUnB"}
@@ -0,0 +1,8 @@
1
+ import type { RouteMatch } from "../routes/index.js";
2
+ import type { Middleware, RequestContext, Route } from "../types/index.js";
3
+ /**
4
+ * Execute route handler with middleware chain.
5
+ * Returns the response from the handler or middleware.
6
+ */
7
+ export declare function executeRoute(match: RouteMatch, ctx: RequestContext, getMiddlewareChain: (route: Route) => Middleware[]): Promise<Response>;
8
+ //# sourceMappingURL=route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../src/handlers/route.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAmB,UAAU,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE5F;;;GAGG;AACH,wBAAsB,YAAY,CACjC,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,cAAc,EACnB,kBAAkB,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,UAAU,EAAE,GAChD,OAAO,CAAC,QAAQ,CAAC,CAgBnB"}
package/dist/index.d.ts CHANGED
@@ -18,6 +18,6 @@
18
18
  *
19
19
  * @packageDocumentation
20
20
  */
21
- export type { AppOptions, BunaryApp, BunaryServer, GroupCallback, GroupOptions, GroupRouter, HandlerResponse, HttpMethod, Middleware, PathParams, QueryParams, RequestContext, Route, RouteBuilder, RouteHandler, RouteInfo, } from "./types/index.js";
22
21
  export { createApp } from "./app.js";
22
+ export type { AppOptions, BunaryApp, BunaryServer, GroupCallback, GroupOptions, GroupRouter, HandlerResponse, HttpMethod, ListenOptions, Middleware, PathParams, RequestContext, Route, RouteBuilder, RouteHandler, RouteInfo, } from "./types/index.js";
23
23
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,YAAY,EACX,UAAU,EACV,SAAS,EACT,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,WAAW,EACX,eAAe,EACf,UAAU,EACV,UAAU,EACV,UAAU,EACV,WAAW,EACX,cAAc,EACd,KAAK,EACL,YAAY,EACZ,YAAY,EACZ,SAAS,GACT,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAErC,YAAY,EACX,UAAU,EACV,SAAS,EACT,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,WAAW,EACX,eAAe,EACf,UAAU,EACV,aAAa,EACb,UAAU,EACV,UAAU,EACV,cAAc,EACd,KAAK,EACL,YAAY,EACZ,YAAY,EACZ,SAAS,GACT,MAAM,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -25,55 +25,18 @@ function toResponse(result) {
25
25
  });
26
26
  }
27
27
 
28
- // src/router.ts
29
- function compilePath(path) {
30
- const paramNames = [];
31
- const optionalParams = [];
32
- let regexString = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33
- regexString = regexString.replace(/\/:([a-zA-Z_][a-zA-Z0-9_]*)(\\\?)?/g, (_match, paramName, isOptional) => {
34
- if (paramNames.includes(paramName)) {
35
- throw new Error(`Duplicate parameter name ":${paramName}" in route pattern "${path}". Each parameter name must be unique within a route.`);
36
- }
37
- paramNames.push(paramName);
38
- if (isOptional) {
39
- optionalParams.push(paramName);
40
- return "(?:/([^/]+))?";
41
- }
42
- return "/([^/]+)";
43
- });
44
- regexString += "/?";
45
- return {
46
- pattern: new RegExp(`^${regexString}$`),
47
- paramNames,
48
- optionalParams
49
- };
50
- }
51
- function extractParams(path, route) {
52
- const match = path.match(route.pattern);
53
- if (!match)
54
- return {};
55
- const params = {};
56
- for (let i = 0;i < route.paramNames.length; i++) {
57
- const value = match[i + 1];
58
- if (value !== undefined && value !== "") {
59
- params[route.paramNames[i]] = value;
60
- }
61
- }
62
- return params;
63
- }
64
- function checkConstraints(params, constraints) {
65
- if (!constraints)
66
- return true;
67
- for (const [param, pattern] of Object.entries(constraints)) {
68
- const value = params[param];
69
- if (value === undefined)
70
- continue;
71
- if (!pattern.test(value))
72
- return false;
28
+ // src/handlers/error.ts
29
+ async function handleError(ctx, error, options) {
30
+ if (options?.onError) {
31
+ const result = await options.onError(ctx, error);
32
+ return toResponse(result);
73
33
  }
74
- return true;
34
+ const message = error instanceof Error ? error.message : "Internal server error";
35
+ return new Response(JSON.stringify({ error: message }), {
36
+ status: 500,
37
+ headers: { "Content-Type": "application/json" }
38
+ });
75
39
  }
76
-
77
40
  // src/routes/builder.ts
78
41
  function compilePattern(pattern, param) {
79
42
  try {
@@ -195,6 +158,55 @@ function wrapBuilderWithNamePrefix(builder, namePrefix) {
195
158
  }
196
159
  });
197
160
  }
161
+ // src/router.ts
162
+ function compilePath(path) {
163
+ const paramNames = [];
164
+ const optionalParams = [];
165
+ let regexString = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
166
+ regexString = regexString.replace(/\/:([a-zA-Z_][a-zA-Z0-9_]*)(\\\?)?/g, (_match, paramName, isOptional) => {
167
+ if (paramNames.includes(paramName)) {
168
+ throw new Error(`Duplicate parameter name ":${paramName}" in route pattern "${path}". Each parameter name must be unique within a route.`);
169
+ }
170
+ paramNames.push(paramName);
171
+ if (isOptional) {
172
+ optionalParams.push(paramName);
173
+ return "(?:/([^/]+))?";
174
+ }
175
+ return "/([^/]+)";
176
+ });
177
+ regexString += "/?";
178
+ return {
179
+ pattern: new RegExp(`^${regexString}$`),
180
+ paramNames,
181
+ optionalParams
182
+ };
183
+ }
184
+ function extractParams(path, route) {
185
+ const match = path.match(route.pattern);
186
+ if (!match)
187
+ return {};
188
+ const params = {};
189
+ for (let i = 0;i < route.paramNames.length; i++) {
190
+ const value = match[i + 1];
191
+ if (value !== undefined && value !== "") {
192
+ params[route.paramNames[i]] = value;
193
+ }
194
+ }
195
+ return params;
196
+ }
197
+ function checkConstraints(params, constraints) {
198
+ if (!constraints)
199
+ return true;
200
+ for (const [param, pattern] of Object.entries(constraints)) {
201
+ const value = params[param];
202
+ if (value === undefined)
203
+ continue;
204
+ if (!pattern.test(value))
205
+ return false;
206
+ }
207
+ return true;
208
+ }
209
+
198
210
  // src/routes/find.ts
199
211
  function findRoute(routes, method, path) {
200
212
  for (const route of routes) {
@@ -218,6 +230,18 @@ function hasMatchingPath(routes, path) {
218
230
  return checkConstraints(params, route.constraints);
219
231
  });
220
232
  }
233
+ function getAllowedMethods(routes, path) {
234
+ const methods = new Set;
235
+ for (const route of routes) {
236
+ if (route.pattern.test(path)) {
237
+ const params = extractParams(path, route);
238
+ if (checkConstraints(params, route.constraints)) {
239
+ methods.add(route.method);
240
+ }
241
+ }
242
+ }
243
+ return Array.from(methods).sort();
244
+ }
221
245
  // src/pathUtils.ts
222
246
  function normalizePrefix(prefix) {
223
247
  let normalized = prefix;
@@ -277,11 +301,111 @@ function createGroupRouter(prefix, groupMiddleware, namePrefix, addRoute) {
277
301
  };
278
302
  return router;
279
303
  }
304
+ // src/handlers/head.ts
305
+ function normalizeHeadMethod(method, path, routes) {
306
+ if (method !== "HEAD") {
307
+ return method;
308
+ }
309
+ const headMatch = findRoute(routes, "HEAD", path);
310
+ if (headMatch) {
311
+ return "HEAD";
312
+ }
313
+ const getMatch = findRoute(routes, "GET", path);
314
+ if (getMatch) {
315
+ return "GET";
316
+ }
317
+ return "HEAD";
318
+ }
319
+ function toHeadResponse(response) {
320
+ return new Response(null, {
321
+ status: response.status,
322
+ statusText: response.statusText,
323
+ headers: response.headers
324
+ });
325
+ }
326
+ // src/handlers/methodNotAllowed.ts
327
+ async function handleMethodNotAllowed(request, path, routes, options) {
328
+ const url = new URL(request.url);
329
+ const allowedMethods = getAllowedMethods(routes, path);
330
+ const methodNotAllowedCtx = {
331
+ request,
332
+ params: {},
333
+ query: url.searchParams,
334
+ locals: {}
335
+ };
336
+ if (options?.onMethodNotAllowed) {
337
+ const result = await options.onMethodNotAllowed(methodNotAllowedCtx, allowedMethods);
338
+ const response = toResponse(result);
339
+ const allowHeader = response.headers.get("Allow");
340
+ if (!allowHeader) {
341
+ const headers = new Headers(response.headers);
342
+ headers.set("Allow", allowedMethods.join(", "));
343
+ return new Response(response.body, {
344
+ status: response.status,
345
+ statusText: response.statusText,
346
+ headers
347
+ });
348
+ }
349
+ return response;
350
+ }
351
+ return new Response(JSON.stringify({ error: "Method not allowed" }), {
352
+ status: 405,
353
+ headers: {
354
+ "Content-Type": "application/json",
355
+ Allow: allowedMethods.join(", ")
356
+ }
357
+ });
358
+ }
359
+ // src/handlers/notFound.ts
360
+ async function handleNotFound(request, _path, options) {
361
+ const url = new URL(request.url);
362
+ const notFoundCtx = {
363
+ request,
364
+ params: {},
365
+ query: url.searchParams,
366
+ locals: {}
367
+ };
368
+ if (options?.onNotFound) {
369
+ const result = await options.onNotFound(notFoundCtx);
370
+ return toResponse(result);
371
+ }
372
+ return new Response(JSON.stringify({ error: "Not found" }), {
373
+ status: 404,
374
+ headers: { "Content-Type": "application/json" }
375
+ });
376
+ }
377
+ // src/handlers/options.ts
378
+ async function handleOptions(request, path, routes, options) {
379
+ if (hasMatchingPath(routes, path)) {
380
+ const allowedMethods = getAllowedMethods(routes, path);
381
+ return new Response(null, {
382
+ status: 204,
383
+ headers: { Allow: allowedMethods.join(", ") }
384
+ });
385
+ }
386
+ return await handleNotFound(request, path, options);
387
+ }
388
+ // src/handlers/route.ts
389
+ async function executeRoute(match, ctx, getMiddlewareChain) {
390
+ const allMiddleware = getMiddlewareChain(match.route);
391
+ let index = 0;
392
+ const next = async () => {
393
+ if (index < allMiddleware.length) {
394
+ const middleware = allMiddleware[index++];
395
+ return await middleware(ctx, next);
396
+ }
397
+ return await match.route.handler(ctx);
398
+ };
399
+ const result = await next();
400
+ return toResponse(result);
401
+ }
280
402
  // src/app.ts
281
- function createApp() {
403
+ function createApp(options) {
282
404
  const routes = [];
283
405
  const middlewares = [];
284
406
  const namedRoutes = new Map;
407
+ const normalizedBasePath = options?.basePath ? normalizePrefix(options.basePath) : "";
408
+ const basePath = normalizedBasePath === "/" ? "" : normalizedBasePath;
285
409
  let globalMiddlewareVersion = 0;
286
410
  const middlewareCache = new WeakMap;
287
411
  function getMiddlewareChain(route) {
@@ -294,10 +418,11 @@ function createApp() {
294
418
  return chain;
295
419
  }
296
420
  function addRoute(method, path, handler, groupMiddleware = []) {
297
- const { pattern, paramNames, optionalParams } = compilePath(path);
421
+ const fullPath = basePath ? joinPaths(basePath, path) : path;
422
+ const { pattern, paramNames, optionalParams } = compilePath(fullPath);
298
423
  const route = {
299
424
  method,
300
- path,
425
+ path: fullPath,
301
426
  pattern,
302
427
  paramNames,
303
428
  handler,
@@ -311,18 +436,16 @@ function createApp() {
311
436
  const url = new URL(request.url);
312
437
  const path = url.pathname;
313
438
  const method = request.method;
314
- const match = findRoute(routes, method, path);
439
+ if (method === "OPTIONS") {
440
+ return await handleOptions(request, path, routes, options);
441
+ }
442
+ const actualMethod = normalizeHeadMethod(method, path, routes);
443
+ const match = findRoute(routes, actualMethod, path);
315
444
  if (!match) {
316
445
  if (hasMatchingPath(routes, path)) {
317
- return new Response(JSON.stringify({ error: "Method not allowed" }), {
318
- status: 405,
319
- headers: { "Content-Type": "application/json" }
320
- });
446
+ return await handleMethodNotAllowed(request, path, routes, options);
321
447
  }
322
- return new Response(JSON.stringify({ error: "Not found" }), {
323
- status: 404,
324
- headers: { "Content-Type": "application/json" }
325
- });
448
+ return await handleNotFound(request, path, options);
326
449
  }
327
450
  const ctx = {
328
451
  request,
@@ -331,23 +454,13 @@ function createApp() {
331
454
  locals: {}
332
455
  };
333
456
  try {
334
- const allMiddleware = getMiddlewareChain(match.route);
335
- let index = 0;
336
- const next = async () => {
337
- if (index < allMiddleware.length) {
338
- const middleware = allMiddleware[index++];
339
- return await middleware(ctx, next);
340
- }
341
- return await match.route.handler(ctx);
342
- };
343
- const result = await next();
344
- return toResponse(result);
457
+ const response = await executeRoute(match, ctx, getMiddlewareChain);
458
+ if (method === "HEAD") {
459
+ return toHeadResponse(response);
460
+ }
461
+ return response;
345
462
  } catch (error) {
346
- const message = error instanceof Error ? error.message : "Internal server error";
347
- return new Response(JSON.stringify({ error: message }), {
348
- status: 500,
349
- headers: { "Content-Type": "application/json" }
350
- });
463
+ return await handleError(ctx, error, options);
351
464
  }
352
465
  }
353
466
  const app = {
@@ -419,7 +532,17 @@ function createApp() {
419
532
  path: route.path
420
533
  }));
421
534
  },
422
- listen: (port = 3000, hostname = "localhost") => {
535
+ listen: (portOrOptions, hostnameArg) => {
536
+ let port;
537
+ let hostname;
538
+ const isOptionsObject = portOrOptions !== undefined && portOrOptions !== null && typeof portOrOptions === "object";
539
+ if (isOptionsObject) {
540
+ port = portOrOptions.port ?? 3000;
541
+ hostname = portOrOptions.hostname ?? "localhost";
542
+ } else {
543
+ port = typeof portOrOptions === "number" ? portOrOptions : 3000;
544
+ hostname = hostnameArg ?? "localhost";
545
+ }
423
546
  const server = Bun.serve({
424
547
  port,
425
548
  hostname,
@@ -24,4 +24,13 @@ export declare function findRoute(routes: Route[], method: string, path: string)
24
24
  * Check if any route matches the path (regardless of method).
25
25
  */
26
26
  export declare function hasMatchingPath(routes: Route[], path: string): boolean;
27
+ /**
28
+ * Get all allowed HTTP methods for a given path.
29
+ * Respects route constraints when determining matches.
30
+ *
31
+ * @param routes - All registered routes
32
+ * @param path - Path to check
33
+ * @returns Array of allowed HTTP methods
34
+ */
35
+ export declare function getAllowedMethods(routes: Route[], path: string): string[];
27
36
  //# sourceMappingURL=find.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"find.d.ts","sourceRoot":"","sources":["../../src/routes/find.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CAC3C;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAc1F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAMtE"}
1
+ {"version":3,"file":"find.d.ts","sourceRoot":"","sources":["../../src/routes/find.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CAC3C;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAc1F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAMtE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAYzE"}
@@ -1,4 +1,4 @@
1
- export { createRouteBuilder, compilePattern, wrapBuilderWithNamePrefix } from "./builder.js";
2
- export { findRoute, hasMatchingPath, type RouteMatch } from "./find.js";
3
- export { createGroupRouter, type AddRouteFn } from "./group.js";
1
+ export { compilePattern, createRouteBuilder, wrapBuilderWithNamePrefix } from "./builder.js";
2
+ export { findRoute, getAllowedMethods, hasMatchingPath, type RouteMatch } from "./find.js";
3
+ export { type AddRouteFn, createGroupRouter } from "./group.js";
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/routes/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAC7F,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/routes/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAC7F,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAC3F,OAAO,EAAE,KAAK,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC"}
@@ -1,8 +1,72 @@
1
+ import type { HandlerResponse } from "./handlerResponse.js";
2
+ import type { RequestContext } from "./requestContext.js";
1
3
  /**
2
4
  * Configuration options for creating a Bunary app.
3
5
  */
4
6
  export interface AppOptions {
5
7
  /** Base path prefix for all routes (default: "") */
6
8
  basePath?: string;
9
+ /**
10
+ * Custom handler for 404 Not Found responses.
11
+ * Called when no route matches the request path.
12
+ *
13
+ * @param ctx - Request context (params will be empty, query available)
14
+ * @returns Response, HandlerResponse, or Promise of either (will be converted to Response)
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const app = createApp({
19
+ * onNotFound: async (ctx) => {
20
+ * await logToExternalService(ctx.request.url);
21
+ * return new Response("Custom 404", { status: 404 });
22
+ * }
23
+ * });
24
+ * ```
25
+ */
26
+ onNotFound?: (ctx: RequestContext) => Response | HandlerResponse | Promise<Response | HandlerResponse>;
27
+ /**
28
+ * Custom handler for 405 Method Not Allowed responses.
29
+ * Called when a route matches the path but not the HTTP method.
30
+ *
31
+ * @param ctx - Request context (params will be empty, query available)
32
+ * @param allowedMethods - Array of allowed HTTP methods for this path
33
+ * @returns Response, HandlerResponse, or Promise of either (will be converted to Response)
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const app = createApp({
38
+ * onMethodNotAllowed: async (ctx, allowed) => {
39
+ * await logMethodNotAllowed(ctx.request.url, allowed);
40
+ * return new Response(
41
+ * JSON.stringify({ error: "Method not allowed", allowed }),
42
+ * { status: 405, headers: { "Content-Type": "application/json" } }
43
+ * );
44
+ * }
45
+ * });
46
+ * ```
47
+ */
48
+ onMethodNotAllowed?: (ctx: RequestContext, allowedMethods: string[]) => Response | HandlerResponse | Promise<Response | HandlerResponse>;
49
+ /**
50
+ * Custom handler for 500 Internal Server Error responses.
51
+ * Called when a route handler or middleware throws an error.
52
+ *
53
+ * @param ctx - Request context
54
+ * @param error - The error that was thrown
55
+ * @returns Response, HandlerResponse, or Promise of either (will be converted to Response)
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * const app = createApp({
60
+ * onError: async (ctx, error) => {
61
+ * await logErrorToExternalService(error, ctx.request.url);
62
+ * return new Response(
63
+ * JSON.stringify({ error: "Internal server error" }),
64
+ * { status: 500, headers: { "Content-Type": "application/json" } }
65
+ * );
66
+ * }
67
+ * });
68
+ * ```
69
+ */
70
+ onError?: (ctx: RequestContext, error: unknown) => Response | HandlerResponse | Promise<Response | HandlerResponse>;
7
71
  }
8
72
  //# sourceMappingURL=appOptions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"appOptions.d.ts","sourceRoot":"","sources":["../../src/types/appOptions.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB"}
1
+ {"version":3,"file":"appOptions.d.ts","sourceRoot":"","sources":["../../src/types/appOptions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;;;;;OAgBG;IACH,UAAU,CAAC,EAAE,CACZ,GAAG,EAAE,cAAc,KACf,QAAQ,GAAG,eAAe,GAAG,OAAO,CAAC,QAAQ,GAAG,eAAe,CAAC,CAAC;IACtE;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,kBAAkB,CAAC,EAAE,CACpB,GAAG,EAAE,cAAc,EACnB,cAAc,EAAE,MAAM,EAAE,KACpB,QAAQ,GAAG,eAAe,GAAG,OAAO,CAAC,QAAQ,GAAG,eAAe,CAAC,CAAC;IACtE;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,EAAE,CACT,GAAG,EAAE,cAAc,EACnB,KAAK,EAAE,OAAO,KACV,QAAQ,GAAG,eAAe,GAAG,OAAO,CAAC,QAAQ,GAAG,eAAe,CAAC,CAAC;CACtE"}
@@ -1,6 +1,7 @@
1
1
  import type { BunaryServer } from "./bunaryServer.js";
2
2
  import type { GroupOptions } from "./groupOptions.js";
3
3
  import type { GroupCallback } from "./groupRouter.js";
4
+ import type { ListenOptions } from "./listenOptions.js";
4
5
  import type { Middleware } from "./middleware.js";
5
6
  import type { RouteBuilder } from "./routeBuilder.js";
6
7
  import type { RouteHandler } from "./routeHandler.js";
@@ -82,11 +83,16 @@ export interface BunaryApp {
82
83
  getRoutes: () => RouteInfo[];
83
84
  /**
84
85
  * Start the HTTP server.
85
- * @param port - Port number to listen on (default: 3000)
86
- * @param hostname - Hostname to bind to (default: "localhost")
86
+ *
87
+ * Supports two call styles:
88
+ * - `listen(port?, hostname?)` - positional arguments
89
+ * - `listen({ port?, hostname? })` - options object
90
+ *
91
+ * @param portOrOptions - Port number, or options object with port and hostname
92
+ * @param hostname - Hostname to bind to (when using positional form)
87
93
  * @returns Server instance with stop() method
88
94
  */
89
- listen: (port?: number, hostname?: string) => BunaryServer;
95
+ listen: ((port?: number, hostname?: string) => BunaryServer) & ((options: ListenOptions) => BunaryServer);
90
96
  /**
91
97
  * Handle an incoming request (used internally and for testing).
92
98
  * @param request - The incoming Request object
@@ -1 +1 @@
1
- {"version":3,"file":"bunaryApp.d.ts","sourceRoot":"","sources":["../../src/types/bunaryApp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,SAAS;IACzB;;;;OAIG;IACH,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,YAAY,CAAC;IAE3D;;;;OAIG;IACH,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,YAAY,CAAC;IAE5D;;;;OAIG;IACH,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,YAAY,CAAC;IAE3D;;;;OAIG;IACH,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,YAAY,CAAC;IAE9D;;;;OAIG;IACH,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,YAAY,CAAC;IAE7D;;;;OAIG;IACH,GAAG,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,SAAS,CAAC;IAE3C;;;;OAIG;IACH,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,KAAK,SAAS,CAAC,GAC9D,CAAC,CAAC,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,KAAK,SAAS,CAAC,CAAC;IAEjE;;;;;;OAMG;IACH,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,CAAC;IAE1E;;;;OAIG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAEpC;;;OAGG;IACH,SAAS,EAAE,MAAM,SAAS,EAAE,CAAC;IAE7B;;;;;OAKG;IACH,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,YAAY,CAAC;IAE3D;;;;OAIG;IACH,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC/C"}
1
+ {"version":3,"file":"bunaryApp.d.ts","sourceRoot":"","sources":["../../src/types/bunaryApp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,SAAS;IACzB;;;;OAIG;IACH,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,YAAY,CAAC;IAE3D;;;;OAIG;IACH,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,YAAY,CAAC;IAE5D;;;;OAIG;IACH,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,YAAY,CAAC;IAE3D;;;;OAIG;IACH,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,YAAY,CAAC;IAE9D;;;;OAIG;IACH,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,YAAY,CAAC;IAE7D;;;;OAIG;IACH,GAAG,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,SAAS,CAAC;IAE3C;;;;OAIG;IACH,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,KAAK,SAAS,CAAC,GAC9D,CAAC,CAAC,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,KAAK,SAAS,CAAC,CAAC;IAEjE;;;;;;OAMG;IACH,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,CAAC;IAE1E;;;;OAIG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAEpC;;;OAGG;IACH,SAAS,EAAE,MAAM,SAAS,EAAE,CAAC;IAE7B;;;;;;;;;;OAUG;IACH,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,YAAY,CAAC,GAC3D,CAAC,CAAC,OAAO,EAAE,aAAa,KAAK,YAAY,CAAC,CAAC;IAE5C;;;;OAIG;IACH,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC/C"}
@@ -4,13 +4,13 @@
4
4
  export type { AppOptions } from "./appOptions.js";
5
5
  export type { BunaryApp } from "./bunaryApp.js";
6
6
  export type { BunaryServer } from "./bunaryServer.js";
7
- export type { GroupCallback, GroupRouter } from "./groupRouter.js";
8
7
  export type { GroupOptions } from "./groupOptions.js";
8
+ export type { GroupCallback, GroupRouter } from "./groupRouter.js";
9
9
  export type { HandlerResponse } from "./handlerResponse.js";
10
10
  export type { HttpMethod } from "./httpMethod.js";
11
+ export type { ListenOptions } from "./listenOptions.js";
11
12
  export type { Middleware } from "./middleware.js";
12
13
  export type { PathParams } from "./pathParams.js";
13
- export type { QueryParams } from "./queryParams.js";
14
14
  export type { RequestContext } from "./requestContext.js";
15
15
  export type { Route } from "./route.js";
16
16
  export type { RouteBuilder } from "./routeBuilder.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACnE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,YAAY,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACnE,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,YAAY,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Options for app.listen() when using the object form.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * app.listen({ port: 3000, hostname: "localhost" });
7
+ * app.listen({ port: 8080 });
8
+ * ```
9
+ */
10
+ export interface ListenOptions {
11
+ /** Port number to listen on (default: 3000) */
12
+ port?: number;
13
+ /** Hostname to bind to (default: "localhost") */
14
+ hostname?: string;
15
+ }
16
+ //# sourceMappingURL=listenOptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listenOptions.d.ts","sourceRoot":"","sources":["../../src/types/listenOptions.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC7B,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunary/http",
3
- "version": "0.0.5",
3
+ "version": "0.1.0",
4
4
  "description": "HTTP routing and middleware for Bunary - a Bun-first backend framework inspired by Laravel",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -44,11 +44,9 @@
44
44
  "engines": {
45
45
  "bun": ">=1.0.0"
46
46
  },
47
- "dependencies": {
48
- "@bunary/core": "^0.0.2"
49
- },
47
+ "dependencies": {},
50
48
  "devDependencies": {
51
- "@biomejs/biome": "^1.9.4",
49
+ "@biomejs/biome": "^2.3.13",
52
50
  "bun-types": "^1.0.0",
53
51
  "typescript": "^5.0.0"
54
52
  }