@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.
- package/LICENSE +21 -0
- package/README.md +400 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/adapter/hono/create-http-request.d.ts +15 -0
- package/dist/adapter/hono/create-http-request.d.ts.map +1 -0
- package/dist/adapter/hono/create-http-request.js +57 -0
- package/dist/adapter/hono/create-http-request.js.map +1 -0
- package/dist/adapter/hono/get-request-attributes.d.ts +2 -0
- package/dist/adapter/hono/get-request-attributes.d.ts.map +1 -0
- package/dist/adapter/hono/get-request-attributes.js +10 -0
- package/dist/adapter/hono/get-request-attributes.js.map +1 -0
- package/dist/adapter/hono/hono-middleware.adapter.d.ts +10 -0
- package/dist/adapter/hono/hono-middleware.adapter.d.ts.map +1 -0
- package/dist/adapter/hono/hono-middleware.adapter.js +13 -0
- package/dist/adapter/hono/hono-middleware.adapter.js.map +1 -0
- package/dist/adapter/hono/hono-request.adapter.d.ts +12 -0
- package/dist/adapter/hono/hono-request.adapter.d.ts.map +1 -0
- package/dist/adapter/hono/hono-request.adapter.js +99 -0
- package/dist/adapter/hono/hono-request.adapter.js.map +1 -0
- package/dist/adapter/hono/hono.server.d.ts +30 -0
- package/dist/adapter/hono/hono.server.d.ts.map +1 -0
- package/dist/adapter/hono/hono.server.js +64 -0
- package/dist/adapter/hono/hono.server.js.map +1 -0
- package/dist/adapter/hono/hono.types.d.ts +9 -0
- package/dist/adapter/hono/hono.types.d.ts.map +1 -0
- package/dist/adapter/hono/hono.types.js +4 -0
- package/dist/adapter/hono/hono.types.js.map +1 -0
- package/dist/adapter/hono/index.d.ts +2 -0
- package/dist/adapter/hono/index.d.ts.map +1 -0
- package/dist/adapter/hono/index.js +2 -0
- package/dist/adapter/hono/index.js.map +1 -0
- package/dist/decorator/authorize-scope.decorator.d.ts +6 -0
- package/dist/decorator/authorize-scope.decorator.d.ts.map +1 -0
- package/dist/decorator/authorize-scope.decorator.js +30 -0
- package/dist/decorator/authorize-scope.decorator.js.map +1 -0
- package/dist/decorator/controller.decorator.d.ts +2 -0
- package/dist/decorator/controller.decorator.d.ts.map +1 -0
- package/dist/decorator/controller.decorator.js +7 -0
- package/dist/decorator/controller.decorator.js.map +1 -0
- package/dist/decorator/index.d.ts +5 -0
- package/dist/decorator/index.d.ts.map +1 -0
- package/dist/decorator/index.js +5 -0
- package/dist/decorator/index.js.map +1 -0
- package/dist/decorator/method.decorators.d.ts +11 -0
- package/dist/decorator/method.decorators.d.ts.map +1 -0
- package/dist/decorator/method.decorators.js +27 -0
- package/dist/decorator/method.decorators.js.map +1 -0
- package/dist/decorator/route.metadata.d.ts +22 -0
- package/dist/decorator/route.metadata.d.ts.map +1 -0
- package/dist/decorator/route.metadata.js +61 -0
- package/dist/decorator/route.metadata.js.map +1 -0
- package/dist/decorator/validate-request.decorator.d.ts +7 -0
- package/dist/decorator/validate-request.decorator.d.ts.map +1 -0
- package/dist/decorator/validate-request.decorator.js +57 -0
- package/dist/decorator/validate-request.decorator.js.map +1 -0
- package/dist/error/index.d.ts +2 -0
- package/dist/error/index.d.ts.map +1 -0
- package/dist/error/index.js +2 -0
- package/dist/error/index.js.map +1 -0
- package/dist/error/resolve-http-error.d.ts +7 -0
- package/dist/error/resolve-http-error.d.ts.map +1 -0
- package/dist/error/resolve-http-error.js +43 -0
- package/dist/error/resolve-http-error.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/request/authenticated-token.interface.d.ts +4 -0
- package/dist/request/authenticated-token.interface.d.ts.map +1 -0
- package/dist/request/authenticated-token.interface.js +2 -0
- package/dist/request/authenticated-token.interface.js.map +1 -0
- package/dist/request/http-attributes.d.ts +6 -0
- package/dist/request/http-attributes.d.ts.map +1 -0
- package/dist/request/http-attributes.js +6 -0
- package/dist/request/http-attributes.js.map +1 -0
- package/dist/request/http-middleware.type.d.ts +3 -0
- package/dist/request/http-middleware.type.d.ts.map +1 -0
- package/dist/request/http-middleware.type.js +2 -0
- package/dist/request/http-middleware.type.js.map +1 -0
- package/dist/request/http-request.interface.d.ts +18 -0
- package/dist/request/http-request.interface.d.ts.map +1 -0
- package/dist/request/http-request.interface.js +2 -0
- package/dist/request/http-request.interface.js.map +1 -0
- package/dist/request/http-response.type.d.ts +18 -0
- package/dist/request/http-response.type.d.ts.map +1 -0
- package/dist/request/http-response.type.js +2 -0
- package/dist/request/http-response.type.js.map +1 -0
- package/dist/request/index.d.ts +6 -0
- package/dist/request/index.d.ts.map +1 -0
- package/dist/request/index.js +2 -0
- package/dist/request/index.js.map +1 -0
- package/dist/router/auth-middleware-stack.type.d.ts +13 -0
- package/dist/router/auth-middleware-stack.type.d.ts.map +1 -0
- package/dist/router/auth-middleware-stack.type.js +2 -0
- package/dist/router/auth-middleware-stack.type.js.map +1 -0
- package/dist/router/controller-registration.type.d.ts +7 -0
- package/dist/router/controller-registration.type.d.ts.map +1 -0
- package/dist/router/controller-registration.type.js +2 -0
- package/dist/router/controller-registration.type.js.map +1 -0
- package/dist/router/http-module-meta.type.d.ts +8 -0
- package/dist/router/http-module-meta.type.d.ts.map +1 -0
- package/dist/router/http-module-meta.type.js +2 -0
- package/dist/router/http-module-meta.type.js.map +1 -0
- package/dist/router/index.d.ts +6 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +2 -0
- package/dist/router/index.js.map +1 -0
- package/dist/router/normalized-route.type.d.ts +20 -0
- package/dist/router/normalized-route.type.d.ts.map +1 -0
- package/dist/router/normalized-route.type.js +2 -0
- package/dist/router/normalized-route.type.js.map +1 -0
- package/dist/router/router-options.type.d.ts +37 -0
- package/dist/router/router-options.type.d.ts.map +1 -0
- package/dist/router/router-options.type.js +2 -0
- package/dist/router/router-options.type.js.map +1 -0
- package/dist/router/router.d.ts +11 -0
- package/dist/router/router.d.ts.map +1 -0
- package/dist/router/router.js +143 -0
- package/dist/router/router.js.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/middleware-adapter.interface.d.ts +5 -0
- package/dist/server/middleware-adapter.interface.d.ts.map +1 -0
- package/dist/server/middleware-adapter.interface.js +2 -0
- package/dist/server/middleware-adapter.interface.js.map +1 -0
- package/dist/server/request-adapter.interface.d.ts +7 -0
- package/dist/server/request-adapter.interface.d.ts.map +1 -0
- package/dist/server/request-adapter.interface.js +2 -0
- package/dist/server/request-adapter.interface.js.map +1 -0
- package/dist/server/web-server.interface.d.ts +6 -0
- package/dist/server/web-server.interface.d.ts.map +1 -0
- package/dist/server/web-server.interface.js +2 -0
- package/dist/server/web-server.interface.js.map +1 -0
- package/dist/validator/index.d.ts +4 -0
- package/dist/validator/index.d.ts.map +1 -0
- package/dist/validator/index.js +2 -0
- package/dist/validator/index.js.map +1 -0
- package/dist/validator/request-source.type.d.ts +2 -0
- package/dist/validator/request-source.type.d.ts.map +1 -0
- package/dist/validator/request-source.type.js +2 -0
- package/dist/validator/request-source.type.js.map +1 -0
- package/dist/validator/request-validator.interface.d.ts +30 -0
- package/dist/validator/request-validator.interface.d.ts.map +1 -0
- package/dist/validator/request-validator.interface.js +2 -0
- package/dist/validator/request-validator.interface.js.map +1 -0
- package/dist/validator/validation-result.type.d.ts +8 -0
- package/dist/validator/validation-result.type.d.ts.map +1 -0
- package/dist/validator/validation-result.type.js +2 -0
- package/dist/validator/validation-result.type.js.map +1 -0
- package/dist/validator/zod.d.ts +29 -0
- package/dist/validator/zod.d.ts.map +1 -0
- package/dist/validator/zod.js +43 -0
- package/dist/validator/zod.js.map +1 -0
- 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.
|