@bunary/http 0.0.1 → 0.0.4

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/README.md +237 -3
  3. package/dist/app.d.ts +7 -25
  4. package/dist/app.d.ts.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +342 -41
  8. package/dist/pathUtils.d.ts +34 -0
  9. package/dist/pathUtils.d.ts.map +1 -0
  10. package/dist/response.d.ts +26 -0
  11. package/dist/response.d.ts.map +1 -0
  12. package/dist/router.d.ts +49 -0
  13. package/dist/router.d.ts.map +1 -0
  14. package/dist/routes/builder.d.ts +17 -0
  15. package/dist/routes/builder.d.ts.map +1 -0
  16. package/dist/routes/find.d.ts +27 -0
  17. package/dist/routes/find.d.ts.map +1 -0
  18. package/dist/routes/group.d.ts +7 -0
  19. package/dist/routes/group.d.ts.map +1 -0
  20. package/dist/routes/index.d.ts +4 -0
  21. package/dist/routes/index.d.ts.map +1 -0
  22. package/dist/types/appOptions.d.ts +8 -0
  23. package/dist/types/appOptions.d.ts.map +1 -0
  24. package/dist/types/bunaryApp.d.ts +97 -0
  25. package/dist/types/bunaryApp.d.ts.map +1 -0
  26. package/dist/types/bunaryServer.d.ts +14 -0
  27. package/dist/types/bunaryServer.d.ts.map +1 -0
  28. package/dist/types/groupOptions.d.ts +13 -0
  29. package/dist/types/groupOptions.d.ts.map +1 -0
  30. package/dist/types/groupRouter.d.ts +26 -0
  31. package/dist/types/groupRouter.d.ts.map +1 -0
  32. package/dist/types/handlerResponse.d.ts +8 -0
  33. package/dist/types/handlerResponse.d.ts.map +1 -0
  34. package/dist/types/httpMethod.d.ts +5 -0
  35. package/dist/types/httpMethod.d.ts.map +1 -0
  36. package/dist/types/index.d.ts +19 -0
  37. package/dist/types/index.d.ts.map +1 -0
  38. package/dist/types/middleware.d.ts +21 -0
  39. package/dist/types/middleware.d.ts.map +1 -0
  40. package/dist/types/pathParams.d.ts +6 -0
  41. package/dist/types/pathParams.d.ts.map +1 -0
  42. package/dist/types/queryParams.d.ts +5 -0
  43. package/dist/types/queryParams.d.ts.map +1 -0
  44. package/dist/types/requestContext.d.ts +22 -0
  45. package/dist/types/requestContext.d.ts.map +1 -0
  46. package/dist/types/route.d.ts +27 -0
  47. package/dist/types/route.d.ts.map +1 -0
  48. package/dist/types/routeBuilder.d.ts +24 -0
  49. package/dist/types/routeBuilder.d.ts.map +1 -0
  50. package/dist/types/routeHandler.d.ts +17 -0
  51. package/dist/types/routeHandler.d.ts.map +1 -0
  52. package/dist/types/routeInfo.d.ts +13 -0
  53. package/dist/types/routeInfo.d.ts.map +1 -0
  54. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,7 +5,27 @@ 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.0.1] - 2025-01-27
8
+ ## [0.0.2] - 2026-01-24
9
+
10
+ ### Added
11
+
12
+ - Comprehensive middleware test suite (19 tests)
13
+ - Middleware documentation in README with examples:
14
+ - Basic logging middleware
15
+ - Error handling middleware
16
+ - Authentication middleware pattern
17
+ - Middleware chain execution order
18
+
19
+ ### Verified
20
+
21
+ - Middleware pipeline executes in registration order (FR-016)
22
+ - `app.use(middleware)` adds middleware to pipeline (FR-015)
23
+ - Middleware can call `next()` for chain continuation
24
+ - Middleware can return early without calling `next()`
25
+ - Middleware can catch and handle errors from `next()`
26
+ - Middleware errors return 500 response
27
+
28
+ ## [0.0.1] - 2026-01-24
9
29
 
10
30
  ### Added
