@shaferllc/keel 0.59.0 → 0.68.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/AGENTS.md +167 -0
- package/README.md +30 -1
- package/bin/keel-mcp.mjs +9 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +14 -2
- package/dist/core/auth.d.ts +47 -0
- package/dist/core/auth.js +77 -0
- package/dist/core/authorization.d.ts +9 -1
- package/dist/core/authorization.js +22 -2
- package/dist/core/cache.d.ts +82 -5
- package/dist/core/cache.js +181 -23
- package/dist/core/cli/stubs.d.ts +12 -0
- package/dist/core/cli/stubs.js +120 -0
- package/dist/core/container.d.ts +20 -0
- package/dist/core/container.js +52 -0
- package/dist/core/cors.d.ts +29 -0
- package/dist/core/cors.js +72 -0
- package/dist/core/crypto.d.ts +40 -4
- package/dist/core/crypto.js +66 -6
- package/dist/core/csrf.d.ts +25 -0
- package/dist/core/csrf.js +78 -0
- package/dist/core/database.d.ts +49 -4
- package/dist/core/database.js +89 -21
- package/dist/core/events.d.ts +129 -5
- package/dist/core/events.js +165 -7
- package/dist/core/health.d.ts +141 -0
- package/dist/core/health.js +226 -0
- package/dist/core/helpers.d.ts +15 -3
- package/dist/core/helpers.js +23 -3
- package/dist/core/index.d.ts +33 -18
- package/dist/core/index.js +16 -8
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +82 -4
- package/dist/core/logger.js +141 -23
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/model.d.ts +2 -0
- package/dist/core/model.js +16 -14
- package/dist/core/provider.d.ts +7 -0
- package/dist/core/provider.js +7 -0
- package/dist/core/queue.d.ts +134 -9
- package/dist/core/queue.js +304 -14
- package/dist/core/rate-limit.js +3 -0
- package/dist/core/relations.js +13 -13
- package/dist/core/request.d.ts +26 -0
- package/dist/core/request.js +77 -0
- package/dist/core/shield.d.ts +39 -0
- package/dist/core/shield.js +60 -0
- package/dist/core/social.d.ts +173 -0
- package/dist/core/social.js +337 -0
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +287 -7
- package/dist/core/tokens.d.ts +74 -0
- package/dist/core/tokens.js +155 -0
- package/dist/db/d1.d.ts +32 -0
- package/dist/db/d1.js +26 -0
- package/dist/db/libsql.d.ts +29 -0
- package/dist/db/libsql.js +32 -0
- package/dist/db/pg.d.ts +29 -0
- package/dist/db/pg.js +33 -0
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/docs/ai-manifest.json +2472 -0
- package/docs/ai.md +128 -0
- package/docs/architecture.md +331 -0
- package/docs/authentication.md +453 -0
- package/docs/authorization.md +167 -0
- package/docs/broadcasting.md +137 -0
- package/docs/broker.md +500 -0
- package/docs/cache.md +558 -0
- package/docs/configuration.md +311 -0
- package/docs/console.md +356 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +530 -0
- package/docs/debugging.md +129 -0
- package/docs/decorators.md +127 -0
- package/docs/errors.md +395 -0
- package/docs/events.md +496 -0
- package/docs/examples/architecture-app.ts +27 -0
- package/docs/examples/authentication.ts +61 -0
- package/docs/examples/authorization.ts +79 -0
- package/docs/examples/broadcasting.ts +60 -0
- package/docs/examples/broker-cache-validate.ts +34 -0
- package/docs/examples/broker-fault-tolerance.ts +29 -0
- package/docs/examples/broker-middleware.ts +27 -0
- package/docs/examples/broker.ts +203 -0
- package/docs/examples/cache.ts +222 -0
- package/docs/examples/configuration.ts +81 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +118 -0
- package/docs/examples/debugging.ts +41 -0
- package/docs/examples/decorators.ts +40 -0
- package/docs/examples/errors.ts +121 -0
- package/docs/examples/events.ts +204 -0
- package/docs/examples/factories.ts +84 -0
- package/docs/examples/hashing.ts +71 -0
- package/docs/examples/health.ts +94 -0
- package/docs/examples/helpers.ts +171 -0
- package/docs/examples/hooks.ts +54 -0
- package/docs/examples/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +92 -0
- package/docs/examples/mail.ts +160 -0
- package/docs/examples/middleware.ts +119 -0
- package/docs/examples/migrations.ts +126 -0
- package/docs/examples/models.ts +239 -0
- package/docs/examples/notification.ts +124 -0
- package/docs/examples/providers.ts +123 -0
- package/docs/examples/queues.ts +254 -0
- package/docs/examples/rate-limiting.ts +42 -0
- package/docs/examples/redis.ts +99 -0
- package/docs/examples/request-response.ts +197 -0
- package/docs/examples/routing.ts +186 -0
- package/docs/examples/scheduling.ts +62 -0
- package/docs/examples/sessions.ts +102 -0
- package/docs/examples/static-files.ts +63 -0
- package/docs/examples/storage.ts +132 -0
- package/docs/examples/templates.ts +58 -0
- package/docs/examples/testing.ts +66 -0
- package/docs/examples/transformer.ts +141 -0
- package/docs/examples/transformers.ts +49 -0
- package/docs/examples/url-builder.ts +86 -0
- package/docs/examples/validation.ts +102 -0
- package/docs/examples/views.tsx +62 -0
- package/docs/examples/vite.ts +106 -0
- package/docs/factories.md +166 -0
- package/docs/getting-started.md +290 -0
- package/docs/hashing.md +259 -0
- package/docs/health.md +225 -0
- package/docs/helpers.md +347 -0
- package/docs/hono.md +186 -0
- package/docs/hooks.md +118 -0
- package/docs/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +290 -0
- package/docs/mail.md +678 -0
- package/docs/middleware.md +425 -0
- package/docs/migrations.md +476 -0
- package/docs/models.md +810 -0
- package/docs/notifications.md +474 -0
- package/docs/providers.md +363 -0
- package/docs/queues.md +679 -0
- package/docs/rate-limiting.md +155 -0
- package/docs/redis.md +178 -0
- package/docs/request-response.md +953 -0
- package/docs/routing.md +804 -0
- package/docs/scheduling.md +110 -0
- package/docs/security.md +85 -0
- package/docs/sessions.md +354 -0
- package/docs/social-auth.md +174 -0
- package/docs/static-files.md +211 -0
- package/docs/storage.md +450 -0
- package/docs/templates.md +315 -0
- package/docs/testing.md +125 -0
- package/docs/transformers.md +381 -0
- package/docs/url-builder.md +295 -0
- package/docs/validation.md +288 -0
- package/docs/views.md +267 -0
- package/docs/vite.md +434 -0
- package/llms-full.txt +17694 -0
- package/llms.txt +116 -0
- package/package.json +38 -7
package/docs/routing.md
ADDED
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
# Routing
|
|
2
|
+
|
|
3
|
+
Routes live in `routes/web.ts`. The default export receives the `Router` and
|
|
4
|
+
registers routes on it. The HTTP kernel later compiles them onto Hono.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import type { Router } from "@shaferllc/keel/core";
|
|
8
|
+
import { json, text, param } from "@shaferllc/keel/core";
|
|
9
|
+
import { HomeController } from "../app/Controllers/HomeController.js";
|
|
10
|
+
|
|
11
|
+
export default function routes(router: Router): void {
|
|
12
|
+
router.get("/", [HomeController, "index"]); // controller
|
|
13
|
+
router.get("/health", json({ status: "ok" })); // static response
|
|
14
|
+
router.get("/hi/:name", () => text(`Hi ${param("name")}`)); // dynamic
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## HTTP verbs
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
router.get(path, handler);
|
|
22
|
+
router.post(path, handler);
|
|
23
|
+
router.put(path, handler);
|
|
24
|
+
router.patch(path, handler);
|
|
25
|
+
router.delete(path, handler);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Each returns the router, so calls chain.
|
|
29
|
+
|
|
30
|
+
## Three kinds of handler
|
|
31
|
+
|
|
32
|
+
**Controller actions** — a `[Controller, method]` tuple, resolved from the
|
|
33
|
+
[container](./container.md) with dependency injection:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
router.get("/users/:id", [UserController, "show"]);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Static responses** — pass a ready-made response directly, no closure:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
router.get("/health", json({ status: "ok" }));
|
|
43
|
+
router.get("/robots.txt", text("User-agent: *\nAllow: /"));
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Closures** — a function that runs per request. Use this whenever the response
|
|
47
|
+
depends on the request (route params, query, body), because those must be read
|
|
48
|
+
at request time:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
router.get("/users/:id", () => json({ id: param("id") }));
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> Rule of thumb: response is the same every time → pass it directly. Response
|
|
55
|
+
> depends on the request → wrap it in `() =>`.
|
|
56
|
+
|
|
57
|
+
## Reading the request
|
|
58
|
+
|
|
59
|
+
The `request` accessor (or the standalone shortcuts) read the current request —
|
|
60
|
+
no `c` needed:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
request.param("id"); // route parameter
|
|
64
|
+
request.query("q"); // query string
|
|
65
|
+
request.header("authorization");
|
|
66
|
+
await request.json(); // parse a JSON body
|
|
67
|
+
|
|
68
|
+
// standalone equivalents
|
|
69
|
+
param("id"); query("q"); header("authorization"); await body();
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`request` also exposes `request.method`, `request.path`, `request.url`,
|
|
73
|
+
`request.status`, and `request.raw` (the underlying web `Request`).
|
|
74
|
+
|
|
75
|
+
## Writing the response
|
|
76
|
+
|
|
77
|
+
Build responses with the standalone helpers or the `response` accessor — they're
|
|
78
|
+
the same thing:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
json({ ok: true }); // JSON response
|
|
82
|
+
text("hello"); // plain text
|
|
83
|
+
html("<h1>Hi</h1>"); // HTML
|
|
84
|
+
redirect("/login"); // redirect
|
|
85
|
+
|
|
86
|
+
response.json({ ok: true });
|
|
87
|
+
response.status(201).json(created); // set status, chainable
|
|
88
|
+
response.header("x-total", "42").json(rows);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Returning a **string** from a handler is shorthand — Keel wraps it as HTML.
|
|
92
|
+
|
|
93
|
+
## The full helper set
|
|
94
|
+
|
|
95
|
+
| Read (`request.*` or standalone) | Write (`response.*` or standalone) |
|
|
96
|
+
|----------------------------------|------------------------------------|
|
|
97
|
+
| `param(name)` · `query(name)` · `header(name)` | `json(data, status?)` · `text()` · `html()` |
|
|
98
|
+
| `body<T>()` (parse JSON body) | `redirect(location, status?)` |
|
|
99
|
+
| `request.method` · `.path` · `.status` · `.raw` | `response.status(code)` · `response.header(k, v)` |
|
|
100
|
+
|
|
101
|
+
All of these are powered by async-context storage the HTTP kernel enables for
|
|
102
|
+
every request, so they only work inside a request. You can always still take `c`
|
|
103
|
+
explicitly — both styles work.
|
|
104
|
+
|
|
105
|
+
## Named routes & URL generation
|
|
106
|
+
|
|
107
|
+
Name a route, then build its URL by name — no hardcoded paths:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
router.get("/users/:id", [UserController, "show"]).name("users.show");
|
|
111
|
+
|
|
112
|
+
router.url("users.show", { id: 42 }); // "/users/42"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Building URLs from names — plain and tamper-proof signed URLs — is its own
|
|
116
|
+
topic. See the [URL builder](./url-builder.md) for `router.url()`,
|
|
117
|
+
`router.signedUrl()`, and `router.hasValidSignature()`.
|
|
118
|
+
|
|
119
|
+
## Route groups
|
|
120
|
+
|
|
121
|
+
Share a prefix, middleware, and/or name prefix across many routes:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
router
|
|
125
|
+
.group(() => {
|
|
126
|
+
router.get("/status", json({ up: true })).name("status");
|
|
127
|
+
router.get("/me", [MeController, "show"]).name("me");
|
|
128
|
+
})
|
|
129
|
+
.prefix("/api") // -> /api/status, /api/me
|
|
130
|
+
.middleware([auth]) // runs before each route in the group
|
|
131
|
+
.as("api"); // -> names "api.status", "api.me"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Groups nest — inner prefixes and middleware compose with the outer group's.
|
|
135
|
+
|
|
136
|
+
## Resource routes
|
|
137
|
+
|
|
138
|
+
Generate RESTful routes for a controller in one line:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
router.resource("posts", PostController);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
| Verb | Path | Action |
|
|
145
|
+
|------|------|--------|
|
|
146
|
+
| GET | `/posts` | `index` |
|
|
147
|
+
| GET | `/posts/create` | `create` |
|
|
148
|
+
| POST | `/posts` | `store` |
|
|
149
|
+
| GET | `/posts/:id` | `show` |
|
|
150
|
+
| GET | `/posts/:id/edit` | `edit` |
|
|
151
|
+
| PUT/PATCH | `/posts/:id` | `update` |
|
|
152
|
+
| DELETE | `/posts/:id` | `destroy` |
|
|
153
|
+
|
|
154
|
+
Trim the set with `.only([...])`, `.except([...])`, or `.apiOnly()` (drops the
|
|
155
|
+
HTML-form `create`/`edit` actions).
|
|
156
|
+
|
|
157
|
+
## Param constraints
|
|
158
|
+
|
|
159
|
+
Constrain a parameter with a regex, a matcher, or a `{ match }` object —
|
|
160
|
+
non-matching requests fall through to a 404:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
router.get("/users/:id", [UserController, "show"]).where("id", /\d+/);
|
|
164
|
+
|
|
165
|
+
// built-in matchers
|
|
166
|
+
router.get("/u/:id", handler).where("id", router.matchers.number());
|
|
167
|
+
router.get("/a/:id", handler).where("id", router.matchers.uuid());
|
|
168
|
+
router.get("/s/:slug", handler).where("slug", router.matchers.slug());
|
|
169
|
+
|
|
170
|
+
// a global constraint applied to every matching :id
|
|
171
|
+
router.where("id", router.matchers.number());
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Groups take constraints too: `group(...).where("id", router.matchers.uuid())`.
|
|
175
|
+
|
|
176
|
+
## Per-route middleware
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
router.get("/dashboard", [DashboardController, "index"]).middleware([auth]);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Brisk routes: redirects, views & Inertia
|
|
183
|
+
|
|
184
|
+
`on()` is a shortcut for routes with no controller:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
router.on("/old").redirect("/new"); // path/URL redirect
|
|
188
|
+
router.on("/ext").redirectToPath("https://x.com"); // alias of redirect
|
|
189
|
+
router.on("/posts").redirectToRoute("articles.index", {}, { qs: { page: 1 } });
|
|
190
|
+
|
|
191
|
+
router.on("/about").render(AboutPage, { title: "About" }); // render a view
|
|
192
|
+
router.on("/dashboard").renderInertia("Dashboard", { user }); // Inertia page
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
See [Inertia](./inertia.md) for the full Inertia adapter.
|
|
196
|
+
|
|
197
|
+
## Domain & subdomain routing
|
|
198
|
+
|
|
199
|
+
Bind routes (or a group) to a host pattern. `:segments` capture subdomain
|
|
200
|
+
params, readable with `request.subdomain()`:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
router
|
|
204
|
+
.group(() => {
|
|
205
|
+
router.get("/", () => json({ tenant: request.subdomain("tenant") }));
|
|
206
|
+
})
|
|
207
|
+
.domain(":tenant.example.com");
|
|
208
|
+
|
|
209
|
+
router.get("/", [BlogController, "index"]).domain("blog.example.com");
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Requests are dispatched by their `Host` header; non-matching hosts fall through
|
|
213
|
+
to your default (undomained) routes.
|
|
214
|
+
|
|
215
|
+
## Route config
|
|
216
|
+
|
|
217
|
+
Attach arbitrary metadata to a route (or a whole group) with `.config()`, then
|
|
218
|
+
read it in the handler or route middleware via `request.route.config` — for
|
|
219
|
+
per-route flags like an auth scope, a rate tier, or a layout choice:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
router.get("/admin", [Admin, "index"]).config({ scope: "admin", rateTier: "high" });
|
|
223
|
+
|
|
224
|
+
router
|
|
225
|
+
.group(() => {
|
|
226
|
+
router.get("/billing", [Billing, "index"]); // inherits { area: "billing" }
|
|
227
|
+
router.get("/billing/export", [Billing, "export"]).config({ heavy: true });
|
|
228
|
+
})
|
|
229
|
+
.config({ area: "billing" }); // a route's own config wins on conflict
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
// in a guard middleware attached to the route/group:
|
|
234
|
+
if (request.route?.config.scope === "admin") await authorize("access-admin");
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Group config is merged into every route in the group, with a route's own keys
|
|
238
|
+
winning. Route config is available to **route/group middleware** and the handler
|
|
239
|
+
(not global middleware, which runs before route matching).
|
|
240
|
+
|
|
241
|
+
## The current route
|
|
242
|
+
|
|
243
|
+
`request.route` exposes the matched route, and `request.routeIs()` checks it:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
request.route; // { name, pattern, methods, config }
|
|
247
|
+
request.routeIs("posts.show"); // boolean
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## More verbs
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
router.any("/webhook", [HookController, "handle"]); // every verb
|
|
254
|
+
router.route(["GET", "POST"], "/search", handler); // a specific set
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Inspecting routes
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
npm run keel routes
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
GET / HomeController@index
|
|
265
|
+
GET /health Static
|
|
266
|
+
GET /users/:id Closure
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Adding more route files
|
|
270
|
+
|
|
271
|
+
`bootstrap/app.ts` loads `routes/web.ts`. To split routes (e.g. an `api.ts`),
|
|
272
|
+
import and call it there:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
import registerWebRoutes from "../routes/web.js";
|
|
276
|
+
import registerApiRoutes from "../routes/api.js";
|
|
277
|
+
|
|
278
|
+
registerWebRoutes(app.make(Router));
|
|
279
|
+
registerApiRoutes(app.make(Router));
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## API reference
|
|
285
|
+
|
|
286
|
+
Registration methods live on `Router`. Each verb method hands back a `Route` you
|
|
287
|
+
chain to name, guard, or constrain it; `group()`, `resource()`, and `on()` hand
|
|
288
|
+
back `RouteGroup`, `RouteResource`, and a brisk-route matcher respectively. You
|
|
289
|
+
never construct these classes — the framework builds the `Router` and passes it
|
|
290
|
+
to your routes file, and the rest come back from its methods.
|
|
291
|
+
|
|
292
|
+
URL generation (`url`, `signedUrl`, `hasValidSignature`) also lives on `Router`
|
|
293
|
+
but is documented separately in the [URL builder](./url-builder.md).
|
|
294
|
+
|
|
295
|
+
### `matchers`
|
|
296
|
+
|
|
297
|
+
`matchers: { number(): RegExp; uuid(): RegExp; slug(): RegExp; alpha(): RegExp }`
|
|
298
|
+
|
|
299
|
+
The built-in parameter matchers, also reachable as `router.matchers`. Each
|
|
300
|
+
returns a fresh (un-anchored) `RegExp` to hand to `.where()`.
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
import { matchers } from "@shaferllc/keel/core";
|
|
304
|
+
|
|
305
|
+
router.get("/u/:id", handler).where("id", matchers.number());
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Notes:** `number` → `\d+`, `uuid` → a canonical UUID, `slug` →
|
|
309
|
+
`a-z0-9` words joined by `-`, `alpha` → letters only. They are plain regexes, so
|
|
310
|
+
you can also pass your own `/.../ ` or a `{ match }` object.
|
|
311
|
+
|
|
312
|
+
### Router
|
|
313
|
+
|
|
314
|
+
The route registrar. Injected into your routes file; resolve it elsewhere with
|
|
315
|
+
`app.make(Router)`.
|
|
316
|
+
|
|
317
|
+
#### `get(path, handler)` · `post` · `put` · `patch` · `delete`
|
|
318
|
+
|
|
319
|
+
`get(path: string, handler: RouteHandler): Route`
|
|
320
|
+
|
|
321
|
+
Registers a route for the one HTTP verb and returns the `Route` for chaining.
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
router.get("/users/:id", [UserController, "show"]);
|
|
325
|
+
router.post("/users", [UserController, "store"]);
|
|
326
|
+
router.delete("/users/:id", [UserController, "destroy"]);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**Notes:** `post`, `put`, `patch`, and `delete` share the identical signature.
|
|
330
|
+
`handler` is a closure, a `[Controller, "method"]` tuple, or a ready-made
|
|
331
|
+
`Response` — see [`RouteHandler`](#routehandler). Paths are normalized (a
|
|
332
|
+
trailing slash is trimmed; `"/"` stays `"/"`).
|
|
333
|
+
|
|
334
|
+
#### `any(path, handler)`
|
|
335
|
+
|
|
336
|
+
`any(path: string, handler: RouteHandler): Route`
|
|
337
|
+
|
|
338
|
+
Registers the route for every HTTP verb (`GET POST PUT PATCH DELETE OPTIONS HEAD`).
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
router.any("/webhook", [HookController, "handle"]);
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
#### `route(methods, path, handler)`
|
|
345
|
+
|
|
346
|
+
`route(methods: Method[], path: string, handler: RouteHandler): Route`
|
|
347
|
+
|
|
348
|
+
Registers the route for a specific set of verbs.
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
router.route(["GET", "POST"], "/search", handler);
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Notes:** `Method` is the uppercase verb union — pass them exactly (`"GET"`, not
|
|
355
|
+
`"get"`).
|
|
356
|
+
|
|
357
|
+
#### `on(path)`
|
|
358
|
+
|
|
359
|
+
`on(path: string): RouteMatcher`
|
|
360
|
+
|
|
361
|
+
Opens a brisk-route matcher for controller-less routes (redirects, views,
|
|
362
|
+
Inertia pages). See [`RouteMatcher`](#routematcher).
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
router.on("/old").redirect("/new");
|
|
366
|
+
router.on("/about").render(AboutPage, { title: "About" });
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
#### `group(callback)`
|
|
370
|
+
|
|
371
|
+
`group(callback: () => void): RouteGroup`
|
|
372
|
+
|
|
373
|
+
Runs `callback` (which registers routes on the router) and returns a
|
|
374
|
+
[`RouteGroup`](#routegroup) wrapping exactly the routes it added, so you can
|
|
375
|
+
apply a shared prefix / middleware / name prefix to them.
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
router
|
|
379
|
+
.group(() => {
|
|
380
|
+
router.get("/status", json({ up: true })).name("status");
|
|
381
|
+
router.get("/me", [MeController, "show"]).name("me");
|
|
382
|
+
})
|
|
383
|
+
.prefix("/api")
|
|
384
|
+
.middleware([auth])
|
|
385
|
+
.as("api");
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**Notes:** the grouping (prefix, middleware, name prefix) is applied *after*
|
|
389
|
+
registration by the returned `RouteGroup` — the callback itself sees no prefix.
|
|
390
|
+
Nest by calling `.prefix()` on the inner group before the outer group's; the
|
|
391
|
+
outer prefix is prepended, so `/api` + `/v1/...` composes correctly.
|
|
392
|
+
|
|
393
|
+
#### `resource(name, controller)`
|
|
394
|
+
|
|
395
|
+
`resource(name: string, controller: ControllerRef): RouteResource`
|
|
396
|
+
|
|
397
|
+
Registers the seven RESTful routes (`index create store show edit update
|
|
398
|
+
destroy`) for `controller` and returns a [`RouteResource`](#routeresource) to
|
|
399
|
+
trim or rename them.
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
router.resource("posts", PostController);
|
|
403
|
+
router.resource("posts.comments", CommentController); // nested
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**Notes:** each route is auto-named `${name}.${action}`. A dotted `name` nests
|
|
407
|
+
resources — `"posts.comments"` yields `/posts/:post_id/comments/:id`. The
|
|
408
|
+
controller may be a class or a lazy `() => import(...)` loader.
|
|
409
|
+
|
|
410
|
+
#### `where(param, matcher)`
|
|
411
|
+
|
|
412
|
+
`where(param: string, matcher: Matcher): this`
|
|
413
|
+
|
|
414
|
+
Registers a **global** parameter constraint, applied at `all()` time to every
|
|
415
|
+
route whose path contains `:param` and that doesn't already constrain it.
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
router.where("id", matchers.number());
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**Notes:** per-route and group `.where()` win over a global one. Returns the
|
|
422
|
+
router for chaining.
|
|
423
|
+
|
|
424
|
+
#### `named(map)`
|
|
425
|
+
|
|
426
|
+
`named(map: Record<string, MiddlewareHandler>): this`
|
|
427
|
+
|
|
428
|
+
Registers named middleware you can later reference by string in `.middleware()`
|
|
429
|
+
/ `.use()`.
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
router.named({ auth, admin });
|
|
433
|
+
router.get("/panel", handler).use("auth");
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**Notes:** merges into any previously named middleware. Referencing an
|
|
437
|
+
unregistered name throws at resolve time (see `resolveMiddleware`).
|
|
438
|
+
|
|
439
|
+
#### `resolveMiddleware(ref)`
|
|
440
|
+
|
|
441
|
+
`resolveMiddleware(ref: MiddlewareRef): MiddlewareHandler`
|
|
442
|
+
|
|
443
|
+
Resolves a middleware reference — a handler passes through; a string is looked up
|
|
444
|
+
in the `named()` registry.
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
const mw = router.resolveMiddleware("auth");
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Notes:** throws `No named middleware [name]…` if a string isn't registered.
|
|
451
|
+
Mostly used by the HTTP kernel; handy in tests.
|
|
452
|
+
|
|
453
|
+
#### `all()`
|
|
454
|
+
|
|
455
|
+
`all(): RouteDefinition[]`
|
|
456
|
+
|
|
457
|
+
Returns every live route definition, after folding in global `where()`
|
|
458
|
+
constraints and dropping routes trimmed to zero methods (by `only`/`except`).
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
for (const r of router.all()) console.log(r.methods, r.path, r.name);
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Notes:** this is the list the HTTP kernel compiles onto Hono. Trimmed resource
|
|
465
|
+
actions are excluded here, but `url()` can still find them by name.
|
|
466
|
+
|
|
467
|
+
#### `resolve(handler)`
|
|
468
|
+
|
|
469
|
+
`resolve(handler: RouteHandler): HandlerFn`
|
|
470
|
+
|
|
471
|
+
Turns a `RouteHandler` into a callable `(c: Ctx) => …`, resolving controller
|
|
472
|
+
tuples through the container and lazy loaders.
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
const fn = router.resolve([UserController, "show"]);
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
**Notes:** a bare `[Controller]` tuple calls the controller's `handle` method. A
|
|
479
|
+
`Response` handler is cloned per call. Throws if the named controller method
|
|
480
|
+
doesn't exist. Called by the kernel; you rarely call it directly.
|
|
481
|
+
|
|
482
|
+
### Route
|
|
483
|
+
|
|
484
|
+
Returned by every verb method (`get`/`post`/…). Chain to name, guard, or
|
|
485
|
+
constrain a single route. Exposes a readonly `def: RouteDefinition`.
|
|
486
|
+
|
|
487
|
+
#### `name(name)` · `as(name)`
|
|
488
|
+
|
|
489
|
+
`name(name: string): this`
|
|
490
|
+
|
|
491
|
+
Names the route for URL generation. `as()` is an alias.
|
|
492
|
+
|
|
493
|
+
```ts
|
|
494
|
+
router.get("/users/:id", handler).name("users.show");
|
|
495
|
+
router.get("/users/:id", handler).as("users.show");
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
#### `middleware(mw)` · `use(mw)`
|
|
499
|
+
|
|
500
|
+
`middleware(mw: MiddlewareRef | MiddlewareRef[]): this`
|
|
501
|
+
|
|
502
|
+
Attaches middleware that runs only for this route, after any group middleware.
|
|
503
|
+
`use()` is an alias.
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
router.get("/dashboard", handler).middleware([auth]);
|
|
507
|
+
router.get("/admin", handler).use(["auth", "admin"]);
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
**Notes:** accepts a single ref or an array; appends (order preserved). A string
|
|
511
|
+
ref is resolved against `named()`.
|
|
512
|
+
|
|
513
|
+
#### `where(param, matcher)`
|
|
514
|
+
|
|
515
|
+
`where(param: string, matcher: Matcher): this`
|
|
516
|
+
|
|
517
|
+
Constrains a route parameter; non-matching requests fall through to a 404.
|
|
518
|
+
|
|
519
|
+
```ts
|
|
520
|
+
router.get("/users/:id", handler).where("id", /\d+/);
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
#### `domain(pattern)`
|
|
524
|
+
|
|
525
|
+
`domain(pattern: string): this`
|
|
526
|
+
|
|
527
|
+
Binds the route to a host pattern; `:segments` capture subdomain params.
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
router.get("/", [BlogController, "index"]).domain("blog.example.com");
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### RouteGroup
|
|
534
|
+
|
|
535
|
+
Returned by `group()`. Its fluent methods apply across every route the group
|
|
536
|
+
callback registered. All return `this`.
|
|
537
|
+
|
|
538
|
+
#### `prefix(prefix)`
|
|
539
|
+
|
|
540
|
+
`prefix(prefix: string): this`
|
|
541
|
+
|
|
542
|
+
Prepends a path prefix to every route in the group.
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
router.group(() => { /* … */ }).prefix("/api");
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**Notes:** leading/trailing slashes are normalized. Applying to the group's root
|
|
549
|
+
route (`"/"`) yields just the prefix.
|
|
550
|
+
|
|
551
|
+
#### `middleware(mw)` · `use(mw)`
|
|
552
|
+
|
|
553
|
+
`middleware(mw: MiddlewareRef | MiddlewareRef[]): this`
|
|
554
|
+
|
|
555
|
+
Prepends middleware to every route in the group, so group middleware runs before
|
|
556
|
+
each route's own. `use()` is an alias.
|
|
557
|
+
|
|
558
|
+
```ts
|
|
559
|
+
router.group(() => { /* … */ }).middleware([auth]);
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
#### `where(param, matcher)`
|
|
563
|
+
|
|
564
|
+
`where(param: string, matcher: Matcher): this`
|
|
565
|
+
|
|
566
|
+
Constrains a parameter across the group, skipping routes that already constrain
|
|
567
|
+
it themselves.
|
|
568
|
+
|
|
569
|
+
```ts
|
|
570
|
+
router.group(() => { /* … */ }).where("id", matchers.uuid());
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
#### `as(namePrefix)`
|
|
574
|
+
|
|
575
|
+
`as(namePrefix: string): this`
|
|
576
|
+
|
|
577
|
+
Prefixes the name of every *already-named* route in the group.
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
router.group(() => { /* named routes */ }).as("api"); // status -> api.status
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
**Notes:** routes without a `name()` are left untouched — name them inside the
|
|
584
|
+
callback for `as()` to reach them.
|
|
585
|
+
|
|
586
|
+
#### `domain(pattern)`
|
|
587
|
+
|
|
588
|
+
`domain(pattern: string): this`
|
|
589
|
+
|
|
590
|
+
Binds every route in the group to a host pattern.
|
|
591
|
+
|
|
592
|
+
```ts
|
|
593
|
+
router.group(() => { /* … */ }).domain(":tenant.example.com");
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### RouteResource
|
|
597
|
+
|
|
598
|
+
Returned by `resource()`. Chain to trim, rename, or guard the generated actions.
|
|
599
|
+
All return `this`.
|
|
600
|
+
|
|
601
|
+
#### `only(actions)` · `except(actions)`
|
|
602
|
+
|
|
603
|
+
`only(actions: string[]): this`
|
|
604
|
+
`except(actions: string[]): this`
|
|
605
|
+
|
|
606
|
+
Keep only the listed actions, or drop the listed actions.
|
|
607
|
+
|
|
608
|
+
```ts
|
|
609
|
+
router.resource("posts", PostController).only(["index", "show"]);
|
|
610
|
+
router.resource("posts", PostController).except(["destroy"]);
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
**Notes:** trimming empties a route's `methods`; `all()` then filters it out. The
|
|
614
|
+
route name still exists, so `url()` can resolve it even when it won't be served.
|
|
615
|
+
|
|
616
|
+
#### `apiOnly()`
|
|
617
|
+
|
|
618
|
+
`apiOnly(): this`
|
|
619
|
+
|
|
620
|
+
Drops the HTML-form actions (`create`, `edit`) — the shorthand for
|
|
621
|
+
`.except(["create", "edit"])`.
|
|
622
|
+
|
|
623
|
+
```ts
|
|
624
|
+
router.resource("posts", PostController).apiOnly();
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
#### `as(name)`
|
|
628
|
+
|
|
629
|
+
`as(name: string): this`
|
|
630
|
+
|
|
631
|
+
Renames the route-name prefix for every action.
|
|
632
|
+
|
|
633
|
+
```ts
|
|
634
|
+
router.resource("posts", PostController).as("articles"); // articles.index, …
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
#### `params(map)`
|
|
638
|
+
|
|
639
|
+
`params(map: Record<string, string>): this`
|
|
640
|
+
|
|
641
|
+
Renames route parameters. Maps a resource segment to a new param name.
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
router.resource("posts", PostController).params({ posts: "post" }); // :id -> :post
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
**Notes:** for the resource's own segment the underlying param is `:id`; for a
|
|
648
|
+
parent segment in a nested resource it's `:{singular}_id`. Only the first
|
|
649
|
+
matching occurrence in each path is renamed.
|
|
650
|
+
|
|
651
|
+
#### `use(actions, mw)`
|
|
652
|
+
|
|
653
|
+
`use(actions: string[] | "*", mw: MiddlewareRef | MiddlewareRef[]): this`
|
|
654
|
+
|
|
655
|
+
Attaches middleware to specific actions, or to all with `"*"`.
|
|
656
|
+
|
|
657
|
+
```ts
|
|
658
|
+
router.resource("posts", PostController)
|
|
659
|
+
.use(["store", "update", "destroy"], "auth")
|
|
660
|
+
.use("*", logRequests);
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### RouteMatcher
|
|
664
|
+
|
|
665
|
+
Returned by `router.on(path)` — a builder for controller-less `GET` routes. Each
|
|
666
|
+
method registers the route and returns the underlying `Route`.
|
|
667
|
+
|
|
668
|
+
#### `redirect(to, status?)` · `redirectToPath(to, status?)`
|
|
669
|
+
|
|
670
|
+
`redirect(to: string, status?: number): Route`
|
|
671
|
+
|
|
672
|
+
Registers a route that redirects to a path or URL (default status `302`).
|
|
673
|
+
`redirectToPath` is an alias.
|
|
674
|
+
|
|
675
|
+
```ts
|
|
676
|
+
router.on("/old").redirect("/new");
|
|
677
|
+
router.on("/ext").redirectToPath("https://example.com", 301);
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
#### `redirectToRoute(name, params?, options?)`
|
|
681
|
+
|
|
682
|
+
`redirectToRoute(name: string, params?: Record<string, string | number>, options?: { qs?: Record<string, string | number>; status?: number }): Route`
|
|
683
|
+
|
|
684
|
+
Registers a route that redirects to a named route, resolving its URL (and
|
|
685
|
+
optional query string).
|
|
686
|
+
|
|
687
|
+
```ts
|
|
688
|
+
router.on("/posts").redirectToRoute("articles.index", {}, { qs: { page: 1 } });
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
#### `render(component, props?)`
|
|
692
|
+
|
|
693
|
+
`render(component: (props?: any) => unknown, props?: any): Route`
|
|
694
|
+
|
|
695
|
+
Registers a route that renders a view component directly.
|
|
696
|
+
|
|
697
|
+
```ts
|
|
698
|
+
router.on("/about").render(AboutPage, { title: "About" });
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
#### `renderInertia(component, props?)`
|
|
702
|
+
|
|
703
|
+
`renderInertia(component: string, props?: Record<string, unknown>): Route`
|
|
704
|
+
|
|
705
|
+
Registers a route that renders an Inertia page component by name. See
|
|
706
|
+
[Inertia](./inertia.md).
|
|
707
|
+
|
|
708
|
+
```ts
|
|
709
|
+
router.on("/dashboard").renderInertia("Dashboard", { user });
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
### Interfaces & types
|
|
713
|
+
|
|
714
|
+
#### `Ctx`
|
|
715
|
+
|
|
716
|
+
`type Ctx = Context` (Hono's request context)
|
|
717
|
+
|
|
718
|
+
The context handed to every handler and middleware. Every closure handler
|
|
719
|
+
receives it, though the `request`/`response` accessors mean you rarely read from
|
|
720
|
+
it directly.
|
|
721
|
+
|
|
722
|
+
```ts
|
|
723
|
+
router.get("/", (c: Ctx) => c.text("hi"));
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
#### `RouteHandler`
|
|
727
|
+
|
|
728
|
+
`type RouteHandler = HandlerFn | ControllerAction | Response`
|
|
729
|
+
|
|
730
|
+
What you pass as the second argument to a verb method. One of three shapes:
|
|
731
|
+
|
|
732
|
+
- **`HandlerFn`** — `(c: Ctx) => Response | string | Promise<Response | string>`;
|
|
733
|
+
returning a bare string is wrapped as HTML.
|
|
734
|
+
- **`ControllerAction`** — `[Controller]` (calls `handle`) or
|
|
735
|
+
`[Controller, "method"]`; the controller may be a lazy `() => import(...)`.
|
|
736
|
+
- **`Response`** — a ready-made response, cloned per request.
|
|
737
|
+
|
|
738
|
+
```ts
|
|
739
|
+
router.get("/a", (c) => c.json({ ok: true })); // HandlerFn
|
|
740
|
+
router.get("/b", [UserController, "show"]); // ControllerAction
|
|
741
|
+
router.get("/c", json({ up: true })); // Response
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
#### `Matcher`
|
|
745
|
+
|
|
746
|
+
`type Matcher = RegExp | string | { match: RegExp }`
|
|
747
|
+
|
|
748
|
+
A route-parameter constraint accepted by `.where()`. A regex, a regex-source
|
|
749
|
+
string, or a `{ match }` wrapper (the shape the built-in `matchers` conform to).
|
|
750
|
+
|
|
751
|
+
```ts
|
|
752
|
+
const a: Matcher = /\d+/;
|
|
753
|
+
const b: Matcher = "\\d+";
|
|
754
|
+
const c: Matcher = { match: /[a-z]+/ };
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
#### `Method`
|
|
758
|
+
|
|
759
|
+
`type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "HEAD"`
|
|
760
|
+
|
|
761
|
+
The HTTP verbs. Passed to `route()`; also the type of `RouteDefinition.methods`.
|
|
762
|
+
|
|
763
|
+
```ts
|
|
764
|
+
router.route(["GET", "POST"] as Method[], "/search", handler);
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
#### `MiddlewareRef`
|
|
768
|
+
|
|
769
|
+
`type MiddlewareRef = MiddlewareHandler | string`
|
|
770
|
+
|
|
771
|
+
A middleware handler, or the name of one registered with `router.named()`.
|
|
772
|
+
Accepted by every `.middleware()` / `.use()`.
|
|
773
|
+
|
|
774
|
+
```ts
|
|
775
|
+
router.get("/a", handler).use("auth"); // named
|
|
776
|
+
router.get("/b", handler).use(rateLimiterMw); // handler
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
#### `RouteDefinition`
|
|
780
|
+
|
|
781
|
+
```ts
|
|
782
|
+
interface RouteDefinition {
|
|
783
|
+
methods: Method[];
|
|
784
|
+
path: string;
|
|
785
|
+
handler: RouteHandler;
|
|
786
|
+
name?: string;
|
|
787
|
+
middleware: MiddlewareRef[];
|
|
788
|
+
wheres: Record<string, string>;
|
|
789
|
+
domain?: string;
|
|
790
|
+
}
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
The compiled record for one route — what `all()` returns and the kernel reads.
|
|
794
|
+
You inspect these (e.g. to print a route table); you don't build them by hand.
|
|
795
|
+
`wheres` holds each param's regex *source* string, keyed by param name.
|
|
796
|
+
|
|
797
|
+
```ts
|
|
798
|
+
for (const def of router.all()) {
|
|
799
|
+
console.log(def.methods.join("|"), def.path, def.name ?? "");
|
|
800
|
+
}
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
> `UrlOptions` and `SignedUrlOptions` are exported from this module too, but they
|
|
804
|
+
> belong to URL generation — see the [URL builder](./url-builder.md).
|