@minisylar/express-typed-router 1.9.4 → 1.9.6

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
@@ -173,6 +173,44 @@ router.get("/admin", { middleware: [requireAdmin] }, (req, res) => {
173
173
 
174
174
  > **Note:** `useMiddleware` returns a new router instance. Use method chaining or capture the return value — see [Common Patterns](#common-patterns).
175
175
 
176
+ ### Reusable route handlers
177
+
178
+ Use `InferSchemaHandler` when the same typed handler is registered for multiple
179
+ routes. Pass the same route options as the route; middleware can be supplied as
180
+ one middleware type or as a tuple.
181
+
182
+ ```ts
183
+ import {
184
+ InferSchemaHandler,
185
+ type TypedMiddleware,
186
+ } from "@minisylar/express-typed-router";
187
+
188
+ const auth: TypedMiddleware<{ userId: string }> = (req, _res, next) => {
189
+ req.userId = "user-123";
190
+ next();
191
+ };
192
+
193
+ type WebhookHandler = InferSchemaHandler<{
194
+ bodySchema: typeof WebhookSchema;
195
+ middleware: typeof auth;
196
+ }>;
197
+
198
+ const consolidatedHandler: WebhookHandler = (req, res) => {
199
+ res.json({ receivedBy: req.userId, event: req.body });
200
+ };
201
+
202
+ router.post(
203
+ "/webhooks/events/*path",
204
+ { bodySchema: WebhookSchema, middleware: [auth] },
205
+ consolidatedHandler,
206
+ );
207
+ router.post(
208
+ "/hooks/event",
209
+ { bodySchema: WebhookSchema, middleware: [auth] },
210
+ consolidatedHandler,
211
+ );
212
+ ```
213
+
176
214
  ---
177
215
 
178
216
  ## ✨ OpenAPI and docs
@@ -227,6 +265,21 @@ app.use("/docs", api.docs({ title: "My API", version: "1.0.0" }));
227
265
  // Discovers all sub-routers and merges routes with correct prefixes
228
266
  ```
229
267
 
268
+ **Regex route docs** — simple top-level alternatives are expanded into
269
+ separate OpenAPI paths automatically:
270
+
271
+ ```ts
272
+ router.post(
273
+ /(\/webhooks\/events.*)|(\/hooks\/event\/?$)/,
274
+ { bodySchema: WebhookSchema, middleware: [auth] },
275
+ consolidatedHandler,
276
+ );
277
+ ```
278
+
279
+ This produces `/webhooks/events/{path}` and `/hooks/event` in the spec.
280
+ For complex regular expressions, provide `pathExample` as a documentation
281
+ stand-in; runtime matching is unaffected.
282
+
230
283
  ### Response schemas from live traffic
231
284
 
232
285
  You don't have to declare what your routes return. The library **observes real responses** (`res.json` / `res.send`), **infers a JSON Schema** from them, and **merges across samples** — so it learns field types, which fields are nullable, and which are optional. This drives both the docs UI and `openapi-typescript` (real response types instead of `unknown`).
@@ -249,21 +302,27 @@ By default this runs in **redacted** mode: only the shape is kept, never the val
249
302
 
250
303
  Control it with `sampleResponses`:
251
304
 
252
- | Value | Behavior |
253
- |---|---|
254
- | `true` _(default)_ | **Redacted** — infer schema only. Real values discarded at capture time. Safe to expose. |
255
- | `"live"` | Infer schema **and** attach one real captured response as an example. ⚠️ Examples contain actual data — use only for trusted/internal docs. |
256
- | `false` | Don't observe responses at all. |
305
+ | Value | Behavior |
306
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
307
+ | `true` _(default)_ | **Redacted** — infer schema only. Real values discarded at capture time. Safe to expose. |
308
+ | `"live"` | Infer schema **and** attach one real captured response as an example. ⚠️ Examples contain actual data — use only for trusted/internal docs. |
309
+ | `false` | Don't observe responses at all. |
257
310
 
258
311
  ```ts
259
312
  // Safe default — schema only, no real data
260
313
  app.use("/docs", api.docs({ title: "My API", version: "1.0.0" }));
261
314
 
262
315
  // Show real example payloads (internal docs only)
263
- app.use("/docs", api.docs({ title: "My API", version: "1.0.0", sampleResponses: "live" }));
316
+ app.use(
317
+ "/docs",
318
+ api.docs({ title: "My API", version: "1.0.0", sampleResponses: "live" }),
319
+ );
264
320
 
265
321
  // Disable entirely
266
- app.use("/docs", api.docs({ title: "My API", version: "1.0.0", sampleResponses: false }));
322
+ app.use(
323
+ "/docs",
324
+ api.docs({ title: "My API", version: "1.0.0", sampleResponses: false }),
325
+ );
267
326
  ```
268
327
 
269
328
  > Exclude an individual sensitive route from docs with `hidden: true` in its route options — works in any mode.
@@ -352,7 +411,7 @@ Edit a route, save, and your client types update on their own.
352
411
 
353
412
  **Prefer to keep it manual?** Skip `nodemon` and `npm-run-all2` entirely — just run the server with `node --watch src/server.ts` and regenerate types on demand with `openapi-typescript ./openapi.json -o ./api.d.ts` whenever you change your API.
354
413
 
355
- > ⚠️ **Avoid a restart loop.** Write the generated `api.d.ts` **outside** the path your server watcher restarts on (or add it to the watcher's ignore list). If your server watches `*.ts` in `src/` and you output the types *into* `src/`, you get: type-gen writes `api.d.ts` → server restarts → spec rewrites → type-gen runs again → ♻️. Putting it in a separate folder (e.g. `shared/`, `generated/`) avoids this.
414
+ > ⚠️ **Avoid a restart loop.** Write the generated `api.d.ts` **outside** the path your server watcher restarts on (or add it to the watcher's ignore list). If your server watches `*.ts` in `src/` and you output the types _into_ `src/`, you get: type-gen writes `api.d.ts` → server restarts → spec rewrites → type-gen runs again → ♻️. Putting it in a separate folder (e.g. `shared/`, `generated/`) avoids this.
356
415
 
357
416
  ### Use with `openapi-fetch`
358
417
 
@@ -493,6 +552,44 @@ app.use("/docs", api.docs({ title: "My API", version: "1.0.0" }));
493
552
 
494
553
  ---
495
554
 
555
+ ## Gotchas
556
+
557
+ ### `querySchema` booleans: `req.query.flag` is text, not a real boolean
558
+
559
+ Query strings have no wire format for booleans — `?flag=false` arrives at your schema as the _string_ `"false"`, no matter what the client intended. This isn't specific to this library; it's true of every Express app, but it trips people up specifically when picking a schema for `querySchema`:
560
+
561
+ ```ts
562
+ router.get(
563
+ "/search",
564
+ { querySchema: z.object({ flag: z.boolean() }) }, // ❌ always rejects
565
+ handler,
566
+ );
567
+ ```
568
+
569
+ `z.boolean()` rejects **every** request here, since `typeof "false" !== "boolean"` — it fails before it even looks at the text.
570
+
571
+ The obvious fix has its own trap:
572
+
573
+ ```ts
574
+ {
575
+ querySchema: z.object({ flag: z.coerce.boolean() });
576
+ } // ❌ silently wrong
577
+ ```
578
+
579
+ `z.coerce.boolean()` does plain JS `Boolean(value)` — and `Boolean("false")` is `true`, because any non-empty string is truthy in JavaScript. `?flag=false` stops erroring and instead silently becomes `true`.
580
+
581
+ Use a schema that actually parses the text:
582
+
583
+ ```ts
584
+ {
585
+ querySchema: z.object({ flag: z.stringbool() });
586
+ } // ✅ zod v4+
587
+ ```
588
+
589
+ `z.stringbool()` parses `"true"`/`"false"` (and a few common variants) into the correct boolean. This isn't an express-typed-router limitation to work around — the router runs whatever schema you give it exactly as written; the fix is picking the right schema primitive, not something the router could safely guess on your behalf.
590
+
591
+ ---
592
+
496
593
  ## API surface
497
594
 
498
595
  | | |
@@ -1,12 +1,12 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,s)=>(s=n==null?{}:e(i(n)),o(r||!n||!n.__esModule||!a.call(n,`default`)?t(s,`default`,{value:n,enumerable:!0}):s,n));let c=require("express");c=s(c,1);let l=require("@standard-schema/utils");function u(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`){let e=n[`~standard`].validate(t);if(e instanceof Promise)throw TypeError(`Async schema validation is not supported by parseSchema`);if(e.issues)throw new l.SchemaError(e.issues);return e.value}throw TypeError(`Unsupported schema shape for parseSchema`)}function d(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`)return n[`~standard`].validate(t);if(n&&typeof n.safeParse==`function`)return n.safeParse(t);if(n&&typeof n.parse==`function`)try{return{value:n.parse(t)}}catch(e){return{issues:[{message:e?.message??String(e)}]}}if(n&&typeof n.validate==`function`){let e=n.validate(t);return e&&e.then&&typeof e.then==`function`?e.then(e=>e.error?{issues:[{message:e.error.message}]}:e.issues?{issues:e.issues}:{value:e.value??e}):e&&e.error?{issues:[{message:e.error.message}]}:e&&e.issues?{issues:e.issues}:{value:e.value??e}}return{issues:[{message:`Unsupported schema shape`}]}}function f(e){return typeof e==`object`&&!!e&&`issues`in e&&Array.isArray(e.issues)}const p=Function(`m`,`return import(m)`);let m,h;async function g(e){try{m??=await p(`module`),h??=await p(`url`);let t=globalThis.process?.cwd?.()??``,n=m.createRequire(h.pathToFileURL(t+`/`).href).resolve(e);return p(h.pathToFileURL(n).href)}catch{return p(e)}}const _=new WeakMap;function v(e){return e.replace(/\{([^{}]*)\}/g,`$1`).replace(/:([A-Za-z0-9_]+)(?:\([^)]*\))?[?+*]?/g,`{$1}`).replace(/\/{2,}/g,`/`)}function y(e){return[...e.replace(/\{([^{}]*)\}/g,`$1`).matchAll(/:([A-Za-z0-9_]+)/g)].map(e=>e[1])}function b(e){return e.replace(/[{}]/g,``).split(`/`).filter(Boolean).find(e=>!e.startsWith(`:`)&&!e.startsWith(`*`))??`default`}function x(e,t){let n=t.replace(/[{}]/g,``).split(`/`).filter(e=>e&&!e.startsWith(`:`)&&!e.startsWith(`*`)),r=n[n.length-1]??`resource`;return`${{get:`Get`,post:`Create`,put:`Update`,patch:`Patch`,delete:`Delete`,head:`Head`,options:`Options`}[e]??e} ${r}`}async function S(e){let t=_.get(e);if(t)return t;let n=await C(e);return _.set(e,n),n}async function C(e){if(typeof e.toJsonSchema==`function`)try{return e.toJsonSchema()}catch{}let t=e[`~standard`]?.vendor;if(t===`zod`){try{let t=await g(`zod`);if(typeof t.toJSONSchema==`function`)return t.toJSONSchema(e)}catch{}try{let t=await g(`zod-to-json-schema`),n=t.zodToJsonSchema??t.default?.zodToJsonSchema;if(typeof n==`function`)return n(e)}catch{}}if(t===`valibot`)try{let t=await g(`@valibot/to-json-schema`),n=t.toJsonSchema??t.default?.toJsonSchema;if(typeof n==`function`)return n(e)}catch{}if(t===`effect`)try{let t=await g(`effect`),n=t.JSONSchema?.make??t.default?.JSONSchema?.make;if(typeof n==`function`)return n(e)}catch{}return{}}function w(e){return T(e,0,new WeakSet)}function T(e,t,n){if(e==null)return{type:`null`};if(t>=12)return{};if(e instanceof Date)return{type:`string`,format:`date-time`};if(typeof e==`bigint`)return{type:`integer`};if(typeof e==`object`&&typeof e.toJSON==`function`)return T(e.toJSON(),t,n);if(Array.isArray(e)){if(e.length===0||n.has(e))return{type:`array`,items:{}};n.add(e);let r=Math.min(e.length,20),i=T(e[0],t+1,n);for(let a=1;a<r;a++)i=O(i,T(e[a],t+1,n));return n.delete(e),{type:`array`,items:i}}switch(typeof e){case`string`:return{type:`string`};case`boolean`:return{type:`boolean`};case`number`:return Number.isFinite(e)?{type:Number.isInteger(e)?`integer`:`number`}:{type:`null`};case`object`:{if(n.has(e))return{type:`object`};n.add(e);let r={},i=[];for(let[a,o]of Object.entries(e))typeof o!=`function`&&o!==void 0&&(r[a]=T(o,t+1,n),i.push(a));n.delete(e);let a={type:`object`,properties:r};return i.length&&(a.required=i),a}default:return{}}}function E(e){return!e||e.type===void 0?new Set:new Set(Array.isArray(e.type)?e.type:[e.type])}function D(e){e.has(`integer`)&&e.has(`number`)&&e.delete(`integer`);let t=[...e];if(t.length!==0)return t.length===1?t[0]:t}function O(e,t){if(!e||Object.keys(e).length===0)return t??{};if(!t||Object.keys(t).length===0)return e??{};let n=new Set([...E(e),...E(t)]),r={},i=D(n);if(i!==void 0&&(r.type=i),n.has(`object`)&&(e.properties||t.properties)){let n=e.properties??{},i=t.properties??{},a={};for(let e of new Set([...Object.keys(n),...Object.keys(i)]))a[e]=O(n[e],i[e]);r.properties=a;let o=e.required??[],s=t.required??[],c=o.filter(e=>s.includes(e));c.length&&(r.required=c)}if(n.has(`array`)){let n=O(e.items,t.items);n&&Object.keys(n).length&&(r.items=n)}return r}function k(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`)}const A=`https://cdn.jsdelivr.net/npm/@scalar/api-reference`;function j(e,t,n){return`<!doctype html>
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,s)=>(s=n==null?{}:e(i(n)),o(r||!n||!n.__esModule||!a.call(n,`default`)?t(s,`default`,{value:n,enumerable:!0}):s,n));let c=require("express");c=s(c,1);let l=require("@standard-schema/utils");function u(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`){let e=n[`~standard`].validate(t);if(e instanceof Promise)throw TypeError(`Async schema validation is not supported by parseSchema`);if(e.issues)throw new l.SchemaError(e.issues);return e.value}throw TypeError(`Unsupported schema shape for parseSchema`)}function d(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`)return n[`~standard`].validate(t);if(n&&typeof n.safeParse==`function`)return n.safeParse(t);if(n&&typeof n.parse==`function`)try{return{value:n.parse(t)}}catch(e){return{issues:[{message:e?.message??String(e)}]}}if(n&&typeof n.validate==`function`){let e=n.validate(t);return e&&e.then&&typeof e.then==`function`?e.then(e=>e.error?{issues:[{message:e.error.message}]}:e.issues?{issues:e.issues}:{value:e.value??e}):e&&e.error?{issues:[{message:e.error.message}]}:e&&e.issues?{issues:e.issues}:{value:e.value??e}}return{issues:[{message:`Unsupported schema shape`}]}}function f(e){return typeof e==`object`&&!!e&&`issues`in e&&Array.isArray(e.issues)}const p=Function(`m`,`return import(m)`);let m,h;async function g(e){m??=await p(`module`),h??=await p(`url`);let t=[],n=globalThis.process?.argv?.[1];n&&t.push(h.pathToFileURL(n).href);let r=globalThis.process?.cwd?.()??``;r&&t.push(h.pathToFileURL(r+`/`).href);for(let n of t)try{let t=m.createRequire(n).resolve(e);return await p(h.pathToFileURL(t).href)}catch{}return p(e)}const _=new WeakMap,v=/\(\?<[^>]+>/;function y(e){return typeof e==`string`&&v.test(e)?new RegExp(e):e}function b(e){if(typeof e.path==`string`)return e.path;if(e.pathExample)return e.pathExample;let t=0;return e.path.source.replace(/\\\//g,`/`).replace(/\((?!\?)[^()]*\)/g,()=>`:${t++}`)}function x(e){let t=[],n=0,r=0,i=!1,a=!1;for(let o=0;o<e.length;o++){let s=e[o];if(a){a=!1;continue}if(s===`\\`){a=!0;continue}if(s===`[`){i=!0;continue}if(s===`]`){i=!1;continue}i||(s===`(`?r++:s===`)`?r=Math.max(0,r-1):s===`|`&&r===0&&(t.push(e.slice(n,o)),n=o+1))}return t.push(e.slice(n)),t.length>1?t:[e]}function S(e){if(!e.startsWith(`(`)||!e.endsWith(`)`))return e;let t=0,n=!1,r=!1;for(let i=0;i<e.length;i++){let a=e[i];if(r){r=!1;continue}if(a===`\\`){r=!0;continue}if(a===`[`)n=!0;else if(a===`]`)n=!1;else if(!n&&a===`(`)t++;else if(!n&&a===`)`&&(t--,t===0&&i!==e.length-1))return e}return e.slice(1,-1).replace(/^\?:/,``)}function C(e){let t=0;return S(e).replace(/^\^|\$$/g,``).replace(/\\\//g,`/`).replace(/\.\*|\.\+/g,`/:path`).replace(/\/?\?$/,``).replace(/\(\?<([A-Za-z0-9_]+)>[^()]*\)/g,`:$1`).replace(/\((?!\?)[^()]*\)/g,()=>`:${t++}`)}function w(e){if(typeof e.path==`string`||e.pathExample)return[b(e)];let t=x(e.path.source);return t.length===1?[b(e)]:t.map(C)}function T(e){return e.replace(/\{([^{}]*)\}/g,`$1`).replace(/:([A-Za-z0-9_]+)(?:\([^)]*\))?[?+*]?/g,`{$1}`).replace(/\(\?<([A-Za-z0-9_]+)>[^)]*\)/g,`{$1}`).replace(/^\^|\$$/g,``).replace(/\/{2,}/g,`/`)}function E(e){let t=e.replace(/\{([^{}]*)\}/g,`$1`);return[...t.matchAll(/:([A-Za-z0-9_]+)/g),...t.matchAll(/\(\?<([A-Za-z0-9_]+)>/g)].map(e=>e[1])}function D(e){return e.startsWith(`:`)||e.startsWith(`*`)||e.includes(`(?<`)}function O(e){return e.replace(/^\^|\$$/g,``).replace(/[{}]/g,``).split(`/`).filter(Boolean).find(e=>!D(e))??`default`}function k(e,t){let n=t.replace(/^\^|\$$/g,``).replace(/[{}]/g,``).split(`/`).filter(e=>e&&!D(e)),r=n[n.length-1]??`resource`;return`${{get:`Get`,post:`Create`,put:`Update`,patch:`Patch`,delete:`Delete`,head:`Head`,options:`Options`}[e]??e} ${r}`}async function A(e){let t=_.get(e);if(t)return t;let n=await j(e);return _.set(e,n),n}async function j(e){if(typeof e.toJsonSchema==`function`)try{return e.toJsonSchema()}catch{}let t=e[`~standard`]?.vendor;if(t===`zod`){try{let t=await g(`zod`);if(typeof t.toJSONSchema==`function`)return t.toJSONSchema(e)}catch{}try{let t=await g(`zod-to-json-schema`),n=t.zodToJsonSchema??t.default?.zodToJsonSchema;if(typeof n==`function`)return n(e)}catch{}}if(t===`valibot`)try{let t=await g(`@valibot/to-json-schema`),n=t.toJsonSchema??t.default?.toJsonSchema;if(typeof n==`function`)return n(e)}catch{}if(t===`effect`)try{let t=await g(`effect`),n=t.JSONSchema?.make??t.default?.JSONSchema?.make;if(typeof n==`function`)return n(e)}catch{}return{}}function M(e){return N(e,0,new WeakSet)}function N(e,t,n){if(e==null)return{type:`null`};if(t>=12)return{};if(e instanceof Date)return{type:`string`,format:`date-time`};if(typeof e==`bigint`)return{type:`integer`};if(typeof e==`object`&&typeof e.toJSON==`function`)return N(e.toJSON(),t,n);if(Array.isArray(e)){if(e.length===0||n.has(e))return{type:`array`,items:{}};n.add(e);let r=Math.min(e.length,20),i=N(e[0],t+1,n);for(let a=1;a<r;a++)i=I(i,N(e[a],t+1,n));return n.delete(e),{type:`array`,items:i}}switch(typeof e){case`string`:return{type:`string`};case`boolean`:return{type:`boolean`};case`number`:return Number.isFinite(e)?{type:Number.isInteger(e)?`integer`:`number`}:{type:`null`};case`object`:{if(n.has(e))return{type:`object`};n.add(e);let r={},i=[];for(let[a,o]of Object.entries(e))typeof o!=`function`&&o!==void 0&&(r[a]=N(o,t+1,n),i.push(a));n.delete(e);let a={type:`object`,properties:r};return i.length&&(a.required=i),a}default:return{}}}function P(e){return!e||e.type===void 0?new Set:new Set(Array.isArray(e.type)?e.type:[e.type])}function F(e){e.has(`integer`)&&e.has(`number`)&&e.delete(`integer`);let t=[...e];if(t.length!==0)return t.length===1?t[0]:t}function I(e,t){if(!e||Object.keys(e).length===0)return t??{};if(!t||Object.keys(t).length===0)return e??{};let n=new Set([...P(e),...P(t)]),r={},i=F(n);if(i!==void 0&&(r.type=i),n.has(`object`)&&(e.properties||t.properties)){let n=e.properties??{},i=t.properties??{},a={};for(let e of new Set([...Object.keys(n),...Object.keys(i)]))a[e]=I(n[e],i[e]);r.properties=a;let o=e.required??[],s=t.required??[],c=o.filter(e=>s.includes(e));c.length&&(r.required=c)}if(n.has(`array`)){let n=I(e.items,t.items);n&&Object.keys(n).length&&(r.items=n)}return r}function L(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`)}const R=`https://cdn.jsdelivr.net/npm/@scalar/api-reference`;function z(e,t,n){return`<!doctype html>
2
2
  <html>
3
3
  <head>
4
- <title>${k(e)}</title>
4
+ <title>${L(e)}</title>
5
5
  <meta charset="utf-8" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7
7
  </head>
8
8
  <body>
9
- <script id="api-reference" data-url="${k(t)}"><\/script>
10
- <script src="${k(n)}"><\/script>
9
+ <script id="api-reference" data-url="${L(t)}"><\/script>
10
+ <script src="${L(n)}"><\/script>
11
11
  </body>
12
- </html>`}async function M(e,t){let n=Object.create(null);for(let t of e){if(t.method===`all`||t.hidden)continue;let e=v(t.path);n[e]||(n[e]={});let r=y(t.path).map(e=>({name:e,in:`path`,required:!0,schema:{type:`string`}}));if(t.querySchema){let e=await S(t.querySchema),n=e.properties??{},i=e.required??[];for(let[e,t]of Object.entries(n))r.push({name:e,in:`query`,required:i.includes(e),schema:t})}let i={summary:t.summary??x(t.method,t.path),tags:t.tags??[b(t.path)],parameters:r};t.description&&(i.description=t.description),t.deprecated&&(i.deprecated=!0),t.bodySchema&&(i.requestBody={required:!0,content:{"application/json":{schema:await S(t.bodySchema)}}});let a={};for(let[e,n]of t.responseSamples){let t={schema:n.schema};n.example!==void 0&&(t.example=n.example),a[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":t}}}if(t.responseSchema){let e=await S(t.responseSchema);if(e&&Object.keys(e).length){let n=`200`;for(let e of t.responseSamples.keys())if(e>=200&&e<300){n=String(e);break}a[n]={description:`Success`,content:{"application/json":{schema:e}}}}}Object.keys(a).length===0&&(a[200]={description:`Success`}),i.responses=a,n[e][t.method]=i}return{openapi:`3.1.0`,info:{title:t.title??`API`,version:t.version??`1.0.0`,...t.description?{description:t.description}:{}},...t.servers?{servers:t.servers}:{},paths:n}}const N=new WeakMap;var P=class e{router;routes=[];mountedRouters=[];sampleMode=`off`;scheduleSpecWrite;constructor(){this.router=c.default.Router(),N.set(this.router,this)}useMiddleware(e){return this.router.use(e),this}getRouter(){return this.router}use(t,...n){let r=typeof t==`string`,i=r?t:``,a=(r?n:[t,...n]).map(t=>{if(t instanceof e)return this.trackMounted(i,t),t.getRouter();let n=N.get(t);return n&&this.trackMounted(i,n),t});return r?this.router.use(t,...a):this.router.use(...a),this}mount(e,t){if(typeof e==`string`){let n=t;this.router.use(e,n.getRouter()),this.trackMounted(e,n)}else this.router.use(e.getRouter()),this.trackMounted(``,e);return this}getRouteMetadata(){let e=this.mountedRouters.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,path:e+t.path})));return[...this.routes,...e]}enableSampling(e=`redacted`,t,n=new Set){if(!n.has(this)){n.add(this),this.sampleMode=e,this.scheduleSpecWrite=t;for(let{router:r}of this.mountedRouters)r.enableSampling(e,t,n)}}trackMounted(e,t){this.mountedRouters.some(n=>n.router===t&&n.prefix===e)||(this.mountedRouters.push({prefix:e,router:t}),this.sampleMode!==`off`&&t.enableSampling(this.sampleMode,this.scheduleSpecWrite))}hydrateResponses(e,t=``,n=new Set){if(n.has(this))return;n.add(this);let r=e?.paths??{};for(let e of this.routes){let n=r[v(t+e.path)]?.[e.method]?.responses;if(n)for(let t of Object.keys(n)){let r=Number(t);if(Number.isNaN(r)||e.responseSamples.has(r))continue;let i=n[t]?.content?.[`application/json`];i?.schema&&e.responseSamples.set(r,i.example===void 0?{schema:i.schema}:{schema:i.schema,example:i.example})}}for(let{prefix:r,router:i}of this.mountedRouters)i.hydrateResponses(e,t+r,n)}docs(e={}){let t=c.default.Router(),n;if(e.specOutputPath){let t=e.specOutputPath,r=!1,i=!1,a=async()=>{if(r){i=!0;return}r=!0;try{let n=await M(this.getRouteMetadata(),e),r=await p(`fs/promises`),i=t.replace(/[/\\][^/\\]*$/,``);i&&i!==t&&await r.mkdir(i,{recursive:!0}).catch(()=>{});let a=globalThis.process?.pid??`0`,o=`${t}.${a}.tmp`;await r.writeFile(o,JSON.stringify(n,null,2),`utf8`),await r.rename(o,t)}catch{}finally{r=!1,i&&(i=!1,a())}},o;n=()=>{o&&clearTimeout(o),o=setTimeout(a,300),o.unref?.()},setImmediate(async()=>{try{let e=await(await p(`fs/promises`)).readFile(t,`utf8`).catch(()=>null);if(e)try{this.hydrateResponses(JSON.parse(e))}catch{}}catch{}await a()})}return e.sampleResponses!==!1&&this.enableSampling(e.sampleResponses===`live`?`live`:`redacted`,n),t.get(`/openapi.json`,async(t,n)=>{try{let t=await M(this.getRouteMetadata(),e);n.json(t)}catch(e){n.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),t.get(`/`,(t,n)=>{let r=`${t.baseUrl}/openapi.json`,i=e.cdnUrl??A;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(j(e.title??`API`,r,i))}),t}get(e,t,n){return this.registerRoute(`get`,e,t,n)}post(e,t,n){return this.registerRoute(`post`,e,t,n)}put(e,t,n){return this.registerRoute(`put`,e,t,n)}patch(e,t,n){return this.registerRoute(`patch`,e,t,n)}delete(e,t,n){return this.registerRoute(`delete`,e,t,n)}options(e,t,n){return this.registerRoute(`options`,e,t,n)}head(e,t,n){return this.registerRoute(`head`,e,t,n)}all(e,t,n){return this.registerRoute(`all`,e,t,n)}registerRoute(e,t,n,r){let i=[],a={method:e,path:t,responseSamples:new Map};if(this.routes.push(a),typeof n==`object`){let e=n;a.bodySchema=e.bodySchema,a.querySchema=e.querySchema,a.tags=e.tags,a.description=e.description,a.summary=e.summary,a.deprecated=e.deprecated,a.responseSchema=e.responseSchema,a.hidden=e.hidden,e.middleware&&i.push(...e.middleware),e.bodySchema&&i.push(this.createBodyValidationMiddleware(e.bodySchema)),e.querySchema&&i.push(this.createQueryValidationMiddleware(e.querySchema)),i.push(r)}else i.push(n);let o=0,s=(e,t)=>{let n=e.statusCode,r=w(t),i=a.responseSamples.get(n),o=i?O(i.schema,r):r,s=this.sampleMode===`live`?i?.example??t:void 0,c=!i||JSON.stringify(i.schema)!==JSON.stringify(o);a.responseSamples.set(n,{schema:o,example:s}),c&&this.scheduleSpecWrite?.()};return this.router[e](t,(e,t,n)=>{if(this.sampleMode===`off`||a.hidden||o>=50||a.responseSamples.size>=10){n();return}o++;let r=t.json;t.json=function(e){return s(t,e),r.call(this,e)};let i=t.send;t.send=function(e){return typeof e==`object`&&e&&!Buffer.isBuffer(e)&&s(t,e),i.call(this,e)},n()},...i),this}createBodyValidationMiddleware(e){return async(t,n,r)=>{try{let i=d(e,t.body),a=i&&typeof i.then==`function`?await i:i;if(a&&`issues`in a&&a.issues){n.status(400).json({error:`Validation failed`,details:a.errors||a.issues});return}t.body=a&&`value`in a?a.value:a,r()}catch(e){f(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):r(e)}}}createQueryValidationMiddleware(e){return async(t,n,r)=>{try{let i=d(e,t.query),a=i&&typeof i.then==`function`?await i:i;if(a&&`issues`in a&&a.issues){n.status(400).json({error:`Validation failed`,details:a.errors||a.issues});return}let o=a&&`value`in a?a.value:a;Object.defineProperty(t,"query",{value:o,writable:!1,enumerable:!0,configurable:!0}),r()}catch(e){f(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):r(e)}}}};function F(){return new P}function I(e){let t=new P;return e?.errorHandler&&t.getRouter().use(e.errorHandler),t}function L(...e){let t=new P;for(let n of e)t=t.useMiddleware(n);return t}function R(e,t={}){let n=(Array.isArray(e)?e:[e]).map(e=>`prefix`in e?e:{prefix:``,router:e});if(t.sampleResponses!==!1){let e=t.sampleResponses===`live`?`live`:`redacted`;for(let{router:t}of n)t.enableSampling(e)}let r=c.default.Router();return r.get(`/openapi.json`,async(e,r)=>{try{let e=await M(n.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,path:e+t.path}))),t);r.json(e)}catch(e){r.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),r.get(`/`,(e,n)=>{let r=`${e.baseUrl}/openapi.json`,i=t.cdnUrl??A;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(j(t.title??`API`,r,i))}),r}exports.TypedRouter=P,exports.createDocs=R,exports.createTypedRouter=F,exports.createTypedRouterWithConfig=I,exports.createTypedRouterWithMiddleware=L,exports.inferJsonSchema=w,exports.isSchemaError=f,exports.parseSchema=u,exports.safeParseSchema=d;
12
+ </html>`}async function B(e,t){let n=Object.create(null);for(let t of e)if(!(t.method===`all`||t.hidden))for(let e of w(t)){let r=T(e);n[r]||(n[r]={});let i;if(t.paramsSchema){let e=await A(t.paramsSchema),n=e.properties??{},r=e.required??[];i=Object.entries(n).map(([e,t])=>({name:e,in:`path`,required:r.includes(e),schema:t}))}else i=E(e).map(e=>({name:e,in:`path`,required:!0,schema:{type:`string`}}));if(t.querySchema){let e=await A(t.querySchema),n=e.properties??{},r=e.required??[];for(let[e,t]of Object.entries(n))i.push({name:e,in:`query`,required:r.includes(e),schema:t})}let a={summary:t.summary??k(t.method,e),tags:t.tags??[O(e)],parameters:i};t.description&&(a.description=t.description),t.deprecated&&(a.deprecated=!0),t.bodySchema&&(a.requestBody={required:!0,content:{"application/json":{schema:await A(t.bodySchema)}}});let o={};for(let[e,n]of t.responseSamples){let t={schema:n.schema};n.example!==void 0&&(t.example=n.example),o[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":t}}}if(t.responseSchema){let e=await A(t.responseSchema);if(e&&Object.keys(e).length){let n=`200`;for(let e of t.responseSamples.keys())if(e>=200&&e<300){n=String(e);break}o[n]={description:`Success`,content:{"application/json":{schema:e}}}}}Object.keys(o).length===0&&(o[200]={description:`Success`}),a.responses=o,n[r][t.method]=a}return{openapi:`3.1.0`,info:{title:t.title??`API`,version:t.version??`1.0.0`,...t.description?{description:t.description}:{}},...t.servers?{servers:t.servers}:{},paths:n}}const V=new WeakMap;var H=class e{router;routes=[];mountedRouters=[];sampleMode=`off`;scheduleSpecWrite;constructor(){this.router=c.default.Router(),V.set(this.router,this)}useMiddleware(e){return this.router.use(e),this}getRouter(){return this.router}use(t,...n){let r=typeof t==`string`,i=r?t:``,a=(r?n:[t,...n]).map(t=>{if(t instanceof e)return this.trackMounted(i,t),t.getRouter();let n=V.get(t);return n&&this.trackMounted(i,n),t});return r?this.router.use(t,...a):this.router.use(...a),this}mount(e,t){if(typeof e==`string`){let n=t;this.router.use(e,n.getRouter()),this.trackMounted(e,n)}else this.router.use(e.getRouter()),this.trackMounted(``,e);return this}getRouteMetadata(){let e=this.mountedRouters.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+b(t)}})));return[...this.routes,...e]}enableSampling(e=`redacted`,t,n=new Set){if(!n.has(this)){n.add(this),this.sampleMode=e,this.scheduleSpecWrite=t;for(let{router:r}of this.mountedRouters)r.enableSampling(e,t,n)}}trackMounted(e,t){this.mountedRouters.some(n=>n.router===t&&n.prefix===e)||(this.mountedRouters.push({prefix:e,router:t}),this.sampleMode!==`off`&&t.enableSampling(this.sampleMode,this.scheduleSpecWrite))}hydrateResponses(e,t=``,n=new Set){if(n.has(this))return;n.add(this);let r=e?.paths??{};for(let e of this.routes)for(let n of w(e)){let i=r[T(t+n)]?.[e.method]?.responses;if(i)for(let t of Object.keys(i)){let n=Number(t);if(Number.isNaN(n)||e.responseSamples.has(n))continue;let r=i[t]?.content?.[`application/json`];r?.schema&&e.responseSamples.set(n,r.example===void 0?{schema:r.schema}:{schema:r.schema,example:r.example})}}for(let{prefix:r,router:i}of this.mountedRouters)i.hydrateResponses(e,t+r,n)}docs(e={}){let t=c.default.Router(),n;if(e.specOutputPath){let t=e.specOutputPath,r=!1,i=!1,a=async()=>{if(r){i=!0;return}r=!0;try{let n=await B(this.getRouteMetadata(),e),r=await p(`fs/promises`),i=t.replace(/[/\\][^/\\]*$/,``);i&&i!==t&&await r.mkdir(i,{recursive:!0}).catch(()=>{});let a=globalThis.process?.pid??`0`,o=`${t}.${a}.tmp`;await r.writeFile(o,JSON.stringify(n,null,2),`utf8`),await r.rename(o,t)}catch{}finally{r=!1,i&&(i=!1,a())}},o;n=()=>{o&&clearTimeout(o),o=setTimeout(a,300),o.unref?.()},setImmediate(async()=>{try{let e=await(await p(`fs/promises`)).readFile(t,`utf8`).catch(()=>null);if(e)try{this.hydrateResponses(JSON.parse(e))}catch{}}catch{}await a()})}return e.sampleResponses!==!1&&this.enableSampling(e.sampleResponses===`live`?`live`:`redacted`,n),t.get(`/openapi.json`,async(t,n)=>{try{let t=await B(this.getRouteMetadata(),e);n.json(t)}catch(e){n.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),t.get(`/`,(t,n)=>{let r=`${t.baseUrl}/openapi.json`,i=e.cdnUrl??R;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(z(e.title??`API`,r,i))}),t}get(e,t,n){return this.registerRoute(`get`,e,t,n)}post(e,t,n){return this.registerRoute(`post`,e,t,n)}put(e,t,n){return this.registerRoute(`put`,e,t,n)}patch(e,t,n){return this.registerRoute(`patch`,e,t,n)}delete(e,t,n){return this.registerRoute(`delete`,e,t,n)}options(e,t,n){return this.registerRoute(`options`,e,t,n)}head(e,t,n){return this.registerRoute(`head`,e,t,n)}all(e,t,n){return this.registerRoute(`all`,e,t,n)}registerRoute(e,t,n,r){let i=[],a={method:e,path:t,responseSamples:new Map};if(this.routes.push(a),typeof n==`object`){let e=n;a.bodySchema=e.bodySchema,a.querySchema=e.querySchema,a.paramsSchema=e.paramsSchema,a.tags=e.tags,a.description=e.description,a.summary=e.summary,a.deprecated=e.deprecated,a.responseSchema=e.responseSchema,a.hidden=e.hidden,a.pathExample=e.pathExample,e.middleware&&i.push(...e.middleware),e.bodySchema&&i.push(this.createBodyValidationMiddleware(e.bodySchema)),e.querySchema&&i.push(this.createQueryValidationMiddleware(e.querySchema)),e.paramsSchema&&i.push(this.createParamsValidationMiddleware(e.paramsSchema)),i.push(r)}else i.push(n);let o=0,s=(e,t)=>{let n=e.statusCode,r=M(t),i=a.responseSamples.get(n),o=i?I(i.schema,r):r,s=this.sampleMode===`live`?i?.example??t:void 0,c=!i||JSON.stringify(i.schema)!==JSON.stringify(o);a.responseSamples.set(n,{schema:o,example:s}),c&&this.scheduleSpecWrite?.()};return this.router[e](y(t),(e,t,n)=>{if(this.sampleMode===`off`||a.hidden||o>=50||a.responseSamples.size>=10){n();return}o++;let r=t.json;t.json=function(e){return s(t,e),r.call(this,e)};let i=t.send;t.send=function(e){return typeof e==`object`&&e&&!Buffer.isBuffer(e)&&s(t,e),i.call(this,e)},n()},...i),this}createBodyValidationMiddleware(e){return async(t,n,r)=>{try{let i=d(e,t.body),a=i&&typeof i.then==`function`?await i:i;if(a&&`issues`in a&&a.issues){n.status(400).json({error:`Validation failed`,details:a.errors||a.issues});return}t.body=a&&`value`in a?a.value:a,r()}catch(e){f(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):r(e)}}}createParamsValidationMiddleware(e){return async(t,n,r)=>{try{let i=d(e,t.params),a=i&&typeof i.then==`function`?await i:i;if(a&&`issues`in a&&a.issues){n.status(400).json({error:`Validation failed`,details:a.errors||a.issues});return}t.params=a&&`value`in a?a.value:a,r()}catch(e){f(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):r(e)}}}createQueryValidationMiddleware(e){return async(t,n,r)=>{try{let i=d(e,t.query),a=i&&typeof i.then==`function`?await i:i;if(a&&`issues`in a&&a.issues){n.status(400).json({error:`Validation failed`,details:a.errors||a.issues});return}let o=a&&`value`in a?a.value:a;Object.defineProperty(t,"query",{value:o,writable:!1,enumerable:!0,configurable:!0}),r()}catch(e){f(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):r(e)}}}};function U(){return new H}function W(e){let t=new H;return e?.errorHandler&&t.getRouter().use(e.errorHandler),t}function G(...e){let t=new H;for(let n of e)t=t.useMiddleware(n);return t}function K(e,t={}){let n=(Array.isArray(e)?e:[e]).map(e=>`prefix`in e?e:{prefix:``,router:e});if(t.sampleResponses!==!1){let e=t.sampleResponses===`live`?`live`:`redacted`;for(let{router:t}of n)t.enableSampling(e)}let r=c.default.Router();return r.get(`/openapi.json`,async(e,r)=>{try{let e=await B(n.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+b(t)}}))),t);r.json(e)}catch(e){r.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),r.get(`/`,(e,n)=>{let r=`${e.baseUrl}/openapi.json`,i=t.cdnUrl??R;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(z(t.title??`API`,r,i))}),r}exports.TypedRouter=H,exports.createDocs=K,exports.createTypedRouter=U,exports.createTypedRouterWithConfig=W,exports.createTypedRouterWithMiddleware=G,exports.inferJsonSchema=M,exports.isSchemaError=f,exports.parseSchema=u,exports.safeParseSchema=d;
@@ -49,8 +49,16 @@ declare function isSchemaError(error: unknown): error is {
49
49
  * - Optional parameters: /posts/:year/:month? → { year: string; month?: string }
50
50
  * - Wildcard parameters: /files/* → { "0": string }
51
51
  * - Multiple wildcards: /a/star/b/star → { "0": string; "1": string }
52
+ *
53
+ * A path containing a named regex capture group — `(?<id>...)` — is treated
54
+ * as a raw regex pattern instead: /^\/legacy\/(?<id>\d+)$/ → { id: string }.
55
+ * This syntax never appears in Express's own path syntax, so detecting it is
56
+ * unambiguous; registerRoute converts the string to a real RegExp at
57
+ * runtime so Express matches it as one.
52
58
  */
53
- type ExtractRouteParams<Path extends string> = string extends Path ? Record<string, string> : ExtractParams<Path>;
59
+ type ExtractRouteParams<Path extends string> = string extends Path ? Record<string, string> : Path extends `${infer _Before}(?<${infer _Name}>${infer _Rest}` ? ExtractRegexGroupParams<Path> : ExtractParams<Path>;
60
+ type ExtractRegexGroupParams<S extends string> = S extends `${infer _Before}(?<${infer Name}>${infer _Rest}` ? { [K in Name]: string; } & ExtractRegexGroupParams<RemoveFirstRegexGroup<S>> : {};
61
+ type RemoveFirstRegexGroup<S extends string> = S extends `${infer _Before}(?<${infer _Name}>${infer After}` ? After extends `${infer _Inner})${infer Rest}` ? Rest : "" : "";
54
62
  /**
55
63
  * Main parameter extraction logic - enhanced for Express 5 support with recursion depth limit
56
64
  */
@@ -106,24 +114,39 @@ type RequestOnlyMiddleware<TReq extends Record<string, any>> = TypedMiddleware<T
106
114
  type LocalsOnlyMiddleware<TLocals extends Record<string, any>> = TypedMiddleware<{}, TLocals>;
107
115
  type InferMiddlewareProps<T extends readonly TypedMiddleware<any, any>[]> = T extends readonly [infer First, ...infer Rest] ? First extends TypedMiddleware<infer FirstReq, any> ? Rest extends readonly TypedMiddleware<any, any>[] ? FirstReq & InferMiddlewareProps<Rest> : FirstReq : {} : {};
108
116
  type InferMiddlewareLocals<T extends readonly TypedMiddleware<any, any>[]> = T extends readonly [infer First, ...infer Rest] ? First extends TypedMiddleware<any, infer FirstLocals> ? Rest extends readonly TypedMiddleware<any, any>[] ? FirstLocals & InferMiddlewareLocals<Rest> : FirstLocals : {} : {};
109
- type SchemaRequest<Path extends string = string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}> = Omit<Request, "params" | "query" | "body"> & {
110
- params: ExtractRouteParams<Path>;
117
+ type SchemaRequest<Path extends string = string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}, ParamsSchema extends SchemaLike | undefined = undefined, ParamsOverride extends Record<string, any> | undefined = undefined> = Omit<Request, "params" | "query" | "body"> & {
118
+ params: ParamsOverride extends undefined ? ParamsSchema extends undefined ? ExtractRouteParams<Path> : InferSchemaOutput<ParamsSchema> : ParamsOverride;
111
119
  body: BodySchema extends undefined ? unknown : InferSchemaOutput<BodySchema>;
112
120
  query: QuerySchema extends undefined ? unknown : InferSchemaOutput<QuerySchema>;
113
121
  } & MiddlewareProps;
114
- type SchemaRouteHandler<Path extends string = string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}, ResponseLocals extends Record<string, any> = {}> = (req: SchemaRequest<Path, BodySchema, QuerySchema, MiddlewareProps>, res: Response<any, ResponseLocals>, next?: NextFunction) => void | undefined | Promise<void | undefined> | Response | Promise<Response> | Promise<Response | undefined>;
122
+ type SchemaRouteHandler<Path extends string = string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}, ResponseLocals extends Record<string, any> = {}, ParamsSchema extends SchemaLike | undefined = undefined, ParamsOverride extends Record<string, any> | undefined = undefined> = (req: SchemaRequest<Path, BodySchema, QuerySchema, MiddlewareProps, ParamsSchema, ParamsOverride>, res: Response<any, ResponseLocals>, next?: NextFunction) => void | undefined | Promise<void | undefined> | Response | Promise<Response> | Promise<Response | undefined>;
115
123
  /**
116
124
  * Options for defining a typed route, including schemas and middleware.
117
125
  *
118
126
  * @template BodySchema - Schema for request body validation.
119
127
  * @template QuerySchema - Schema for query parameter validation.
128
+ * @template ParamsSchema - Schema for route param validation.
120
129
  * @property bodySchema - Optional schema for validating the request body.
121
130
  * @property querySchema - Optional schema for validating the query string.
131
+ * @property paramsSchema - Optional schema for validating route params.
122
132
  * @property middleware - Optional array of TypedMiddleware for this route.
123
133
  */
124
- interface RouteOptions<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined> {
134
+ interface RouteOptions<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined> {
125
135
  bodySchema?: BodySchema;
136
+ /**
137
+ * Every value here arrives as a string, never a real boolean/number —
138
+ * `z.coerce.boolean()` treats `"false"` as truthy. Use a text-aware parser
139
+ * like `z.stringbool()` for boolean fields.
140
+ * @see https://github.com/Mini-Sylar/express-typed-router#gotchas
141
+ */
126
142
  querySchema?: QuerySchema;
143
+ /**
144
+ * Overrides the inferred `req.params` type with this schema's output —
145
+ * useful for coercing a numeric-looking param (`z.coerce.number()`) since
146
+ * Express never converts params from strings on its own. Same
147
+ * string-arrival caveat as `querySchema` applies; see its docs above.
148
+ */
149
+ paramsSchema?: ParamsSchema;
127
150
  middleware?: TypedMiddleware<any, any>[];
128
151
  tags?: string[];
129
152
  description?: string;
@@ -132,8 +155,43 @@ interface RouteOptions<BodySchema extends SchemaLike | undefined = undefined, Qu
132
155
  responseSchema?: AnyStandardSchema;
133
156
  /** Exclude this route from the generated OpenAPI spec entirely. */
134
157
  hidden?: boolean;
158
+ /**
159
+ * Only used when `path` is a `RegExp`. A readable stand-in path (e.g.
160
+ * `/legacy/:id`) for the OpenAPI doc, since one can't be derived from a
161
+ * `RegExp` value. Defaults to `regex.toString()`. Doc-only — `req.params`
162
+ * types as `Record<string, string>` for RegExp routes unless overridden
163
+ * by `paramsSchema`.
164
+ */
165
+ pathExample?: string;
135
166
  }
136
- type DocMeta = Pick<RouteOptions, "tags" | "summary" | "description" | "deprecated" | "responseSchema" | "hidden">;
167
+ /**
168
+ * The route options accepted by {@link InferSchemaHandler}.
169
+ *
170
+ * This is derived from {@link RouteOptions}, so adding a route option keeps
171
+ * this helper's accepted shape in sync automatically. Middleware may be
172
+ * written as a single middleware type for reusable handlers, or as the tuple
173
+ * passed to a route.
174
+ */
175
+ type InferSchemaHandlerOptions = Partial<Omit<RouteOptions<any, any, any>, "middleware">> & {
176
+ middleware?: TypedMiddleware<any, any> | readonly TypedMiddleware<any, any>[];
177
+ };
178
+ type NormalizeHandlerMiddleware<Middleware> = Middleware extends TypedMiddleware<any, any> ? readonly [Middleware] : Middleware extends readonly TypedMiddleware<any, any>[] ? Middleware : readonly [];
179
+ type InferHandlerOption<Options extends InferSchemaHandlerOptions, Key extends "bodySchema" | "querySchema" | "paramsSchema" | "middleware"> = Key extends keyof Options ? Options[Key] : undefined;
180
+ /**
181
+ * Infer a reusable route handler from the same options passed to a route.
182
+ *
183
+ * The path is intentionally `string` because the handler can be registered
184
+ * for multiple paths. Use `paramsSchema` when those paths share a validated
185
+ * params shape that should be reflected in the handler type.
186
+ *
187
+ * @example
188
+ * type ListenHandler = InferSchemaHandler<{
189
+ * bodySchema: typeof ListenSchema;
190
+ * middleware: typeof auth;
191
+ * }>;
192
+ */
193
+ type InferSchemaHandler<Options extends InferSchemaHandlerOptions = {}> = SchemaRouteHandler<string, InferHandlerOption<Options, "bodySchema">, InferHandlerOption<Options, "querySchema">, InferMiddlewareProps<NormalizeHandlerMiddleware<InferHandlerOption<Options, "middleware">>>, InferMiddlewareLocals<NormalizeHandlerMiddleware<InferHandlerOption<Options, "middleware">>>, InferHandlerOption<Options, "paramsSchema">, Record<string, string | string[] | undefined>>;
194
+ type DocMeta = Pick<RouteOptions, "tags" | "summary" | "description" | "deprecated" | "responseSchema" | "hidden" | "pathExample">;
137
195
  type HttpMethod = "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
138
196
  interface DocsOptions {
139
197
  title?: string;
@@ -190,9 +248,12 @@ interface DocsOptions {
190
248
  }
191
249
  interface RouteMetadata {
192
250
  method: HttpMethod;
193
- path: string;
251
+ path: string | RegExp;
252
+ /** Doc-only path override for RegExp routes — see RouteOptions.pathExample. */
253
+ pathExample?: string | undefined;
194
254
  bodySchema?: AnyStandardSchema | undefined;
195
255
  querySchema?: AnyStandardSchema | undefined;
256
+ paramsSchema?: AnyStandardSchema | undefined;
196
257
  tags?: string[] | undefined;
197
258
  description?: string | undefined;
198
259
  summary?: string | undefined;
@@ -325,52 +386,114 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
325
386
  */
326
387
  docs(options?: DocsOptions): express.Router & express.RequestHandler;
327
388
  get<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
328
- get<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
389
+ get<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
390
+ bodySchema?: BodySchema;
391
+ querySchema?: QuerySchema;
392
+ paramsSchema?: ParamsSchema;
393
+ middleware?: [...M];
394
+ }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
395
+ get(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
396
+ get<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
329
397
  bodySchema?: BodySchema;
330
398
  querySchema?: QuerySchema;
399
+ paramsSchema?: ParamsSchema;
331
400
  middleware?: [...M];
332
- }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>>): TypedRouter<Req, Locals>;
401
+ }, handler: SchemaRouteHandler<string, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
333
402
  post<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
334
- post<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
403
+ post<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
335
404
  bodySchema?: BodySchema;
336
405
  querySchema?: QuerySchema;
406
+ paramsSchema?: ParamsSchema;
337
407
  middleware?: [...M];
338
- }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>>): TypedRouter<Req, Locals>;
408
+ }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
409
+ post(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
410
+ post<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
411
+ bodySchema?: BodySchema;
412
+ querySchema?: QuerySchema;
413
+ paramsSchema?: ParamsSchema;
414
+ middleware?: [...M];
415
+ }, handler: SchemaRouteHandler<string, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
339
416
  put<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
340
- put<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
417
+ put<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
341
418
  bodySchema?: BodySchema;
342
419
  querySchema?: QuerySchema;
420
+ paramsSchema?: ParamsSchema;
343
421
  middleware?: [...M];
344
- }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>>): TypedRouter<Req, Locals>;
422
+ }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
423
+ put(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
424
+ put<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
425
+ bodySchema?: BodySchema;
426
+ querySchema?: QuerySchema;
427
+ paramsSchema?: ParamsSchema;
428
+ middleware?: [...M];
429
+ }, handler: SchemaRouteHandler<string, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
345
430
  patch<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
346
- patch<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
431
+ patch<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
432
+ bodySchema?: BodySchema;
433
+ querySchema?: QuerySchema;
434
+ paramsSchema?: ParamsSchema;
435
+ middleware?: [...M];
436
+ }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
437
+ patch(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
438
+ patch<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
347
439
  bodySchema?: BodySchema;
348
440
  querySchema?: QuerySchema;
441
+ paramsSchema?: ParamsSchema;
349
442
  middleware?: [...M];
350
- }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>>): TypedRouter<Req, Locals>;
443
+ }, handler: SchemaRouteHandler<string, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
351
444
  delete<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
352
- delete<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
445
+ delete<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
353
446
  querySchema?: QuerySchema;
447
+ paramsSchema?: ParamsSchema;
354
448
  middleware?: [...M];
355
- }, handler: SchemaRouteHandler<Path, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>>): TypedRouter<Req, Locals>;
449
+ }, handler: SchemaRouteHandler<Path, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
450
+ delete(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
451
+ delete<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
452
+ querySchema?: QuerySchema;
453
+ paramsSchema?: ParamsSchema;
454
+ middleware?: [...M];
455
+ }, handler: SchemaRouteHandler<string, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
356
456
  options<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
357
- options<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
457
+ options<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
358
458
  querySchema?: QuerySchema;
459
+ paramsSchema?: ParamsSchema;
359
460
  middleware?: [...M];
360
- }, handler: SchemaRouteHandler<Path, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>>): TypedRouter<Req, Locals>;
461
+ }, handler: SchemaRouteHandler<Path, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
462
+ options(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
463
+ options<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
464
+ querySchema?: QuerySchema;
465
+ paramsSchema?: ParamsSchema;
466
+ middleware?: [...M];
467
+ }, handler: SchemaRouteHandler<string, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
361
468
  head<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
362
- head<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
469
+ head<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
363
470
  querySchema?: QuerySchema;
471
+ paramsSchema?: ParamsSchema;
364
472
  middleware?: [...M];
365
- }, handler: SchemaRouteHandler<Path, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>>): TypedRouter<Req, Locals>;
473
+ }, handler: SchemaRouteHandler<Path, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
474
+ head(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
475
+ head<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
476
+ querySchema?: QuerySchema;
477
+ paramsSchema?: ParamsSchema;
478
+ middleware?: [...M];
479
+ }, handler: SchemaRouteHandler<string, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
366
480
  all<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
367
- all<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
481
+ all<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
482
+ bodySchema?: BodySchema;
483
+ querySchema?: QuerySchema;
484
+ paramsSchema?: ParamsSchema;
485
+ middleware?: [...M];
486
+ }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
487
+ all(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
488
+ all<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
368
489
  bodySchema?: BodySchema;
369
490
  querySchema?: QuerySchema;
491
+ paramsSchema?: ParamsSchema;
370
492
  middleware?: [...M];
371
- }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>>): TypedRouter<Req, Locals>;
493
+ }, handler: SchemaRouteHandler<string, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
372
494
  private registerRoute;
373
495
  private createBodyValidationMiddleware;
496
+ private createParamsValidationMiddleware;
374
497
  private createQueryValidationMiddleware;
375
498
  }
376
499
  /**
@@ -475,4 +598,4 @@ type RouterDocEntry = TypedRouter<any, any> | {
475
598
  */
476
599
  declare function createDocs(routers: RouterDocEntry | RouterDocEntry[], options?: DocsOptions): express.Router & express.RequestHandler;
477
600
  //#endregion
478
- export { AdditionalLocals, AdditionalReqProps, AnyStandardSchema, DocsOptions, ExtractRouteParams, HttpMethod, InferInput, InferOutput, InferSchemaOutput, LocalsOnlyMiddleware, RequestOnlyMiddleware, RouteOptions, RouterConfig, RouterDocEntry, SafeParseResult, SchemaLike, SchemaRequest, SchemaRouteHandler, TypedMiddleware, TypedRouter, createDocs, createTypedRouter, createTypedRouterWithConfig, createTypedRouterWithMiddleware, inferJsonSchema, isSchemaError, parseSchema, safeParseSchema };
601
+ export { AdditionalLocals, AdditionalReqProps, AnyStandardSchema, DocsOptions, ExtractRouteParams, HttpMethod, InferInput, InferOutput, InferSchemaHandler, InferSchemaHandlerOptions, InferSchemaOutput, LocalsOnlyMiddleware, RequestOnlyMiddleware, RouteOptions, RouterConfig, RouterDocEntry, SafeParseResult, SchemaLike, SchemaRequest, SchemaRouteHandler, TypedMiddleware, TypedRouter, createDocs, createTypedRouter, createTypedRouterWithConfig, createTypedRouterWithMiddleware, inferJsonSchema, isSchemaError, parseSchema, safeParseSchema };
@@ -49,8 +49,16 @@ declare function isSchemaError(error: unknown): error is {
49
49
  * - Optional parameters: /posts/:year/:month? → { year: string; month?: string }
50
50
  * - Wildcard parameters: /files/* → { "0": string }
51
51
  * - Multiple wildcards: /a/star/b/star → { "0": string; "1": string }
52
+ *
53
+ * A path containing a named regex capture group — `(?<id>...)` — is treated
54
+ * as a raw regex pattern instead: /^\/legacy\/(?<id>\d+)$/ → { id: string }.
55
+ * This syntax never appears in Express's own path syntax, so detecting it is
56
+ * unambiguous; registerRoute converts the string to a real RegExp at
57
+ * runtime so Express matches it as one.
52
58
  */
53
- type ExtractRouteParams<Path extends string> = string extends Path ? Record<string, string> : ExtractParams<Path>;
59
+ type ExtractRouteParams<Path extends string> = string extends Path ? Record<string, string> : Path extends `${infer _Before}(?<${infer _Name}>${infer _Rest}` ? ExtractRegexGroupParams<Path> : ExtractParams<Path>;
60
+ type ExtractRegexGroupParams<S extends string> = S extends `${infer _Before}(?<${infer Name}>${infer _Rest}` ? { [K in Name]: string; } & ExtractRegexGroupParams<RemoveFirstRegexGroup<S>> : {};
61
+ type RemoveFirstRegexGroup<S extends string> = S extends `${infer _Before}(?<${infer _Name}>${infer After}` ? After extends `${infer _Inner})${infer Rest}` ? Rest : "" : "";
54
62
  /**
55
63
  * Main parameter extraction logic - enhanced for Express 5 support with recursion depth limit
56
64
  */
@@ -106,24 +114,39 @@ type RequestOnlyMiddleware<TReq extends Record<string, any>> = TypedMiddleware<T
106
114
  type LocalsOnlyMiddleware<TLocals extends Record<string, any>> = TypedMiddleware<{}, TLocals>;
107
115
  type InferMiddlewareProps<T extends readonly TypedMiddleware<any, any>[]> = T extends readonly [infer First, ...infer Rest] ? First extends TypedMiddleware<infer FirstReq, any> ? Rest extends readonly TypedMiddleware<any, any>[] ? FirstReq & InferMiddlewareProps<Rest> : FirstReq : {} : {};
108
116
  type InferMiddlewareLocals<T extends readonly TypedMiddleware<any, any>[]> = T extends readonly [infer First, ...infer Rest] ? First extends TypedMiddleware<any, infer FirstLocals> ? Rest extends readonly TypedMiddleware<any, any>[] ? FirstLocals & InferMiddlewareLocals<Rest> : FirstLocals : {} : {};
109
- type SchemaRequest<Path extends string = string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}> = Omit<Request, "params" | "query" | "body"> & {
110
- params: ExtractRouteParams<Path>;
117
+ type SchemaRequest<Path extends string = string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}, ParamsSchema extends SchemaLike | undefined = undefined, ParamsOverride extends Record<string, any> | undefined = undefined> = Omit<Request, "params" | "query" | "body"> & {
118
+ params: ParamsOverride extends undefined ? ParamsSchema extends undefined ? ExtractRouteParams<Path> : InferSchemaOutput<ParamsSchema> : ParamsOverride;
111
119
  body: BodySchema extends undefined ? unknown : InferSchemaOutput<BodySchema>;
112
120
  query: QuerySchema extends undefined ? unknown : InferSchemaOutput<QuerySchema>;
113
121
  } & MiddlewareProps;
114
- type SchemaRouteHandler<Path extends string = string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}, ResponseLocals extends Record<string, any> = {}> = (req: SchemaRequest<Path, BodySchema, QuerySchema, MiddlewareProps>, res: Response<any, ResponseLocals>, next?: NextFunction) => void | undefined | Promise<void | undefined> | Response | Promise<Response> | Promise<Response | undefined>;
122
+ type SchemaRouteHandler<Path extends string = string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, MiddlewareProps extends Record<string, any> = {}, ResponseLocals extends Record<string, any> = {}, ParamsSchema extends SchemaLike | undefined = undefined, ParamsOverride extends Record<string, any> | undefined = undefined> = (req: SchemaRequest<Path, BodySchema, QuerySchema, MiddlewareProps, ParamsSchema, ParamsOverride>, res: Response<any, ResponseLocals>, next?: NextFunction) => void | undefined | Promise<void | undefined> | Response | Promise<Response> | Promise<Response | undefined>;
115
123
  /**
116
124
  * Options for defining a typed route, including schemas and middleware.
117
125
  *
118
126
  * @template BodySchema - Schema for request body validation.
119
127
  * @template QuerySchema - Schema for query parameter validation.
128
+ * @template ParamsSchema - Schema for route param validation.
120
129
  * @property bodySchema - Optional schema for validating the request body.
121
130
  * @property querySchema - Optional schema for validating the query string.
131
+ * @property paramsSchema - Optional schema for validating route params.
122
132
  * @property middleware - Optional array of TypedMiddleware for this route.
123
133
  */
124
- interface RouteOptions<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined> {
134
+ interface RouteOptions<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined> {
125
135
  bodySchema?: BodySchema;
136
+ /**
137
+ * Every value here arrives as a string, never a real boolean/number —
138
+ * `z.coerce.boolean()` treats `"false"` as truthy. Use a text-aware parser
139
+ * like `z.stringbool()` for boolean fields.
140
+ * @see https://github.com/Mini-Sylar/express-typed-router#gotchas
141
+ */
126
142
  querySchema?: QuerySchema;
143
+ /**
144
+ * Overrides the inferred `req.params` type with this schema's output —
145
+ * useful for coercing a numeric-looking param (`z.coerce.number()`) since
146
+ * Express never converts params from strings on its own. Same
147
+ * string-arrival caveat as `querySchema` applies; see its docs above.
148
+ */
149
+ paramsSchema?: ParamsSchema;
127
150
  middleware?: TypedMiddleware<any, any>[];
128
151
  tags?: string[];
129
152
  description?: string;
@@ -132,8 +155,43 @@ interface RouteOptions<BodySchema extends SchemaLike | undefined = undefined, Qu
132
155
  responseSchema?: AnyStandardSchema;
133
156
  /** Exclude this route from the generated OpenAPI spec entirely. */
134
157
  hidden?: boolean;
158
+ /**
159
+ * Only used when `path` is a `RegExp`. A readable stand-in path (e.g.
160
+ * `/legacy/:id`) for the OpenAPI doc, since one can't be derived from a
161
+ * `RegExp` value. Defaults to `regex.toString()`. Doc-only — `req.params`
162
+ * types as `Record<string, string>` for RegExp routes unless overridden
163
+ * by `paramsSchema`.
164
+ */
165
+ pathExample?: string;
135
166
  }
136
- type DocMeta = Pick<RouteOptions, "tags" | "summary" | "description" | "deprecated" | "responseSchema" | "hidden">;
167
+ /**
168
+ * The route options accepted by {@link InferSchemaHandler}.
169
+ *
170
+ * This is derived from {@link RouteOptions}, so adding a route option keeps
171
+ * this helper's accepted shape in sync automatically. Middleware may be
172
+ * written as a single middleware type for reusable handlers, or as the tuple
173
+ * passed to a route.
174
+ */
175
+ type InferSchemaHandlerOptions = Partial<Omit<RouteOptions<any, any, any>, "middleware">> & {
176
+ middleware?: TypedMiddleware<any, any> | readonly TypedMiddleware<any, any>[];
177
+ };
178
+ type NormalizeHandlerMiddleware<Middleware> = Middleware extends TypedMiddleware<any, any> ? readonly [Middleware] : Middleware extends readonly TypedMiddleware<any, any>[] ? Middleware : readonly [];
179
+ type InferHandlerOption<Options extends InferSchemaHandlerOptions, Key extends "bodySchema" | "querySchema" | "paramsSchema" | "middleware"> = Key extends keyof Options ? Options[Key] : undefined;
180
+ /**
181
+ * Infer a reusable route handler from the same options passed to a route.
182
+ *
183
+ * The path is intentionally `string` because the handler can be registered
184
+ * for multiple paths. Use `paramsSchema` when those paths share a validated
185
+ * params shape that should be reflected in the handler type.
186
+ *
187
+ * @example
188
+ * type ListenHandler = InferSchemaHandler<{
189
+ * bodySchema: typeof ListenSchema;
190
+ * middleware: typeof auth;
191
+ * }>;
192
+ */
193
+ type InferSchemaHandler<Options extends InferSchemaHandlerOptions = {}> = SchemaRouteHandler<string, InferHandlerOption<Options, "bodySchema">, InferHandlerOption<Options, "querySchema">, InferMiddlewareProps<NormalizeHandlerMiddleware<InferHandlerOption<Options, "middleware">>>, InferMiddlewareLocals<NormalizeHandlerMiddleware<InferHandlerOption<Options, "middleware">>>, InferHandlerOption<Options, "paramsSchema">, Record<string, string | string[] | undefined>>;
194
+ type DocMeta = Pick<RouteOptions, "tags" | "summary" | "description" | "deprecated" | "responseSchema" | "hidden" | "pathExample">;
137
195
  type HttpMethod = "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
138
196
  interface DocsOptions {
139
197
  title?: string;
@@ -190,9 +248,12 @@ interface DocsOptions {
190
248
  }
191
249
  interface RouteMetadata {
192
250
  method: HttpMethod;
193
- path: string;
251
+ path: string | RegExp;
252
+ /** Doc-only path override for RegExp routes — see RouteOptions.pathExample. */
253
+ pathExample?: string | undefined;
194
254
  bodySchema?: AnyStandardSchema | undefined;
195
255
  querySchema?: AnyStandardSchema | undefined;
256
+ paramsSchema?: AnyStandardSchema | undefined;
196
257
  tags?: string[] | undefined;
197
258
  description?: string | undefined;
198
259
  summary?: string | undefined;
@@ -325,52 +386,114 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
325
386
  */
326
387
  docs(options?: DocsOptions): express.Router & express.RequestHandler;
327
388
  get<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
328
- get<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
389
+ get<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
390
+ bodySchema?: BodySchema;
391
+ querySchema?: QuerySchema;
392
+ paramsSchema?: ParamsSchema;
393
+ middleware?: [...M];
394
+ }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
395
+ get(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
396
+ get<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
329
397
  bodySchema?: BodySchema;
330
398
  querySchema?: QuerySchema;
399
+ paramsSchema?: ParamsSchema;
331
400
  middleware?: [...M];
332
- }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>>): TypedRouter<Req, Locals>;
401
+ }, handler: SchemaRouteHandler<string, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
333
402
  post<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
334
- post<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
403
+ post<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
335
404
  bodySchema?: BodySchema;
336
405
  querySchema?: QuerySchema;
406
+ paramsSchema?: ParamsSchema;
337
407
  middleware?: [...M];
338
- }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>>): TypedRouter<Req, Locals>;
408
+ }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
409
+ post(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
410
+ post<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
411
+ bodySchema?: BodySchema;
412
+ querySchema?: QuerySchema;
413
+ paramsSchema?: ParamsSchema;
414
+ middleware?: [...M];
415
+ }, handler: SchemaRouteHandler<string, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
339
416
  put<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
340
- put<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
417
+ put<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
341
418
  bodySchema?: BodySchema;
342
419
  querySchema?: QuerySchema;
420
+ paramsSchema?: ParamsSchema;
343
421
  middleware?: [...M];
344
- }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>>): TypedRouter<Req, Locals>;
422
+ }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
423
+ put(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
424
+ put<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
425
+ bodySchema?: BodySchema;
426
+ querySchema?: QuerySchema;
427
+ paramsSchema?: ParamsSchema;
428
+ middleware?: [...M];
429
+ }, handler: SchemaRouteHandler<string, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
345
430
  patch<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
346
- patch<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
431
+ patch<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
432
+ bodySchema?: BodySchema;
433
+ querySchema?: QuerySchema;
434
+ paramsSchema?: ParamsSchema;
435
+ middleware?: [...M];
436
+ }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
437
+ patch(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
438
+ patch<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
347
439
  bodySchema?: BodySchema;
348
440
  querySchema?: QuerySchema;
441
+ paramsSchema?: ParamsSchema;
349
442
  middleware?: [...M];
350
- }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>>): TypedRouter<Req, Locals>;
443
+ }, handler: SchemaRouteHandler<string, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
351
444
  delete<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
352
- delete<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
445
+ delete<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
353
446
  querySchema?: QuerySchema;
447
+ paramsSchema?: ParamsSchema;
354
448
  middleware?: [...M];
355
- }, handler: SchemaRouteHandler<Path, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>>): TypedRouter<Req, Locals>;
449
+ }, handler: SchemaRouteHandler<Path, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
450
+ delete(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
451
+ delete<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
452
+ querySchema?: QuerySchema;
453
+ paramsSchema?: ParamsSchema;
454
+ middleware?: [...M];
455
+ }, handler: SchemaRouteHandler<string, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
356
456
  options<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
357
- options<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
457
+ options<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
358
458
  querySchema?: QuerySchema;
459
+ paramsSchema?: ParamsSchema;
359
460
  middleware?: [...M];
360
- }, handler: SchemaRouteHandler<Path, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>>): TypedRouter<Req, Locals>;
461
+ }, handler: SchemaRouteHandler<Path, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
462
+ options(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
463
+ options<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
464
+ querySchema?: QuerySchema;
465
+ paramsSchema?: ParamsSchema;
466
+ middleware?: [...M];
467
+ }, handler: SchemaRouteHandler<string, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
361
468
  head<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
362
- head<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
469
+ head<Path extends string, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
363
470
  querySchema?: QuerySchema;
471
+ paramsSchema?: ParamsSchema;
364
472
  middleware?: [...M];
365
- }, handler: SchemaRouteHandler<Path, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>>): TypedRouter<Req, Locals>;
473
+ }, handler: SchemaRouteHandler<Path, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
474
+ head(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
475
+ head<QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
476
+ querySchema?: QuerySchema;
477
+ paramsSchema?: ParamsSchema;
478
+ middleware?: [...M];
479
+ }, handler: SchemaRouteHandler<string, undefined, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
366
480
  all<Path extends string>(path: Path, handler: SchemaRouteHandler<Path, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
367
- all<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
481
+ all<Path extends string, BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: Path, options: DocMeta & {
482
+ bodySchema?: BodySchema;
483
+ querySchema?: QuerySchema;
484
+ paramsSchema?: ParamsSchema;
485
+ middleware?: [...M];
486
+ }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
487
+ all(path: RegExp, handler: SchemaRouteHandler<string, undefined, undefined, Req, Locals>): TypedRouter<Req, Locals>;
488
+ all<BodySchema extends SchemaLike | undefined = undefined, QuerySchema extends SchemaLike | undefined = undefined, ParamsSchema extends SchemaLike | undefined = undefined, M extends TypedMiddleware<any, any>[] = []>(path: RegExp, options: DocMeta & {
368
489
  bodySchema?: BodySchema;
369
490
  querySchema?: QuerySchema;
491
+ paramsSchema?: ParamsSchema;
370
492
  middleware?: [...M];
371
- }, handler: SchemaRouteHandler<Path, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>>): TypedRouter<Req, Locals>;
493
+ }, handler: SchemaRouteHandler<string, BodySchema, QuerySchema, Req & InferMiddlewareProps<readonly [...M]>, Locals & InferMiddlewareLocals<readonly [...M]>, ParamsSchema>): TypedRouter<Req, Locals>;
372
494
  private registerRoute;
373
495
  private createBodyValidationMiddleware;
496
+ private createParamsValidationMiddleware;
374
497
  private createQueryValidationMiddleware;
375
498
  }
376
499
  /**
@@ -475,4 +598,4 @@ type RouterDocEntry = TypedRouter<any, any> | {
475
598
  */
476
599
  declare function createDocs(routers: RouterDocEntry | RouterDocEntry[], options?: DocsOptions): express.Router & express.RequestHandler;
477
600
  //#endregion
478
- export { AdditionalLocals, AdditionalReqProps, AnyStandardSchema, DocsOptions, ExtractRouteParams, HttpMethod, InferInput, InferOutput, InferSchemaOutput, LocalsOnlyMiddleware, RequestOnlyMiddleware, RouteOptions, RouterConfig, RouterDocEntry, SafeParseResult, SchemaLike, SchemaRequest, SchemaRouteHandler, TypedMiddleware, TypedRouter, createDocs, createTypedRouter, createTypedRouterWithConfig, createTypedRouterWithMiddleware, inferJsonSchema, isSchemaError, parseSchema, safeParseSchema };
601
+ export { AdditionalLocals, AdditionalReqProps, AnyStandardSchema, DocsOptions, ExtractRouteParams, HttpMethod, InferInput, InferOutput, InferSchemaHandler, InferSchemaHandlerOptions, InferSchemaOutput, LocalsOnlyMiddleware, RequestOnlyMiddleware, RouteOptions, RouterConfig, RouterDocEntry, SafeParseResult, SchemaLike, SchemaRequest, SchemaRouteHandler, TypedMiddleware, TypedRouter, createDocs, createTypedRouter, createTypedRouterWithConfig, createTypedRouterWithMiddleware, inferJsonSchema, isSchemaError, parseSchema, safeParseSchema };
@@ -1,12 +1,12 @@
1
- import e from"express";import{SchemaError as t}from"@standard-schema/utils";function n(e,n){let r=e;if(r&&r[`~standard`]&&typeof r[`~standard`].validate==`function`){let e=r[`~standard`].validate(n);if(e instanceof Promise)throw TypeError(`Async schema validation is not supported by parseSchema`);if(e.issues)throw new t(e.issues);return e.value}throw TypeError(`Unsupported schema shape for parseSchema`)}function r(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`)return n[`~standard`].validate(t);if(n&&typeof n.safeParse==`function`)return n.safeParse(t);if(n&&typeof n.parse==`function`)try{return{value:n.parse(t)}}catch(e){return{issues:[{message:e?.message??String(e)}]}}if(n&&typeof n.validate==`function`){let e=n.validate(t);return e&&e.then&&typeof e.then==`function`?e.then(e=>e.error?{issues:[{message:e.error.message}]}:e.issues?{issues:e.issues}:{value:e.value??e}):e&&e.error?{issues:[{message:e.error.message}]}:e&&e.issues?{issues:e.issues}:{value:e.value??e}}return{issues:[{message:`Unsupported schema shape`}]}}function i(e){return typeof e==`object`&&!!e&&`issues`in e&&Array.isArray(e.issues)}const a=Function(`m`,`return import(m)`);let o,s;async function c(e){try{o??=await a(`module`),s??=await a(`url`);let t=globalThis.process?.cwd?.()??``,n=o.createRequire(s.pathToFileURL(t+`/`).href).resolve(e);return a(s.pathToFileURL(n).href)}catch{return a(e)}}const l=new WeakMap;function u(e){return e.replace(/\{([^{}]*)\}/g,`$1`).replace(/:([A-Za-z0-9_]+)(?:\([^)]*\))?[?+*]?/g,`{$1}`).replace(/\/{2,}/g,`/`)}function d(e){return[...e.replace(/\{([^{}]*)\}/g,`$1`).matchAll(/:([A-Za-z0-9_]+)/g)].map(e=>e[1])}function f(e){return e.replace(/[{}]/g,``).split(`/`).filter(Boolean).find(e=>!e.startsWith(`:`)&&!e.startsWith(`*`))??`default`}function p(e,t){let n=t.replace(/[{}]/g,``).split(`/`).filter(e=>e&&!e.startsWith(`:`)&&!e.startsWith(`*`)),r=n[n.length-1]??`resource`;return`${{get:`Get`,post:`Create`,put:`Update`,patch:`Patch`,delete:`Delete`,head:`Head`,options:`Options`}[e]??e} ${r}`}async function m(e){let t=l.get(e);if(t)return t;let n=await h(e);return l.set(e,n),n}async function h(e){if(typeof e.toJsonSchema==`function`)try{return e.toJsonSchema()}catch{}let t=e[`~standard`]?.vendor;if(t===`zod`){try{let t=await c(`zod`);if(typeof t.toJSONSchema==`function`)return t.toJSONSchema(e)}catch{}try{let t=await c(`zod-to-json-schema`),n=t.zodToJsonSchema??t.default?.zodToJsonSchema;if(typeof n==`function`)return n(e)}catch{}}if(t===`valibot`)try{let t=await c(`@valibot/to-json-schema`),n=t.toJsonSchema??t.default?.toJsonSchema;if(typeof n==`function`)return n(e)}catch{}if(t===`effect`)try{let t=await c(`effect`),n=t.JSONSchema?.make??t.default?.JSONSchema?.make;if(typeof n==`function`)return n(e)}catch{}return{}}function g(e){return _(e,0,new WeakSet)}function _(e,t,n){if(e==null)return{type:`null`};if(t>=12)return{};if(e instanceof Date)return{type:`string`,format:`date-time`};if(typeof e==`bigint`)return{type:`integer`};if(typeof e==`object`&&typeof e.toJSON==`function`)return _(e.toJSON(),t,n);if(Array.isArray(e)){if(e.length===0||n.has(e))return{type:`array`,items:{}};n.add(e);let r=Math.min(e.length,20),i=_(e[0],t+1,n);for(let a=1;a<r;a++)i=b(i,_(e[a],t+1,n));return n.delete(e),{type:`array`,items:i}}switch(typeof e){case`string`:return{type:`string`};case`boolean`:return{type:`boolean`};case`number`:return Number.isFinite(e)?{type:Number.isInteger(e)?`integer`:`number`}:{type:`null`};case`object`:{if(n.has(e))return{type:`object`};n.add(e);let r={},i=[];for(let[a,o]of Object.entries(e))typeof o!=`function`&&o!==void 0&&(r[a]=_(o,t+1,n),i.push(a));n.delete(e);let a={type:`object`,properties:r};return i.length&&(a.required=i),a}default:return{}}}function v(e){return!e||e.type===void 0?new Set:new Set(Array.isArray(e.type)?e.type:[e.type])}function y(e){e.has(`integer`)&&e.has(`number`)&&e.delete(`integer`);let t=[...e];if(t.length!==0)return t.length===1?t[0]:t}function b(e,t){if(!e||Object.keys(e).length===0)return t??{};if(!t||Object.keys(t).length===0)return e??{};let n=new Set([...v(e),...v(t)]),r={},i=y(n);if(i!==void 0&&(r.type=i),n.has(`object`)&&(e.properties||t.properties)){let n=e.properties??{},i=t.properties??{},a={};for(let e of new Set([...Object.keys(n),...Object.keys(i)]))a[e]=b(n[e],i[e]);r.properties=a;let o=e.required??[],s=t.required??[],c=o.filter(e=>s.includes(e));c.length&&(r.required=c)}if(n.has(`array`)){let n=b(e.items,t.items);n&&Object.keys(n).length&&(r.items=n)}return r}function x(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`)}const S=`https://cdn.jsdelivr.net/npm/@scalar/api-reference`;function C(e,t,n){return`<!doctype html>
1
+ import e from"express";import{SchemaError as t}from"@standard-schema/utils";function n(e,n){let r=e;if(r&&r[`~standard`]&&typeof r[`~standard`].validate==`function`){let e=r[`~standard`].validate(n);if(e instanceof Promise)throw TypeError(`Async schema validation is not supported by parseSchema`);if(e.issues)throw new t(e.issues);return e.value}throw TypeError(`Unsupported schema shape for parseSchema`)}function r(e,t){let n=e;if(n&&n[`~standard`]&&typeof n[`~standard`].validate==`function`)return n[`~standard`].validate(t);if(n&&typeof n.safeParse==`function`)return n.safeParse(t);if(n&&typeof n.parse==`function`)try{return{value:n.parse(t)}}catch(e){return{issues:[{message:e?.message??String(e)}]}}if(n&&typeof n.validate==`function`){let e=n.validate(t);return e&&e.then&&typeof e.then==`function`?e.then(e=>e.error?{issues:[{message:e.error.message}]}:e.issues?{issues:e.issues}:{value:e.value??e}):e&&e.error?{issues:[{message:e.error.message}]}:e&&e.issues?{issues:e.issues}:{value:e.value??e}}return{issues:[{message:`Unsupported schema shape`}]}}function i(e){return typeof e==`object`&&!!e&&`issues`in e&&Array.isArray(e.issues)}const a=Function(`m`,`return import(m)`);let o,s;async function c(e){o??=await a(`module`),s??=await a(`url`);let t=[],n=globalThis.process?.argv?.[1];n&&t.push(s.pathToFileURL(n).href);let r=globalThis.process?.cwd?.()??``;r&&t.push(s.pathToFileURL(r+`/`).href);for(let n of t)try{let t=o.createRequire(n).resolve(e);return await a(s.pathToFileURL(t).href)}catch{}return a(e)}const l=new WeakMap,u=/\(\?<[^>]+>/;function d(e){return typeof e==`string`&&u.test(e)?new RegExp(e):e}function f(e){if(typeof e.path==`string`)return e.path;if(e.pathExample)return e.pathExample;let t=0;return e.path.source.replace(/\\\//g,`/`).replace(/\((?!\?)[^()]*\)/g,()=>`:${t++}`)}function p(e){let t=[],n=0,r=0,i=!1,a=!1;for(let o=0;o<e.length;o++){let s=e[o];if(a){a=!1;continue}if(s===`\\`){a=!0;continue}if(s===`[`){i=!0;continue}if(s===`]`){i=!1;continue}i||(s===`(`?r++:s===`)`?r=Math.max(0,r-1):s===`|`&&r===0&&(t.push(e.slice(n,o)),n=o+1))}return t.push(e.slice(n)),t.length>1?t:[e]}function m(e){if(!e.startsWith(`(`)||!e.endsWith(`)`))return e;let t=0,n=!1,r=!1;for(let i=0;i<e.length;i++){let a=e[i];if(r){r=!1;continue}if(a===`\\`){r=!0;continue}if(a===`[`)n=!0;else if(a===`]`)n=!1;else if(!n&&a===`(`)t++;else if(!n&&a===`)`&&(t--,t===0&&i!==e.length-1))return e}return e.slice(1,-1).replace(/^\?:/,``)}function h(e){let t=0;return m(e).replace(/^\^|\$$/g,``).replace(/\\\//g,`/`).replace(/\.\*|\.\+/g,`/:path`).replace(/\/?\?$/,``).replace(/\(\?<([A-Za-z0-9_]+)>[^()]*\)/g,`:$1`).replace(/\((?!\?)[^()]*\)/g,()=>`:${t++}`)}function g(e){if(typeof e.path==`string`||e.pathExample)return[f(e)];let t=p(e.path.source);return t.length===1?[f(e)]:t.map(h)}function _(e){return e.replace(/\{([^{}]*)\}/g,`$1`).replace(/:([A-Za-z0-9_]+)(?:\([^)]*\))?[?+*]?/g,`{$1}`).replace(/\(\?<([A-Za-z0-9_]+)>[^)]*\)/g,`{$1}`).replace(/^\^|\$$/g,``).replace(/\/{2,}/g,`/`)}function v(e){let t=e.replace(/\{([^{}]*)\}/g,`$1`);return[...t.matchAll(/:([A-Za-z0-9_]+)/g),...t.matchAll(/\(\?<([A-Za-z0-9_]+)>/g)].map(e=>e[1])}function y(e){return e.startsWith(`:`)||e.startsWith(`*`)||e.includes(`(?<`)}function b(e){return e.replace(/^\^|\$$/g,``).replace(/[{}]/g,``).split(`/`).filter(Boolean).find(e=>!y(e))??`default`}function x(e,t){let n=t.replace(/^\^|\$$/g,``).replace(/[{}]/g,``).split(`/`).filter(e=>e&&!y(e)),r=n[n.length-1]??`resource`;return`${{get:`Get`,post:`Create`,put:`Update`,patch:`Patch`,delete:`Delete`,head:`Head`,options:`Options`}[e]??e} ${r}`}async function S(e){let t=l.get(e);if(t)return t;let n=await C(e);return l.set(e,n),n}async function C(e){if(typeof e.toJsonSchema==`function`)try{return e.toJsonSchema()}catch{}let t=e[`~standard`]?.vendor;if(t===`zod`){try{let t=await c(`zod`);if(typeof t.toJSONSchema==`function`)return t.toJSONSchema(e)}catch{}try{let t=await c(`zod-to-json-schema`),n=t.zodToJsonSchema??t.default?.zodToJsonSchema;if(typeof n==`function`)return n(e)}catch{}}if(t===`valibot`)try{let t=await c(`@valibot/to-json-schema`),n=t.toJsonSchema??t.default?.toJsonSchema;if(typeof n==`function`)return n(e)}catch{}if(t===`effect`)try{let t=await c(`effect`),n=t.JSONSchema?.make??t.default?.JSONSchema?.make;if(typeof n==`function`)return n(e)}catch{}return{}}function w(e){return T(e,0,new WeakSet)}function T(e,t,n){if(e==null)return{type:`null`};if(t>=12)return{};if(e instanceof Date)return{type:`string`,format:`date-time`};if(typeof e==`bigint`)return{type:`integer`};if(typeof e==`object`&&typeof e.toJSON==`function`)return T(e.toJSON(),t,n);if(Array.isArray(e)){if(e.length===0||n.has(e))return{type:`array`,items:{}};n.add(e);let r=Math.min(e.length,20),i=T(e[0],t+1,n);for(let a=1;a<r;a++)i=O(i,T(e[a],t+1,n));return n.delete(e),{type:`array`,items:i}}switch(typeof e){case`string`:return{type:`string`};case`boolean`:return{type:`boolean`};case`number`:return Number.isFinite(e)?{type:Number.isInteger(e)?`integer`:`number`}:{type:`null`};case`object`:{if(n.has(e))return{type:`object`};n.add(e);let r={},i=[];for(let[a,o]of Object.entries(e))typeof o!=`function`&&o!==void 0&&(r[a]=T(o,t+1,n),i.push(a));n.delete(e);let a={type:`object`,properties:r};return i.length&&(a.required=i),a}default:return{}}}function E(e){return!e||e.type===void 0?new Set:new Set(Array.isArray(e.type)?e.type:[e.type])}function D(e){e.has(`integer`)&&e.has(`number`)&&e.delete(`integer`);let t=[...e];if(t.length!==0)return t.length===1?t[0]:t}function O(e,t){if(!e||Object.keys(e).length===0)return t??{};if(!t||Object.keys(t).length===0)return e??{};let n=new Set([...E(e),...E(t)]),r={},i=D(n);if(i!==void 0&&(r.type=i),n.has(`object`)&&(e.properties||t.properties)){let n=e.properties??{},i=t.properties??{},a={};for(let e of new Set([...Object.keys(n),...Object.keys(i)]))a[e]=O(n[e],i[e]);r.properties=a;let o=e.required??[],s=t.required??[],c=o.filter(e=>s.includes(e));c.length&&(r.required=c)}if(n.has(`array`)){let n=O(e.items,t.items);n&&Object.keys(n).length&&(r.items=n)}return r}function k(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`)}const A=`https://cdn.jsdelivr.net/npm/@scalar/api-reference`;function j(e,t,n){return`<!doctype html>
2
2
  <html>
3
3
  <head>
4
- <title>${x(e)}</title>
4
+ <title>${k(e)}</title>
5
5
  <meta charset="utf-8" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7
7
  </head>
8
8
  <body>
9
- <script id="api-reference" data-url="${x(t)}"><\/script>
10
- <script src="${x(n)}"><\/script>
9
+ <script id="api-reference" data-url="${k(t)}"><\/script>
10
+ <script src="${k(n)}"><\/script>
11
11
  </body>
12
- </html>`}async function w(e,t){let n=Object.create(null);for(let t of e){if(t.method===`all`||t.hidden)continue;let e=u(t.path);n[e]||(n[e]={});let r=d(t.path).map(e=>({name:e,in:`path`,required:!0,schema:{type:`string`}}));if(t.querySchema){let e=await m(t.querySchema),n=e.properties??{},i=e.required??[];for(let[e,t]of Object.entries(n))r.push({name:e,in:`query`,required:i.includes(e),schema:t})}let i={summary:t.summary??p(t.method,t.path),tags:t.tags??[f(t.path)],parameters:r};t.description&&(i.description=t.description),t.deprecated&&(i.deprecated=!0),t.bodySchema&&(i.requestBody={required:!0,content:{"application/json":{schema:await m(t.bodySchema)}}});let a={};for(let[e,n]of t.responseSamples){let t={schema:n.schema};n.example!==void 0&&(t.example=n.example),a[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":t}}}if(t.responseSchema){let e=await m(t.responseSchema);if(e&&Object.keys(e).length){let n=`200`;for(let e of t.responseSamples.keys())if(e>=200&&e<300){n=String(e);break}a[n]={description:`Success`,content:{"application/json":{schema:e}}}}}Object.keys(a).length===0&&(a[200]={description:`Success`}),i.responses=a,n[e][t.method]=i}return{openapi:`3.1.0`,info:{title:t.title??`API`,version:t.version??`1.0.0`,...t.description?{description:t.description}:{}},...t.servers?{servers:t.servers}:{},paths:n}}const T=new WeakMap;var E=class t{router;routes=[];mountedRouters=[];sampleMode=`off`;scheduleSpecWrite;constructor(){this.router=e.Router(),T.set(this.router,this)}useMiddleware(e){return this.router.use(e),this}getRouter(){return this.router}use(e,...n){let r=typeof e==`string`,i=r?e:``,a=(r?n:[e,...n]).map(e=>{if(e instanceof t)return this.trackMounted(i,e),e.getRouter();let n=T.get(e);return n&&this.trackMounted(i,n),e});return r?this.router.use(e,...a):this.router.use(...a),this}mount(e,t){if(typeof e==`string`){let n=t;this.router.use(e,n.getRouter()),this.trackMounted(e,n)}else this.router.use(e.getRouter()),this.trackMounted(``,e);return this}getRouteMetadata(){let e=this.mountedRouters.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,path:e+t.path})));return[...this.routes,...e]}enableSampling(e=`redacted`,t,n=new Set){if(!n.has(this)){n.add(this),this.sampleMode=e,this.scheduleSpecWrite=t;for(let{router:r}of this.mountedRouters)r.enableSampling(e,t,n)}}trackMounted(e,t){this.mountedRouters.some(n=>n.router===t&&n.prefix===e)||(this.mountedRouters.push({prefix:e,router:t}),this.sampleMode!==`off`&&t.enableSampling(this.sampleMode,this.scheduleSpecWrite))}hydrateResponses(e,t=``,n=new Set){if(n.has(this))return;n.add(this);let r=e?.paths??{};for(let e of this.routes){let n=r[u(t+e.path)]?.[e.method]?.responses;if(n)for(let t of Object.keys(n)){let r=Number(t);if(Number.isNaN(r)||e.responseSamples.has(r))continue;let i=n[t]?.content?.[`application/json`];i?.schema&&e.responseSamples.set(r,i.example===void 0?{schema:i.schema}:{schema:i.schema,example:i.example})}}for(let{prefix:r,router:i}of this.mountedRouters)i.hydrateResponses(e,t+r,n)}docs(t={}){let n=e.Router(),r;if(t.specOutputPath){let e=t.specOutputPath,n=!1,i=!1,o=async()=>{if(n){i=!0;return}n=!0;try{let n=await w(this.getRouteMetadata(),t),r=await a(`fs/promises`),i=e.replace(/[/\\][^/\\]*$/,``);i&&i!==e&&await r.mkdir(i,{recursive:!0}).catch(()=>{});let o=globalThis.process?.pid??`0`,s=`${e}.${o}.tmp`;await r.writeFile(s,JSON.stringify(n,null,2),`utf8`),await r.rename(s,e)}catch{}finally{n=!1,i&&(i=!1,o())}},s;r=()=>{s&&clearTimeout(s),s=setTimeout(o,300),s.unref?.()},setImmediate(async()=>{try{let t=await(await a(`fs/promises`)).readFile(e,`utf8`).catch(()=>null);if(t)try{this.hydrateResponses(JSON.parse(t))}catch{}}catch{}await o()})}return t.sampleResponses!==!1&&this.enableSampling(t.sampleResponses===`live`?`live`:`redacted`,r),n.get(`/openapi.json`,async(e,n)=>{try{let e=await w(this.getRouteMetadata(),t);n.json(e)}catch(e){n.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),n.get(`/`,(e,n)=>{let r=`${e.baseUrl}/openapi.json`,i=t.cdnUrl??S;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(C(t.title??`API`,r,i))}),n}get(e,t,n){return this.registerRoute(`get`,e,t,n)}post(e,t,n){return this.registerRoute(`post`,e,t,n)}put(e,t,n){return this.registerRoute(`put`,e,t,n)}patch(e,t,n){return this.registerRoute(`patch`,e,t,n)}delete(e,t,n){return this.registerRoute(`delete`,e,t,n)}options(e,t,n){return this.registerRoute(`options`,e,t,n)}head(e,t,n){return this.registerRoute(`head`,e,t,n)}all(e,t,n){return this.registerRoute(`all`,e,t,n)}registerRoute(e,t,n,r){let i=[],a={method:e,path:t,responseSamples:new Map};if(this.routes.push(a),typeof n==`object`){let e=n;a.bodySchema=e.bodySchema,a.querySchema=e.querySchema,a.tags=e.tags,a.description=e.description,a.summary=e.summary,a.deprecated=e.deprecated,a.responseSchema=e.responseSchema,a.hidden=e.hidden,e.middleware&&i.push(...e.middleware),e.bodySchema&&i.push(this.createBodyValidationMiddleware(e.bodySchema)),e.querySchema&&i.push(this.createQueryValidationMiddleware(e.querySchema)),i.push(r)}else i.push(n);let o=0,s=(e,t)=>{let n=e.statusCode,r=g(t),i=a.responseSamples.get(n),o=i?b(i.schema,r):r,s=this.sampleMode===`live`?i?.example??t:void 0,c=!i||JSON.stringify(i.schema)!==JSON.stringify(o);a.responseSamples.set(n,{schema:o,example:s}),c&&this.scheduleSpecWrite?.()};return this.router[e](t,(e,t,n)=>{if(this.sampleMode===`off`||a.hidden||o>=50||a.responseSamples.size>=10){n();return}o++;let r=t.json;t.json=function(e){return s(t,e),r.call(this,e)};let i=t.send;t.send=function(e){return typeof e==`object`&&e&&!Buffer.isBuffer(e)&&s(t,e),i.call(this,e)},n()},...i),this}createBodyValidationMiddleware(e){return async(t,n,a)=>{try{let i=r(e,t.body),o=i&&typeof i.then==`function`?await i:i;if(o&&`issues`in o&&o.issues){n.status(400).json({error:`Validation failed`,details:o.errors||o.issues});return}t.body=o&&`value`in o?o.value:o,a()}catch(e){i(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):a(e)}}}createQueryValidationMiddleware(e){return async(t,n,a)=>{try{let i=r(e,t.query),o=i&&typeof i.then==`function`?await i:i;if(o&&`issues`in o&&o.issues){n.status(400).json({error:`Validation failed`,details:o.errors||o.issues});return}let s=o&&`value`in o?o.value:o;Object.defineProperty(t,"query",{value:s,writable:!1,enumerable:!0,configurable:!0}),a()}catch(e){i(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):a(e)}}}};function D(){return new E}function O(e){let t=new E;return e?.errorHandler&&t.getRouter().use(e.errorHandler),t}function k(...e){let t=new E;for(let n of e)t=t.useMiddleware(n);return t}function A(t,n={}){let r=(Array.isArray(t)?t:[t]).map(e=>`prefix`in e?e:{prefix:``,router:e});if(n.sampleResponses!==!1){let e=n.sampleResponses===`live`?`live`:`redacted`;for(let{router:t}of r)t.enableSampling(e)}let i=e.Router();return i.get(`/openapi.json`,async(e,t)=>{try{let e=await w(r.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,path:e+t.path}))),n);t.json(e)}catch(e){t.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),i.get(`/`,(e,t)=>{let r=`${e.baseUrl}/openapi.json`,i=n.cdnUrl??S;t.setHeader(`Content-Type`,`text/html; charset=utf-8`),t.send(C(n.title??`API`,r,i))}),i}export{E as TypedRouter,A as createDocs,D as createTypedRouter,O as createTypedRouterWithConfig,k as createTypedRouterWithMiddleware,g as inferJsonSchema,i as isSchemaError,n as parseSchema,r as safeParseSchema};
12
+ </html>`}async function M(e,t){let n=Object.create(null);for(let t of e)if(!(t.method===`all`||t.hidden))for(let e of g(t)){let r=_(e);n[r]||(n[r]={});let i;if(t.paramsSchema){let e=await S(t.paramsSchema),n=e.properties??{},r=e.required??[];i=Object.entries(n).map(([e,t])=>({name:e,in:`path`,required:r.includes(e),schema:t}))}else i=v(e).map(e=>({name:e,in:`path`,required:!0,schema:{type:`string`}}));if(t.querySchema){let e=await S(t.querySchema),n=e.properties??{},r=e.required??[];for(let[e,t]of Object.entries(n))i.push({name:e,in:`query`,required:r.includes(e),schema:t})}let a={summary:t.summary??x(t.method,e),tags:t.tags??[b(e)],parameters:i};t.description&&(a.description=t.description),t.deprecated&&(a.deprecated=!0),t.bodySchema&&(a.requestBody={required:!0,content:{"application/json":{schema:await S(t.bodySchema)}}});let o={};for(let[e,n]of t.responseSamples){let t={schema:n.schema};n.example!==void 0&&(t.example=n.example),o[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":t}}}if(t.responseSchema){let e=await S(t.responseSchema);if(e&&Object.keys(e).length){let n=`200`;for(let e of t.responseSamples.keys())if(e>=200&&e<300){n=String(e);break}o[n]={description:`Success`,content:{"application/json":{schema:e}}}}}Object.keys(o).length===0&&(o[200]={description:`Success`}),a.responses=o,n[r][t.method]=a}return{openapi:`3.1.0`,info:{title:t.title??`API`,version:t.version??`1.0.0`,...t.description?{description:t.description}:{}},...t.servers?{servers:t.servers}:{},paths:n}}const N=new WeakMap;var P=class t{router;routes=[];mountedRouters=[];sampleMode=`off`;scheduleSpecWrite;constructor(){this.router=e.Router(),N.set(this.router,this)}useMiddleware(e){return this.router.use(e),this}getRouter(){return this.router}use(e,...n){let r=typeof e==`string`,i=r?e:``,a=(r?n:[e,...n]).map(e=>{if(e instanceof t)return this.trackMounted(i,e),e.getRouter();let n=N.get(e);return n&&this.trackMounted(i,n),e});return r?this.router.use(e,...a):this.router.use(...a),this}mount(e,t){if(typeof e==`string`){let n=t;this.router.use(e,n.getRouter()),this.trackMounted(e,n)}else this.router.use(e.getRouter()),this.trackMounted(``,e);return this}getRouteMetadata(){let e=this.mountedRouters.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+f(t)}})));return[...this.routes,...e]}enableSampling(e=`redacted`,t,n=new Set){if(!n.has(this)){n.add(this),this.sampleMode=e,this.scheduleSpecWrite=t;for(let{router:r}of this.mountedRouters)r.enableSampling(e,t,n)}}trackMounted(e,t){this.mountedRouters.some(n=>n.router===t&&n.prefix===e)||(this.mountedRouters.push({prefix:e,router:t}),this.sampleMode!==`off`&&t.enableSampling(this.sampleMode,this.scheduleSpecWrite))}hydrateResponses(e,t=``,n=new Set){if(n.has(this))return;n.add(this);let r=e?.paths??{};for(let e of this.routes)for(let n of g(e)){let i=r[_(t+n)]?.[e.method]?.responses;if(i)for(let t of Object.keys(i)){let n=Number(t);if(Number.isNaN(n)||e.responseSamples.has(n))continue;let r=i[t]?.content?.[`application/json`];r?.schema&&e.responseSamples.set(n,r.example===void 0?{schema:r.schema}:{schema:r.schema,example:r.example})}}for(let{prefix:r,router:i}of this.mountedRouters)i.hydrateResponses(e,t+r,n)}docs(t={}){let n=e.Router(),r;if(t.specOutputPath){let e=t.specOutputPath,n=!1,i=!1,o=async()=>{if(n){i=!0;return}n=!0;try{let n=await M(this.getRouteMetadata(),t),r=await a(`fs/promises`),i=e.replace(/[/\\][^/\\]*$/,``);i&&i!==e&&await r.mkdir(i,{recursive:!0}).catch(()=>{});let o=globalThis.process?.pid??`0`,s=`${e}.${o}.tmp`;await r.writeFile(s,JSON.stringify(n,null,2),`utf8`),await r.rename(s,e)}catch{}finally{n=!1,i&&(i=!1,o())}},s;r=()=>{s&&clearTimeout(s),s=setTimeout(o,300),s.unref?.()},setImmediate(async()=>{try{let t=await(await a(`fs/promises`)).readFile(e,`utf8`).catch(()=>null);if(t)try{this.hydrateResponses(JSON.parse(t))}catch{}}catch{}await o()})}return t.sampleResponses!==!1&&this.enableSampling(t.sampleResponses===`live`?`live`:`redacted`,r),n.get(`/openapi.json`,async(e,n)=>{try{let e=await M(this.getRouteMetadata(),t);n.json(e)}catch(e){n.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),n.get(`/`,(e,n)=>{let r=`${e.baseUrl}/openapi.json`,i=t.cdnUrl??A;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(j(t.title??`API`,r,i))}),n}get(e,t,n){return this.registerRoute(`get`,e,t,n)}post(e,t,n){return this.registerRoute(`post`,e,t,n)}put(e,t,n){return this.registerRoute(`put`,e,t,n)}patch(e,t,n){return this.registerRoute(`patch`,e,t,n)}delete(e,t,n){return this.registerRoute(`delete`,e,t,n)}options(e,t,n){return this.registerRoute(`options`,e,t,n)}head(e,t,n){return this.registerRoute(`head`,e,t,n)}all(e,t,n){return this.registerRoute(`all`,e,t,n)}registerRoute(e,t,n,r){let i=[],a={method:e,path:t,responseSamples:new Map};if(this.routes.push(a),typeof n==`object`){let e=n;a.bodySchema=e.bodySchema,a.querySchema=e.querySchema,a.paramsSchema=e.paramsSchema,a.tags=e.tags,a.description=e.description,a.summary=e.summary,a.deprecated=e.deprecated,a.responseSchema=e.responseSchema,a.hidden=e.hidden,a.pathExample=e.pathExample,e.middleware&&i.push(...e.middleware),e.bodySchema&&i.push(this.createBodyValidationMiddleware(e.bodySchema)),e.querySchema&&i.push(this.createQueryValidationMiddleware(e.querySchema)),e.paramsSchema&&i.push(this.createParamsValidationMiddleware(e.paramsSchema)),i.push(r)}else i.push(n);let o=0,s=(e,t)=>{let n=e.statusCode,r=w(t),i=a.responseSamples.get(n),o=i?O(i.schema,r):r,s=this.sampleMode===`live`?i?.example??t:void 0,c=!i||JSON.stringify(i.schema)!==JSON.stringify(o);a.responseSamples.set(n,{schema:o,example:s}),c&&this.scheduleSpecWrite?.()};return this.router[e](d(t),(e,t,n)=>{if(this.sampleMode===`off`||a.hidden||o>=50||a.responseSamples.size>=10){n();return}o++;let r=t.json;t.json=function(e){return s(t,e),r.call(this,e)};let i=t.send;t.send=function(e){return typeof e==`object`&&e&&!Buffer.isBuffer(e)&&s(t,e),i.call(this,e)},n()},...i),this}createBodyValidationMiddleware(e){return async(t,n,a)=>{try{let i=r(e,t.body),o=i&&typeof i.then==`function`?await i:i;if(o&&`issues`in o&&o.issues){n.status(400).json({error:`Validation failed`,details:o.errors||o.issues});return}t.body=o&&`value`in o?o.value:o,a()}catch(e){i(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):a(e)}}}createParamsValidationMiddleware(e){return async(t,n,a)=>{try{let i=r(e,t.params),o=i&&typeof i.then==`function`?await i:i;if(o&&`issues`in o&&o.issues){n.status(400).json({error:`Validation failed`,details:o.errors||o.issues});return}t.params=o&&`value`in o?o.value:o,a()}catch(e){i(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):a(e)}}}createQueryValidationMiddleware(e){return async(t,n,a)=>{try{let i=r(e,t.query),o=i&&typeof i.then==`function`?await i:i;if(o&&`issues`in o&&o.issues){n.status(400).json({error:`Validation failed`,details:o.errors||o.issues});return}let s=o&&`value`in o?o.value:o;Object.defineProperty(t,"query",{value:s,writable:!1,enumerable:!0,configurable:!0}),a()}catch(e){i(e)?n.status(400).json({error:`Validation failed`,details:e.errors||e.issues}):a(e)}}}};function F(){return new P}function I(e){let t=new P;return e?.errorHandler&&t.getRouter().use(e.errorHandler),t}function L(...e){let t=new P;for(let n of e)t=t.useMiddleware(n);return t}function R(t,n={}){let r=(Array.isArray(t)?t:[t]).map(e=>`prefix`in e?e:{prefix:``,router:e});if(n.sampleResponses!==!1){let e=n.sampleResponses===`live`?`live`:`redacted`;for(let{router:t}of r)t.enableSampling(e)}let i=e.Router();return i.get(`/openapi.json`,async(e,t)=>{try{let e=await M(r.flatMap(({prefix:e,router:t})=>t.getRouteMetadata().map(t=>({...t,...typeof t.path==`string`?{path:e+t.path}:{path:t.path,pathExample:e+f(t)}}))),n);t.json(e)}catch(e){t.status(500).json({error:`Failed to generate spec`,details:String(e)})}}),i.get(`/`,(e,t)=>{let r=`${e.baseUrl}/openapi.json`,i=n.cdnUrl??A;t.setHeader(`Content-Type`,`text/html; charset=utf-8`),t.send(j(n.title??`API`,r,i))}),i}export{P as TypedRouter,R as createDocs,F as createTypedRouter,I as createTypedRouterWithConfig,L as createTypedRouterWithMiddleware,w as inferJsonSchema,i as isSchemaError,n as parseSchema,r as safeParseSchema};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minisylar/express-typed-router",
3
- "version": "1.9.4",
3
+ "version": "1.9.6",
4
4
  "description": "A strongly-typed Express router with Zod validation and automatic type inference for params, body, query, and middleware",
5
5
  "type": "module",
6
6
  "main": "dist/schema-router.cjs",