11
31
 
package/README.md CHANGED
@@ -11,6 +11,10 @@ Part of the [Bunary](https://github.com/bunary-dev) ecosystem - a Bun-first back
11
11
  - 🔒 **Type-safe** - Full TypeScript support with strict types
12
12
  - ⚡ **Fast** - Minimal overhead, direct routing
13
13
  - 🧩 **Simple API** - Chainable route registration with automatic JSON serialization
14
+ - 📂 **Route Groups** - Organize routes with shared prefixes, middleware, and name prefixes
15
+ - 🏷️ **Named Routes** - URL generation with route names
16
+ - ✅ **Route Constraints** - Validate parameters with regex patterns
17
+ - ❓ **Optional Parameters** - Flexible routes with optional path segments
14
18
 
15
19
  ## Installation
16
20
 
@@ -144,9 +148,234 @@ const data = await response.json();
144
148
  // { message: 'hi' }
145
149
  ```
146
150
 
151
+ ## Middleware
152
+
153
+ Add middleware to handle cross-cutting concerns like logging, authentication, and error handling.
154
+
155
+ ### Basic Middleware
156
+
157
+ ```typescript
158
+ // Logging middleware
159
+ app.use(async (ctx, next) => {
160
+ const start = Date.now();
161
+ const result = await next();
162
+ console.log(`${ctx.request.method} ${new URL(ctx.request.url).pathname} - ${Date.now() - start}ms`);
163
+ return result;
164
+ });
165
+ ```
166
+
167
+ ### Middleware Chain
168
+
169
+ Middleware executes in registration order. Each middleware can:
170
+ - Run code before calling `next()`
171
+ - Call `next()` to continue the chain
172
+ - Run code after `next()` returns
173
+ - Return early without calling `next()`
174
+
175
+ ```typescript
176
+ app
177
+ .use(async (ctx, next) => {
178
+ console.log('First - before');
179
+ const result = await next();
180
+ console.log('First - after');
181
+ return result;
182
+ })
183
+ .use(async (ctx, next) => {
184
+ console.log('Second - before');
185
+ const result = await next();
186
+ console.log('Second - after');
187
+ return result;
188
+ });
189
+
190
+ // Output order: First-before, Second-before, handler, Second-after, First-after
191
+ ```
192
+
193
+ ### Error Handling Middleware
194
+
195
+ ```typescript
196
+ app.use(async (ctx, next) => {
197
+ try {
198
+ return await next();
199
+ } catch (error) {
200
+ console.error('Error:', error);
201
+ return new Response(JSON.stringify({ error: error.message }), {
202
+ status: 500,
203
+ headers: { 'Content-Type': 'application/json' }
204
+ });
205
+ }
206
+ });
207
+ ```
208
+
209
+ ### Auth Middleware (Example)
210
+
211
+ ```typescript
212
+ app.use(async (ctx, next) => {
213
+ const token = ctx.request.headers.get('Authorization');
214
+ if (!token) {
215
+ return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 });
216
+ }
217
+ // Validate token...
218
+ return await next();
219
+ });
220
+ ```
221
+
222
+ ## Route Groups
223
+
224
+ Group routes together with shared prefixes, middleware, and name prefixes.
225
+
226
+ ### Basic Groups
227
+
228
+ ```typescript
229
+ // Simple prefix
230
+ app.group('/api', (router) => {
231
+ router.get('/users', () => ({ users: [] })); // /api/users
232
+ router.get('/posts', () => ({ posts: [] })); // /api/posts
233
+ });
234
+ ```
235
+
236
+ ### Groups with Options
237
+
238
+ ```typescript
239
+ // Auth middleware for protected routes
240
+ const authMiddleware = async (ctx, next) => {
241
+ const token = ctx.request.headers.get('Authorization');
242
+ if (!token) return new Response('Unauthorized', { status: 401 });
243
+ return await next();
244
+ };
245
+
246
+ app.group({
247
+ prefix: '/admin',
248
+ middleware: [authMiddleware],
249
+ name: 'admin.'
250
+ }, (router) => {
251
+ router.get('/dashboard', () => ({})).name('dashboard'); // name: admin.dashboard
252
+ router.get('/users', () => ({})).name('users'); // name: admin.users
253
+ });
254
+ ```
255
+
256
+ ### Nested Groups
257
+
258
+ ```typescript
259
+ app.group('/api', (api) => {
260
+ api.group('/v1', (v1) => {
261
+ v1.get('/users', () => ({})); // /api/v1/users
262
+ });
263
+ api.group('/v2', (v2) => {
264
+ v2.get('/users', () => ({})); // /api/v2/users
265
+ });
266
+ });
267
+ ```
268
+
269
+ ## Named Routes
270
+
271
+ Assign names to routes for URL generation.
272
+
273
+ ### Naming Routes
274
+
275
+ ```typescript
276
+ app.get('/users/:id', (ctx) => ({})).name('users.show');
277
+ app.get('/posts/:slug', (ctx) => ({})).name('posts.show');
278
+ ```
279
+
280
+ ### Generating URLs
281
+
282
+ ```typescript
283
+ // Basic URL generation
284
+ const url = app.route('users.show', { id: 42 });
285
+ // "/users/42"
286
+
287
+ // With query string
288
+ const searchUrl = app.route('users.show', { id: 42, tab: 'profile' });
289
+ // "/users/42?tab=profile"
290
+
291
+ // Check if route exists
292
+ if (app.hasRoute('users.show')) {
293
+ // ...
294
+ }
295
+
296
+ // List all routes
297
+ const routes = app.getRoutes();
298
+ // [{ name: 'users.show', method: 'GET', path: '/users/:id' }, ...]
299
+ ```
300
+
301
+ ## Route Constraints
302
+
303
+ Add regex constraints to validate route parameters.
304
+
305
+ ### Basic Constraints
306
+
307
+ ```typescript
308
+ // Only match if :id is numeric
309
+ app.get('/users/:id', (ctx) => ({}))
310
+ .where('id', /^\d+$/);
311
+
312
+ // Using string pattern
313
+ app.get('/posts/:slug', (ctx) => ({}))
314
+ .where('slug', '^[a-z0-9-]+$');
315
+
316
+ // Multiple constraints
317
+ app.get('/users/:id/posts/:postId', (ctx) => ({}))
318
+ .where({ id: /^\d+$/, postId: /^\d+$/ });
319
+ ```
320
+
321
+ ### Helper Methods
322
+
323
+ ```typescript
324
+ // whereNumber - digits only
325
+ app.get('/users/:id', () => ({})).whereNumber('id');
326
+
327
+ // whereAlpha - letters only (a-zA-Z)
328
+ app.get('/categories/:name', () => ({})).whereAlpha('name');
329
+
330
+ // whereAlphaNumeric - letters and digits
331
+ app.get('/codes/:code', () => ({})).whereAlphaNumeric('code');
332
+
333
+ // whereUuid - UUID format
334
+ app.get('/items/:uuid', () => ({})).whereUuid('uuid');
335
+
336
+ // whereUlid - ULID format
337
+ app.get('/records/:ulid', () => ({})).whereUlid('ulid');
338
+
339
+ // whereIn - specific allowed values
340
+ app.get('/status/:status', () => ({})).whereIn('status', ['active', 'pending', 'archived']);
341
+ ```
342
+
343
+ ### Chaining Constraints
344
+
345
+ ```typescript
346
+ app.get('/users/:id/posts/:slug', (ctx) => ({}))
347
+ .whereNumber('id')
348
+ .whereAlpha('slug')
349
+ .name('users.posts');
350
+ ```
351
+
352
+ ## Optional Parameters
353
+
354
+ Use `?` to mark route parameters as optional.
355
+
356
+ ```typescript
357
+ // :id is optional
358
+ app.get('/users/:id?', (ctx) => {
359
+ if (ctx.params.id) {
360
+ return { user: ctx.params.id };
361
+ }
362
+ return { users: [] };
363
+ });
364
+
365
+ // Multiple optional params
366
+ app.get('/archive/:year?/:month?', (ctx) => {
367
+ const { year, month } = ctx.params;
368
+ // year and month may be undefined
369
+ return { year, month };
370
+ });
371
+
372
+ // Constraints work with optional params
373
+ app.get('/posts/:id?', (ctx) => ({})).whereNumber('id');
374
+ ```
375
+
147
376
  ## Error Handling
148
377
 
149
- Uncaught errors in handlers return a 500 response:
378
+ Uncaught errors in handlers return a 500 response with the error message:
150
379
 
151
380
  ```typescript
