@orieljs/framework 0.1.0-alpha.6 → 0.1.0-alpha.8

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 (86) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-POEP4B2M.js → chunk-7ZQZYDOC.js} +745 -105
  5. package/dist/chunk-7ZQZYDOC.js.map +1 -0
  6. package/dist/{chunk-I63ZFMX6.js → chunk-GVNLZZTN.js} +1 -1
  7. package/dist/chunk-GVNLZZTN.js.map +1 -0
  8. package/dist/{chunk-ZOEPKBVF.js → chunk-SPQUX3C7.js} +1347 -428
  9. package/dist/chunk-SPQUX3C7.js.map +1 -0
  10. package/dist/{chunk-MFTZQMZM.js → chunk-VOYW3R5J.js} +4 -4
  11. package/dist/chunk-VOYW3R5J.js.map +1 -0
  12. package/dist/contracts.d.ts +515 -17
  13. package/dist/contracts.js +1 -1
  14. package/dist/index.d-WQpfdIbG.d.ts +630 -0
  15. package/dist/index.d.ts +964 -152
  16. package/dist/index.js +4279 -866
  17. package/dist/index.js.map +1 -1
  18. package/dist/{jsx-runtime-CBZQYYDS.d.ts → jsx-runtime-6um4Mwfb.d.ts} +96 -5
  19. package/dist/jsx-runtime.d.ts +2 -2
  20. package/dist/jsx-runtime.js +3 -3
  21. package/dist/{testing-BM481Hdz.d.ts → testing-CKwfWJGn.d.ts} +180 -15
  22. package/dist/testing.d.ts +2 -2
  23. package/dist/testing.js +3 -3
  24. package/docs/authentication.md +487 -0
  25. package/docs/authorization.md +435 -0
  26. package/docs/cache.md +647 -0
  27. package/docs/collections.md +2270 -0
  28. package/docs/configuration.md +334 -0
  29. package/docs/console.md +651 -0
  30. package/docs/container.md +430 -0
  31. package/docs/controllers.md +406 -0
  32. package/docs/csrf.md +126 -0
  33. package/docs/database-testing.md +144 -0
  34. package/docs/database.md +236 -0
  35. package/docs/deployment.md +276 -0
  36. package/docs/eloquent-collections.md +218 -0
  37. package/docs/eloquent-factories.md +255 -0
  38. package/docs/eloquent-mutators.md +254 -0
  39. package/docs/eloquent-observers.md +145 -0
  40. package/docs/eloquent-relationships.md +557 -0
  41. package/docs/eloquent.md +494 -0
  42. package/docs/encryption.md +104 -0
  43. package/docs/errors.md +318 -0
  44. package/docs/events.md +336 -0
  45. package/docs/facades.md +278 -0
  46. package/docs/hashing.md +118 -0
  47. package/docs/helpers.md +1281 -0
  48. package/docs/http-tests.md +386 -0
  49. package/docs/inertia.md +485 -0
  50. package/docs/installation.md +209 -0
  51. package/docs/lifecycle.md +234 -0
  52. package/docs/live-tests.md +226 -0
  53. package/docs/live.md +910 -0
  54. package/docs/logging.md +255 -0
  55. package/docs/mail.md +619 -0
  56. package/docs/middleware.md +384 -0
  57. package/docs/migrations.md +416 -0
  58. package/docs/mocking.md +264 -0
  59. package/docs/navigation.json +273 -0
  60. package/docs/notifications.md +225 -0
  61. package/docs/pagination.md +247 -0
  62. package/docs/passwords.md +207 -0
  63. package/docs/providers.md +187 -0
  64. package/docs/queries.md +600 -0
  65. package/docs/queues.md +330 -0
  66. package/docs/requests.md +533 -0
  67. package/docs/responses.md +444 -0
  68. package/docs/routing.md +738 -0
  69. package/docs/scheduling.md +277 -0
  70. package/docs/seeding.md +122 -0
  71. package/docs/session.md +409 -0
  72. package/docs/starter-kits.md +396 -0
  73. package/docs/storage.md +192 -0
  74. package/docs/structure.md +264 -0
  75. package/docs/testing.md +165 -0
  76. package/docs/time-travel.md +138 -0
  77. package/docs/validation.md +1792 -0
  78. package/docs/verification.md +152 -0
  79. package/docs/views.md +566 -0
  80. package/docs/vite.md +322 -0
  81. package/package.json +34 -32
  82. package/dist/chunk-I63ZFMX6.js.map +0 -1
  83. package/dist/chunk-MFTZQMZM.js.map +0 -1
  84. package/dist/chunk-POEP4B2M.js.map +0 -1
  85. package/dist/chunk-ZOEPKBVF.js.map +0 -1
  86. package/dist/index.d-BJyJx6U1.d.ts +0 -319
