@quilla-be-kit/http 0.2.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 (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +400 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/adapter/hono/create-http-request.d.ts +15 -0
  5. package/dist/adapter/hono/create-http-request.d.ts.map +1 -0
  6. package/dist/adapter/hono/create-http-request.js +57 -0
  7. package/dist/adapter/hono/create-http-request.js.map +1 -0
  8. package/dist/adapter/hono/get-request-attributes.d.ts +2 -0
  9. package/dist/adapter/hono/get-request-attributes.d.ts.map +1 -0
  10. package/dist/adapter/hono/get-request-attributes.js +10 -0
  11. package/dist/adapter/hono/get-request-attributes.js.map +1 -0
  12. package/dist/adapter/hono/hono-middleware.adapter.d.ts +10 -0
  13. package/dist/adapter/hono/hono-middleware.adapter.d.ts.map +1 -0
  14. package/dist/adapter/hono/hono-middleware.adapter.js +13 -0
  15. package/dist/adapter/hono/hono-middleware.adapter.js.map +1 -0
  16. package/dist/adapter/hono/hono-request.adapter.d.ts +12 -0
  17. package/dist/adapter/hono/hono-request.adapter.d.ts.map +1 -0
  18. package/dist/adapter/hono/hono-request.adapter.js +99 -0
  19. package/dist/adapter/hono/hono-request.adapter.js.map +1 -0
  20. package/dist/adapter/hono/hono.server.d.ts +30 -0
  21. package/dist/adapter/hono/hono.server.d.ts.map +1 -0
  22. package/dist/adapter/hono/hono.server.js +64 -0
  23. package/dist/adapter/hono/hono.server.js.map +1 -0
  24. package/dist/adapter/hono/hono.types.d.ts +9 -0
  25. package/dist/adapter/hono/hono.types.d.ts.map +1 -0
  26. package/dist/adapter/hono/hono.types.js +4 -0
  27. package/dist/adapter/hono/hono.types.js.map +1 -0
  28. package/dist/adapter/hono/index.d.ts +2 -0
  29. package/dist/adapter/hono/index.d.ts.map +1 -0
  30. package/dist/adapter/hono/index.js +2 -0
  31. package/dist/adapter/hono/index.js.map +1 -0
  32. package/dist/decorator/authorize-scope.decorator.d.ts +6 -0
  33. package/dist/decorator/authorize-scope.decorator.d.ts.map +1 -0
  34. package/dist/decorator/authorize-scope.decorator.js +30 -0
  35. package/dist/decorator/authorize-scope.decorator.js.map +1 -0
  36. package/dist/decorator/controller.decorator.d.ts +2 -0
  37. package/dist/decorator/controller.decorator.d.ts.map +1 -0
  38. package/dist/decorator/controller.decorator.js +7 -0
  39. package/dist/decorator/controller.decorator.js.map +1 -0
  40. package/dist/decorator/index.d.ts +5 -0
  41. package/dist/decorator/index.d.ts.map +1 -0
  42. package/dist/decorator/index.js +5 -0
  43. package/dist/decorator/index.js.map +1 -0
  44. package/dist/decorator/method.decorators.d.ts +11 -0
  45. package/dist/decorator/method.decorators.d.ts.map +1 -0
  46. package/dist/decorator/method.decorators.js +27 -0
  47. package/dist/decorator/method.decorators.js.map +1 -0
  48. package/dist/decorator/route.metadata.d.ts +22 -0
  49. package/dist/decorator/route.metadata.d.ts.map +1 -0
  50. package/dist/decorator/route.metadata.js +61 -0
  51. package/dist/decorator/route.metadata.js.map +1 -0
  52. package/dist/decorator/validate-request.decorator.d.ts +7 -0
  53. package/dist/decorator/validate-request.decorator.d.ts.map +1 -0
  54. package/dist/decorator/validate-request.decorator.js +57 -0
  55. package/dist/decorator/validate-request.decorator.js.map +1 -0
  56. package/dist/error/index.d.ts +2 -0
  57. package/dist/error/index.d.ts.map +1 -0
  58. package/dist/error/index.js +2 -0
  59. package/dist/error/index.js.map +1 -0
  60. package/dist/error/resolve-http-error.d.ts +7 -0
  61. package/dist/error/resolve-http-error.d.ts.map +1 -0
  62. package/dist/error/resolve-http-error.js +43 -0
  63. package/dist/error/resolve-http-error.js.map +1 -0
  64. package/dist/index.d.ts +6 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +6 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/request/authenticated-token.interface.d.ts +4 -0
  69. package/dist/request/authenticated-token.interface.d.ts.map +1 -0
  70. package/dist/request/authenticated-token.interface.js +2 -0
  71. package/dist/request/authenticated-token.interface.js.map +1 -0
  72. package/dist/request/http-attributes.d.ts +6 -0
  73. package/dist/request/http-attributes.d.ts.map +1 -0
  74. package/dist/request/http-attributes.js +6 -0
  75. package/dist/request/http-attributes.js.map +1 -0
  76. package/dist/request/http-middleware.type.d.ts +3 -0
  77. package/dist/request/http-middleware.type.d.ts.map +1 -0
  78. package/dist/request/http-middleware.type.js +2 -0
  79. package/dist/request/http-middleware.type.js.map +1 -0
  80. package/dist/request/http-request.interface.d.ts +18 -0
  81. package/dist/request/http-request.interface.d.ts.map +1 -0
  82. package/dist/request/http-request.interface.js +2 -0
  83. package/dist/request/http-request.interface.js.map +1 -0
  84. package/dist/request/http-response.type.d.ts +18 -0
  85. package/dist/request/http-response.type.d.ts.map +1 -0
  86. package/dist/request/http-response.type.js +2 -0
  87. package/dist/request/http-response.type.js.map +1 -0
  88. package/dist/request/index.d.ts +6 -0
  89. package/dist/request/index.d.ts.map +1 -0
  90. package/dist/request/index.js +2 -0
  91. package/dist/request/index.js.map +1 -0
  92. package/dist/router/auth-middleware-stack.type.d.ts +13 -0
  93. package/dist/router/auth-middleware-stack.type.d.ts.map +1 -0
  94. package/dist/router/auth-middleware-stack.type.js +2 -0
  95. package/dist/router/auth-middleware-stack.type.js.map +1 -0
  96. package/dist/router/controller-registration.type.d.ts +7 -0
  97. package/dist/router/controller-registration.type.d.ts.map +1 -0
  98. package/dist/router/controller-registration.type.js +2 -0
  99. package/dist/router/controller-registration.type.js.map +1 -0
  100. package/dist/router/http-module-meta.type.d.ts +8 -0
  101. package/dist/router/http-module-meta.type.d.ts.map +1 -0
  102. package/dist/router/http-module-meta.type.js +2 -0
  103. package/dist/router/http-module-meta.type.js.map +1 -0
  104. package/dist/router/index.d.ts +6 -0
  105. package/dist/router/index.d.ts.map +1 -0
  106. package/dist/router/index.js +2 -0
  107. package/dist/router/index.js.map +1 -0
  108. package/dist/router/normalized-route.type.d.ts +20 -0
  109. package/dist/router/normalized-route.type.d.ts.map +1 -0
  110. package/dist/router/normalized-route.type.js +2 -0
  111. package/dist/router/normalized-route.type.js.map +1 -0
  112. package/dist/router/router-options.type.d.ts +37 -0
  113. package/dist/router/router-options.type.d.ts.map +1 -0
  114. package/dist/router/router-options.type.js +2 -0
  115. package/dist/router/router-options.type.js.map +1 -0
  116. package/dist/router/router.d.ts +11 -0
  117. package/dist/router/router.d.ts.map +1 -0
  118. package/dist/router/router.js +143 -0
  119. package/dist/router/router.js.map +1 -0
  120. package/dist/server/index.d.ts +2 -0
  121. package/dist/server/index.d.ts.map +1 -0
  122. package/dist/server/index.js +2 -0
  123. package/dist/server/index.js.map +1 -0
  124. package/dist/server/middleware-adapter.interface.d.ts +5 -0
  125. package/dist/server/middleware-adapter.interface.d.ts.map +1 -0
  126. package/dist/server/middleware-adapter.interface.js +2 -0
  127. package/dist/server/middleware-adapter.interface.js.map +1 -0
  128. package/dist/server/request-adapter.interface.d.ts +7 -0
  129. package/dist/server/request-adapter.interface.d.ts.map +1 -0
  130. package/dist/server/request-adapter.interface.js +2 -0
  131. package/dist/server/request-adapter.interface.js.map +1 -0
  132. package/dist/server/web-server.interface.d.ts +6 -0
  133. package/dist/server/web-server.interface.d.ts.map +1 -0
  134. package/dist/server/web-server.interface.js +2 -0
  135. package/dist/server/web-server.interface.js.map +1 -0
  136. package/dist/validator/index.d.ts +4 -0
  137. package/dist/validator/index.d.ts.map +1 -0
  138. package/dist/validator/index.js +2 -0
  139. package/dist/validator/index.js.map +1 -0
  140. package/dist/validator/request-source.type.d.ts +2 -0
  141. package/dist/validator/request-source.type.d.ts.map +1 -0
  142. package/dist/validator/request-source.type.js +2 -0
  143. package/dist/validator/request-source.type.js.map +1 -0
  144. package/dist/validator/request-validator.interface.d.ts +30 -0
  145. package/dist/validator/request-validator.interface.d.ts.map +1 -0
  146. package/dist/validator/request-validator.interface.js +2 -0
  147. package/dist/validator/request-validator.interface.js.map +1 -0
  148. package/dist/validator/validation-result.type.d.ts +8 -0
  149. package/dist/validator/validation-result.type.d.ts.map +1 -0
  150. package/dist/validator/validation-result.type.js +2 -0
  151. package/dist/validator/validation-result.type.js.map +1 -0
  152. package/dist/validator/zod.d.ts +29 -0
  153. package/dist/validator/zod.d.ts.map +1 -0
  154. package/dist/validator/zod.js +43 -0
  155. package/dist/validator/zod.js.map +1 -0
  156. package/package.json +85 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Max Martinez and quilla-kit contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,400 @@
1
+ # @quilla-be-kit/http
2
+
3
+ Framework-agnostic HTTP layer for a quilla-be-kit service:
4
+
5
+ - **Controller decorators** — `@Controller`, `@Get` / `@Post` / `@Put` / `@Patch` / `@Delete` + `*Public` variants, `@AuthorizeScope`, `@ValidateRequest`.
6
+ - **Router** — walks decorated controller instances, composes prefixes, sorts routes by specificity, bridges to `ComponentRegistry<HttpModuleMeta>` from `@quilla-be-kit/runtime`, and (when `executionContext` is configured) installs a **system-owned execution-context bootstrap** so every handler can rely on `provider.getContext()`.
7
+ - **Typed auth middleware stack** — `AuthMiddlewareStack` enforces phase ordering (`tokenVerification` → `sessionLoad?`) so consumers can't misorder security middlewares. Compose it directly from `@quilla-be-kit/security`'s middleware factories.
8
+ - **Request / response contracts** — `HttpRequest`, `HttpResponse`, `HttpMiddleware`, `AuthenticatedToken`, `HttpAttributes`.
9
+ - **Validator contract** — `RequestValidator` interface; wire Zod / Joi / Valibot / ArkType with a ~5-line adapter.
10
+ - **Hono adapter** — `@quilla-be-kit/http/adapter/hono` sub-path ships a `HonoServer` that implements `WebServer`. `hono` is an optional peer dep.
11
+
12
+ Runtime deps: `@quilla-be-kit/errors`, `@quilla-be-kit/execution-context`, `@quilla-be-kit/observability`, `@quilla-be-kit/runtime`.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ # Core:
18
+ pnpm add @quilla-be-kit/http @quilla-be-kit/errors @quilla-be-kit/execution-context \
19
+ @quilla-be-kit/observability @quilla-be-kit/runtime
20
+
21
+ # Plus Hono adapter:
22
+ pnpm add hono
23
+ ```
24
+
25
+ Node 22+.
26
+
27
+ ## TypeScript configuration
28
+
29
+ Controllers rely on **stage-3 decorators** (not the legacy `experimentalDecorators`). Your `tsconfig.json` needs:
30
+
31
+ ```json
32
+ {
33
+ "compilerOptions": {
34
+ "target": "ES2022", // or higher
35
+ "module": "NodeNext",
36
+ "moduleResolution": "NodeNext"
37
+ // experimentalDecorators — must be false or omitted
38
+ // emitDecoratorMetadata — must be false or omitted
39
+ }
40
+ }
41
+ ```
42
+
43
+ - **TypeScript 5.0+** (5.2+ recommended).
44
+ - **`experimentalDecorators` must be `false` (or absent).** If you have it on for legacy reasons, `@Controller`/`@Get`/etc. will compile under the old decorator protocol and route metadata won't register. TS 5.x defaults to stage-3 when this flag is absent.
45
+ - **`target` ≥ `"ES2022"`.** Stage-3 decorators compile on top of ES2022 class semantics.
46
+
47
+ Consumers do **not** need to polyfill `Symbol.metadata` themselves — the library installs a shared identity (`Symbol.for('Symbol.metadata')`) at module load. You also do not need `emitDecoratorMetadata`; that's a legacy-decorator flag and does nothing for stage-3.
48
+
49
+ ## Quick start
50
+
51
+ ```ts
52
+ import { AsyncExecutionContextProvider } from '@quilla-be-kit/execution-context';
53
+ import {
54
+ Controller,
55
+ Get,
56
+ Post,
57
+ GetPublic,
58
+ AuthorizeScope,
59
+ ValidateRequest,
60
+ Router,
61
+ type HttpRequest,
62
+ type HttpResponse,
63
+ type RequestValidator,
64
+ } from '@quilla-be-kit/http';
65
+ import { HonoServer } from '@quilla-be-kit/http/adapter/hono';
66
+ import { Runtime, ShutdownManager, ComponentRegistry } from '@quilla-be-kit/runtime';
67
+ import {
68
+ authenticatedSessionMiddleware,
69
+ bearerTokenMiddleware,
70
+ } from '@quilla-be-kit/security';
71
+ import { serve } from '@hono/node-server';
72
+
73
+ @Controller('/users')
74
+ class UsersController {
75
+ @GetPublic('/healthz')
76
+ async health(_req: HttpRequest): Promise<HttpResponse> {
77
+ return { httpCode: 200, payload: { ok: true } };
78
+ }
79
+
80
+ @Get('/:id')
81
+ @AuthorizeScope('user:read')
82
+ async show(req: HttpRequest): Promise<HttpResponse> {
83
+ const id = req.getParams()['id'];
84
+ return { httpCode: 200, payload: { id } };
85
+ }
86
+
87
+ @Post('/')
88
+ @AuthorizeScope('user:write')
89
+ @ValidateRequest(CreateUserRequestDto, ['body'])
90
+ async create(req: HttpRequest): Promise<HttpResponse> {
91
+ const input = req.getValidatedInput<CreateUserCommand>();
92
+ // ... application logic
93
+ return { httpCode: 201, payload: { id: 'new-id' } };
94
+ }
95
+ }
96
+
97
+ const provider = new AsyncExecutionContextProvider();
98
+
99
+ const components = new ComponentRegistry<{
100
+ readonly controllers?: readonly object[];
101
+ }>();
102
+
103
+ components.register({
104
+ name: 'users',
105
+ meta: { controllers: [new UsersController()] },
106
+ });
107
+
108
+ const router = new Router({
109
+ modules: components.getAll(),
110
+ executionContext: { provider },
111
+ globalMiddlewares: [/* your custom globals (cors, rate-limit, request-logger, ...) */],
112
+ authMiddlewares: {
113
+ tokenVerification: bearerTokenMiddleware({ tokenService }),
114
+ sessionLoad: authenticatedSessionMiddleware({
115
+ sessionStore,
116
+ executionContextProvider: provider,
117
+ }),
118
+ },
119
+ });
120
+
121
+ const server = new HonoServer({
122
+ port: 3000,
123
+ router,
124
+ requestValidator: zodRequestValidator, // see below
125
+ serve: (app, port) => {
126
+ const handle = serve({ fetch: app.fetch, port });
127
+ return {
128
+ close: () =>
129
+ new Promise<void>((resolve, reject) =>
130
+ handle.close((err) => (err ? reject(err) : resolve())),
131
+ ),
132
+ };
133
+ },
134
+ });
135
+
136
+ const shutdown = new ShutdownManager({ timeoutMs: 10_000 });
137
+ shutdown.addPhase({
138
+ name: 'http',
139
+ participants: [{ name: 'HonoServer', dispose: () => server.close() }],
140
+ });
141
+
142
+ const runtime = new Runtime({ shutdownManager: shutdown });
143
+ await runtime.run(async () => {
144
+ await server.listen();
145
+ });
146
+ ```
147
+
148
+ ## Decorators
149
+
150
+ ### `@Controller(prefix)`
151
+
152
+ Class decorator. Every route on the class gets `prefix` prepended.
153
+
154
+ ```ts
155
+ @Controller('/users')
156
+ class UsersController { ... }
157
+ ```
158
+
159
+ ### HTTP method decorators
160
+
161
+ ```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
+ ```
168
+
169
+ 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
+
171
+ ### `@AuthorizeScope(scope, mode?)`
172
+
173
+ Scope-based authorization. Reads an `AuthenticatedToken` from `request.getAttribute(HttpAttributes.VERIFIED_TOKEN)` and checks the token's `scopes` against the required scope(s).
174
+
175
+ ```ts
176
+ @AuthorizeScope('user:read') // default: 'any' — passes if token has user:read
177
+ @AuthorizeScope(['user:read', 'admin']) // passes if token has any of these
178
+ @AuthorizeScope(['user:write', 'admin'], 'all') // requires both
179
+ ```
180
+
181
+ Throws `ForbiddenError` on missing token or mismatch. An auth middleware (from `@quilla-be-kit/security` or consumer code) must have populated the `VERIFIED_TOKEN` attribute.
182
+
183
+ ### `@ValidateRequest(schema, sources)`
184
+
185
+ Merges data from the configured sources (`'body'`, `'params'`, `'query'`), injects `scopeId` and `userId` from `ExecutionContext.session` **only when the schema declares those keys and a session is active**, validates against `schema` using the server's `RequestValidator`, and attaches the validated value to the request. Retrieve with `request.getValidatedInput<T>()`.
186
+
187
+ Auth-injection requires two things:
188
+ - A live `session` on the request's `ExecutionContext` (i.e. the route ran through auth middleware that established one — anonymous and system contexts get no injection).
189
+ - The `RequestValidator` implements the optional `describeSchema(schema)` method (see [`RequestValidator` adapter](#requestvalidator-adapter) below). Without it, auth-injection is skipped entirely — a fail-safe default that keeps surprise fields out of schemas that didn't ask for them.
190
+
191
+ ```ts
192
+ @Post('/')
193
+ @ValidateRequest(CreateUserRequestDto, ['body'])
194
+ async create(req: HttpRequest): Promise<HttpResponse> {
195
+ const input = req.getValidatedInput<CreateUserCommand>();
196
+ // input is typed as CreateUserCommand — consumer asserts the runtime shape
197
+ }
198
+ ```
199
+
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`.
201
+
202
+ ## `RequestValidator` adapter
203
+
204
+ ### Zod — use the out-of-the-box helper
205
+
206
+ The toolkit ships a ready-made Zod 4 adapter under `@quilla-be-kit/http/validator/zod`. It implements both `validate` and the optional `describeSchema` — the latter unwraps `ZodPipe` (produced by `.transform(...)`) so schemas from `@quilla-be-kit/persistence/query-schema` interoperate without any extra wiring.
207
+
208
+ ```ts
209
+ import { createZodRequestValidator } from '@quilla-be-kit/http/validator/zod';
210
+
211
+ const server = new HonoServer({
212
+ requestValidator: createZodRequestValidator(),
213
+ // ...
214
+ });
215
+ ```
216
+
217
+ Accepts an `extractIssues(error)` hook if you want to reshape Zod's raw issue array before it lands in `ValidationError.context.issues`:
218
+
219
+ ```ts
220
+ createZodRequestValidator({
221
+ extractIssues: (err) => err.issues.map((i) => ({ path: i.path, message: i.message })),
222
+ });
223
+ ```
224
+
225
+ `zod` is an **optional** peer dep of `@quilla-be-kit/http` — required only when importing from this sub-path.
226
+
227
+ ### Other validators — ~5 lines
228
+
229
+ If you use Joi, Valibot, ArkType, or anything else, implement `RequestValidator` directly:
230
+
231
+ ```ts
232
+ // Joi
233
+ import type { Schema } from 'joi';
234
+
235
+ const joiRequestValidator: RequestValidator = {
236
+ validate: (schema, input) => {
237
+ const result = (schema as Schema).validate(input, { abortEarly: false });
238
+ return result.error
239
+ ? { success: false, error: result.error.details }
240
+ : { success: true, data: result.value };
241
+ },
242
+ // Optional: implement describeSchema to enable conditional auth-injection
243
+ // in @ValidateRequest. Return { keys } for schemas whose top-level keys
244
+ // are enumerable, null otherwise.
245
+ };
246
+ ```
247
+
248
+ Pass to `new HonoServer({ requestValidator, ... })`. The library handles conversion from the `{ success, error }` tuple to a thrown `ValidationError` — consumers never construct quilla-be-kit errors directly.
249
+
250
+ ## Router
251
+
252
+ ```ts
253
+ const router = new Router({
254
+ controllers: [new UsersController()], // plain controller instances
255
+ // OR via modules from ComponentRegistry<HttpModuleMeta>:
256
+ modules: registry.getAll(),
257
+
258
+ // Optional — when provided, Router installs a system execution-context
259
+ // bootstrap before any consumer middleware. Every route (public and
260
+ // non-public) gets a baseline anonymous context with a correlation id
261
+ // read from `correlationIdHeader` (default `'x-correlation-id'`) or a
262
+ // generated UUID if absent.
263
+ // **Required iff `authMiddlewares` is set** — Router throws at construction
264
+ // otherwise. Skip it for pure-public services that never call
265
+ // `request.getExecutionContext()`. The provider carries its own factory
266
+ // (default `executionContextFactory`); pass a custom factory via
267
+ // `new AsyncExecutionContextProvider({ factory })` if you've extended the
268
+ // ExecutionContext shape.
269
+ executionContext: {
270
+ provider,
271
+ correlationIdHeader: 'x-request-id', // optional, defaults to 'x-correlation-id'
272
+ },
273
+
274
+ globalMiddlewares: [...], // custom — run on every route after system bootstrap
275
+ authMiddlewares: { tokenVerification, sessionLoad? }, // typed stack — non-public routes only
276
+ });
277
+ ```
278
+
279
+ - Controllers can be registered as plain instances (no extra metadata) or wrapped in `{ controller, prefix?, middlewares? }` for per-controller prefix + middlewares.
280
+ - Routes are sorted by **specificity** (static segments > parametric > wildcard) so `/users/healthz` matches before `/users/:id`.
281
+ - Path composition: `[module prefix] + [registration prefix] + [@Controller prefix] + [@Route path]`, normalized to a single leading slash and no trailing slash.
282
+ - Duplicate routes (same method + path) throw at construction time — you catch double-registrations at startup, not under load.
283
+
284
+ ### Middleware chain order
285
+
286
+ On a **non-public** route:
287
+
288
+ ```
289
+ system executionContext bootstrap → globalMiddlewares[] → tokenVerification → sessionLoad? → route middlewares → handler
290
+ ```
291
+
292
+ On a **`*Public` route**, the entire `authMiddlewares` stack is skipped:
293
+
294
+ ```
295
+ system executionContext bootstrap → globalMiddlewares[] → route middlewares → handler
296
+ ```
297
+
298
+ The system bootstrap is Router-owned and not configurable from outside — this eliminates "I forgot to add `executionContextMiddleware`" as a failure mode for services that use auth or read `ExecutionContext`. When `executionContext` is omitted, the bootstrap step is skipped entirely; services that never read context pay no boilerplate. Router throws at construction if `authMiddlewares` is set without `executionContext` — the known-static dependency is caught at startup, not at the first authenticated request. The typed `AuthMiddlewareStack` prevents phase misordering at the type level; the array in `globalMiddlewares` stays open-ended because custom middleware ordering is consumer-owned.
299
+
300
+ ## Bridge to `ComponentRegistry<HttpModuleMeta>`
301
+
302
+ `ComponentRegistry<HttpModuleMeta>` is the shared spine between `@quilla-be-kit/runtime` and `@quilla-be-kit/http`:
303
+
304
+ ```ts
305
+ import { ComponentRegistry } from '@quilla-be-kit/runtime';
306
+ import { type HttpModuleMeta } from '@quilla-be-kit/http';
307
+
308
+ const registry = new ComponentRegistry<HttpModuleMeta>({
309
+ contracts: [IAM_CONTRACT, DM_CONTRACT],
310
+ });
311
+
312
+ registry
313
+ .register({
314
+ name: 'iam',
315
+ meta: {
316
+ prefix: '/api/v1',
317
+ controllers: [usersController, authController],
318
+ middlewares: [iamModuleMw],
319
+ },
320
+ dispose: () => iamModule.dispose(),
321
+ })
322
+ .register({
323
+ name: 'dm',
324
+ meta: {
325
+ prefix: '/api/v1',
326
+ controllers: [documentsController],
327
+ },
328
+ });
329
+
330
+ // Router reads the registry directly:
331
+ const router = new Router({ modules: registry.getAll(), ... });
332
+
333
+ // Shutdown phase reads the same registry:
334
+ shutdown.addPhase(registry.toShutdownPhase('modules'));
335
+ ```
336
+
337
+ One source of truth: adding a new module means one `.register(...)` call, and both the route table and the shutdown ordering pick it up automatically.
338
+
339
+ ## `WebServer` interface
340
+
341
+ ```ts
342
+ export interface WebServer {
343
+ bootstrap(): void | Promise<void>;
344
+ listen(): Promise<void>;
345
+ close(): Promise<void>;
346
+ }
347
+ ```
348
+
349
+ - `bootstrap()` — wires routes, middlewares, error handler onto the underlying framework. Idempotent.
350
+ - `listen()` — starts accepting connections.
351
+ - `close()` — stops accepting connections and awaits in-flight requests.
352
+
353
+ `HonoServer implements WebServer`. Future adapters (Express, Fastify) would ship as additional sub-paths implementing the same interface — `const server: WebServer = new HonoServer(...)` stays the shape your composition root depends on.
354
+
355
+ ## Hono adapter
356
+
357
+ Sub-path: `@quilla-be-kit/http/adapter/hono`. Ships `HonoServer` only. `hono` is an optional peer dep pinned to `4.x.x`.
358
+
359
+ ```ts
360
+ import { HonoServer, type HonoServeFn } from '@quilla-be-kit/http/adapter/hono';
361
+ import { serve } from '@hono/node-server';
362
+
363
+ const honoServe: HonoServeFn = (app, port) => {
364
+ const handle = serve({ fetch: app.fetch, port });
365
+ return {
366
+ close: () =>
367
+ new Promise<void>((resolve, reject) =>
368
+ handle.close((err) => (err ? reject(err) : resolve())),
369
+ ),
370
+ };
371
+ };
372
+
373
+ const server = new HonoServer({
374
+ port: 3000,
375
+ router, // HonoServer reads the execution-context provider from Router
376
+ requestValidator, // optional — required only if any route uses @ValidateRequest
377
+ logger, // optional — used for startup/shutdown/error logs
378
+ serve: honoServe,
379
+ });
380
+ ```
381
+
382
+ The `serve` callback is where you pick your Node runtime — `@hono/node-server`, Bun's native serve, Deno's native serve, a test stub, etc. Runtime-specific so the adapter stays portable.
383
+
384
+ Consumer never constructs `HonoRequestAdapter` or `HonoMiddlewareAdapter` directly — `HonoServer` wires them internally.
385
+
386
+ ## Other frameworks
387
+
388
+ If you need Express or Fastify: open an issue. Adapter sub-paths ship as library additions when they exist, not as consumer extension points.
389
+
390
+ ## Testing controllers
391
+
392
+ Since controllers are plain classes with decorators, you test them the way you'd test any class — construct an instance, pass a fake `HttpRequest`, assert the `HttpResponse`. No framework, no server, no adapter.
393
+
394
+ ```ts
395
+ const controller = new UsersController();
396
+ const response = await controller.show(fakeRequest({ params: { id: '42' } }));
397
+ expect(response.httpCode).toBe(200);
398
+ ```
399
+
400
+ For integration tests, use `HonoServer` with a `serve` callback that captures `app.fetch` — see this package's adapter tests for the pattern.