@minisylar/express-typed-router 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -21
- package/dist/schema-router.cjs +1 -1
- package/dist/schema-router.d.cts +13 -0
- package/dist/schema-router.d.mts +13 -0
- package/dist/schema-router.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,10 @@ Define routes once, infer `params` / `body` / `query`, and generate a clean API
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## Documentation generated from your codebase
|
|
12
|
+
|
|
9
13
|
## What you get
|
|
10
14
|
|
|
11
15
|
- **Typed route handlers** — `req.params`, `req.body`, `req.query` inferred from your route + schema
|
|
@@ -13,7 +17,7 @@ Define routes once, infer `params` / `body` / `query`, and generate a clean API
|
|
|
13
17
|
- **✨ OpenAPI docs** — generated from routes, schemas, and captured responses
|
|
14
18
|
- **Schema-agnostic** — any Standard Schema-compatible validator (Zod, Yup, Valibot, Arktype, Joi...)
|
|
15
19
|
- **Express 4 & 5** — common patterns supported
|
|
16
|
-
- **Client-friendly output** — generate `
|
|
20
|
+
- **Client-friendly output** — generate `api.types.ts` and build any client wrapper
|
|
17
21
|
|
|
18
22
|
---
|
|
19
23
|
|
|
@@ -219,23 +223,22 @@ const api = createTypedRouter()
|
|
|
219
223
|
|
|
220
224
|
app.use("/docs", api.docs({ title: "My API", version: "1.0.0" }));
|
|
221
225
|
// Discovers all sub-routers and merges routes with correct prefixes
|
|
222
|
-
|
|
223
226
|
```
|
|
224
227
|
|
|
225
228
|
### Schema library support for docs
|
|
226
229
|
|
|
227
230
|
All validators work for **request validation**. For **OpenAPI schema generation** (showing field names and types in the spec), some libraries need an extra converter package installed in your project. This library auto-detects them at runtime — install the one you need and it just works, no config required.
|
|
228
231
|
|
|
229
|
-
| Library
|
|
230
|
-
|
|
231
|
-
| Zod 4
|
|
232
|
-
| Zod 3
|
|
233
|
-
| Valibot
|
|
234
|
-
| ArkType
|
|
235
|
-
| Effect
|
|
236
|
-
| Yup
|
|
237
|
-
| Joi
|
|
238
|
-
| Decoders / ts.data.json / unhoax | ✅
|
|
232
|
+
| Library | Validation | Docs schema | Extra install |
|
|
233
|
+
| -------------------------------- | ---------- | ----------- | ------------------------------------------------- |
|
|
234
|
+
| Zod 4 | ✅ | ✅ | none — built-in |
|
|
235
|
+
| Zod 3 | ✅ | ✅ | `zod-to-json-schema` |
|
|
236
|
+
| Valibot | ✅ | ✅ | `@valibot/to-json-schema` |
|
|
237
|
+
| ArkType | ✅ | ✅ | none — built-in |
|
|
238
|
+
| Effect | ✅ | ✅ | none — built-in |
|
|
239
|
+
| Yup | ✅ | ⚠️ | not supported — no official JSON Schema converter |
|
|
240
|
+
| Joi | ✅ | ⚠️ | not supported — no official JSON Schema converter |
|
|
241
|
+
| Decoders / ts.data.json / unhoax | ✅ | ⚠️ | not supported — no schema introspection |
|
|
239
242
|
|
|
240
243
|
> **⚠️ Partial docs** means routes still appear in the spec with paths, methods, and captured response examples — only the request body/query field shapes are missing.
|
|
241
244
|
|
|
@@ -243,27 +246,63 @@ All validators work for **request validation**. For **OpenAPI schema generation*
|
|
|
243
246
|
|
|
244
247
|
## Client types
|
|
245
248
|
|
|
246
|
-
|
|
249
|
+
Set `specOutputPath` in your docs options and the library writes `openapi.json` to disk automatically every time the server starts. That file is a standard OpenAPI 3.1 spec — use it with any OpenAPI-compatible tool: code generators, client SDKs, linters, mocking tools, and more.
|
|
247
250
|
|
|
248
|
-
|
|
251
|
+
For TypeScript projects, [openapi-typescript](https://github.com/openapi-ts/openapi-typescript) is a great option — it generates a `.d.ts` file from the spec that you can use with any HTTP client.
|
|
249
252
|
|
|
250
|
-
|
|
253
|
+
### Setup
|
|
251
254
|
|
|
252
|
-
|
|
253
|
-
|
|
255
|
+
**1. Enable spec output:**
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
app.use("/docs", router.docs({
|
|
259
|
+
title: "My API",
|
|
260
|
+
version: "1.0.0",
|
|
261
|
+
specOutputPath: "./openapi.json", // written automatically on every server start
|
|
262
|
+
}));
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**2. Add the scripts to your `package.json`:**
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
{
|
|
269
|
+
"scripts": {
|
|
270
|
+
"dev": "run-p dev:server dev:types",
|
|
271
|
+
"dev:server": "node --watch src/server.ts",
|
|
272
|
+
"dev:types": "nodemon -L --watch openapi.json --exec \"openapi-typescript ./openapi.json -o ./api.types.ts\""
|
|
273
|
+
},
|
|
274
|
+
"devDependencies": {
|
|
275
|
+
"openapi-typescript": "^7.0.0",
|
|
276
|
+
"nodemon": "^3.0.0",
|
|
277
|
+
"npm-run-all2": "^7.0.0"
|
|
278
|
+
}
|
|
279
|
+
}
|
|
254
280
|
```
|
|
255
281
|
|
|
256
|
-
|
|
282
|
+
**3. Run it:**
|
|
257
283
|
|
|
258
284
|
```bash
|
|
259
|
-
|
|
285
|
+
npm run dev
|
|
260
286
|
```
|
|
261
287
|
|
|
288
|
+
That's it — one command runs everything in parallel:
|
|
289
|
+
|
|
290
|
+
- `dev:server` — runs your server with `node --watch` (Node 18.11+; no `tsx` needed on Node 23.6+). On every save the server restarts and the library **rewrites `openapi.json` automatically**.
|
|
291
|
+
- `dev:types` — `nodemon` watches `openapi.json` and regenerates `api.types.ts` whenever it changes.
|
|
292
|
+
|
|
293
|
+
Edit a route, save, and your client types update on their own.
|
|
294
|
+
|
|
295
|
+
> **Why `nodemon -L`?** On Windows, native file watchers miss in-place file writes — the `-L` flag forces polling so the regen reliably fires. On macOS/Linux you can drop it.
|
|
296
|
+
|
|
297
|
+
> Add `openapi.json` and `api.types.ts` to `.gitignore` — both are generated.
|
|
298
|
+
|
|
299
|
+
**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.types.ts` whenever you change your API.
|
|
300
|
+
|
|
262
301
|
### Use with `openapi-fetch`
|
|
263
302
|
|
|
264
303
|
```ts
|
|
265
304
|
import createClient from "openapi-fetch";
|
|
266
|
-
import type { paths } from "./
|
|
305
|
+
import type { paths } from "./api.types";
|
|
267
306
|
|
|
268
307
|
const client = createClient<paths>({ baseUrl: "http://localhost:3000/api" });
|
|
269
308
|
|
|
@@ -282,7 +321,7 @@ const { data: user } = await client.POST("/users", {
|
|
|
282
321
|
If you prefer not to add `openapi-fetch`, use the generated types directly with standard `fetch`:
|
|
283
322
|
|
|
284
323
|
```ts
|
|
285
|
-
import type { paths } from "./
|
|
324
|
+
import type { paths } from "./api.types";
|
|
286
325
|
|
|
287
326
|
type Body<
|
|
288
327
|
P extends keyof paths,
|
package/dist/schema-router.cjs
CHANGED
|
@@ -9,4 +9,4 @@ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.
|
|
|
9
9
|
<script id="api-reference" data-url="${w(t)}"><\/script>
|
|
10
10
|
<script src="${w(n)}"><\/script>
|
|
11
11
|
</body>
|
|
12
|
-
</html>`}async function D(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={};if(t.responseSamples.size>0)for(let[e,n]of t.responseSamples)a[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":{example:n}}};else 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 O=new WeakMap;var k=class e{router;routes=[];mountedRouters=[];constructor(){this.router=c.default.Router(),O.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.
|
|
12
|
+
</html>`}async function D(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={};if(t.responseSamples.size>0)for(let[e,n]of t.responseSamples)a[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":{example:n}}};else 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 O=new WeakMap;var k=class e{router;routes=[];mountedRouters=[];samplingEnabled=!1;constructor(){this.router=c.default.Router(),O.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=O.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=new Set){if(!e.has(this)){e.add(this),this.samplingEnabled=!0;for(let{router:t}of this.mountedRouters)t.enableSampling(e)}}trackMounted(e,t){this.mountedRouters.some(n=>n.router===t&&n.prefix===e)||(this.mountedRouters.push({prefix:e,router:t}),this.samplingEnabled&&t.enableSampling())}docs(e={}){this.enableSampling();let t=c.default.Router();if(e.specOutputPath){let t=this,n=e.specOutputPath;setImmediate(async()=>{try{let r=await D(t.getRouteMetadata(),e);await(await p(`fs/promises`)).writeFile(n,JSON.stringify(r,null,2),`utf8`)}catch{}})}return t.get(`/openapi.json`,async(t,n)=>{try{let t=await D(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??T;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(E(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;return this.router[e](t,(e,t,n)=>{if(!this.samplingEnabled||a.hidden||o>=50||a.responseSamples.size>=10){n();return}o++;let r=t.json;t.json=e=>(a.responseSamples.has(t.statusCode)||a.responseSamples.set(t.statusCode,e),r.call(t,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 A(){return new k}function j(e){let t=new k;return e?.errorHandler&&t.getRouter().use(e.errorHandler),t}function M(...e){let t=new k;for(let n of e)t=t.useMiddleware(n);return t}function N(e,t={}){let n=(Array.isArray(e)?e:[e]).map(e=>`prefix`in e?e:{prefix:``,router:e});for(let{router:e}of n)e.enableSampling();let r=c.default.Router();return r.get(`/openapi.json`,async(e,r)=>{try{let e=await D(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??T;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(E(t.title??`API`,r,i))}),r}exports.TypedRouter=k,exports.createDocs=N,exports.createTypedRouter=A,exports.createTypedRouterWithConfig=j,exports.createTypedRouterWithMiddleware=M,exports.isSchemaError=f,exports.parseSchema=u,exports.safeParseSchema=d;
|
package/dist/schema-router.d.cts
CHANGED
|
@@ -198,6 +198,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
198
198
|
private router;
|
|
199
199
|
private routes;
|
|
200
200
|
private mountedRouters;
|
|
201
|
+
private samplingEnabled;
|
|
201
202
|
constructor();
|
|
202
203
|
/**
|
|
203
204
|
* Add typed middleware that extends the request with additional properties
|
|
@@ -258,6 +259,18 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
258
259
|
* Used internally by .docs() and by createDocs() for multi-router merging.
|
|
259
260
|
*/
|
|
260
261
|
getRouteMetadata(): RouteMetadata[];
|
|
262
|
+
/**
|
|
263
|
+
* Turn on response sampling for this router and every router mounted under
|
|
264
|
+
* it. Called by .docs() and createDocs() so examples are captured only when
|
|
265
|
+
* docs are actually generated. The visited set guards against mount cycles.
|
|
266
|
+
* @internal Public only so createDocs() can reach it; not part of the API.
|
|
267
|
+
*/
|
|
268
|
+
enableSampling(visited?: Set<TypedRouter<any, any>>): void;
|
|
269
|
+
/**
|
|
270
|
+
* Record a sub-router for docs, de-duplicating identical (prefix, router)
|
|
271
|
+
* pairs and propagating sampling if docs were already requested.
|
|
272
|
+
*/
|
|
273
|
+
private trackMounted;
|
|
261
274
|
/**
|
|
262
275
|
* Returns an Express router that serves OpenAPI docs.
|
|
263
276
|
* Mount it anywhere on your app — routes are auto-discovered.
|
package/dist/schema-router.d.mts
CHANGED
|
@@ -198,6 +198,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
198
198
|
private router;
|
|
199
199
|
private routes;
|
|
200
200
|
private mountedRouters;
|
|
201
|
+
private samplingEnabled;
|
|
201
202
|
constructor();
|
|
202
203
|
/**
|
|
203
204
|
* Add typed middleware that extends the request with additional properties
|
|
@@ -258,6 +259,18 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
258
259
|
* Used internally by .docs() and by createDocs() for multi-router merging.
|
|
259
260
|
*/
|
|
260
261
|
getRouteMetadata(): RouteMetadata[];
|
|
262
|
+
/**
|
|
263
|
+
* Turn on response sampling for this router and every router mounted under
|
|
264
|
+
* it. Called by .docs() and createDocs() so examples are captured only when
|
|
265
|
+
* docs are actually generated. The visited set guards against mount cycles.
|
|
266
|
+
* @internal Public only so createDocs() can reach it; not part of the API.
|
|
267
|
+
*/
|
|
268
|
+
enableSampling(visited?: Set<TypedRouter<any, any>>): void;
|
|
269
|
+
/**
|
|
270
|
+
* Record a sub-router for docs, de-duplicating identical (prefix, router)
|
|
271
|
+
* pairs and propagating sampling if docs were already requested.
|
|
272
|
+
*/
|
|
273
|
+
private trackMounted;
|
|
261
274
|
/**
|
|
262
275
|
* Returns an Express router that serves OpenAPI docs.
|
|
263
276
|
* Mount it anywhere on your app — routes are auto-discovered.
|
package/dist/schema-router.mjs
CHANGED
|
@@ -9,4 +9,4 @@ import e from"express";import{SchemaError as t}from"@standard-schema/utils";func
|
|
|
9
9
|
<script id="api-reference" data-url="${g(t)}"><\/script>
|
|
10
10
|
<script src="${g(n)}"><\/script>
|
|
11
11
|
</body>
|
|
12
|
-
</html>`}async function y(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={};if(t.responseSamples.size>0)for(let[e,n]of t.responseSamples)a[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":{example:n}}};else 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 b=new WeakMap;var x=class t{router;routes=[];mountedRouters=[];constructor(){this.router=e.Router(),b.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.
|
|
12
|
+
</html>`}async function y(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={};if(t.responseSamples.size>0)for(let[e,n]of t.responseSamples)a[String(e)]={description:e<400?`Success`:`Error`,content:{"application/json":{example:n}}};else 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 b=new WeakMap;var x=class t{router;routes=[];mountedRouters=[];samplingEnabled=!1;constructor(){this.router=e.Router(),b.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=b.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=new Set){if(!e.has(this)){e.add(this),this.samplingEnabled=!0;for(let{router:t}of this.mountedRouters)t.enableSampling(e)}}trackMounted(e,t){this.mountedRouters.some(n=>n.router===t&&n.prefix===e)||(this.mountedRouters.push({prefix:e,router:t}),this.samplingEnabled&&t.enableSampling())}docs(t={}){this.enableSampling();let n=e.Router();if(t.specOutputPath){let e=this,n=t.specOutputPath;setImmediate(async()=>{try{let r=await y(e.getRouteMetadata(),t);await(await a(`fs/promises`)).writeFile(n,JSON.stringify(r,null,2),`utf8`)}catch{}})}return n.get(`/openapi.json`,async(e,n)=>{try{let e=await y(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??_;n.setHeader(`Content-Type`,`text/html; charset=utf-8`),n.send(v(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;return this.router[e](t,(e,t,n)=>{if(!this.samplingEnabled||a.hidden||o>=50||a.responseSamples.size>=10){n();return}o++;let r=t.json;t.json=e=>(a.responseSamples.has(t.statusCode)||a.responseSamples.set(t.statusCode,e),r.call(t,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 S(){return new x}function C(e){let t=new x;return e?.errorHandler&&t.getRouter().use(e.errorHandler),t}function w(...e){let t=new x;for(let n of e)t=t.useMiddleware(n);return t}function T(t,n={}){let r=(Array.isArray(t)?t:[t]).map(e=>`prefix`in e?e:{prefix:``,router:e});for(let{router:e}of r)e.enableSampling();let i=e.Router();return i.get(`/openapi.json`,async(e,t)=>{try{let e=await y(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??_;t.setHeader(`Content-Type`,`text/html; charset=utf-8`),t.send(v(n.title??`API`,r,i))}),i}export{x as TypedRouter,T as createDocs,S as createTypedRouter,C as createTypedRouterWithConfig,w as createTypedRouterWithMiddleware,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.
|
|
3
|
+
"version": "1.8.0",
|
|
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",
|