@rudderjs/router 1.0.0 → 1.1.1

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/README.md CHANGED
@@ -39,6 +39,33 @@ router.get('/invoices/:id/download', handler, [ValidateSignature()]).name('invoi
39
39
 
40
40
  ---
41
41
 
42
+ ## Parameter constraints (`where*()`)
43
+
44
+ Constrain `:param` segments to a regex with Laravel-style `where*()` shortcuts. Internally, the path is rewritten to Hono's `:param{regex}` syntax so non-matching requests 404 before they reach the handler.
45
+
46
+ ```ts
47
+ router.get('/users/:id', handler).whereNumber('id').name('users.show')
48
+ router.get('/u/:id', handler).whereUuid('id')
49
+ router.get('/posts/:status', handler).whereIn('status', ['draft', 'published'])
50
+ router.get('/n/:n', handler).where('n', /\d{3,5}/)
51
+ ```
52
+
53
+ | Method | Pattern |
54
+ |--------|---------|
55
+ | `where(param, regex)` | Custom — string or `RegExp` (uses `.source`) |
56
+ | `whereNumber(param)` | `[0-9]+` |
57
+ | `whereAlpha(param)` | `[A-Za-z]+` |
58
+ | `whereAlphaNumeric(param)` | `[A-Za-z0-9]+` |
59
+ | `whereUuid(param)` | UUID, any version |
60
+ | `whereUlid(param)` | Crockford base32 ULID (26 chars) |
61
+ | `whereIn(param, values)` | Alternation over regex-escaped literals |
62
+
63
+ Chains in any order with `.name()`; calling another `where*()` for the same param overwrites. Throws if the route path has no `:param` segment. The patterns are also exported as constants — `ROUTE_PATTERN_NUMBER`, `ROUTE_PATTERN_ALPHA`, `ROUTE_PATTERN_ALPHANUM`, `ROUTE_PATTERN_UUID`, `ROUTE_PATTERN_ULID`.
64
+
65
+ > Decorator routes (`@Get('/users/:id')`) don't return a `RouteBuilder`, so `where*()` is fluent-only in v1.
66
+
67
+ ---
68
+
42
69
  ## `route()` — URL generation
43
70
 
44
71
  Generate a URL from a named route. Route parameters are substituted; unused params are appended as a query string.
@@ -146,6 +173,176 @@ router.post('/admin', handler, [authMiddleware, adminMiddleware])
146
173
 
147
174
  ---
148
175
 
176
+ ## Route model binding
177
+
178
+ Bind a `:param` segment to any class with a static `findForRoute(value)` method (a `@rudderjs/orm` Model is the canonical fit). Resolution runs as a prepended per-route middleware before your handler, exposing the result as `req.bound!.<name>`.
179
+
180
+ ```ts
181
+ import { router } from '@rudderjs/router'
182
+ import { User } from '../app/Models/User.js'
183
+
184
+ router.bind('user', User)
185
+
186
+ router.get('/users/:user', (req) => {
187
+ const user = req.bound!['user'] // a User instance, or 404 was thrown
188
+ return user
189
+ })
190
+
191
+ // Optional binding — null instead of 404 when missing
192
+ router.bind('viewer', User, { optional: true })
193
+ ```
194
+
195
+ Returning `null` from `findForRoute` triggers `RouteModelNotFoundError` (HTTP 404). The raw string remains in `req.params[name]` regardless. Routes whose path doesn't include a bound param are unaffected.
196
+
197
+ The `RouteResolver` contract is duck-typed — `name: string` + `findForRoute(value): unknown | Promise<unknown | null>` — so the router doesn't depend on `@rudderjs/orm`.
198
+
199
+ ### Custom 404 with `.missing()`
200
+
201
+ Override the default 404 response per route. Receives the request and the binding error; return a value the route handler may return — `Response`, plain object → JSON, string → body, or `undefined` (you wrote to `res` directly).
202
+
203
+ ```ts
204
+ router.get('/users/:user', show)
205
+ .missing((_req, err) => Response.json({ error: err.message }, { status: 404 }))
206
+
207
+ router.get('/posts/:post', show)
208
+ .missing((_req, err) => ({ message: `Post ${err.value} not found` })) // → 200 JSON
209
+ ```
210
+
211
+ Optional bindings do NOT trigger `.missing()` — they quietly resolve to `null` instead.
212
+
213
+ ---
214
+
215
+ ## Route groups (`router.group()`)
216
+
217
+ Apply a `prefix`, `domain`, or shared `middleware` stack to every route registered inside the callback. Nested groups concatenate prefixes and middleware; the innermost defined `domain` wins (hosts can't compose).
218
+
219
+ ```ts
220
+ import { router } from '@rudderjs/router'
221
+
222
+ router.group({ prefix: '/admin', middleware: [adminAuth] }, () => {
223
+ router.get('/users', listUsers) // GET /admin/users (with adminAuth)
224
+ router.get('/posts', listPosts) // GET /admin/posts (with adminAuth)
225
+ })
226
+
227
+ router.group({ domain: ':tenant.example.com', prefix: '/api' }, () => {
228
+ router.get('/me', me) // GET :tenant.example.com/api/me
229
+ })
230
+
231
+ // Nested
232
+ router.group({ prefix: '/api' }, () => {
233
+ router.group({ prefix: '/v1', middleware: [throttle] }, () => {
234
+ router.get('/users', listUsers) // GET /api/v1/users (with throttle)
235
+ })
236
+ })
237
+ ```
238
+
239
+ `router.group()` is the user-facing scoping primitive. Distinct from `runWithGroup('web' | 'api', …)` — that tags routes with their middleware-group label and is called once by the framework's route loader. Both can be active at the same time.
240
+
241
+ ---
242
+
243
+ ## Subdomain routing (`.domain()`)
244
+
245
+ Restrict a route to a specific host. The template is matched against the request's `Host` header (port stripped, case-insensitive); `:param` segments capture into `req.params` alongside path params.
246
+
247
+ ```ts
248
+ router.get('/users', listUsers).domain('api.example.com')
249
+ router.get('/me', me).domain(':tenant.example.com')
250
+ // req.params.tenant === 'acme' for Host: acme.example.com
251
+
252
+ router.group({ domain: 'admin.example.com', middleware: [adminAuth] }, () => {
253
+ router.get('/dashboard', dash) // GET admin.example.com/dashboard
254
+ })
255
+ ```
256
+
257
+ Mismatched hosts return 404. Subdomain `:param` and path `:param` of the same name collide — path wins.
258
+
259
+ ---
260
+
261
+ ## Resource controllers (`router.resource()`)
262
+
263
+ Wire the seven canonical CRUD routes from a plain controller class — Laravel's `Route::resource` for RudderJS. No decorators; methods are matched by name.
264
+
265
+ ```ts
266
+ import { router } from '@rudderjs/router'
267
+
268
+ class PostController {
269
+ async index (_ctx) { /* GET /posts → list */ }
270
+ async create (_ctx) { /* GET /posts/create → form */ }
271
+ async store (_ctx) { /* POST /posts → persist */ }
272
+ async show (_ctx) { /* GET /posts/:post → one */ }
273
+ async edit (_ctx) { /* GET /posts/:post/edit → form */ }
274
+ async update (_ctx) { /* PUT|PATCH /posts/:post → update */ }
275
+ async destroy (_ctx) { /* DELETE /posts/:post → delete */ }
276
+ }
277
+
278
+ router.resource('posts', PostController)
279
+ ```
280
+
281
+ | Verb | Method | Path | Route name |
282
+ |--------|--------|------|------------|
283
+ | index | `GET` | `/posts` | `posts.index` |
284
+ | create | `GET` | `/posts/create` | `posts.create` |
285
+ | store | `POST` | `/posts` | `posts.store` |
286
+ | show | `GET` | `/posts/:post` | `posts.show` |
287
+ | edit | `GET` | `/posts/:post/edit` | `posts.edit` |
288
+ | update | `PUT`+`PATCH` | `/posts/:post` | `posts.update` |
289
+ | destroy | `DELETE` | `/posts/:post` | `posts.destroy` |
290
+
291
+ Methods the controller doesn't implement are silently skipped, so partial controllers work with no `only`/`except` boilerplate.
292
+
293
+ ### `apiResource()` — drops the HTML form pages
294
+
295
+ ```ts
296
+ router.apiResource('posts', PostController)
297
+ // no posts.create, no posts.edit
298
+ ```
299
+
300
+ ### `singleton()` — for "the one of these"
301
+
302
+ ```ts
303
+ router.singleton('profile', ProfileController)
304
+ // GET /profile posts.show (named profile.show)
305
+ // GET /profile/edit profile.edit
306
+ // PUT /profile profile.update
307
+ // PATCH /profile (alias)
308
+
309
+ router.singleton('profile', ProfileController).creatable() // adds GET /profile/create + POST /profile
310
+ router.singleton('profile', ProfileController).destroyable() // adds DELETE /profile
311
+ ```
312
+
313
+ ### Options
314
+
315
+ ```ts
316
+ router.resource('posts', PostController, {
317
+ only: ['index', 'show'],
318
+ except: ['destroy'],
319
+ parameters: { posts: 'article' }, // /posts/:article instead of /posts/:post
320
+ names: { show: 'posts.detail' },
321
+ middleware: [authMw],
322
+ })
323
+ ```
324
+
325
+ ### Per-route customisation via `.builders[]`
326
+
327
+ The registration object exposes the underlying `RouteBuilder[]` — apply `where*()`, additional `.middleware()`, or rename one route in isolation:
328
+
329
+ ```ts
330
+ const reg = router.resource('posts', PostController)
331
+ reg.builders[3].whereNumber('post') // constrain show route only
332
+ ```
333
+
334
+ Builders are in declaration order: `index`, `create`, `store`, `show`, `edit`, `update` (PUT), `update` (PATCH alias), `destroy` — minus any verb the controller skipped.
335
+
336
+ ### Scaffolder
337
+
338
+ ```bash
339
+ pnpm rudder make:controller PostController --resource # full 7-verb stub
340
+ pnpm rudder make:controller PostController --api # API-only (no create/edit)
341
+ pnpm rudder make:controller ProfileController --singleton
342
+ ```
343
+
344
+ ---
345
+
149
346
  ## Mounting onto a server adapter
150
347
 
151
348
  ```ts
@@ -169,20 +366,35 @@ router.mount(serverAdapter)
169
366
  | `all(path, handler, mw?)` | `RouteBuilder` | Register route matching any method |
170
367
  | `add(method, path, handler, mw?)` | `this` | Register route with explicit method |
171
368
  | `use(middleware)` | `this` | Register global middleware |
369
+ | `bind(name, resolver, opts?)` | `this` | Bind a `:param` to a `RouteResolver` (e.g. an ORM Model) for auto-resolution |
370
+ | `listBindings()` | `Record<string, RouteResolver>` | All registered route bindings |
371
+ | `group(opts, fn)` | `this` | Apply prefix/domain/middleware to every route registered inside `fn` |
372
+ | `resource(name, Ctrl, opts?)` | `ResourceRegistration` | Register the seven canonical RESTful routes |
373
+ | `apiResource(name, Ctrl, opts?)` | `ResourceRegistration` | Resource minus `create`/`edit` |
374
+ | `singleton(name, Ctrl, opts?)` | `SingletonRegistration` | `show`/`edit`/`update` for a single-instance resource |
172
375
  | `registerController(Class)` | `this` | Register decorator-based controller |
173
376
  | `mount(serverAdapter)` | `void` | Apply middleware + routes to adapter |
174
377
  | `list()` | `RouteDefinition[]` | All registered routes |
175
378
  | `listNamed()` | `Record<string, string>` | All named routes |
176
379
  | `getNamedRoute(name)` | `string \| undefined` | Path for a named route |
177
- | `reset()` | `this` | Clear routes, middleware, and named routes |
380
+ | `reset()` | `this` | Clear routes, middleware, named routes, and bindings |
178
381
 
179
382
  ### `RouteBuilder`
180
383
 
181
- Returned by the shorthand route methods. Allows naming the registered route.
384
+ Returned by the shorthand route methods. Allows naming the registered route and constraining `:param` segments.
182
385
 
183
386
  | Method | Description |
184
387
  |--------|-------------|
185
388
  | `.name(n)` | Assign a name to the route |
389
+ | `.where(param, regex)` | Constrain `:param` to a custom regex (string or `RegExp`) |
390
+ | `.whereNumber(param)` | Shortcut for `[0-9]+` |
391
+ | `.whereAlpha(param)` | Shortcut for `[A-Za-z]+` |
392
+ | `.whereAlphaNumeric(param)` | Shortcut for `[A-Za-z0-9]+` |
393
+ | `.whereUuid(param)` | Shortcut for any-version UUID |
394
+ | `.whereUlid(param)` | Shortcut for Crockford base32 ULID |
395
+ | `.whereIn(param, values)` | Constrain `:param` to one of the supplied literals |
396
+ | `.domain(template)` | Restrict to a host; `:param` segments capture into `req.params` |
397
+ | `.missing(fn)` | Custom 404 callback when an explicit binding fails to resolve |
186
398
 
187
399
  ### `Url`
188
400
 
@@ -34,6 +34,59 @@ route('posts.show', { slug: 'hello' }) // optional ':id?' segment omitted
34
34
 
35
35
  Throws if a required parameter is missing or the name is not registered.
36
36
 
37
+ ### Parameter constraints
38
+
39
+ ```ts
40
+ import { Route } from '@rudderjs/router'
41
+
42
+ Route.get('/users/:id', handler).whereNumber('id').name('users.show')
43
+ Route.get('/u/:id', handler).whereUuid('id')
44
+ Route.get('/posts/:status', handler).whereIn('status', ['draft', 'published'])
45
+ Route.get('/n/:n', handler).where('n', /\d{3,5}/)
46
+ ```
47
+
48
+ Available shortcuts: `whereNumber` / `whereAlpha` / `whereAlphaNumeric` / `whereUuid` / `whereUlid` / `whereIn(param, values)`. Base method `.where(param, regex)` accepts a string or `RegExp`. Throws when the path has no `:param` segment, or when `whereIn` gets an empty values array. Order-independent against `.name()`.
49
+
50
+ > Fluent-only — decorator routes (`@Get('/users/:id')`) don't return a `RouteBuilder`.
51
+
52
+ ### Route groups
53
+
54
+ ```ts
55
+ import { router } from '@rudderjs/router'
56
+
57
+ router.group({ prefix: '/admin', middleware: [adminAuth] }, () => {
58
+ router.get('/users', listUsers) // GET /admin/users (with adminAuth)
59
+ })
60
+
61
+ router.group({ domain: ':tenant.example.com', prefix: '/api' }, () => {
62
+ router.get('/me', me) // GET :tenant.example.com/api/me
63
+ })
64
+ ```
65
+
66
+ Nested groups concatenate prefixes and middleware; innermost defined `domain` wins. `router.group()` is the user-facing scoping primitive — distinct from `runWithGroup('web' | 'api', …)` (the framework's web/api middleware-group tag).
67
+
68
+ ### Subdomain routing
69
+
70
+ ```ts
71
+ router.get('/users', listUsers).domain('api.example.com')
72
+ router.get('/me', me).domain(':tenant.example.com')
73
+ // req.params.tenant === 'acme' for Host: acme.example.com
74
+ ```
75
+
76
+ Mismatched hosts return 404. Subdomain `:param` and path `:param` of the same name collide — path wins.
77
+
78
+ ### Route binding 404 customisation
79
+
80
+ ```ts
81
+ router.get('/users/:user', show)
82
+ .missing((_req, err) => Response.json({ error: err.message }, { status: 404 }))
83
+
84
+ router.get('/posts/:post', show)
85
+ .missing((_req, err) => ({ message: `Post ${err.value} not found` }))
86
+ ```
87
+
88
+ Returns: `Response`, plain object → JSON, string → body, or `undefined` (callback wrote to `res` directly). Optional bindings do NOT trigger `.missing()`.
89
+
37
90
  ### Route-level middleware
38
91
 
39
92
  ```ts
package/dist/index.d.ts CHANGED
@@ -12,11 +12,22 @@ export declare const Put: (path?: string) => MethodDecorator;
12
12
  export declare const Patch: (path?: string) => MethodDecorator;
13
13
  export declare const Delete: (path?: string) => MethodDecorator;
14
14
  export declare const Options: (path?: string) => MethodDecorator;
15
+ /** Matches one or more digits — `[0-9]+`. */
16
+ export declare const ROUTE_PATTERN_NUMBER = "[0-9]+";
17
+ /** Matches one or more ASCII letters — `[A-Za-z]+`. */
18
+ export declare const ROUTE_PATTERN_ALPHA = "[A-Za-z]+";
19
+ /** Matches one or more ASCII letters or digits — `[A-Za-z0-9]+`. */
20
+ export declare const ROUTE_PATTERN_ALPHANUM = "[A-Za-z0-9]+";
21
+ /** Matches a UUID of any version (case-insensitive). */
22
+ export declare const ROUTE_PATTERN_UUID = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
23
+ /** Matches a Crockford base32 ULID (26 chars). */
24
+ export declare const ROUTE_PATTERN_ULID = "[0-7][0-9A-HJKMNP-TV-Z]{25}";
15
25
  /**
16
- * Returned by `router.get/post/etc` — allows naming the registered route.
26
+ * Returned by `router.get/post/etc` — allows naming the registered route and
27
+ * constraining `:param` segments via Laravel-style `where*` shortcuts.
17
28
  *
18
29
  * @example
19
- * router.get('/users/:id', handler).name('users.show')
30
+ * router.get('/users/:id', handler).name('users.show').whereNumber('id')
20
31
  * route('users.show', { id: 1 }) // → '/users/1'
21
32
  */
22
33
  export declare class RouteBuilder {
@@ -25,14 +36,125 @@ export declare class RouteBuilder {
25
36
  constructor(definition: RouteDefinition, _router: Router);
26
37
  /** Assign a name to this route for use with `route()` and `Url.signedRoute()`. */
27
38
  name(n: string): this;
39
+ /**
40
+ * Constrain a `:param` segment with a custom regex. Accepts either a string
41
+ * (used verbatim) or a `RegExp` (its `.source` is taken — flags are dropped
42
+ * automatically; anchors `^` / `$` pass through but are typically redundant
43
+ * since Hono anchors per-segment).
44
+ *
45
+ * Mutates the route's path in place to `:param{pattern}` (Hono regex syntax).
46
+ * Calling `where*` again on the same param overwrites the previous pattern.
47
+ *
48
+ * Throws if the route path has no `:param` segment.
49
+ */
50
+ where(param: string, regex: string | RegExp): this;
51
+ /** Constrain `:param` to one or more digits. */
52
+ whereNumber(param: string): this;
53
+ /** Constrain `:param` to one or more ASCII letters. */
54
+ whereAlpha(param: string): this;
55
+ /** Constrain `:param` to one or more ASCII letters or digits. */
56
+ whereAlphaNumeric(param: string): this;
57
+ /** Constrain `:param` to a UUID of any version. */
58
+ whereUuid(param: string): this;
59
+ /** Constrain `:param` to a Crockford base32 ULID. */
60
+ whereUlid(param: string): this;
61
+ /**
62
+ * Constrain `:param` to one of the supplied literal values. Each value is
63
+ * regex-escaped, so `'a.b'` matches the literal string `a.b`, not "a then any
64
+ * char then b". Throws when `values` is empty.
65
+ *
66
+ * @example
67
+ * router.get('/posts/:status', handler).whereIn('status', ['draft', 'published'])
68
+ */
69
+ whereIn(param: string, values: readonly (string | number)[]): this;
70
+ /**
71
+ * Restrict this route to a specific subdomain. The template is matched against
72
+ * the request's `Host` header (port stripped, case-insensitive); `:param`
73
+ * segments capture into `req.params` alongside path params.
74
+ *
75
+ * @example
76
+ * router.get('/users', listUsers).domain('api.example.com')
77
+ * router.get('/admin', dash).domain(':tenant.example.com') // captures req.params.tenant
78
+ */
79
+ domain(template: string): this;
80
+ /**
81
+ * Custom response when an explicit route binding (`router.bind('user', User)`)
82
+ * resolves to `null`. Receives the request and the not-found error; return any
83
+ * value a handler may return (`Response`, plain object → JSON, string → body,
84
+ * or `undefined` after writing to `res` directly). Does not fire for optional
85
+ * bindings — those quietly resolve to `null` instead.
86
+ *
87
+ * @example
88
+ * router.get('/users/:user', show)
89
+ * .missing((_req, err) => Response.json({ error: err.message }, { status: 404 }))
90
+ */
91
+ missing(fn: NonNullable<RouteDefinition['missing']>): this;
92
+ }
93
+ /**
94
+ * Options accepted by `router.group(opts, fn)`. Each route registered inside
95
+ * `fn` inherits the prefix, domain, and middleware. Nested groups concatenate
96
+ * prefixes and middleware; the innermost defined `domain` wins (hosts can't
97
+ * compose). Distinct from `runWithGroup('web' | 'api', …)`, which only tags
98
+ * routes with their middleware-group label.
99
+ */
100
+ export interface RouteGroupOptions {
101
+ /** Path prefix applied to every route registered in the group. */
102
+ prefix?: string;
103
+ /** Subdomain template applied to every route registered in the group. */
104
+ domain?: string;
105
+ /** Middleware prepended to every route's chain (before per-route middleware). */
106
+ middleware?: MiddlewareHandler[];
107
+ }
108
+ /**
109
+ * Duck-typed contract for any object that resolves a string route parameter
110
+ * into a value (typically a Model instance, but the router doesn't depend on
111
+ * `@rudderjs/orm` — anything with a static `findForRoute` method works).
112
+ *
113
+ * Returning `null` signals "not found" — the router maps that to a thrown
114
+ * `RouteModelNotFoundError`, which the framework's HTTP layer renders as a 404.
115
+ */
116
+ export interface RouteResolver {
117
+ /** Owning class name — used for error messages only. */
118
+ name: string;
119
+ /** Resolve the raw param value. Return `null` for not-found. */
120
+ findForRoute(value: string): Promise<unknown | null> | unknown | null;
121
+ }
122
+ export interface RouteBindingOptions {
123
+ /**
124
+ * When `true`, an absent or unresolvable param value silently sets
125
+ * `req.bound[name] = null` instead of throwing. Useful for shared routes
126
+ * that may or may not have a logged-in subject.
127
+ */
128
+ optional?: boolean;
129
+ }
130
+ /**
131
+ * Thrown by route binding middleware when a required `{param}` cannot be
132
+ * resolved into a model instance. `@rudderjs/core` picks up the duck-typed
133
+ * `httpStatus` and renders this as an HTTP 404; apps can catch it explicitly
134
+ * to render a custom not-found page.
135
+ */
136
+ export declare class RouteModelNotFoundError extends Error {
137
+ readonly model: string;
138
+ readonly param: string;
139
+ readonly value: string;
140
+ /** Duck-typed signal to `@rudderjs/core`'s exception handler. */
141
+ readonly httpStatus = 404;
142
+ constructor(model: string, param: string, value: string);
28
143
  }
29
144
  export declare class Router {
30
145
  private routes;
31
146
  private globalMiddleware;
32
147
  private namedRoutes;
148
+ private bindings;
149
+ /**
150
+ * Active `group()` scopes, outermost first. Synchronous module-level state
151
+ * is fine — route loaders execute synchronously at module evaluation, and
152
+ * `group()` only mutates the stack inside its own callback's lifetime.
153
+ */
154
+ private _groupStack;
33
155
  /** @internal — called by RouteBuilder */
34
- _registerName(name: string, path: string): void;
35
- /** Look up a named route's path. */
156
+ _registerName(name: string, def: RouteDefinition): void;
157
+ /** Look up a named route's path. Reflects any `where*()` mutations. */
36
158
  getNamedRoute(name: string): string | undefined;
37
159
  /**
38
160
  * Check whether a named route is registered.
@@ -47,10 +169,77 @@ export declare class Router {
47
169
  has(name: string): boolean;
48
170
  /** All registered named routes. */
49
171
  listNamed(): Record<string, string>;
50
- /** Clear registered routes, middleware, and named routes. */
172
+ /** Clear registered routes, middleware, named routes, and route bindings. */
51
173
  reset(): this;
174
+ /**
175
+ * Run `fn` with a group scope active. Every route registered (via fluent
176
+ * `.get()`/`.post()`/etc. or `registerController()`) inside `fn` inherits
177
+ * the group's `prefix`, `domain`, and `middleware`. Nested calls compose:
178
+ * prefixes concatenate, middleware stacks accumulate, the innermost defined
179
+ * `domain` wins.
180
+ *
181
+ * Distinct from `runWithGroup('web' | 'api', …)` — that tags routes with
182
+ * their middleware-group label (web vs api) and is called once by the
183
+ * framework's route loader. `router.group()` is the user-facing scoping
184
+ * primitive; both can be active at the same time.
185
+ *
186
+ * @example
187
+ * router.group({ prefix: '/admin', middleware: [adminAuth] }, () => {
188
+ * router.get('/users', listUsers) // GET /admin/users (with adminAuth)
189
+ * router.get('/posts', listPosts) // GET /admin/posts (with adminAuth)
190
+ * })
191
+ *
192
+ * router.group({ domain: ':tenant.example.com', prefix: '/api' }, () => {
193
+ * router.get('/me', me) // GET :tenant.example.com/api/me
194
+ * })
195
+ */
196
+ group(opts: RouteGroupOptions, fn: () => void): this;
197
+ /**
198
+ * Compose the active group stack into the values used to register a route.
199
+ * Path prefixes concatenate (and collapse `/+` to `/`), middleware stacks
200
+ * concatenate, the innermost defined `host` wins.
201
+ */
202
+ private _applyGroupStack;
52
203
  /** Register a global middleware (runs on every route). */
53
204
  use(middleware: MiddlewareHandler): this;
205
+ /**
206
+ * Bind a route parameter name to a resolver. When a route's path contains
207
+ * `:<name>`, the matching string param is resolved before the handler runs;
208
+ * the result is exposed as `req.bound[name]`. The raw string remains in
209
+ * `req.params[name]` so existing code keeps working.
210
+ *
211
+ * Resolvers are duck-typed — pass any class with a static `findForRoute(val)`
212
+ * method (`@rudderjs/orm` Model classes match by default). Bindings are
213
+ * opt-in: routes whose path does not include the bound `:name` are unaffected.
214
+ *
215
+ * @example
216
+ * import { router } from '@rudderjs/router'
217
+ * import { User } from '../app/Models/User.js'
218
+ *
219
+ * router.bind('user', User)
220
+ * router.get('/users/:user', (req) => req.bound!['user'])
221
+ *
222
+ * // Custom column → declare on the model:
223
+ * class Post extends Model {
224
+ * static override routeKey = 'slug'
225
+ * }
226
+ * router.bind('post', Post) // resolves /posts/:post by slug
227
+ *
228
+ * // Optional binding — null when missing instead of 404:
229
+ * router.bind('viewer', User, { optional: true })
230
+ */
231
+ bind(name: string, resolver: RouteResolver, options?: RouteBindingOptions): this;
232
+ /** All registered route bindings, keyed by param name. */
233
+ listBindings(): Record<string, RouteResolver>;
234
+ /**
235
+ * Build the per-route binding middleware. Walks the route's `:param` segments,
236
+ * looks them up in the binding map, and resolves each before calling `next()`.
237
+ * No-op for routes whose path contains no bound params.
238
+ *
239
+ * Takes the full `RouteDefinition` (not just path) so the closure can capture
240
+ * `def.missing` — the per-route 404 customisation set via `RouteBuilder.missing()`.
241
+ */
242
+ private _buildBindingMiddleware;
54
243
  /** Manually register a route. Returns `this` for bulk registration. */
55
244
  add(method: HttpMethod, path: string, handler: RouteHandler, middleware?: MiddlewareHandler[]): this;
56
245
  get(path: string, handler: RouteHandler, middleware?: MiddlewareHandler[]): RouteBuilder;
@@ -66,6 +255,107 @@ export declare class Router {
66
255
  mount(server: ServerAdapter): void;
67
256
  /** All registered routes — useful for `routes:list`. */
68
257
  list(): RouteDefinition[];
258
+ /**
259
+ * Register the canonical seven CRUD routes for a plain controller class:
260
+ * `index`, `create`, `store`, `show`, `edit`, `update`, `destroy`. Methods
261
+ * the controller doesn't implement are silently skipped, so a partial
262
+ * controller works without `only`/`except` boilerplate.
263
+ *
264
+ * The `update` route is registered for both `PUT` and `PATCH`. Route names
265
+ * default to `<name>.<verb>` (`posts.show`, `posts.update`). The path param
266
+ * defaults to a naive singular (`posts` → `:post`); pass
267
+ * `{ parameters: { posts: 'article' } }` to override.
268
+ *
269
+ * Use plain method names — no decorators. Call `router.registerController()`
270
+ * for decorator-driven controllers instead.
271
+ *
272
+ * @example
273
+ * router.resource('posts', PostController)
274
+ * router.resource('posts', PostController, { only: ['index', 'show'] })
275
+ * router.resource('posts', PostController, { middleware: [authMw] })
276
+ */
277
+ resource(name: string, Ctrl: new () => object, opts?: ResourceOptions): ResourceRegistration;
278
+ /**
279
+ * Register an API-only resource — the same routes as `resource()` minus
280
+ * `create` and `edit`, since those render HTML forms and have no JSON
281
+ * equivalent.
282
+ *
283
+ * @example
284
+ * router.apiResource('posts', PostController)
285
+ */
286
+ apiResource(name: string, Ctrl: new () => object, opts?: ResourceOptions): ResourceRegistration;
287
+ /**
288
+ * Register a singleton resource — `show`, `edit`, `update` only. Use for
289
+ * "the current user's profile" / "the application's settings" style
290
+ * resources where there's only ever one of the thing.
291
+ *
292
+ * Add a creation flow with `.creatable()` (registers `create` + `store`) or
293
+ * a deletion flow with `.destroyable()` (registers `destroy`).
294
+ *
295
+ * @example
296
+ * router.singleton('profile', ProfileController) // /profile + /profile/edit
297
+ * router.singleton('profile', ProfileController).creatable() // also /profile/create + POST /profile
298
+ */
299
+ singleton(name: string, Ctrl: new () => object, opts?: ResourceOptions): SingletonRegistration;
300
+ /** @internal — shared registration loop for resource/apiResource/singleton. */
301
+ _registerResource(name: string, Ctrl: new () => object, table: readonly ResourceVerbSpec[], opts: ResourceOptions): ResourceRegistration;
302
+ }
303
+ /** The seven canonical RESTful verbs Laravel's `Route::resource` exposes. */
304
+ export type ResourceVerb = 'index' | 'create' | 'store' | 'show' | 'edit' | 'update' | 'destroy';
305
+ interface ResourceVerbSpec {
306
+ verb: ResourceVerb;
307
+ method: HttpMethod;
308
+ path: (name: string, param: string) => string;
309
+ nameSuffix: string;
310
+ }
311
+ /**
312
+ * Options accepted by `router.resource`/`apiResource`/`singleton`.
313
+ *
314
+ * - `only`/`except` — restrict the verbs registered.
315
+ * - `parameters` — override the `:param` segment name for a given resource
316
+ * (e.g. `{ posts: 'article' }` → `/posts/:article`).
317
+ * - `names` — override the generated route names per verb.
318
+ * - `middleware` — applied to every route registered by the resource.
319
+ */
320
+ export interface ResourceOptions {
321
+ only?: readonly ResourceVerb[];
322
+ except?: readonly ResourceVerb[];
323
+ parameters?: Record<string, string>;
324
+ names?: Partial<Record<ResourceVerb, string>>;
325
+ middleware?: MiddlewareHandler[];
326
+ }
327
+ /**
328
+ * Returned by `router.resource()`/`apiResource()`. The `builders` array holds
329
+ * one `RouteBuilder` per registered route in declaration order — apply
330
+ * `where*()`, additional middleware, or rename individual routes by indexing
331
+ * directly. The `update` PATCH alias is included as a separate builder
332
+ * immediately after its PUT counterpart.
333
+ */
334
+ export declare class ResourceRegistration {
335
+ readonly builders: RouteBuilder[];
336
+ constructor(builders: RouteBuilder[]);
337
+ }
338
+ /**
339
+ * Returned by `router.singleton()`. Adds two opt-in helpers on top of
340
+ * `ResourceRegistration` for resources that also expose a creation flow
341
+ * (`.creatable()`) or deletion flow (`.destroyable()`).
342
+ */
343
+ export declare class SingletonRegistration extends ResourceRegistration {
344
+ private readonly _router;
345
+ private readonly _name;
346
+ private readonly _Ctrl;
347
+ private readonly _opts;
348
+ constructor(builders: RouteBuilder[], _router: Router, _name: string, _Ctrl: new () => object, _opts: ResourceOptions);
349
+ /**
350
+ * Add `GET /<name>/create` and `POST /<name>` — the create/store half of a
351
+ * full resource. Skipped for any verb the controller doesn't implement.
352
+ */
353
+ creatable(): this;
354
+ /**
355
+ * Add `DELETE /<name>` — the destroy half of a full resource. Skipped if
356
+ * the controller doesn't implement `destroy()`.
357
+ */
358
+ destroyable(): this;
69
359
  }
70
360
  export declare const router: Router;
71
361
  /** Alias for router — Laravel-style capitalised name */
@@ -126,4 +416,5 @@ export declare class Url {
126
416
  * router.get('/invoice/:id/download', handler, [ValidateSignature()])
127
417
  */
128
418
  export declare function ValidateSignature(): MiddlewareHandler;
419
+ export {};
129
420
  //# sourceMappingURL=index.d.ts.map