@quilla-be-kit/http 0.4.0 → 0.6.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.
Files changed (68) hide show
  1. package/README.md +159 -12
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/adapter/hono/hono-request.adapter.d.ts +4 -1
  4. package/dist/adapter/hono/hono-request.adapter.d.ts.map +1 -1
  5. package/dist/adapter/hono/hono-request.adapter.js +20 -21
  6. package/dist/adapter/hono/hono-request.adapter.js.map +1 -1
  7. package/dist/adapter/hono/hono.server.d.ts +3 -0
  8. package/dist/adapter/hono/hono.server.d.ts.map +1 -1
  9. package/dist/adapter/hono/hono.server.js +7 -3
  10. package/dist/adapter/hono/hono.server.js.map +1 -1
  11. package/dist/decorator/controller.decorator.d.ts +4 -1
  12. package/dist/decorator/controller.decorator.d.ts.map +1 -1
  13. package/dist/decorator/controller.decorator.js +5 -3
  14. package/dist/decorator/controller.decorator.js.map +1 -1
  15. package/dist/decorator/index.d.ts +2 -2
  16. package/dist/decorator/index.d.ts.map +1 -1
  17. package/dist/decorator/index.js.map +1 -1
  18. package/dist/decorator/method.decorators.d.ts +13 -10
  19. package/dist/decorator/method.decorators.d.ts.map +1 -1
  20. package/dist/decorator/method.decorators.js +2 -1
  21. package/dist/decorator/method.decorators.js.map +1 -1
  22. package/dist/decorator/route.metadata.d.ts +3 -0
  23. package/dist/decorator/route.metadata.d.ts.map +1 -1
  24. package/dist/decorator/route.metadata.js +19 -0
  25. package/dist/decorator/route.metadata.js.map +1 -1
  26. package/dist/error/default.resolver.d.ts +6 -0
  27. package/dist/error/default.resolver.d.ts.map +1 -0
  28. package/dist/error/default.resolver.js +45 -0
  29. package/dist/error/default.resolver.js.map +1 -0
  30. package/dist/error/{resolve-http-error.d.ts → error-resolver.interface.d.ts} +4 -2
  31. package/dist/error/error-resolver.interface.d.ts.map +1 -0
  32. package/dist/error/error-resolver.interface.js +2 -0
  33. package/dist/error/error-resolver.interface.js.map +1 -0
  34. package/dist/error/index.d.ts +2 -1
  35. package/dist/error/index.d.ts.map +1 -1
  36. package/dist/error/index.js +1 -1
  37. package/dist/error/index.js.map +1 -1
  38. package/dist/index.d.ts +1 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/request/default.serializer.d.ts +6 -0
  43. package/dist/request/default.serializer.d.ts.map +1 -0
  44. package/dist/request/default.serializer.js +7 -0
  45. package/dist/request/default.serializer.js.map +1 -0
  46. package/dist/request/index.d.ts +2 -0
  47. package/dist/request/index.d.ts.map +1 -1
  48. package/dist/request/index.js +1 -0
  49. package/dist/request/index.js.map +1 -1
  50. package/dist/request/response-serializer.interface.d.ts +5 -0
  51. package/dist/request/response-serializer.interface.d.ts.map +1 -0
  52. package/dist/request/response-serializer.interface.js +2 -0
  53. package/dist/request/response-serializer.interface.js.map +1 -0
  54. package/dist/router/http-module-meta.type.d.ts +1 -0
  55. package/dist/router/http-module-meta.type.d.ts.map +1 -1
  56. package/dist/router/router.d.ts.map +1 -1
  57. package/dist/router/router.js +6 -2
  58. package/dist/router/router.js.map +1 -1
  59. package/dist/server/http-conventions.type.d.ts +7 -0
  60. package/dist/server/http-conventions.type.d.ts.map +1 -0
  61. package/dist/server/http-conventions.type.js +2 -0
  62. package/dist/server/http-conventions.type.js.map +1 -0
  63. package/dist/server/index.d.ts +1 -0
  64. package/dist/server/index.d.ts.map +1 -1
  65. package/package.json +1 -1
  66. package/dist/error/resolve-http-error.d.ts.map +0 -1
  67. package/dist/error/resolve-http-error.js +0 -43
  68. package/dist/error/resolve-http-error.js.map +0 -1
