@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.
- package/README.md +159 -12
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapter/hono/hono-request.adapter.d.ts +4 -1
- package/dist/adapter/hono/hono-request.adapter.d.ts.map +1 -1
- package/dist/adapter/hono/hono-request.adapter.js +20 -21
- package/dist/adapter/hono/hono-request.adapter.js.map +1 -1
- package/dist/adapter/hono/hono.server.d.ts +3 -0
- package/dist/adapter/hono/hono.server.d.ts.map +1 -1
- package/dist/adapter/hono/hono.server.js +7 -3
- package/dist/adapter/hono/hono.server.js.map +1 -1
- package/dist/decorator/controller.decorator.d.ts +4 -1
- package/dist/decorator/controller.decorator.d.ts.map +1 -1
- package/dist/decorator/controller.decorator.js +5 -3
- package/dist/decorator/controller.decorator.js.map +1 -1
- package/dist/decorator/index.d.ts +2 -2
- package/dist/decorator/index.d.ts.map +1 -1
- package/dist/decorator/index.js.map +1 -1
- package/dist/decorator/method.decorators.d.ts +13 -10
- package/dist/decorator/method.decorators.d.ts.map +1 -1
- package/dist/decorator/method.decorators.js +2 -1
- package/dist/decorator/method.decorators.js.map +1 -1
- package/dist/decorator/route.metadata.d.ts +3 -0
- package/dist/decorator/route.metadata.d.ts.map +1 -1
- package/dist/decorator/route.metadata.js +19 -0
- package/dist/decorator/route.metadata.js.map +1 -1
- package/dist/error/default.resolver.d.ts +6 -0
- package/dist/error/default.resolver.d.ts.map +1 -0
- package/dist/error/default.resolver.js +45 -0
- package/dist/error/default.resolver.js.map +1 -0
- package/dist/error/{resolve-http-error.d.ts → error-resolver.interface.d.ts} +4 -2
- package/dist/error/error-resolver.interface.d.ts.map +1 -0
- package/dist/error/error-resolver.interface.js +2 -0
- package/dist/error/error-resolver.interface.js.map +1 -0
- package/dist/error/index.d.ts +2 -1
- package/dist/error/index.d.ts.map +1 -1
- package/dist/error/index.js +1 -1
- package/dist/error/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/request/default.serializer.d.ts +6 -0
- package/dist/request/default.serializer.d.ts.map +1 -0
- package/dist/request/default.serializer.js +7 -0
- package/dist/request/default.serializer.js.map +1 -0
- package/dist/request/index.d.ts +2 -0
- package/dist/request/index.d.ts.map +1 -1
- package/dist/request/index.js +1 -0
- package/dist/request/index.js.map +1 -1
- package/dist/request/response-serializer.interface.d.ts +5 -0
- package/dist/request/response-serializer.interface.d.ts.map +1 -0
- package/dist/request/response-serializer.interface.js +2 -0
- package/dist/request/response-serializer.interface.js.map +1 -0
- package/dist/router/http-module-meta.type.d.ts +1 -0
- package/dist/router/http-module-meta.type.d.ts.map +1 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +6 -2
- package/dist/router/router.js.map +1 -1
- package/dist/server/http-conventions.type.d.ts +7 -0
- package/dist/server/http-conventions.type.d.ts.map +1 -0
- package/dist/server/http-conventions.type.js +2 -0
- package/dist/server/http-conventions.type.js.map +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/error/resolve-http-error.d.ts.map +0 -1
- package/dist/error/resolve-http-error.js +0 -43
- 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). `
|
|
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 `
|
|
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: '/
|
|
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: '/
|
|
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.
|