@@ -0,0 +1,738 @@
1
+ # Routing
2
+
3
+ Routes map a URL and an HTTP method to a closure or a controller action. Once `route:types` has run, TypeScript
4
+ checks every link to a named route.
5
+
6
+ ## Basic routing
7
+
8
+ A basic route takes a URI and a closure:
9
+
10
+ ```ts title="routes/web.ts"
11
+ import { Route } from '@orieljs/framework'
12
+
13
+ Route.get('/greeting', () => 'Hello World')
14
+ ```
15
+
16
+ A string, a number or a boolean is sent as HTML, `null` and `undefined` as an empty 204 response, and an object, an
17
+ array or a model as JSON. Return a view, a redirect or a `Response` for anything else: [Responses](responses.md) has
18
+ the list.
19
+
20
+ ### The default route files
21
+
22
+ Your routes live in the `routes` folder, and `bootstrap/app.ts` names the files to load. `routes/web.ts` is for your
23
+ pages: its routes are in the `web` [middleware group](middleware.md#middleware-groups), which gives them cookies, the
24
+ session and [CSRF protection](csrf.md). The kits with pages split their routes over three files, loaded in this order:
25
+
26
+ ```ts
27
+ Application.configure({ basePath: '.' }).withRouting({
28
+ web: ['routes/web.ts', 'routes/settings.ts', 'routes/auth.ts'],
29
+ console: 'routes/console.ts',
30
+ health: '/up',
31
+ })
32
+ ```
33
+
34
+ > [!WARNING]
35
+ > List every route file in `withRouting()` rather than importing one from another. Each listed file is loaded again
36
+ > for every application your tests create; a file imported from another is loaded once per process, so its routes
37
+ > would be missing from the second application.
38
+
39
+ A route file usually calls the `Route` facade at its top level. It may also export a default function, which is
40
+ called with the router.
41
+
42
+ #### API routes
43
+
44
+ If your application also offers a stateless API, `install:api` adds one:
45
+
46
+ ```sh
47
+ node oriel install:api
48
+ ```
49
+
50
+ It writes `routes/api.ts` and adds it to `withRouting()` in `bootstrap/app.ts`:
51
+
52
+ ```ts title="routes/api.ts"
53
+ import { request, Route } from '@orieljs/framework'
54
+
55
+ Route.get('/user', () => request().user()).middleware('auth:token')
56
+ ```
57
+
58
+ The routes in `routes/api.ts` are in the `api` middleware group, which has no session, no cookie encryption and no CSRF
59
+ check, and their URIs start with `/api`. `auth:token` signs the user in from a personal access token, whose table is in
60
+ a new app's migrations ([API tokens](authentication.md#api-tokens) covers them). The API starter kit has its own
61
+ `routes/api.ts`, with its token routes.
62
+
63
+ ### Available router methods
64
+
65
+ There is a method for each HTTP verb:
66
+
67
+ ```ts
68
+ declare const handler: () => string
69
+ // ---cut---
70
+ Route.get('/posts', handler)
71
+ Route.post('/posts', handler)
72
+ Route.put('/posts/{id}', handler)
73
+ Route.patch('/posts/{id}', handler)
74
+ Route.delete('/posts/{id}', handler)
75
+ Route.options('/posts', handler)
76
+ ```
77
+
78
+ `match()` takes several verbs, and `any()` answers them all:
79
+
80
+ ```ts
81
+ declare const handler: () => string
82
+ // ---cut---
83
+ Route.match(['get', 'post'], '/search', handler)
84
+ Route.any('/webhook', handler)
85
+ ```
86
+
87
+ A `GET` route answers `HEAD` too, with the same status and headers and no body. A request whose method the URI
88
+ doesn't accept gets a 405 with an `Allow` header listing the methods it does, and an `OPTIONS` request to a URI with
89
+ no `OPTIONS` route gets a 200 with that header.
90
+
91
+ > [!NOTE]
92
+ > Laravel tries routes in the order you define them, so `/posts/create` has to come before `/posts/{post}`. Oriel
93
+ > tries the most specific route first, so the order of your routes doesn't decide which one answers: segment by
94
+ > segment, fixed text before a parameter, and a parameter before an optional one. Routes of the same shape keep the
95
+ > order you defined them in, and fallback routes always come last.
96
+
97
+ #### Dependency injection
98
+
99
+ A route's closure receives its route parameters and nothing else. The current request comes from the `request()`
100
+ helper, which works anywhere in your code while a request is handled:
101
+
102
+ ```ts
103
+ Route.get('/users', () => {
104
+ const page = request().integer('page', 1)
105
+
106
+ return User.query().forPage(page).get()
107
+ })
108
+ ```
109
+
110
+ > [!NOTE]
111
+ > Laravel injects the request, and any class you type-hint, into a route's closure. TypeScript types are gone when
112
+ > your app runs, so Oriel can't see them: it passes only the route parameters. Take the request from `request()`,
113
+ > and a service from `inject()` or its [facade](facades.md). [Requests](requests.md) covers the request, and
114
+ > [Injecting with `inject()`](container.md#injecting-with-inject) the services.
115
+
116
+ #### CSRF protection
117
+
118
+ An HTML form that posts to a route in the `web` group must send the CSRF token, or the request is refused with a
119
+ 419. `csrfField()` writes the hidden field:
120
+
121
+ ```tsx title="resources/views/profile.tsx"
122
+ import { csrfField } from '@orieljs/framework'
123
+
124
+ export default function Profile() {
125
+ return (
126
+ <form method="post" action="/profile">
127
+ {csrfField()}
128
+ <button type="submit">Save</button>
129
+ </form>
130
+ )
131
+ }
132
+ ```
133
+
134
+ [CSRF protection](csrf.md) explains the token and the other ways to send it.
135
+
136
+ ### Redirect routes
137
+
138
+ A route that only redirects needs no closure. `Route.redirect()` answers every verb with a 302, or the status you
139
+ give it, and `Route.permanentRedirect()` with a 301:
140
+
141
+ ```ts
142
+ Route.redirect('/here', '/there')
143
+ Route.redirect('/here', '/there', 301)
144
+ Route.permanentRedirect('/old-blog', '/blog')
145
+ ```
146
+
147
+ The target may use the route's parameters: `Route.redirect('/posts/{id}', '/articles/{id}')`.
148
+
149
+ ### View routes
150
+
151
+ `Route.view()` shows a view, with the data you give it and the route's parameters:
152
+
153
+ ```ts
154
+ Route.view('/welcome', 'welcome')
155
+ Route.view('/welcome', 'welcome', { name: 'Taylor' })
156
+ ```
157
+
158
+ `Route.inertia()` does the same for an Inertia page, with its props:
159
+
160
+ ```ts
161
+ Route.inertia('/settings/appearance', 'Settings/Appearance')
162
+ ```
163
+
164
+ ### Resource routes
165
+
166
+ `Route.resource()` registers the seven routes of a resource controller in one line, and `Route.apiResource()` the
167
+ five an API needs, without the pages for its forms:
168
+
169
+ ```ts
170
+ declare class PhotoController {}
171
+ // ---cut---
172
+ Route.resource('photos', PhotoController)
173
+ Route.apiResource('photos', PhotoController)
174
+ ```
175
+
176
+ [Controllers](controllers.md#resource-controllers) lists their URIs, names and options.
177
+
178
+ ### Listing your routes
179
+
180
+ `route:list` shows every route with its methods, URI, name and action, sorted by URI:
181
+
182
+ ```sh
183
+ node oriel route:list
184
+ ```
185
+
186
+ Filter it by method, by part of the name or by part of the URI, or leave out the URIs that contain a text:
187
+
188
+ ```sh
189
+ node oriel route:list --method=post
190
+ node oriel route:list --name=posts
191
+ node oriel route:list --path=admin --except-path=admin/reports
192
+ ```
193
+
194
+ `--reverse` reverses the order, and `--json` prints the routes as JSON, with each route's middleware.
195
+
196
+ ### Routing customization
197
+
198
+ `withRouting()` in `bootstrap/app.ts` loads your routes once every service provider has booted, so a pattern or a
199
+ binding you set in a [provider's](providers.md) `boot()` is in place when your route files run:
200
+
201
+ ```ts title="bootstrap/app.ts"
202
+ import { dirname } from 'node:path'
203
+ import { Application } from '@orieljs/framework'
204
+ import providers from './providers'
205
+
206
+ export function createApplication(): Promise<Application> {
207
+ return Application.configure({ basePath: dirname(import.meta.dirname) })
208
+ .withRouting({ web: 'routes/web.ts', console: 'routes/console.ts', health: '/up' })
209
+ .withProviders(providers)
210
+ .create()
211
+ }
212
+ ```
213
+
214
+ `web` and `api` take one file or a list, `console` names the file of your closure commands, and `health` the URI of
215
+ the health check page, which answers 200 when your app is up. `apiPrefix` changes the API routes' prefix from `api`:
216
+
217
+ ```ts
218
+ Application.configure({ basePath: '.' }).withRouting({ api: 'routes/api.ts', apiPrefix: 'api/admin' })
219
+ ```
220
+
221
+ ## Route parameters
222
+
223
+ ### Required parameters
224
+
225
+ Capture a segment of the URI with braces:
226
+
227
+ ```ts
228
+ Route.get('/user/{id}', (id: string) => `User ${id}`)
229
+ ```
230
+
231
+ A route may have as many parameters as it needs. The action receives them in the order they appear in the URI, so
232
+ the names in your function don't have to match:
233
+
234
+ ```ts
235
+ Route.get('/teams/{team}/members/{member}', (teamId: string, memberId: string) => {
236
+ return { teamId, memberId }
237
+ })
238
+ ```
239
+
240
+ A parameter's name is letters, digits and underscores. A parameter named after one of your models receives the model
241
+ rather than the text (see [Route model binding](#route-model-binding)), so `{team}` stays a string only while your
242
+ app has no `Team` model.
243
+
244
+ #### Parameters and dependency injection
245
+
246
+ Nothing but the route's parameters is passed to the action, whatever else it needs. Read the rest from the request:
247
+
248
+ ```ts
249
+ Route.get('/user/{id}', (id: string) => {
250
+ const tab = request().query('tab', 'profile')
251
+
252
+ return `User ${id}, ${tab}`
253
+ })
254
+ ```
255
+
256
+ ### Optional parameters
257
+
258
+ Add a `?` to make a parameter optional. When the segment is missing, the parameter is `undefined`, so give it a
259
+ default:
260
+
261
+ ```ts
262
+ Route.get('/user/{name?}', (name?: string) => name ?? 'Guest')
263
+
264
+ Route.get('/user/{name?}', (name = 'John') => name)
265
+ ```
266
+
267
+ ### Regular expression constraints
268
+
269
+ `where()` constrains a parameter with a regular expression. A URI whose segment doesn't match gets a 404:
270
+
271
+ ```ts
272
+ Route.get('/user/{name}', (name: string) => name).where('name', '[A-Za-z]+')
273
+
274
+ Route.get('/user/{id}', (id: string) => id).where('id', '[0-9]+')
275
+
276
+ Route.get('/user/{id}/{name}', (id: string, name: string) => ({ id, name })).where({ id: '[0-9]+', name: '[a-z]+' })
277
+ ```
278
+
279
+ The common patterns have their own methods, which take one parameter or a list:
280
+
281
+ ```ts
282
+ Route.get('/user/{id}/{name}', (id: string, name: string) => ({ id, name }))
283
+ .whereNumber('id')
284
+ .whereAlpha('name')
285
+
286
+ Route.get('/user/{name}', (name: string) => name).whereAlphaNumeric('name')
287
+ Route.get('/user/{id}', (id: string) => id).whereUuid('id')
288
+ Route.get('/user/{id}', (id: string) => id).whereUlid('id')
289
+ Route.get('/category/{category}', (category: string) => category).whereIn('category', ['movie', 'song', 'painting'])
290
+ ```
291
+
292
+ `whereIn()` matches each value as written, so a value such as `c++` isn't read as a pattern.
293
+
294
+ #### Global constraints
295
+
296
+ A pattern that every route should share goes in a [service provider's](providers.md#the-boot-method) `boot()`, with
297
+ `Route.pattern()`:
298
+
299
+ ```ts title="app/Providers/AppServiceProvider.ts"
300
+ import { Route, ServiceProvider } from '@orieljs/framework'
301
+
302
+ export class AppServiceProvider extends ServiceProvider {
303
+ override boot(): void {
304
+ Route.pattern('id', '[0-9]+')
305
+ }
306
+ }
307
+ ```
308
+
309
+ From then on, every `{id}` must be a number. A route's own `where()` wins over the global pattern.
310
+
311
+ #### Encoded forward slashes
312
+
313
+ A parameter matches anything but a `/`. To let it match the rest of the path, slashes included, constrain it with
314
+ `.*`:
315
+
316
+ ```ts
317
+ Route.get('/search/{search}', (search: string) => search).where('search', '.*')
318
+ ```
319
+
320
+ Parameters arrive decoded: `/search/a%2Fb` gives `a/b`.
321
+
322
+ ## Named routes
323
+
324
+ A name lets you build a route's URL, or redirect to it, without writing the URI again:
325
+
326
+ ```ts
327
+ Route.get('/user/profile', () => view('profile')).name('profile')
328
+ ```
329
+
330
+ Controller actions take names the same way:
331
+
332
+ ```ts
333
+ Route.get('/posts/{post}', [PostController, 'show']).name('posts.show')
334
+ ```
335
+
336
+ > [!WARNING]
337
+ > Give each route its own name. When two routes share one, the last one defined wins.
338
+
339
+ #### Generating URLs to named routes
340
+
341
+ `route()` builds the URL of a named route, and `redirect().route()` a redirect to it:
342
+
343
+ ```ts
344
+ const url = route('profile')
345
+
346
+ const response = redirect().route('profile')
347
+ ```
348
+
349
+ A route's parameters go in an object. Any key the route doesn't use is added to the query string:
350
+
351
+ ```ts
352
+ Route.get('/user/{id}/profile', (id: string) => id).name('profile')
353
+
354
+ route('profile', { id: 1, photos: 'yes' })
355
+ // http://localhost:8000/user/1/profile?photos=yes
356
+ ```
357
+
358
+ You can also pass a single value for a route's first parameter, or a list of values in the URI's order. A model
359
+ fills a parameter with its route key, or with the column a `{post:slug}` parameter names:
360
+
361
+ ```ts
362
+ declare const post: Post
363
+ // ---cut---
364
+ route('posts.show', post)
365
+ route('posts.show', [post])
366
+ route('posts.show', { post, page: 2 })
367
+ ```
368
+
369
+ `route()` returns an absolute URL on the current request's host, or on `app.url` outside a request. Pass `false` as
370
+ its third argument for a path:
371
+
372
+ ```ts
373
+ declare const post: Post
374
+ // ---cut---
375
+ route('posts.show', post, false) // /posts/1
376
+ ```
377
+
378
+ An optional parameter you leave out is dropped from the URL. A required one is an error: "Missing required parameter
379
+ for [Route: posts.show] [URI: posts/{post}] [Missing parameter: post].".
380
+
381
+ #### Typed route parameters
382
+
383
+ `route:types` reads your routes and writes their names and parameters for TypeScript:
384
+
385
+ ```sh
386
+ node oriel route:types
387
+ ```
388
+
389
+ From then on, `route()` and `redirect().route()` accept only names that exist, and want the parameters each route
390
+ defines: a renamed route or a missing parameter fails the type check everywhere it is linked to.
391
+
392
+ ```ts
393
+ declare const post: Post
394
+ // @filename: types/routes.d.ts
395
+ declare module '@orieljs/framework' {
396
+ interface RouteList {
397
+ 'posts.show': { post: unknown }
398
+ }
399
+ }
400
+ declare module '@orieljs/framework/contracts' {
401
+ interface RouteList {
402
+ 'posts.show': { post: unknown }
403
+ }
404
+ }
405
+ export {}
406
+ // ---cut---
407
+ route('posts.show', post)
408
+ route('posts.show', { post: post.id, page: 2 })
409
+
410
+ // @ts-expect-error: no route is named post.show
411
+ route('post.show', post)
412
+
413
+ // @ts-expect-error: the route needs its post
414
+ route('posts.show')
415
+
416
+ // @ts-expect-error: getKey() is unknown; pass the model or post.id
417
+ route('posts.show', { post: post.getKey() })
418
+ ```
419
+
420
+ A parameter takes a string, a number, a boolean or a model. A model's `getKey()` is typed `unknown`, so pass the
421
+ model itself or its typed `id`.
422
+
423
+ `route:types` writes two files, both ignored by git: `types/routes.d.ts` for your server code, and
424
+ `resources/js/oriel/routes.ts` for your pages. `--server` and `--output` write them somewhere else. The Inertia and
425
+ API starter kits run it in `npm run typecheck`. The Inertia kits' Vite plugin also runs it when Vite starts, and again
426
+ whenever a file in `routes/` or `app/Http/Controllers/` changes while `npm run dev` runs. In the Live kit, run it
427
+ yourself. Until the first run, any name is accepted.
428
+
429
+ #### Route URLs in your pages
430
+
431
+ Your [Inertia](inertia.md#links-and-the-route-helper) pages build URLs with the same `route()`, from your adapter. It
432
+ takes the same names and parameters, and returns a path. The browser's route table leaves out the routes that
433
+ `types.except_routes` in `config/inertia.ts` names, with `*` as a wildcard. Unless you set it, it leaves out `live.*`,
434
+ Live's own routes.
435
+
436
+ ```tsx tab="React" title="resources/js/components/PostLink.tsx"
437
+ import { Link, route } from '@orieljs/inertia-react'
438
+
439
+ export function PostLink({ id, title }: { id: number; title: string }) {
440
+ return <Link href={route('posts.show', { post: id })}>{title}</Link>
441
+ }
442
+ ```
443
+ ```vue tab="Vue" title="resources/js/components/PostLink.vue"
444
+ <script setup lang="ts">
445
+ import { Link, route } from '@orieljs/inertia-vue'
446
+
447
+ defineProps<{ id: number; title: string }>()
448
+ </script>
449
+
450
+ <template>
451
+ <Link :href="route('posts.show', { post: id })">{{ title }}</Link>
452
+ </template>
453
+ ```
454
+ ```svelte tab="Svelte" title="resources/js/components/PostLink.svelte"
455
+ <script lang="ts">
456
+ import { Link, route } from '@orieljs/inertia-svelte'
457
+
458
+ let { id, title }: { id: number; title: string } = $props()
459
+ </script>
460
+
461
+ <Link href={route('posts.show', { post: id })}>{title}</Link>
462
+ ```
463
+ ```tsx tab="Solid" title="resources/js/components/PostLink.tsx"
464
+ import { Link, route } from '@orieljs/inertia-solid'
465
+
466
+ export function PostLink(props: { id: number; title: string }) {
467
+ return <Link href={route('posts.show', { post: props.id })}>{props.title}</Link>
468
+ }
469
+ ```
470
+ ```ts tab="Angular" title="resources/js/components/PostLink.component.ts"
471
+ import { Component, computed, input } from '@angular/core'
472
+ import { Link, route } from '@orieljs/inertia-angular'
473
+
474
+ @Component({
475
+ selector: 'app-post-link',
476
+ imports: [Link],
477
+ template: `<a oLink [href]="href()">{{ title() }}</a>`,
478
+ })
479
+ export class PostLink {
480
+ readonly id = input.required<number>()
481
+ readonly title = input.required<string>()
482
+ readonly href = computed(() => route('posts.show', { post: this.id() }))
483
+ }
484
+ ```
485
+
486
+ #### Inspecting the current route
487
+
488
+ `request().routeIs()` tells you whether the current request matched a route of that name. It takes several names,
489
+ and `*` as a wildcard:
490
+
491
+ ```ts
492
+ if (request().routeIs('admin.*')) {
493
+ // ...
494
+ }
495
+ ```
496
+
497
+ ## Route groups
498
+
499
+ A group shares middleware, a controller, a URI prefix or a name prefix between its routes. Groups can be nested: a
500
+ nested group adds its middleware after its parent's, and its prefixes after its parent's prefixes.
501
+
502
+ ### Middleware
503
+
504
+ `middleware()` before `group()` gives every route in the group its middleware, which run in the order you list them,
505
+ except that `auth` and `guest` run first ([Sorting middleware](middleware.md#sorting-middleware) has the rule):
506
+
507
+ ```ts
508
+ Route.middleware(['auth', 'verified']).group(() => {
509
+ Route.get('/dashboard', () => view('dashboard')).name('dashboard')
510
+ Route.get('/settings', () => view('settings')).name('settings')
511
+ })
512
+ ```
513
+
514
+ For a single route, chain the route's own `middleware()`: `Route.get('/profile', handler).middleware('auth')`.
515
+
516
+ ### Controllers
517
+
518
+ When every route of a group calls the same [controller](controllers.md), name it once with `controller()`. Each route
519
+ then gives only the method's name:
520
+
521
+ ```ts title="routes/web.ts"
522
+ import { Route } from '@orieljs/framework'
523
+ import { PostController } from '../app/Http/Controllers/PostController'
524
+
525
+ Route.controller(PostController).group(() => {
526
+ Route.get('/posts', 'index')
527
+ Route.get('/posts/{post}', 'show')
528
+ Route.post('/posts', 'store')
529
+ })
530
+ ```
531
+
532
+ Inside a controller group, the method's name is a plain string, which TypeScript doesn't check: a misspelt one is a
533
+ 500 ("Method PostController::shwo does not exist.") when the route is called. Outside a group, the
534
+ `[PostController, 'index']` form is checked.
535
+
536
+ ### Route prefixes
537
+
538
+ `prefix()` adds a segment in front of each URI in the group:
539
+
540
+ ```ts
541
+ Route.prefix('admin').group(() => {
542
+ Route.get('/users', () => 'Users') // matches /admin/users
543
+ })
544
+ ```
545
+
546
+ ### Route name prefixes
547
+
548
+ `name()` adds a prefix to each route's name. It is added as written, so end it with a `.`:
549
+
550
+ ```ts
551
+ Route.name('admin.').group(() => {
552
+ Route.get('/users', () => 'Users').name('users') // named admin.users
553
+ })
554
+ ```
555
+
556
+ ## Route model binding
557
+
558
+ A route often looks a model up by the ID in its URI. Route model binding does it for you: the action receives the
559
+ model rather than the ID.
560
+
561
+ ### Implicit binding
562
+
563
+ When a parameter is named after one of your models, its segment is that model's route key. `{user}` finds the
564
+ `User` whose `id` matches the segment, or answers 404 before your code runs:
565
+
566
+ ```ts title="routes/web.ts"
567
+ import { Route } from '@orieljs/framework'
568
+ import type { User } from '../app/Models/User'
569
+
570
+ Route.get('/users/{user}', (user: User) => user.email)
571
+ ```
572
+
573
+ The name is matched to a model in `app/Models` by its StudlyCase form: `{post}` is `Post`, `{blog_post}` and
574
+ `{blogPost}` are `BlogPost`. A parameter that names no model stays a string.
575
+
576
+ > [!NOTE]
577
+ > Laravel finds the model from the parameter's type hint. TypeScript types are gone when your app runs, so Oriel finds
578
+ > it from the parameter's name in the URI.
579
+
580
+ Binding works the same for controller actions:
581
+
582
+ ```ts title="app/Http/Controllers/PostController.ts"
583
+ import { view } from '@orieljs/framework'
584
+ import type { Post } from '../../Models/Post'
585
+ import { Controller } from './Controller'
586
+
587
+ export class PostController extends Controller {
588
+ show(post: Post) {
589
+ return view('posts.show', { post })
590
+ }
591
+ }
592
+ ```
593
+
594
+ #### Soft deleted models
595
+
596
+ A soft deleted model isn't found, so its route answers 404. There is no `withTrashed()` on routes.
597
+
598
+ #### Customizing the key
599
+
600
+ Name a column after a colon to look the model up by that column:
601
+
602
+ ```ts
603
+ Route.get('/posts/{post:slug}', (post: Post) => view('posts.show', { post }))
604
+ ```
605
+
606
+ To use a column for every route of a model, override `getRouteKeyName()` on the model. `route()` then fills the
607
+ parameter from that column too:
608
+
609
+ ```ts title="app/Models/Post.ts"
610
+ import { Model } from '@orieljs/framework'
611
+
612
+ export class Post extends Model {
613
+ override getRouteKeyName(): string {
614
+ return 'slug'
615
+ }
616
+ }
617
+ ```
618
+
619
+ ### Explicit binding
620
+
621
+ A parameter that isn't named after its model is bound with `Route.model()`, in a [service
622
+ provider's](providers.md#the-boot-method) `boot()`:
623
+
624
+ ```ts title="app/Providers/AppServiceProvider.ts"
625
+ import { Route, ServiceProvider } from '@orieljs/framework'
626
+ import { User } from '../Models/User'
627
+
628
+ export class AppServiceProvider extends ServiceProvider {
629
+ override boot(): void {
630
+ Route.model('author', User)
631
+ }
632
+ }
633
+ ```
634
+
635
+ Every `{author}` parameter is now a `User`, found by its route key, and a missing one is a 404:
636
+
637
+ ```ts
638
+ Route.get('/posts/by/{author}', (author: User) => author.posts().get())
639
+ ```
640
+
641
+ #### Customizing the resolution logic
642
+
643
+ `Route.bind()` resolves a parameter however you like. The callback receives the segment, and the action receives
644
+ what it returns:
645
+
646
+ ```ts title="app/Providers/AppServiceProvider.ts"
647
+ import { Route, ServiceProvider } from '@orieljs/framework'
648
+ import { User } from '../Models/User'
649
+
650
+ export class AppServiceProvider extends ServiceProvider {
651
+ override boot(): void {
652
+ Route.bind('member', (value) => User.where('name', value).firstOrFail())
653
+ }
654
+ }
655
+ ```
656
+
657
+ A `Route.bind()` callback comes first, then `Route.model()`, then binding by name. To change how a model is found on
658
+ every route, override the model's `resolveRouteBinding(value, field)`, which returns the model or `undefined`, or its
659
+ `resolveRouteBindingQuery(query, value, field)`, which returns the query that finds it. Inside that override,
660
+ TypeScript can't tell which columns the model has, so every column name is a type error there: a condition such as
661
+ `whereNotNull('published_at')` runs, but doesn't type-check.
662
+
663
+ ## Fallback routes
664
+
665
+ `Route.fallback()` answers any `GET` request that no other route matched, in place of the 404 page. It is always
666
+ tried last, wherever you define it, and when it sits in `routes/web.ts`, the `web` middleware run for it:
667
+
668
+ ```ts
669
+ Route.fallback(() => view('errors.not-found'))
670
+ ```
671
+
672
+ Inside a group with a prefix, a fallback answers only the URIs under that prefix. A `POST` to an unknown URI is still
673
+ a 404.
674
+
675
+ ## Rate limiting
676
+
677
+ ### Attaching rate limiters to routes
678
+
679
+ The `throttle` middleware limits how often a client may call a route. Its parameters are the number of requests and
680
+ the window in minutes:
681
+
682
+ ```ts
683
+ declare const handler: () => string
684
+ // ---cut---
685
+ Route.middleware('throttle:60,1').group(() => {
686
+ Route.post('/audio', handler)
687
+ Route.post('/video', handler)
688
+ })
689
+ ```
690
+
691
+ It counts per signed-in user, on any route, and per IP address for a guest. The counts are kept in your
692
+ cache's default store, or in the store that a `limiter` key in `config/cache.ts` names. Each response carries
693
+ `X-RateLimit-Limit` and `X-RateLimit-Remaining` headers. Past the limit, the route answers 429 ("Too Many Attempts.")
694
+ with `Retry-After`, the seconds to wait, and `X-RateLimit-Reset`, the Unix time the window ends.
695
+
696
+ Every route behind `throttle` adds to the same count for a client, whatever its numbers. To count a group of routes
697
+ on its own, give `throttle` a third parameter, a prefix for its key: `throttle:10,1,uploads`.
698
+
699
+ > [!WARNING]
700
+ > Laravel's named rate limiters (`RateLimiter::for()`) aren't in Oriel: give `throttle` its numbers directly.
701
+
702
+ ## Form method spoofing
703
+
704
+ HTML forms can only send `GET` and `POST`. To reach a `PUT`, `PATCH` or `DELETE` route from a form, post it with a
705
+ hidden `_method` field, which `methodField()` writes:
706
+
707
+ ```tsx title="resources/views/posts/edit.tsx"
708
+ import { csrfField, methodField } from '@orieljs/framework'
709
+ import type { Post } from '../../../app/Models/Post'
710
+
711
+ export default function Edit({ post }: { post: Post }) {
712
+ return (
713
+ <form method="post" action={`/posts/${post.id}`}>
714
+ {methodField('PUT')}
715
+ {csrfField()}
716
+ <button type="submit">Update</button>
717
+ </form>
718
+ )
719
+ }
720
+ ```
721
+
722
+ The `X-HTTP-Method-Override` header does the same for a script. Only a `POST` can be spoofed, and never as `GET` or
723
+ `HEAD`.
724
+
725
+ ## Accessing the current route
726
+
727
+ The `Route` [facade](facades.md) tells you which route is handling the current request, and the request gives you its
728
+ parameters:
729
+
730
+ ```ts
731
+ const current = Route.current() // the route, or undefined outside a request
732
+ const name = Route.currentRouteName() // 'posts.show'
733
+ const post = request().route('post') // the bound Post
734
+ const parameters = request().route() // every parameter, by name
735
+ ```
736
+
737
+ `Route.has('login')` tells you whether a route of that name exists, as a new app's welcome page does to show its login
738
+ link.