package/README.md CHANGED
@@ -147,27 +147,78 @@ await runtime.run(async () => {
147
147
 
148
148
  ## Decorators
149
149
 
150
- ### `@Controller(prefix)`
150
+ ### `@Controller(prefix, options?)`
151
151
 
152
- Class decorator. Every route on the class gets `prefix` prepended.
152
+ Class decorator. Every route on the class gets `prefix` prepended. The optional
153
+ second argument carries a controller-level **version** default (see
154
+ [Versioning](#versioning)).
153
155
 
154
156
  ```ts
155
157
  @Controller('/users')
156
158
  class UsersController { ... }
159
+
160
+ @Controller('/users', { version: '/api/v1' }) // controller-wide version default
161
+ class UsersController { ... }
157
162
  ```
158
163
 
159
164
  ### HTTP method decorators
160
165
 
161
166
  ```ts
162
- @Get(path) @GetPublic(path)
163
- @Post(path) @PostPublic(path)
164
- @Put(path) @PutPublic(path)
165
- @Patch(path) @PatchPublic(path)
166
- @Delete(path) @DeletePublic(path)
167
+ @Get(path, options?) @GetPublic(path, options?)
168
+ @Post(path, options?) @PostPublic(path, options?)
169
+ @Put(path, options?) @PutPublic(path, options?)
170
+ @Patch(path, options?) @PatchPublic(path, options?)
171
+ @Delete(path, options?) @DeletePublic(path, options?)
167
172
  ```
168
173
 
169
174
  The `*Public` variants mark the route as public — **auth middlewares are skipped** for these routes. The non-public variants run every registered `authMiddleware` before the handler.
170
175
 
176
+ The optional `options` argument (`RouteOptions`) carries a per-route **version**
177
+ override (see [Versioning](#versioning)):
178
+
179
+ ```ts
180
+ @Get('/:id', { version: '/api/v2' })
181
+ ```
182
+
183
+ ### Versioning
184
+
185
+ A version segment can be declared at three levels and is inserted
186
+ **resource-first** into the composed path — after the module prefix, before the
187
+ controller — so each module stays a clean future service boundary:
188
+
189
+ ```
190
+ [module prefix] + [effective version] + [registration prefix] + [@Controller prefix] + [@Route path]
191
+ ```
192
+
193
+ The effective version for a route resolves by precedence:
194
+
195
+ ```
196
+ route option ?? @Controller version ?? HttpModuleMeta.version ?? ''
197
+ ```
198
+
199
+ - `HttpModuleMeta.version?` — module-wide default (see the [registry bridge](#bridge-to-componentregistryhttpmodulemeta)).
200
+ - `@Controller(prefix, { version })` — controller-level default.
201
+ - `@Get('/x', { version })` — per-route override (available on every method + `*Public` decorator).
202
+
203
+ ```ts
204
+ @Controller('/auth', { version: '/api/v1' }) // default for the whole controller
205
+ class AuthController {
206
+ @Get('/:id') // → /iam/api/v1/auth/:id (module prefix /iam)
207
+ async show() {}
208
+
209
+ @Get('/:id', { version: '/api/v2' }) // → /iam/api/v2/auth/:id (route override)
210
+ async showV2() {}
211
+ }
212
+ ```
213
+
214
+ Version segments go through the same leading-slash / no-trailing-slash
215
+ normalization as every other segment (`/iam` + `api/v1` → `/iam/api/v1`, no double
216
+ slash). They are static, so they only **add** specificity and never trigger a
217
+ false duplicate; two routes that differ only by version resolve to distinct
218
+ paths. Version is orthogonal to `*Public` / auth — it affects the path only.
219
+ When no version is set anywhere, composed paths are byte-identical to a service
220
+ that never adopted versioning.
221
+
171
222
  ### `@AuthorizeScope(scope, mode?)`
172
223
 
173
224
  Scope-based authorization. Reads an `AuthenticatedToken` from `request.getAttribute(HttpAttributes.VERIFIED_TOKEN)` and checks the token's `scopes` against the required scope(s).
@@ -197,7 +248,7 @@ async create(req: HttpRequest): Promise<HttpResponse> {
197
248
  }
198
249
  ```
199
250
 
200
- On validation failure, throws `ValidationError` with `context.issues` containing the validator's raw error array (e.g. Zod issues, Joi details). `resolveHttpError` surfaces this as a 400 response with `body.error.details.issues`.
251
+ On validation failure, throws `ValidationError` with `context.issues` containing the validator's raw error array (e.g. Zod issues, Joi details). The default error resolver (`DefaultErrorResolver`) surfaces this as a 400 response with `body.error.details.issues`. See [Response and error conventions](#response-and-error-conventions) to override the wire shape.
201
252
 
202
253
  ## Multipart / form-data
203
254
 
@@ -269,7 +320,7 @@ async export(req: HttpRequest): Promise<HttpStreamResponse> {
269
320
 
270
321
  **The real tradeoff is that binary responses lose the envelope convention — no `payload` / `metadata` wrapper around the bytes, and middleware can't introspect stream contents post-hoc.** That's intrinsic to streaming, not a flaw: logging, response shaping, and validators that read response bodies all become no-ops on the binary path. You're opting out of the standard JSON shape so the framework can hand bytes directly to the socket.
271
322
 
272
- Error handling caveat: a handler that throws **before** producing the response still goes through `resolveHttpError` and emits a normal JSON error envelope. A handler that throws **mid-stream** — after the response status is already committed — aborts the connection; the client sees a truncated body, not a JSON error.
323
+ Error handling caveat: a handler that throws **before** producing the response still goes through the configured error resolver (`DefaultErrorResolver` by default) and emits a normal JSON error envelope. A handler that throws **mid-stream** — after the response status is already committed — aborts the connection; the client sees a truncated body, not a JSON error.
273
324
 
274
325
  ## `RequestValidator` adapter
275
326
 
@@ -350,7 +401,7 @@ const router = new Router({
350
401
 
351
402
  - Controllers can be registered as plain instances (no extra metadata) or wrapped in `{ controller, prefix?, middlewares? }` for per-controller prefix + middlewares.
352
403
  - Routes are sorted by **specificity** (static segments > parametric > wildcard) so `/users/healthz` matches before `/users/:id`.
353
- - Path composition: `[module prefix] + [registration prefix] + [@Controller prefix] + [@Route path]`, normalized to a single leading slash and no trailing slash.
404
+ - Path composition: `[module prefix] + [effective version] + [registration prefix] + [@Controller prefix] + [@Route path]`, normalized to a single leading slash and no trailing slash. The **effective version** is resource-first and resolves `route option ?? @Controller version ?? HttpModuleMeta.version ?? ''` — see [Versioning](#versioning).
354
405
  - Duplicate routes (same method + path) throw at construction time — you catch double-registrations at startup, not under load.
355
406
 
356
407
  ### Middleware chain order
@@ -385,7 +436,8 @@ registry
385
436
  .register({
386
437
  name: 'iam',
387
438
  meta: {
388
- prefix: '/api/v1',
439
+ prefix: '/iam',
440
+ version: '/api/v1', // module-wide default; routes/controllers can override
389
441
  controllers: [usersController, authController],
390
442
  middlewares: [iamModuleMw],
391
443
  },
@@ -394,7 +446,8 @@ registry
394
446
  .register({
395
447
  name: 'dm',
396
448
  meta: {
397
- prefix: '/api/v1',
449
+ prefix: '/dm',
450
+ version: '/api/v1',
398
451
  controllers: [documentsController],
399
452
  },
400
453
  });
@@ -447,6 +500,7 @@ const server = new HonoServer({
447
500
  router, // HonoServer reads the execution-context provider from Router
448
501
  requestValidator, // optional — required only if any route uses @ValidateRequest
449
502
  logger, // optional — used for startup/shutdown/error logs
503
+ conventions, // optional — override the error / success wire shapes (see below)
450
504
  serve: honoServe,
451
505
  });
452
506
  ```
@@ -483,6 +537,99 @@ Defaults applied when `cors` is set:
483
537
 
484
538
  If you need non-default values, omit `cors` and wire `hono/cors` yourself inside the `serve` callback, or raise an issue.
485
539
 
540
+ ### Response and error conventions
541
+
542
+ The success/envelope shape and the error shape are both consumer-overridable through the
543
+ optional `conventions` facade on `HonoServer`. It groups two strategies, each defaulting to a
544
+ class that reproduces the built-in wire shape byte-for-byte — omit `conventions` and nothing
545
+ changes.
546
+
547
+ ```ts
548
+ type HttpConventions = {
549
+ readonly errorResolver?: ErrorResolver; // controls the error status + body
550
+ readonly responseSerializer?: ResponseSerializer; // controls the JSON success/envelope body
551
+ };
552
+
553
+ interface ErrorResolver {
554
+ resolve(err: unknown): ResolvedHttpError; // { httpCode, body }
555
+ }
556
+ interface ResponseSerializer {
557
+ serialize(response: HttpJsonResponse): unknown; // return the wire body, or undefined for no body
558
+ }
559
+ ```
560
+
561
+ Defaults are exported so a custom strategy can delegate to them: `DefaultErrorResolver` (the
562
+ status mapping — `ValidationError` → 400, `NotFoundError` → 404, …) and `DefaultResponseSerializer`
563
+ (strips `httpCode`/`headers`, keeps `payload` / `error` / `metadata`, and returns `undefined`
564
+ for an empty body so it becomes a bodyless response).
565
+
566
+ **Custom error format** — e.g. RFC 7807 Problem Details, reusing the default status mapping:
567
+
568
+ ```ts
569
+ import {
570
+ DefaultErrorResolver,
571
+ type ErrorResolver,
572
+ type ResolvedHttpError,
573
+ } from '@quilla-be-kit/http';
574
+
575
+ class ProblemDetailsResolver implements ErrorResolver {
576
+ private readonly base = new DefaultErrorResolver();
577
+
578
+ resolve(err: unknown): ResolvedHttpError {
579
+ const { httpCode, body } = this.base.resolve(err); // reuse status mapping
580
+ return {
581
+ httpCode,
582
+ body: {
583
+ error: {
584
+ name: 'about:blank',
585
+ message: body.error?.message ?? 'Error',
586
+ details: { status: httpCode, ...(body.error?.details ?? {}) },
587
+ },
588
+ },
589
+ };
590
+ }
591
+ }
592
+
593
+ const server = new HonoServer({
594
+ port: 3000,
595
+ router,
596
+ serve: honoServe,
597
+ conventions: { errorResolver: new ProblemDetailsResolver() },
598
+ });
599
+ ```
600
+
601
+ **Custom success envelope** — e.g. rename `payload` → `data` and lift pagination to the top level:
602
+
603
+ ```ts
604
+ import {
605
+ type HttpJsonResponse,
606
+ type ResponseSerializer,
607
+ } from '@quilla-be-kit/http';
608
+
609
+ class DataEnvelopeSerializer implements ResponseSerializer {
610
+ serialize(r: HttpJsonResponse): unknown {
611
+ if (r.error) return { error: r.error };
612
+ if (r.payload === undefined) return undefined; // preserve the bodyless-response branch
613
+ const p = r.metadata?.pagination;
614
+ return {
615
+ data: r.payload,
616
+ ...(p ? { pagination: { page: p.page, perPage: p.limit, total: p.total } } : {}),
617
+ };
618
+ }
619
+ }
620
+
621
+ const server = new HonoServer({
622
+ port: 3000,
623
+ router,
624
+ serve: honoServe,
625
+ conventions: { responseSerializer: new DataEnvelopeSerializer() },
626
+ });
627
+ ```
628
+
629
+ The binary/stream response paths never touch the serializer — they still write bytes directly.
630
+ On the frontend, `@quilla-fe-kit/api-client-react-query` reconciles a custom envelope with a
631
+ `queryTransformer`, and `@quilla-fe-kit/api-client` a custom error shape with an `errorParser`.
632
+
486
633
  ## Other frameworks
487
634
 
488
635
  If you need Express or Fastify: open an issue. Adapter sub-paths ship as library additions when they exist, not as consumer extension points.