@minisylar/express-typed-router 1.7.0 → 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 -20
- package/dist/schema-router.cjs +1 -1
- package/dist/schema-router.d.cts +22 -6
- package/dist/schema-router.d.mts +22 -6
- 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
|
|
|
@@ -225,16 +229,16 @@ app.use("/docs", api.docs({ title: "My API", version: "1.0.0" }));
|
|
|
225
229
|
|
|
226
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.
|
|
227
231
|
|
|
228
|
-
| Library
|
|
229
|
-
|
|
230
|
-
| Zod 4
|
|
231
|
-
| Zod 3
|
|
232
|
-
| Valibot
|
|
233
|
-
| ArkType
|
|
234
|
-
| Effect
|
|
235
|
-
| Yup
|
|
236
|
-
| Joi
|
|
237
|
-
| 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 |
|
|
238
242
|
|
|
239
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.
|
|
240
244
|
|
|
@@ -242,27 +246,63 @@ All validators work for **request validation**. For **OpenAPI schema generation*
|
|
|
242
246
|
|
|
243
247
|
## Client types
|
|
244
248
|
|
|
245
|
-
|
|
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.
|
|
246
250
|
|
|
247
|
-
|
|
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.
|
|
248
252
|
|
|
249
|
-
|
|
253
|
+
### Setup
|
|
250
254
|
|
|
251
|
-
|
|
252
|
-
|
|
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
|
+
}));
|
|
253
263
|
```
|
|
254
264
|
|
|
255
|
-
|
|
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
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**3. Run it:**
|
|
256
283
|
|
|
257
284
|
```bash
|
|
258
|
-
|
|
285
|
+
npm run dev
|
|
259
286
|
```
|
|
260
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
|
+
|
|
261
301
|
### Use with `openapi-fetch`
|
|
262
302
|
|
|
263
303
|
```ts
|
|
264
304
|
import createClient from "openapi-fetch";
|
|
265
|
-
import type { paths } from "./
|
|
305
|
+
import type { paths } from "./api.types";
|
|
266
306
|
|
|
267
307
|
const client = createClient<paths>({ baseUrl: "http://localhost:3000/api" });
|
|
268
308
|
|
|
@@ -281,7 +321,7 @@ const { data: user } = await client.POST("/users", {
|
|
|
281
321
|
If you prefer not to add `openapi-fetch`, use the generated types directly with standard `fetch`:
|
|
282
322
|
|
|
283
323
|
```ts
|
|
284
|
-
import type { paths } from "./
|
|
324
|
+
import type { paths } from "./api.types";
|
|
285
325
|
|
|
286
326
|
type Body<
|
|
287
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{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(
|
|
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
|
|
@@ -225,18 +226,18 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
225
226
|
* automatically recognised and tracked for .docs() — no extra wiring needed.
|
|
226
227
|
*
|
|
227
228
|
* @example
|
|
228
|
-
* // v1.routes.ts —
|
|
229
|
+
* // v1.routes.ts — pass TypedRouter instances directly, no .getRouter() needed
|
|
229
230
|
* export const v1Routes = createTypedRouter()
|
|
230
231
|
*
|
|
231
|
-
* v1Routes.use('/products',
|
|
232
|
-
* v1Routes.use('/profile',
|
|
233
|
-
* v1Routes.use('/',
|
|
232
|
+
* v1Routes.use('/products', productRoutes) // tracked ✓
|
|
233
|
+
* v1Routes.use('/profile', profileRoutes) // tracked ✓
|
|
234
|
+
* v1Routes.use('/', callbackRouter) // plain Express, also works
|
|
234
235
|
*
|
|
235
236
|
* app.use('/v1', v1Routes.getRouter())
|
|
236
237
|
* app.use('/docs', v1Routes.docs({ title: 'My API' })) // just works
|
|
237
238
|
*/
|
|
238
|
-
use(path: string, ...handlers: Array<express.RequestHandler | express.Router
|
|
239
|
-
use(...handlers: Array<express.RequestHandler | express.Router
|
|
239
|
+
use(path: string, ...handlers: Array<express.RequestHandler | express.Router | TypedRouter<any, any>>): TypedRouter<Req, Locals>;
|
|
240
|
+
use(...handlers: Array<express.RequestHandler | express.Router | TypedRouter<any, any>>): TypedRouter<Req, Locals>;
|
|
240
241
|
/**
|
|
241
242
|
* Mount a TypedRouter at a path prefix, registering it both on the Express
|
|
242
243
|
* router and in the docs registry so .docs() picks it up automatically.
|
|
@@ -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.
|
|
@@ -355,6 +368,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
355
368
|
delete<Path extends string, QuerySchema extends AnyStandardSchema | unknown>(path: Path, options: {
|
|
356
369
|
querySchema: QuerySchema;
|
|
357
370
|
}, handler: SchemaRouteHandler<Path, unknown, QuerySchema, Req, Locals>): TypedRouter<Req, Locals>;
|
|
371
|
+
delete<Path extends string>(path: Path, options: DocMeta, handler: SchemaRouteHandler<Path, unknown, unknown, Req, Locals>): TypedRouter<Req, Locals>;
|
|
358
372
|
delete<Path extends string, M extends TypedMiddleware<any, any>[]>(path: Path, options: DocMeta & {
|
|
359
373
|
middleware: [...M];
|
|
360
374
|
}, // Using tuple spread pattern
|
|
@@ -372,6 +386,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
372
386
|
options<Path extends string, QuerySchema extends AnyStandardSchema | unknown>(path: Path, options: {
|
|
373
387
|
querySchema: QuerySchema;
|
|
374
388
|
}, handler: SchemaRouteHandler<Path, unknown, QuerySchema, Req, Locals>): TypedRouter<Req, Locals>;
|
|
389
|
+
options<Path extends string>(path: Path, options: DocMeta, handler: SchemaRouteHandler<Path, unknown, unknown, Req, Locals>): TypedRouter<Req, Locals>;
|
|
375
390
|
options<Path extends string, M extends TypedMiddleware<any, any>[]>(path: Path, options: DocMeta & {
|
|
376
391
|
middleware: [...M];
|
|
377
392
|
}, // Using tuple spread pattern
|
|
@@ -389,6 +404,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
389
404
|
head<Path extends string, QuerySchema extends AnyStandardSchema | unknown>(path: Path, options: {
|
|
390
405
|
querySchema: QuerySchema;
|
|
391
406
|
}, handler: SchemaRouteHandler<Path, unknown, QuerySchema, Req, Locals>): TypedRouter<Req, Locals>;
|
|
407
|
+
head<Path extends string>(path: Path, options: DocMeta, handler: SchemaRouteHandler<Path, unknown, unknown, Req, Locals>): TypedRouter<Req, Locals>;
|
|
392
408
|
head<Path extends string, M extends TypedMiddleware<any, any>[]>(path: Path, options: DocMeta & {
|
|
393
409
|
middleware: [...M];
|
|
394
410
|
}, // Using tuple spread pattern
|
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
|
|
@@ -225,18 +226,18 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
225
226
|
* automatically recognised and tracked for .docs() — no extra wiring needed.
|
|
226
227
|
*
|
|
227
228
|
* @example
|
|
228
|
-
* // v1.routes.ts —
|
|
229
|
+
* // v1.routes.ts — pass TypedRouter instances directly, no .getRouter() needed
|
|
229
230
|
* export const v1Routes = createTypedRouter()
|
|
230
231
|
*
|
|
231
|
-
* v1Routes.use('/products',
|
|
232
|
-
* v1Routes.use('/profile',
|
|
233
|
-
* v1Routes.use('/',
|
|
232
|
+
* v1Routes.use('/products', productRoutes) // tracked ✓
|
|
233
|
+
* v1Routes.use('/profile', profileRoutes) // tracked ✓
|
|
234
|
+
* v1Routes.use('/', callbackRouter) // plain Express, also works
|
|
234
235
|
*
|
|
235
236
|
* app.use('/v1', v1Routes.getRouter())
|
|
236
237
|
* app.use('/docs', v1Routes.docs({ title: 'My API' })) // just works
|
|
237
238
|
*/
|
|
238
|
-
use(path: string, ...handlers: Array<express.RequestHandler | express.Router
|
|
239
|
-
use(...handlers: Array<express.RequestHandler | express.Router
|
|
239
|
+
use(path: string, ...handlers: Array<express.RequestHandler | express.Router | TypedRouter<any, any>>): TypedRouter<Req, Locals>;
|
|
240
|
+
use(...handlers: Array<express.RequestHandler | express.Router | TypedRouter<any, any>>): TypedRouter<Req, Locals>;
|
|
240
241
|
/**
|
|
241
242
|
* Mount a TypedRouter at a path prefix, registering it both on the Express
|
|
242
243
|
* router and in the docs registry so .docs() picks it up automatically.
|
|
@@ -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.
|
|
@@ -355,6 +368,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
355
368
|
delete<Path extends string, QuerySchema extends AnyStandardSchema | unknown>(path: Path, options: {
|
|
356
369
|
querySchema: QuerySchema;
|
|
357
370
|
}, handler: SchemaRouteHandler<Path, unknown, QuerySchema, Req, Locals>): TypedRouter<Req, Locals>;
|
|
371
|
+
delete<Path extends string>(path: Path, options: DocMeta, handler: SchemaRouteHandler<Path, unknown, unknown, Req, Locals>): TypedRouter<Req, Locals>;
|
|
358
372
|
delete<Path extends string, M extends TypedMiddleware<any, any>[]>(path: Path, options: DocMeta & {
|
|
359
373
|
middleware: [...M];
|
|
360
374
|
}, // Using tuple spread pattern
|
|
@@ -372,6 +386,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
372
386
|
options<Path extends string, QuerySchema extends AnyStandardSchema | unknown>(path: Path, options: {
|
|
373
387
|
querySchema: QuerySchema;
|
|
374
388
|
}, handler: SchemaRouteHandler<Path, unknown, QuerySchema, Req, Locals>): TypedRouter<Req, Locals>;
|
|
389
|
+
options<Path extends string>(path: Path, options: DocMeta, handler: SchemaRouteHandler<Path, unknown, unknown, Req, Locals>): TypedRouter<Req, Locals>;
|
|
375
390
|
options<Path extends string, M extends TypedMiddleware<any, any>[]>(path: Path, options: DocMeta & {
|
|
376
391
|
middleware: [...M];
|
|
377
392
|
}, // Using tuple spread pattern
|
|
@@ -389,6 +404,7 @@ declare class TypedRouter<Req extends Record<string, any> = AdditionalReqProps,
|
|
|
389
404
|
head<Path extends string, QuerySchema extends AnyStandardSchema | unknown>(path: Path, options: {
|
|
390
405
|
querySchema: QuerySchema;
|
|
391
406
|
}, handler: SchemaRouteHandler<Path, unknown, QuerySchema, Req, Locals>): TypedRouter<Req, Locals>;
|
|
407
|
+
head<Path extends string>(path: Path, options: DocMeta, handler: SchemaRouteHandler<Path, unknown, unknown, Req, Locals>): TypedRouter<Req, Locals>;
|
|
392
408
|
head<Path extends string, M extends TypedMiddleware<any, any>[]>(path: Path, options: DocMeta & {
|
|
393
409
|
middleware: [...M];
|
|
394
410
|
}, // Using tuple spread pattern
|
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{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,...
|
|
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",
|