@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.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/api-surface.md +283 -0
- package/docs/about.md +676 -0
- package/docs/admin/actions.md +183 -0
- package/docs/admin/auth.md +89 -0
- package/docs/admin/dashboard.md +155 -0
- package/docs/admin/extending-ui.md +229 -0
- package/docs/admin/extending.md +205 -0
- package/docs/admin/forms.md +155 -0
- package/docs/admin/index.md +144 -0
- package/docs/admin/operations.md +224 -0
- package/docs/admin/references.md +174 -0
- package/docs/admin/resources.md +132 -0
- package/docs/admin/structure.md +158 -0
- package/docs/admin/tables.md +185 -0
- package/docs/admin/testing.md +138 -0
- package/docs/ai.md +373 -0
- package/docs/application.md +435 -0
- package/docs/arch.md +263 -0
- package/docs/assets.md +755 -0
- package/docs/audit.md +367 -0
- package/docs/authentication.md +1000 -0
- package/docs/authorization.md +529 -0
- package/docs/broadcasting/channels.md +170 -0
- package/docs/broadcasting/client.md +108 -0
- package/docs/broadcasting/events.md +190 -0
- package/docs/broadcasting/index.md +163 -0
- package/docs/broadcasting/references.md +84 -0
- package/docs/broadcasting/testing.md +132 -0
- package/docs/cache.md +387 -0
- package/docs/carbon.md +830 -0
- package/docs/changelog.md +465 -0
- package/docs/client/auth.md +113 -0
- package/docs/client/errors.md +139 -0
- package/docs/client/files.md +118 -0
- package/docs/client/index.md +128 -0
- package/docs/client/references.md +58 -0
- package/docs/client/requests.md +131 -0
- package/docs/client/resilience.md +141 -0
- package/docs/client/testing.md +146 -0
- package/docs/commands.md +450 -0
- package/docs/components.md +2427 -0
- package/docs/config-system.md +258 -0
- package/docs/container.md +596 -0
- package/docs/context.md +579 -0
- package/docs/contributing.md +164 -0
- package/docs/controllers.md +354 -0
- package/docs/conventions.md +466 -0
- package/docs/cookies.md +101 -0
- package/docs/csrf.md +206 -0
- package/docs/database.md +476 -0
- package/docs/deployment.md +434 -0
- package/docs/devtools.md +1013 -0
- package/docs/email-verification.md +320 -0
- package/docs/encryption.md +380 -0
- package/docs/errors.md +317 -0
- package/docs/events.md +420 -0
- package/docs/flow/components.md +846 -0
- package/docs/flow/decorators.md +458 -0
- package/docs/flow/events.md +420 -0
- package/docs/flow/forms.md +551 -0
- package/docs/flow/index.md +568 -0
- package/docs/flow/layouts.md +793 -0
- package/docs/flow/lifecycle.md +336 -0
- package/docs/flow/pagination.md +402 -0
- package/docs/flow/performance.md +262 -0
- package/docs/flow/references.md +317 -0
- package/docs/flow/routing.md +550 -0
- package/docs/flow/testing.md +447 -0
- package/docs/getting-started.md +286 -0
- package/docs/health.md +269 -0
- package/docs/helpers.md +423 -0
- package/docs/i18n.md +414 -0
- package/docs/index.md +65 -0
- package/docs/inertia/build.md +95 -0
- package/docs/inertia/devtools.md +197 -0
- package/docs/inertia/index.md +267 -0
- package/docs/inertia/middleware.md +81 -0
- package/docs/inertia/props.md +507 -0
- package/docs/inertia/references.md +144 -0
- package/docs/inertia/rendering.md +189 -0
- package/docs/inertia/ssr.md +102 -0
- package/docs/inspirations.md +257 -0
- package/docs/lifecycle.md +331 -0
- package/docs/lock.md +454 -0
- package/docs/logger.md +517 -0
- package/docs/media.md +582 -0
- package/docs/middleware.md +509 -0
- package/docs/migrations.md +529 -0
- package/docs/monitor.md +377 -0
- package/docs/notifications.md +848 -0
- package/docs/orm/casts.md +365 -0
- package/docs/orm/factories.md +231 -0
- package/docs/orm/index.md +663 -0
- package/docs/orm/lifecycle.md +421 -0
- package/docs/orm/queries.md +692 -0
- package/docs/orm/relationships.md +440 -0
- package/docs/orm/serialization.md +251 -0
- package/docs/package-development.md +536 -0
- package/docs/pagination.md +340 -0
- package/docs/password-reset.md +335 -0
- package/docs/providers.md +447 -0
- package/docs/query-builder.md +465 -0
- package/docs/queue.md +549 -0
- package/docs/rate-limiting.md +253 -0
- package/docs/responses.md +336 -0
- package/docs/roles-and-2fa.md +418 -0
- package/docs/routing.md +1008 -0
- package/docs/scaffolding.md +335 -0
- package/docs/scheduler.md +533 -0
- package/docs/seeding.md +229 -0
- package/docs/session.md +451 -0
- package/docs/social.md +524 -0
- package/docs/storage.md +622 -0
- package/docs/structure.md +230 -0
- package/docs/support-policy.md +114 -0
- package/docs/telemetry.md +505 -0
- package/docs/tenancy.md +608 -0
- package/docs/testing/browser.md +153 -0
- package/docs/testing/console.md +130 -0
- package/docs/testing/database.md +238 -0
- package/docs/testing/flow-browser.md +216 -0
- package/docs/testing/http.md +441 -0
- package/docs/testing/index.md +240 -0
- package/docs/testing/mocking.md +410 -0
- package/docs/upgrade.md +192 -0
- package/docs/validator.md +549 -0
- package/docs/view.md +453 -0
- package/package.json +60 -0
- package/src/bin/mcp.ts +73 -0
- package/src/config.ts +63 -0
- package/src/errors.ts +27 -0
- package/src/index.ts +56 -0
- package/src/install/ArchInstallCommand.ts +189 -0
- package/src/install/detect.ts +98 -0
- package/src/install/guidelines.ts +290 -0
- package/src/install/markers.ts +77 -0
- package/src/install/mcpConfig.ts +81 -0
- package/src/mcp/index.ts +27 -0
- package/src/mcp/jsonrpc.ts +133 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/stdio.ts +111 -0
- package/src/mcp/types.ts +146 -0
- package/src/probe/ArchProbeCommand.ts +48 -0
- package/src/probe/sentinel.ts +13 -0
- package/src/probe/topics.ts +291 -0
- package/src/provider/ArchProvider.ts +101 -0
- package/src/tools/_probe.ts +164 -0
- package/src/tools/apiSurface.ts +241 -0
- package/src/tools/appInfo.ts +88 -0
- package/src/tools/baselines.ts +210 -0
- package/src/tools/context.ts +37 -0
- package/src/tools/doctor.ts +93 -0
- package/src/tools/index.ts +50 -0
- package/src/tools/logs.ts +265 -0
- package/src/tools/routes.ts +133 -0
- package/src/tools/schema.ts +119 -0
- 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.
|