@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,164 @@
1
+ ---
2
+ title: Contribution Guide
3
+ description: Get the Zerotal monorepo running locally and pass the checks your change needs to land.
4
+ ---
5
+
6
+ # Contribution Guide
7
+
8
+ Zerotal is a Bun-native monorepo of composable packages. This guide covers getting the
9
+ repo running locally, the project layout, and the checks your change needs to pass.
10
+
11
+ > **Warning** — Bun ≥ 1.1 is required. Node.js is not supported; Zerotal uses `Bun.sql`, `Bun.CryptoHasher`, `Bun.build`, and other Bun-native APIs throughout.
12
+
13
+ ## Getting set up
14
+
15
+ ```bash
16
+ # in your project root
17
+ git clone <repo-url> zerotal
18
+ cd zerotal
19
+ bun install # installs all workspaces
20
+ ```
21
+
22
+ The repo is a Bun workspace (`packages/*` and `apps/*`), so a single `bun install` at
23
+ the root wires every package together via `workspace:*` links.
24
+
25
+ ## Repository layout
26
+
27
+ ```text
28
+ # repo root
29
+ packages/ # the framework — one directory per @zerotal/* package
30
+ core/ # IoC container, Application, router, HTTP pipeline, events, config
31
+ orm/ # models, migrations, query builder, relationships
32
+ auth/ cache/ queue/ … # feature packages
33
+ testing/ # factories, fakes, test app harness
34
+ create-zerotal/ # the `bun create zerotal` scaffolder
35
+ apps/ # example apps used for end-to-end testing and the docs site
36
+ docs/ # this documentation site
37
+ docs/ # the markdown documentation (what you're reading)
38
+ ```
39
+
40
+ Each package owns its own `src/`, tests (`*.test.ts`), and `package.json`.
41
+
42
+ ## The `@zerotal/core` public surface
43
+
44
+ `@zerotal/core` is deliberately split so importing the kernel doesn't drag in heavy or
45
+ rarely-used subsystems. The root barrel (`@zerotal/core`) exports **only the lean kernel** —
46
+ `Application`, the container, `RequestContext`, `HttpContext`/`Pipeline`, providers, errors,
47
+ events, the router, middleware, facades, `Command`, and the common helpers. Everything else
48
+ lives behind an explicit subpath:
49
+
50
+ ```text
51
+ @zerotal/core # kernel: Application, Router, HttpContext, Container, errors, events, middleware, helpers
52
+ @zerotal/core/carbon # Carbon + intervals (pulls the Temporal polyfill — kept out of the kernel)
53
+ @zerotal/core/http # outbound Http client, URL building/signing, uploads, API Resource, negotiation
54
+ @zerotal/core/view # server-side JSX runtime + authoring helpers (SafeHtml, definePage, …)
55
+ @zerotal/core/env # typed environment schema (EnvSchema, t, Def)
56
+ @zerotal/core/config # ConfigManager/Loader + app config shapes (AppConfig, AppAssetsConfig, …)
57
+ @zerotal/core/security # Crypt + Hash
58
+ @zerotal/core/dev # dev-only build/reload tooling (owns Bun.build — inactive outside the dev worker)
59
+ @zerotal/core/assets # asset() URL helper + versioning
60
+ @zerotal/core/health # health checks
61
+ @zerotal/core/metrics # HTTP request metrics
62
+ ```
63
+
64
+ Rule of thumb when adding a public export to core:
65
+
66
+ - **Kernel** (hot, cheap, needed almost everywhere) → add it to `src/index.ts`, and add the name
67
+ to the frozen list in `src/index.barrel.test.ts` (a deliberate, reviewed decision).
68
+ - **Belongs to a subsystem** (a specific concern, or it pulls a heavy dependency) → export it from
69
+ that subpath's `src/<group>/index.ts` and register the subpath in `package.json` `exports`. Do
70
+ **not** add it to the root barrel.
71
+
72
+ The `core barrel surface` guard test (`src/index.barrel.test.ts`) fails if the root barrel grows or
73
+ shrinks unexpectedly — that's the signal to pick one of the two paths above on purpose, so the
74
+ barrel never drifts back into a god-module.
75
+
76
+ ## Everyday commands
77
+
78
+ Run from the repo root — they fan out across every workspace via `bun --filter`:
79
+
80
+ ```bash
81
+ # in your project root
82
+ bun test # run all package test suites
83
+ bun run typecheck # type-check every package
84
+ bun run lint # eslint
85
+ bun run format # prettier --write
86
+ bun run build # build every package
87
+ ```
88
+
89
+ To work on a single package, run its script directly inside it:
90
+
91
+ ```bash
92
+ # in your project root
93
+ cd packages/orm
94
+ bun test # just the ORM suite
95
+ bun test src/db/QueryBuilder.test.ts # a single file
96
+ ```
97
+
98
+ > **Note** — tests run on Bun's built-in runner. Most database tests use SQLite (`:memory:` or a temp file), so they need no external services.
99
+
100
+ ## Making a change
101
+
102
+ 1. **Branch** off the default branch.
103
+ 2. **Write the code and tests.** New behavior needs test coverage; bug fixes should
104
+ add a regression test. Match the style of the surrounding code.
105
+ 3. **Keep the public API typed.** Exported functions and classes should have accurate
106
+ types — the `typecheck` task gates this.
107
+ 4. **Run the checks** before pushing:
108
+
109
+ ```bash
110
+ # in your project root
111
+ bun run typecheck && bun test && bun run lint
112
+ ```
113
+
114
+ 5. **Update the docs.** If you change or add public API, update the relevant page
115
+ under `docs/` (and the nav in `apps/docs/app/routes/_layout.ts` if you add a page).
116
+
117
+ ## Documentation changes
118
+
119
+ The docs are markdown files in `docs/`, served by the docs app in `apps/docs`. A page's
120
+ URL is its path: `docs/query-builder.md` → `/docs/query-builder`, and
121
+ `docs/orm/casts.md` → `/docs/orm/casts` (a directory's `index.md` serves the bare
122
+ slug). To add a page, create the markdown file and add an entry to the `NAV` array in
123
+ `apps/docs/app/routes/_layout.ts`.
124
+
125
+ When documenting an API, verify it against the package source rather than memory —
126
+ accuracy is the priority.
127
+
128
+ ### One page, one subject
129
+
130
+ A page documents its own subject and nothing else. When a feature belongs to another
131
+ area, link to the page that owns it instead of re-explaining it here — a second copy
132
+ drifts out of date, and the reader who needs the detail is better served by the page
133
+ that keeps it complete.
134
+
135
+ - **Own it or link it.** Route-model binding is explained in [Routing](/docs/routing);
136
+ every other page states that the model arrives resolved and links there. `HttpContext`
137
+ members belong to [Requests & Context](/docs/context), not to whichever guide happens
138
+ to show a handler.
139
+ - **Show the default, not the override.** If a param binds implicitly, the example
140
+ registers a plain route. Configuration that only exists for the non-default case
141
+ belongs on the page that owns the mechanism.
142
+ - **Prefer a pointer to a paraphrase.** One sentence naming the behaviour plus a link
143
+ beats a condensed re-teaching that will disagree with the source page after the next
144
+ change.
145
+ - **Package-authoring material goes to
146
+ [Package Development](/docs/package-development)** — macros, driver contracts, and
147
+ provider internals are not app-builder documentation.
148
+
149
+ The same rule governs a page's own length: a section that has grown into a second
150
+ subject is a sign it wants to be its own page, or to move to the one that owns it.
151
+
152
+ ## Pull requests
153
+
154
+ - Keep PRs focused — one logical change per PR.
155
+ - Describe what changed and why; link any related issue.
156
+ - Make sure CI is green (`typecheck`, `test`, `lint`).
157
+ - Note any breaking change clearly so it can be captured in the
158
+ [Release Notes](/docs/changelog).
159
+
160
+ ## Next steps
161
+
162
+ - [Package Development](/docs/package-development) — building a `@zerotal/*`-style package.
163
+ - [Testing](/docs/testing) — the test harness, factories, and fakes.
164
+ - [Upgrade Guide](/docs/upgrade) — how releases are versioned.
@@ -0,0 +1,354 @@
1
+ ---
2
+ title: Controllers
3
+ description: Group related request handlers into classes that map actions to routes and resolve dependencies through the container.
4
+ ---
5
+
6
+ # Controllers
7
+
8
+ A controller groups related request handlers into a single class, where each
9
+ public method is an action that a route can target. Controllers are resolved
10
+ through the IoC container, so a class declaring `@inject(...)` gets its
11
+ dependencies wired up automatically.
12
+
13
+ ## Basic controller
14
+
15
+ A controller is a plain class. Each action receives the request `HttpContext`
16
+ directly, reads input from it, and sets the response by calling a helper on it —
17
+ actions return `void`, not a value.
18
+
19
+ ```typescript
20
+ // app/controllers/PostController.ts
21
+ import type { HttpContext } from "zerotal";
22
+ import { Post } from "../models/Post.ts";
23
+
24
+ export class PostController {
25
+ async index(ctx: HttpContext): Promise<void> {
26
+ const posts = await Post.all();
27
+ ctx.json({ posts });
28
+ }
29
+
30
+ async show(ctx: HttpContext): Promise<void> {
31
+ const post = await Post.findOrFail(Number(ctx.params.id));
32
+ ctx.json(post);
33
+ }
34
+
35
+ async store(ctx: HttpContext): Promise<void> {
36
+ const { title, body } = await ctx.body<{ title: string; body: string }>();
37
+ const post = await Post.create({ title, body });
38
+ ctx.json(post, 201);
39
+ }
40
+
41
+ async destroy(ctx: HttpContext): Promise<void> {
42
+ const post = await Post.findOrFail(Number(ctx.params.id));
43
+ await post.delete();
44
+ ctx.response = new Response(null, { status: 204 });
45
+ }
46
+ }
47
+ ```
48
+
49
+ Map routes to actions by passing the controller class and the action name:
50
+
51
+ ```typescript
52
+ // routes/index.ts
53
+ import { Router } from "zerotal";
54
+ import { PostController } from "../app/controllers/PostController.ts";
55
+ import { AuthMiddleware } from "@zerotal/auth";
56
+
57
+ Router.get("/posts", PostController, "index");
58
+ Router.get("/posts/:id", PostController, "show");
59
+ Router.post("/posts", PostController, "store", [AuthMiddleware]);
60
+ Router.delete("/posts/:id", PostController, "destroy", [AuthMiddleware]);
61
+ ```
62
+
63
+ > **Note** — The fourth argument is the per-route middleware array:
64
+ > `Router.get(path, Controller, action, middleware?)`. See
65
+ > [Routing](/docs/routing) for groups, prefixes, and named routes.
66
+
67
+ ## Dependency injection
68
+
69
+ A controller is constructed by `container.make()`, which auto-wires its
70
+ dependencies. Decorate the **class** with `@inject(...)`, listing the tokens in
71
+ constructor order — the container resolves each token and passes it to the
72
+ constructor:
73
+
74
+ ```typescript
75
+ // app/controllers/PostController.ts
76
+ import type { HttpContext } from "zerotal";
77
+ import { inject } from "zerotal";
78
+ import { CacheManager } from "@zerotal/cache";
79
+
80
+ @inject(CacheManager)
81
+ export class PostController {
82
+ constructor(private cache: CacheManager) {}
83
+
84
+ async index(ctx: HttpContext): Promise<void> {
85
+ const posts = await this.cache.remember("posts.all", 60, () => Post.all());
86
+ ctx.json({ posts });
87
+ }
88
+ }
89
+ ```
90
+
91
+ > **Warning** — `@inject` is a **class** decorator and only supports constructor
92
+ > injection. There is no parameter-level or property-level `@inject` — the tokens
93
+ > are listed on the class and matched to constructor parameters by position.
94
+
95
+ ### Which should I use?
96
+
97
+ - **Facades** — reach for these first for one-off lookups. No constructor wiring,
98
+ resolved from request-scoped storage on demand.
99
+ - **`@inject(...)` on the class** — when the controller depends on a service for
100
+ most of its actions and you want it injected once at construction.
101
+
102
+ ### Resolving via facades
103
+
104
+ Facades are the lightest-weight option — no constructor wiring needed. They read
105
+ the current request from `AsyncLocalStorage`, so they work anywhere in the async
106
+ tree:
107
+
108
+ ```typescript
109
+ // app/controllers/DashboardController.ts
110
+ import type { HttpContext } from "zerotal";
111
+ import { Cache } from "@zerotal/cache";
112
+ import { Auth } from "@zerotal/auth";
113
+
114
+ export class DashboardController {
115
+ async index(ctx: HttpContext): Promise<void> {
116
+ const user = Auth.user(); // resolved from AsyncLocalStorage
117
+ const stats = await Cache.get("stats");
118
+ ctx.json({ user, stats });
119
+ }
120
+ }
121
+ ```
122
+
123
+ ## Model binding
124
+
125
+ A route parameter whose name matches an auto-registered model is bound without any
126
+ declaration — the resolved instance arrives on `ctx.params` under the param's name,
127
+ or you can read it explicitly with `ctx.model()`. `.bind()` and the model's own
128
+ `resolveRouteBinding` are for overriding that default:
129
+
130
+ ```typescript
131
+ // routes/index.ts — :post already resolves through Post
132
+ Router.get("/posts/:post", PostController, "show");
133
+ ```
134
+
135
+ ```typescript
136
+ // app/controllers/PostController.ts — via ctx.params:
137
+ async show(ctx: HttpContext<{ post: Post }>): Promise<void> {
138
+ ctx.json(ctx.params.post);
139
+ }
140
+
141
+ // app/controllers/PostController.ts — or via ctx.model():
142
+ async show(ctx: HttpContext): Promise<void> {
143
+ const post = ctx.model<Post>("post");
144
+ ctx.json(post);
145
+ }
146
+ ```
147
+
148
+ > **Note** — If the bound record is not found, the resolver throws a 404
149
+ > automatically — no guard needed. `ctx.model()` throws if no binding was
150
+ > registered for that param name.
151
+
152
+ ## FormRequest validation
153
+
154
+ Delegate validation to a `FormRequest` class for cleaner controllers. The class
155
+ comes from `@zerotal/validator`:
156
+
157
+ ```typescript
158
+ // app/requests/posts/StorePostRequest.ts
159
+ import { FormRequest, RuleBuilder } from "@zerotal/validator";
160
+
161
+ export class StorePostRequest extends FormRequest {
162
+ // Do NOT annotate the return type — validate() infers the typed shape from it.
163
+ // (Pipe-strings like "required|string" are Flow's @validate syntax; FormRequest
164
+ // uses the RuleBuilder.)
165
+ rules(r: RuleBuilder) {
166
+ return {
167
+ title: r.string().min(3).max(255),
168
+ body: r.string().min(10),
169
+ tags: r.array(r.string()).optional(),
170
+ };
171
+ }
172
+ }
173
+ ```
174
+
175
+ `StorePostRequest.validate()` parses the request body, runs the rules, and
176
+ returns the validated data fully typed from your `rules()` return — it throws a
177
+ validation error on failure:
178
+
179
+ ```typescript
180
+ // app/controllers/PostController.ts
181
+ import { StorePostRequest } from "../requests/posts/StorePostRequest.ts";
182
+
183
+ async store(ctx: HttpContext): Promise<void> {
184
+ const data = await StorePostRequest.validate(); // throws on validation failure
185
+ const post = await Post.create(data);
186
+ ctx.json(post, 201);
187
+ }
188
+ ```
189
+
190
+ > **Tip** — Do not annotate the `rules()` return type explicitly. The static
191
+ > `validate()` infers the result shape from `ReturnType<rules>`, so an explicit
192
+ > annotation would widen the result back to `Record<string, unknown>`.
193
+
194
+ See the [Validator](/docs/validator) guide for the full rules reference.
195
+
196
+ ## Resource controllers
197
+
198
+ A resource controller implements the standard seven RESTful actions:
199
+
200
+ ```typescript
201
+ // app/controllers/ArticleController.ts
202
+ import type { HttpContext } from "zerotal";
203
+
204
+ export class ArticleController {
205
+ async index(ctx: HttpContext): Promise<void> {
206
+ /* GET /articles */
207
+ }
208
+ async create(ctx: HttpContext): Promise<void> {
209
+ /* GET /articles/create */
210
+ }
211
+ async store(ctx: HttpContext): Promise<void> {
212
+ /* POST /articles */
213
+ }
214
+ async show(ctx: HttpContext): Promise<void> {
215
+ /* GET /articles/:id */
216
+ }
217
+ async edit(ctx: HttpContext): Promise<void> {
218
+ /* GET /articles/:id/edit */
219
+ }
220
+ async update(ctx: HttpContext): Promise<void> {
221
+ /* PUT /articles/:id */
222
+ }
223
+ async destroy(ctx: HttpContext): Promise<void> {
224
+ /* DELETE /articles/:id */
225
+ }
226
+ }
227
+ ```
228
+
229
+ Register all seven with a single call, or narrow them with `.only()` / `.except()`:
230
+
231
+ ```typescript
232
+ // routes/index.ts
233
+ Router.resource("articles", ArticleController);
234
+ Router.resource("articles", ArticleController).only(["index", "show"]);
235
+ Router.resource("articles", ArticleController).except(["create", "edit"]);
236
+ ```
237
+
238
+ > **Tip** — Generate either shape with `bun zt make:controller` — add
239
+ > `--resource` for the full CRUD stubs.
240
+
241
+ ## Returning responses
242
+
243
+ Controllers set `ctx.response` via helper methods — they do not return a value
244
+ (the return type is `Promise<void>`):
245
+
246
+ ```typescript
247
+ // in a controller action
248
+ ctx.json(data); // 200 JSON
249
+ ctx.json(data, 201); // 201 JSON
250
+ ctx.view(MyComponent, props); // full HTML document from a view component
251
+ ctx.html("<p>fragment</p>"); // raw HTML, no DOCTYPE
252
+ ctx.redirect("/dashboard", 303); // redirect (default 302)
253
+ ctx.back(); // redirect to a same-origin Referer, else "/"
254
+ ```
255
+
256
+ See the [Responses](/docs/responses) guide for the full API.
257
+
258
+ ## Single-action controllers
259
+
260
+ For an action that doesn't need grouping, give the controller a single method
261
+ and name it when you register the route — a file route function works equally
262
+ well:
263
+
264
+ ```typescript
265
+ // app/controllers/HealthCheckController.ts
266
+ import type { HttpContext } from "zerotal";
267
+ import { Router } from "zerotal";
268
+
269
+ export class HealthCheckController {
270
+ async handle(ctx: HttpContext): Promise<void> {
271
+ ctx.json({ status: "ok", uptime: Math.floor(process.uptime()) });
272
+ }
273
+ }
274
+
275
+ Router.get("/health", HealthCheckController, "handle");
276
+ ```
277
+
278
+ ## Accessing the authenticated user
279
+
280
+ With `@zerotal/auth` installed, the authenticated user is available at
281
+ `ctx.user` after `AuthMiddleware` runs. The `Auth` facade exposes the same data
282
+ from anywhere in the async tree — handy inside services called by the controller:
283
+
284
+ ```typescript
285
+ // in a controller action
286
+ import { Auth } from "@zerotal/auth";
287
+
288
+ async store(ctx: HttpContext): Promise<void> {
289
+ // Via ctx (optional — undefined for guests):
290
+ const userId = ctx.user?.id;
291
+
292
+ // Via the facade (throws ForbiddenError when not authenticated):
293
+ const user = Auth.user();
294
+ const id = Auth.id(); // number
295
+ }
296
+ ```
297
+
298
+ > **Note** — `ctx.user` and the `Auth` facade are contributed by
299
+ > `@zerotal/auth`, not core. See [Authentication](/docs/authentication).
300
+
301
+ ## After-response work
302
+
303
+ Register a callback to run after the response has been sent — useful for
304
+ expensive side effects that shouldn't block the client. `afterResponse` returns
305
+ `this`, so it chains:
306
+
307
+ ```typescript
308
+ // in a controller action
309
+ async store(ctx: HttpContext): Promise<void> {
310
+ const post = await Post.create(data);
311
+ ctx.json(post, 201);
312
+
313
+ ctx.afterResponse(async () => {
314
+ await NotifyFollowersJob.dispatch({ postId: post.id });
315
+ await Cache.forget("posts.all");
316
+ });
317
+ }
318
+ ```
319
+
320
+ > **Note** — An error thrown inside an `afterResponse` callback is logged and
321
+ > swallowed; it never crashes the server or affects the already-sent response.
322
+
323
+ ## References
324
+
325
+ Controller actions interact with the request through the `ctx` object
326
+ (`HttpContext`). The members used most often from a controller:
327
+
328
+ | Member | Signature | Description |
329
+ | -------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------- |
330
+ | `ctx.params` | `Record<string, string>` | Route params, plus resolved model bindings under their name (e.g. `/posts/:id` → `{ id }`). |
331
+ | `ctx.body<T>()` | `() => Promise<T>` | Parse and cache the JSON / form request body. |
332
+ | `ctx.input<T>(key, fallback?)` | `(key, fallback?) => T` | Read from route params → cached body → query, in order. |
333
+ | `ctx.model<T>(name)` | `(name) => T` | The route-model-bound instance for a param; throws if unbound. |
334
+ | `ctx.json(data, status?)` | `(data, status?) => void` | Set a JSON response (default 200). |
335
+ | `ctx.view(component, props?, status?)` | overloaded | Render a view component to a full HTML document. |
336
+ | `ctx.html(markup, status?)` | `(markup, status?) => void` | Set a raw HTML response (no DOCTYPE). |
337
+ | `ctx.redirect(url, status?)` | `(url, status?) => void` | Set a redirect response (default 302). |
338
+ | `ctx.back(status?)` | `(status?) => void` | Redirect to a same-origin `Referer`, else `/`. |
339
+ | `ctx.afterResponse(cb)` | `(cb) => this` | Run work after the response is sent. |
340
+
341
+ Container wiring for controllers:
342
+
343
+ | Member | Signature | Description |
344
+ | -------------------- | --------------- | ---------------------------------------------------------------------------- |
345
+ | `@inject(...tokens)` | class decorator | Declare constructor dependencies in order; auto-wired by `container.make()`. |
346
+
347
+ ## Next steps
348
+
349
+ - [Routing](/docs/routing) — map URLs to controller actions and bind models.
350
+ - [Middleware](/docs/middleware) — protect and transform requests before they reach actions.
351
+ - [Validator](/docs/validator) — the rules reference for `FormRequest` validation.
352
+ - [Responses](/docs/responses) — the full response helper API.
353
+ - [Container](/docs/container) — how `@inject` and `make()` resolve your services.
354
+ - [HttpContext](/docs/context) — the full request/response object reference.