152
381
  app.get('/error', () => {
@@ -154,7 +383,7 @@ app.get('/error', () => {
154
383
  });
155
384
 
156
385
  // Returns: 500 Internal Server Error
157
- // Body: { error: "Internal Server Error" }
386
+ // Body: { error: "Something went wrong" }
158
387
  ```
159
388
 
160
389
  ## Types
@@ -167,7 +396,12 @@ import type {
167
396
  BunaryServer,
168
397
  RequestContext,
169
398
  RouteHandler,
170
- Middleware
399
+ Middleware,
400
+ RouteBuilder,
401
+ GroupOptions,
402
+ GroupRouter,
403
+ GroupCallback,
404
+ RouteInfo
171
405
  } from '@bunary/http';
172
406
  ```
173
407
 
package/dist/app.d.ts CHANGED
@@ -1,21 +1,4 @@
1
- /**
2
- * Create a new Bunary HTTP application instance.
3
- *
4
- * @returns BunaryApp instance with routing and middleware support
5
- *
6
- * @example
7
- * ```ts
8
- * import { createApp } from "@bunary/http";
9
- *
10
- * const app = createApp();
11
- *
12
- * app.get("/", () => ({ message: "Hello!" }));
13
- * app.get("/users/:id", (ctx) => ({ id: ctx.params.id }));
14
- *
15
- * app.listen(3000);
16
- * ```
17
- */
18
- import type { BunaryApp } from "./types.js";
1
+ import type { BunaryApp } from "./types/index.js";
19
2
  /**
20
3
  * Create a new Bunary HTTP application instance.
21
4
  *
@@ -38,15 +21,14 @@ import type { BunaryApp } from "./types.js";
38
21
  * return { id: ctx.params.id };
39
22
  * });
40
23
  *
41
- * // Query parameters
42
- * app.get("/search", (ctx) => {
43
- * return { query: ctx.query.get("q") };
24
+ * // Route groups
25
+ * app.group("/api", (router) => {
26
+ * router.get("/users", () => ({ users: [] }));
44
27
  * });
45
28
  *
46
- * // Custom Response
47
- * app.get("/custom", () => {
48
- * return new Response("Custom", { status: 201 });
49
- * });
29
+ * // Named routes
30
+ * app.get("/users/:id", (ctx) => ({ id: ctx.params.id })).name("users.show");
31
+ * const url = app.route("users.show", { id: 123 });
50
32
  *
51
33
  * app.listen(3000);
52
34
  * ```
package/dist/app.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EACX,SAAS,EAQT,MAAM,YAAY,CAAC;AA0FpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,SAAS,IAAI,SAAS,CAuOrC"}
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,CAmOrC"}
package/dist/index.d.ts CHANGED
@@ -18,6 +18,6 @@
18
18
  *
19
19
  * @packageDocumentation
20
20
  */
21
- export type { AppOptions, BunaryApp, BunaryServer, HandlerResponse, HttpMethod, Middleware, PathParams, QueryParams, RequestContext, Route, RouteHandler, } from "./types.js";
21
+ export type { AppOptions, BunaryApp, BunaryServer, GroupCallback, GroupOptions, GroupRouter, HandlerResponse, HttpMethod, Middleware, PathParams, QueryParams, RequestContext, Route, RouteBuilder, RouteHandler, RouteInfo, } from "./types/index.js";
22
22
  export { createApp } from "./app.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,eAAe,EACf,UAAU,EACV,UAAU,EACV,UAAU,EACV,WAAW,EACX,cAAc,EACd,KAAK,EACL,YAAY,GACZ,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC"}
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"}