@zerotal/arch 1.7.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 (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
@@ -0,0 +1,579 @@
1
+ ---
2
+ title: Requests Context
3
+ description: Every handler and middleware receives the per-request HttpContext directly — route params, typed input helpers, uploads, headers, response building, flash, and after-response hooks on one object.
4
+ ---
5
+
6
+ # Requests Context
7
+
8
+ Every route handler and every middleware receives one argument: the request
9
+ `HttpContext`. There is no separate request object — the incoming `Request` is a
10
+ property on the context (`ctx.request`), and everything you'd reach for to read it
11
+ hangs off the same argument. It holds the raw request, the response being built,
12
+ the matched route params and resolved model bindings (on `ctx.params`), and the
13
+ helpers for reading input and shaping the reply.
14
+
15
+ ```typescript
16
+ // in a controller
17
+ import type { HttpContext } from "zerotal";
18
+
19
+ export class PostController {
20
+ async show(ctx: HttpContext) {
21
+ return ctx.json({ id: ctx.params.id });
22
+ }
23
+ }
24
+ ```
25
+
26
+ You rarely construct one yourself — the framework creates it per request, stores it
27
+ in request-scoped (`AsyncLocalStorage`) storage, and hands it to your code. For
28
+ tests, build one with [`HttpContext.fake()`](#testing).
29
+
30
+ ## Destructuring the context
31
+
32
+ Every method on the context is bound to its instance, so you can destructure
33
+ exactly what a handler needs — methods included — without losing `this`:
34
+
35
+ ```typescript
36
+ // routes: Router.get('/posts/:post/:tab', PostController, 'show')
37
+
38
+ async show({ view, params: { post, tab } }: HttpContext<{ post: Post; tab: string }>) {
39
+ return view(ShowPost, { post, tab });
40
+ }
41
+ ```
42
+
43
+ `view` is a bound method here, `post` is a resolved model binding, and `tab` is a
44
+ raw route param — all pulled straight off the one argument. Name the whole argument
45
+ instead (`async show(ctx: HttpContext)`) when you prefer `ctx.view(...)` /
46
+ `ctx.params.post`; both styles work.
47
+
48
+ > **Warning** — Computed getters (`took`, `subdomains`) are _not_ bound methods, so
49
+ > destructuring them takes a one-time snapshot at destructure time. `took` (elapsed
50
+ > ms) in particular will be frozen — read `ctx.took` directly when you want it live.
51
+
52
+ ## Typed params
53
+
54
+ `HttpContext` is generic over the shape of `params`. With no type argument,
55
+ `ctx.params` is `Record<string, string>` — the raw matched segments. Pass a type
56
+ argument to describe what the route resolves, including model bindings:
57
+
58
+ ```typescript
59
+ // raw params only — every value is a string
60
+ async index(ctx: HttpContext) {
61
+ const page = ctx.params.page; // string | undefined
62
+ }
63
+
64
+ // typed params + a resolved model binding
65
+ async show(ctx: HttpContext<{ post: Post; tab: string }>) {
66
+ ctx.params.post; // Post (resolved binding)
67
+ ctx.params.tab; // string (raw param)
68
+ }
69
+ ```
70
+
71
+ The framework hands every handler the same runtime object; the type argument is a
72
+ declaration _you_ make about what this route resolves — exactly as you'd annotate
73
+ any function parameter.
74
+
75
+ ## Route-model bindings
76
+
77
+ A binding turns a raw `:param` string into a loaded model instance before your
78
+ handler runs. The framework folds the instance onto `ctx.params` under the param's
79
+ name (and exposes it via `ctx.model<T>()`).
80
+
81
+ Usually there is nothing to declare: a param whose name matches an auto-registered
82
+ model binds on its own, so `:post` resolves through `Post` by primary key.
83
+
84
+ ```typescript
85
+ // routes/web.ts — :post is already bound to Post
86
+ Router.get("/posts/:post", PostController, "show");
87
+
88
+ // Declare one only to override the default — a different key, or a custom lookup:
89
+ Router.get("/posts/:post", PostController, "show").bind("post", (slug) =>
90
+ Post.query().where("slug", slug).firstOrFail(),
91
+ );
92
+ ```
93
+
94
+ ```typescript
95
+ // in PostController — ctx.params.post is the resolved Post, not the raw id
96
+ async show(ctx: HttpContext<{ post: Post }>) {
97
+ return ctx.json(ctx.params.post);
98
+ }
99
+
100
+ // the same instance, read explicitly
101
+ async show(ctx: HttpContext) {
102
+ const post = ctx.model<Post>("post");
103
+ return ctx.json(post);
104
+ }
105
+ ```
106
+
107
+ Resolution runs before the middleware pipeline, so a missing record produces a 404
108
+ rendered by the exception handler rather than a half-run handler. An unbound param
109
+ stays a raw string on `ctx.params`. The full binding surface — implicit binding,
110
+ custom resolvers, scoped bindings — lives in [Routing](/docs/routing).
111
+
112
+ ## Reading input
113
+
114
+ The scalar helpers check route params first, then the query string, and coerce to
115
+ the type the method name promises. Each takes an optional fallback.
116
+
117
+ ```typescript
118
+ // in a controller
119
+ ctx.query("page", "1"); // string | undefined — query string only
120
+ ctx.string("sort", "asc"); // string | undefined — param then query
121
+ ctx.integer("id"); // number | undefined — param then query, parsed as int
122
+ ctx.boolean("active", false); // boolean — param then query, coerced
123
+ ```
124
+
125
+ ### Number parsing
126
+
127
+ `integer()` reads the param-then-query value and parses it with `parseInt(raw, 10)`.
128
+ It returns the `fallback` (or `undefined` when you pass none) in two cases: the key
129
+ is absent, or the value is not a valid integer (`NaN`). Because parsing is base-10,
130
+ `"08"` reads as `8` and a trailing-garbage value like `"42abc"` parses to `42` —
131
+ validate with a [form request](/docs/validator) when you need to reject malformed
132
+ input rather than coerce it.
133
+
134
+ ```typescript
135
+ // in a controller — GET /posts?page=3
136
+ ctx.integer("page"); // 3
137
+ ctx.integer("page", 1); // 3 (fallback unused)
138
+ ctx.integer("missing", 1); // 1 (absent → fallback)
139
+ ctx.integer("missing"); // undefined (absent, no fallback)
140
+ ctx.integer("count"); // GET ?count=abc → undefined (NaN → fallback)
141
+ ```
142
+
143
+ ### Boolean coercion
144
+
145
+ `boolean()` reads the param-then-query value and returns `true` only when it is one
146
+ of `'1'`, `'true'`, `'yes'`, or `'on'` (compared case-insensitively). Any other
147
+ present value is `false`; an absent value returns the fallback, which defaults to
148
+ `false`.
149
+
150
+ ```typescript
151
+ // in a controller
152
+ ctx.boolean("active"); // ?active=true / ?active=1 / ?active=ON → true
153
+ ctx.boolean("active"); // ?active=0 / ?active=no / ?active= → false
154
+ ctx.boolean("active"); // absent → false (default fallback)
155
+ ctx.boolean("active", true); // absent → true (custom fallback)
156
+ ```
157
+
158
+ > **Note** — These coercion helpers are for raw string params. A param bound to a
159
+ > model holds the model instance on `ctx.params`, so read it as `ctx.params.post` or
160
+ > `ctx.model<Post>("post")`, never `ctx.integer("post")`.
161
+
162
+ ### The request body
163
+
164
+ `body()` parses and caches the body (JSON, form-urlencoded, or multipart fields) so
165
+ repeated calls are free, and returns `{}` on an absent or invalid body:
166
+
167
+ ```typescript
168
+ const data = await ctx.body<{ title: string; body: string }>();
169
+ ```
170
+
171
+ `input()` reads a single merged value in priority order — route params → cached
172
+ body → query string — without awaiting:
173
+
174
+ ```typescript
175
+ ctx.input("id"); // route :id or ?id=
176
+ ctx.input("q", "all"); // with fallback
177
+ ```
178
+
179
+ > **Warning** — `input()` only sees body data if `await ctx.body()` (or a
180
+ > FormRequest) ran earlier in the lifecycle; otherwise it falls through to the query
181
+ > string. For guaranteed body access, `await ctx.body()` first.
182
+
183
+ Read a header with `header()` (case-insensitive) or pull a Bearer token with
184
+ `bearerToken()`:
185
+
186
+ ```typescript
187
+ ctx.header("x-forwarded-for"); // string | null
188
+ ctx.bearerToken(); // string | null — strips the "Bearer " prefix
189
+ ```
190
+
191
+ ### Uploaded files
192
+
193
+ `file()` returns the first `UploadedFile` for a form field (or `null`); `files()`
194
+ returns all of them. Both parse and cache the multipart body on first call.
195
+
196
+ ```typescript
197
+ const avatar = await ctx.file("avatar"); // UploadedFile | null
198
+ const attachments = await ctx.files("attachments"); // UploadedFile[]
199
+ ```
200
+
201
+ An `UploadedFile` describes the upload and knows how to persist itself. Validate
202
+ before storing, then hand it a disk — `store()` returns the stored path:
203
+
204
+ ```typescript
205
+ // in a controller — single file
206
+ const avatar = await ctx.file("avatar");
207
+
208
+ if (avatar) {
209
+ avatar.originalName; // original filename as sent by the browser
210
+ avatar.size; // bytes
211
+ avatar.mimeType; // MIME type, e.g. 'image/jpeg'
212
+ avatar.extension(); // 'jpg', 'png', etc.
213
+
214
+ const valid = avatar.isValid({
215
+ maxSize: 2 * 1024 * 1024, // 2 MB
216
+ mimes: ["image/jpeg", "image/png"],
217
+ });
218
+
219
+ const path = await avatar.store("avatars", Storage.disk("s3"));
220
+ }
221
+ ```
222
+
223
+ ```typescript
224
+ // in a controller — multiple files
225
+ for (const file of await ctx.files("attachments")) {
226
+ await file.store("uploads", Storage.disk());
227
+ }
228
+ ```
229
+
230
+ > **Danger** — `originalName` is supplied by the client and may be untrusted.
231
+ > `store()` defaults to a `<uuid>.<ext>` filename precisely so attacker-controlled
232
+ > names never reach your filesystem — sanitise the original before displaying it.
233
+
234
+ See [Storage](/docs/storage) for configuring the disks `store()` writes to.
235
+
236
+ ## Building the response
237
+
238
+ The response helpers set `ctx.response` for you and return nothing — they are
239
+ terminal. You can also `return` a value from a controller; see
240
+ [Responses](/docs/responses) for the full set.
241
+
242
+ ```typescript
243
+ ctx.json({ user }); // 200 application/json
244
+ ctx.json({ errors }, 422); // custom status
245
+ ctx.view(WelcomeView, { name }); // full HTML document (prepends <!DOCTYPE html>)
246
+ ctx.html("<p>fragment</p>"); // HTML fragment, no DOCTYPE
247
+ ctx.redirect("/dashboard"); // 302
248
+ ctx.redirect("/dashboard", 303); // 303 — use after POST/PUT/DELETE
249
+ ctx.back(); // redirect to the Referer (same-origin only)
250
+ ctx.back(303); // same, with a 303 status
251
+ ```
252
+
253
+ `view()` accepts either pre-rendered markup, or a **view component plus its props**.
254
+ A view component receives the request `HttpContext` first and your props second:
255
+
256
+ ```typescript
257
+ // resources/views/Welcome.tsx
258
+ export default function Welcome(ctx: HttpContext, { title }: { title: string }) {
259
+ return (
260
+ <html>
261
+ <body>
262
+ <h1>{title}</h1>
263
+ <p>{ctx.url.pathname}</p>
264
+ </body>
265
+ </html>
266
+ );
267
+ }
268
+
269
+ // in a controller
270
+ ctx.view(Welcome, { title: "Hello" });
271
+ ```
272
+
273
+ Route params and model bindings reach the component through `ctx.params`; the second
274
+ argument is strictly the props you pass. `markdown()` renders a Markdown string to a
275
+ full HTML document via `Bun.markdown.html()`.
276
+
277
+ > **Danger** — `back()` falls back to `/` whenever the `Referer` is missing or points
278
+ > to a different origin, preventing open-redirect attacks through a forged `Referer`
279
+ > header. Don't replace it with a raw `redirect(referer)`.
280
+
281
+ ## Middleware
282
+
283
+ Middleware is a class with a `handle(ctx, next)` method. It receives the same
284
+ `HttpContext` and either calls `next()` to pass control down the pipeline or
285
+ short-circuits by producing a response:
286
+
287
+ ```typescript
288
+ // app/middleware/EnsureActive.ts
289
+ import type { HttpContext, NextFn } from "zerotal";
290
+
291
+ export class EnsureActive {
292
+ async handle(ctx: HttpContext, next: NextFn): Promise<Response | void> {
293
+ if (!ctx.boolean("active")) return ctx.redirect("/inactive");
294
+ return next();
295
+ }
296
+ }
297
+ ```
298
+
299
+ Handlers and middleware now receive the exact same object, so there's a single
300
+ mental model and a single way to read params. See [Middleware](/docs/middleware) for
301
+ the pipeline, ordering, and registration.
302
+
303
+ ## The authenticated user
304
+
305
+ `ctx.user` is **not** a built-in field. When `PersistUserMiddleware` (from
306
+ [`@zerotal/auth`](/docs/authentication)) runs, it reads the session's user id, loads
307
+ the matching record, and assigns it to `ctx.user`; `AuthMiddleware` reads it to guard
308
+ routes. It is `undefined` for guests.
309
+
310
+ ```typescript
311
+ if (!ctx.user) throw new UnauthorizedError();
312
+ return ctx.json({ email: ctx.user.email });
313
+ ```
314
+
315
+ The resolver that loads the user from the session id is configured from your
316
+ registered auth model — see [Authentication](/docs/authentication).
317
+
318
+ ## Client IP
319
+
320
+ ```typescript
321
+ ctx.ip(); // string | null — socket-level IP from Bun's server.requestIP()
322
+ ```
323
+
324
+ Returns `null` when no Bun server reference was injected (e.g. in unit tests created
325
+ with `HttpContext.fake()`).
326
+
327
+ > **Warning** — `ctx.ip()` is the socket IP. Behind a trusted proxy it returns the
328
+ > proxy's address, not the end-user's. Read the forwarded header yourself with
329
+ > `ctx.header("x-forwarded-for")`, or let [`ThrottleMiddleware`](/docs/middleware)
330
+ > resolve proxy-aware IPs via its `trustedProxies` option.
331
+
332
+ ## URL & matching helpers
333
+
334
+ ```typescript
335
+ ctx.path(); // "/posts" — pathname only
336
+ ctx.fullUrl(); // "https://app.test/posts?page=2"
337
+ ctx.host(); // "app.test"
338
+ ctx.is("/admin/*"); // glob match against the path (* excludes /, ** includes it)
339
+ ctx.subdomain("tenant"); // string | null — from Router.group({ domain })
340
+ ctx.subdomains; // { tenant: "acme" }
341
+ ```
342
+
343
+ `isJson()` / `wantsJson()` inspect the `Content-Type` / `Accept` headers — handy in
344
+ an exception handler, or anywhere one route serves both browsers and API clients:
345
+
346
+ ```typescript
347
+ // in a controller or middleware
348
+ if (ctx.wantsJson()) {
349
+ ctx.json({ message: "Unauthorized" }, 401);
350
+ } else {
351
+ ctx.redirect("/login", 303);
352
+ }
353
+ ```
354
+
355
+ ## Flash data
356
+
357
+ Flash writes a value to the session for the **next** request only — ideal for
358
+ post-redirect success and error messages. It requires `SessionMiddleware` and
359
+ silently no-ops without it.
360
+
361
+ ```typescript
362
+ // before redirecting
363
+ ctx.flash("success", "Post saved!");
364
+ return ctx.redirect("/posts", 303);
365
+ ```
366
+
367
+ ```typescript
368
+ // on the next request
369
+ const msg = ctx.flashed<string>("success"); // 'Post saved!'
370
+ ```
371
+
372
+ The redirect response builders (`redirect().withSuccess(...)`) wrap this pattern more
373
+ fluently — see [Responses](/docs/responses).
374
+
375
+ ## After-response callbacks
376
+
377
+ Register work to run **after** the response has been sent — fire-and-forget side
378
+ effects that shouldn't delay the client. `afterResponse()` returns `this`, so calls
379
+ chain.
380
+
381
+ ```typescript
382
+ ctx.afterResponse(async () => {
383
+ await analytics.track(ctx.requestId, ctx.url.pathname);
384
+ });
385
+ ```
386
+
387
+ A callback's errors are logged and swallowed, so one failure never affects the
388
+ response or the other callbacks.
389
+
390
+ > **Note** — `afterResponse()` acquires the request-scoped container reference
391
+ > synchronously at registration time, so the scope cannot be flushed before your
392
+ > callback gets a chance to run.
393
+
394
+ ## Accessing the context anywhere
395
+
396
+ You don't have to thread `ctx` through every function. From anywhere in the async
397
+ call chain, `request()` returns the current request's `HttpContext`:
398
+
399
+ ```typescript
400
+ import { request } from "zerotal";
401
+
402
+ request(); // the HttpContext — throws outside a request
403
+ request("page", "1"); // shorthand for request().input("page", "1")
404
+ ```
405
+
406
+ The rule is two lines: take `ctx` where it's handed to you (handlers, hooks,
407
+ middleware); call `request()` anywhere else. Code that also runs **outside** a
408
+ request — a service shared with CLI commands or queue workers — uses
409
+ `HttpContext.tryGet()`, which returns `undefined` instead of throwing.
410
+
411
+ ## Asking once per request
412
+
413
+ `RequestContext.remember(key, factory)` runs `factory` at most once per request
414
+ for a given key and hands every later caller the same answer:
415
+
416
+ ```typescript
417
+ import { RequestContext } from "zerotal";
418
+
419
+ const settings = await RequestContext.remember(`household:${id}:settings`, () =>
420
+ Settings.query().where("household_id", id).first(),
421
+ );
422
+ ```
423
+
424
+ This is the other half of the [N+1 detector](/docs/database#n1-detection): the
425
+ detector tells you a query ran too many times, and when the answer is the same
426
+ every time, "ask once" is the fix rather than eager loading.
427
+
428
+ Two behaviours are deliberate, and both are the ones a hand-rolled version
429
+ usually gets wrong:
430
+
431
+ - **The promise is cached, not the resolved value.** Cache after the `await` and
432
+ a `Promise.all` of ten readers all miss — none has resolved when the others
433
+ look. Caching the promise makes the first caller's in-flight work the answer
434
+ for the other nine.
435
+ - **A rejected promise is evicted.** Otherwise one transient failure poisons
436
+ every later read in the same request, including the retry.
437
+
438
+ Outside a request it is a pass-through. A queue worker has no request to scope
439
+ to, and quietly sharing a value across jobs would be worse than not caching.
440
+
441
+ `RequestContext.forget(key)` drops a value when a write invalidates a read taken
442
+ earlier in the same request.
443
+
444
+ ## Testing
445
+
446
+ `HttpContext.fake()` builds a context without a live server — perfect for unit
447
+ testing controllers and middleware:
448
+
449
+ ```typescript
450
+ const ctx = HttpContext.fake("http://localhost/posts?page=2", {
451
+ method: "GET",
452
+ headers: { Authorization: "Bearer token" },
453
+ });
454
+
455
+ await new PostController().index(ctx);
456
+ expect(ctx.response?.status).toBe(200);
457
+ ```
458
+
459
+ Pass a `body` to exercise handlers that read one, and assign `ctx.params` directly
460
+ when the handler expects route params a real match would have provided:
461
+
462
+ ```typescript
463
+ // in a test — a POST with a JSON body
464
+ const ctx = HttpContext.fake("http://localhost/posts", {
465
+ method: "POST",
466
+ headers: { "Content-Type": "application/json" },
467
+ body: JSON.stringify({ title: "Hello", body: "World" }),
468
+ });
469
+ ctx.params = { id: "42" };
470
+ ```
471
+
472
+ The signature is `HttpContext.fake(url?, init?, container?)`; all three are optional.
473
+ See [Testing](/docs/testing/index) for the full harness, and
474
+ [HTTP testing](/docs/testing/http) for the higher-level request helpers.
475
+
476
+ ## References
477
+
478
+ ### `HttpContext<TParams>`
479
+
480
+ ```typescript
481
+ class HttpContext<TParams extends Record<string, unknown> = Record<string, string>> {
482
+ constructor(request: Request, container: ScopedResolver);
483
+ }
484
+ ```
485
+
486
+ The per-request object every handler and middleware receives. `TParams` types
487
+ `ctx.params` — defaulting to `Record<string, string>` (raw params), or the bag of
488
+ raw params and resolved model bindings you declare for a route.
489
+
490
+ #### Properties
491
+
492
+ | Property | Type | Description |
493
+ | ----------- | ----------------------- | ------------------------------------------------------------ |
494
+ | `request` | `Request` | The raw Bun/Web API `Request` object. |
495
+ | `response` | `Response \| undefined` | Set by handlers; read by the framework to send the reply. |
496
+ | `url` | `URL` | Parsed URL (`pathname`, `searchParams`, `origin`, …). |
497
+ | `params` | `TParams` | Route params, plus resolved model bindings under their name. |
498
+ | `requestId` | `string` | UUID generated per request (`crypto.randomUUID()`). |
499
+ | `startedAt` | `number` | `performance.now()` timestamp captured at construction. |
500
+ | `locale` | `string` | Current locale (default `'en'`); set by `I18nMiddleware`. |
501
+ | `took` | `number` (getter) | Whole milliseconds elapsed since `startedAt`. |
502
+ | `container` | `ScopedResolver` | The request-scoped container for this request. |
503
+
504
+ #### Input methods
505
+
506
+ | Method | Signature | Description |
507
+ | ------------- | ---------------------------------------- | ------------------------------------------------ |
508
+ | `query` | `(key, fallback?): string \| undefined` | Read a query-string param. |
509
+ | `string` | `(key, fallback?): string \| undefined` | Read a param then query as a string. |
510
+ | `integer` | `(key, fallback?): number \| undefined` | Read a param then query, parsed base-10. |
511
+ | `boolean` | `(key, fallback = false): boolean` | Coerce a param/query to a boolean. |
512
+ | `input` | `<T>(key, fallback?): T` | Merged read: params → cached body → query. |
513
+ | `body` | `<T>(): Promise<T>` | Parse and cache the request body. |
514
+ | `header` | `(key, fallback = null): string \| null` | Read a request header (case-insensitive). |
515
+ | `bearerToken` | `(): string \| null` | Extract the `Authorization: Bearer` token. |
516
+ | `file` | `(field): Promise<UploadedFile \| null>` | First uploaded file for a form field. |
517
+ | `files` | `(field): Promise<UploadedFile[]>` | All uploaded files for a form field. |
518
+ | `model` | `<T>(name): T` | Resolved route-model binding; throws if unbound. |
519
+
520
+ #### Response methods
521
+
522
+ | Method | Signature | Description |
523
+ | ---------- | -------------------------------------------- | --------------------------------------------------- |
524
+ | `json` | `(data, status = 200): void` | Set a JSON response. |
525
+ | `view` | `(componentOrMarkup, props?, status?): void` | Render a full HTML document (`<!DOCTYPE html>`). |
526
+ | `markdown` | `(content, options?, status = 200): void` | Render Markdown to a full HTML page. |
527
+ | `html` | `(markup, status = 200): void` | Set a raw HTML fragment (no DOCTYPE). |
528
+ | `redirect` | `(url, status = 302): void` | Set a redirect (`301 \| 302 \| 303 \| 307 \| 308`). |
529
+ | `back` | `(status = 302): void` | Redirect to the same-origin `Referer`, else `/`. |
530
+
531
+ #### Request, session & lifecycle methods
532
+
533
+ | Method | Signature | Description |
534
+ | --------------- | -------------------------- | ------------------------------------------------------ |
535
+ | `path` | `(): string` | Request pathname (no query). |
536
+ | `fullUrl` | `(): string` | Full URL including query string. |
537
+ | `host` | `(): string` | Host portion of the URL. |
538
+ | `is` | `(pattern): boolean` | Glob-match the path (`*` excludes `/`, `**` includes). |
539
+ | `subdomain` | `(name): string \| null` | A single subdomain param from a `domain` group. |
540
+ | `isJson` | `(): boolean` | True when the body is `application/json`. |
541
+ | `wantsJson` | `(): boolean` | True when `Accept` includes `application/json`. |
542
+ | `ip` | `(): string \| null` | Socket-level client IP, or `null`. |
543
+ | `flash` | `(key, value): void` | Write a value to the session for the next request. |
544
+ | `flashed` | `<T>(key): T \| undefined` | Read a value flashed in the previous request. |
545
+ | `afterResponse` | `(callback): this` | Run a callback after the response is sent. |
546
+
547
+ #### Static methods
548
+
549
+ | Method | Signature | Description |
550
+ | -------- | ---------------------------------------- | ----------------------------------------------------- |
551
+ | `tryGet` | `(): HttpContext \| undefined` | The active context, or `undefined` outside a request. |
552
+ | `fake` | `(url?, init?, container?): HttpContext` | Build a context for unit tests. |
553
+
554
+ > **Note** — `requestId` is echoed back to the client in the `X-Request-Id` response
555
+ > header by [`LoggerMiddleware`](/docs/logger), not by the framework core. The id
556
+ > itself always exists on the context.
557
+
558
+ ### `UploadedFile`
559
+
560
+ Returned by `file()` and `files()`.
561
+
562
+ | Member | Signature | Description |
563
+ | ---------------- | ------------------------------------------------------------------------------------------ | ------------------------------------- |
564
+ | `originalName` | `originalName: string` | Client-supplied filename (untrusted). |
565
+ | `mimeType` | `mimeType: string` | Browser-reported MIME type. |
566
+ | `size` | `size: number` | File size in bytes. |
567
+ | `extension` | `extension(): string` | Lowercase extension without the dot. |
568
+ | `isValid` | `isValid(options?: FileValidationOptions): boolean` | Check `maxSize` / `mimes` rules. |
569
+ | `store` | `store(directory: string, disk: StorageDisk, filename?: string): Promise<string>` | Write to a disk, returns the path. |
570
+ | `storeAndGetUrl` | `storeAndGetUrl(directory: string, disk: StorageDisk, filename?: string): Promise<string>` | Store and return the public URL. |
571
+
572
+ ## Next steps
573
+
574
+ - [Controllers](/docs/controllers) — where the context is most often used.
575
+ - [Validator](/docs/validator) — validate the input you read here, with `FormRequest`.
576
+ - [Responses](/docs/responses) — the full response-building API.
577
+ - [Middleware](/docs/middleware) — how the context flows through the pipeline.
578
+ - [Routing](/docs/routing) — route params and route-model binding.
579
+ - [Storage](/docs/storage) — the disks `UploadedFile.store()` writes to.