@minisylar/express-typed-router 1.6.1 → 1.7.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 CHANGED
@@ -1,79 +1,36 @@
1
1
  # @minisylar/express-typed-router
2
2
 
3
- A strongly-typed Express router with schema validation and automatic type inference for params, body, query, and middleware.
3
+ A strongly typed Express router with **Standard Schema** validation, automatic type inference, and OpenAPI docs.
4
4
 
5
- ## Features
5
+ Define routes once, infer `params` / `body` / `query`, and generate a clean API spec for docs or client generation.
6
6
 
7
- - 🚀 **Full TypeScript support** with automatic type inference for route parameters
8
- - 🛡️ **Schema validation** for request body, query parameters, and route params (Zod, Yup, Valibot, Arktype,Joi,Effect,decoders,
9
- ts.data.json,
10
- unhoax, etc.)
11
- - 🔗 **Express.js compatibility** - works with Express 4 and Express 5
12
- - 🤝 **Mix with existing Express routes** - seamlessly integrates with your current codebase
13
- - 📝 **JSDoc documentation** with comprehensive examples
14
- - 📦 **ES Modules** and CommonJS support
15
- - 🎯 **Zero runtime overhead** for type checking
7
+ ---
16
8
 
17
- ## Installation
9
+ ## What you get
18
10
 
19
- ```bash
20
- npm install @minisylar/express-typed-router
21
- # or
22
- pnpm add @minisylar/express-typed-router
23
- # or
24
- yarn add @minisylar/express-typed-router
25
- ```
26
-
27
- > **Note:** This package requires Express 4.18.0+ or Express 5.0.0+. For schema validation the library works with multiple popular schema libraries (examples below).
11
+ - **Typed route handlers** — `req.params`, `req.body`, `req.query` inferred from your route + schema
12
+ - **Typed middleware** — middleware can extend `req` and `res.locals`
13
+ - **✨ OpenAPI docs** — generated from routes, schemas, and captured responses
14
+ - **Schema-agnostic** — any Standard Schema-compatible validator (Zod, Yup, Valibot, Arktype, Joi...)
15
+ - **Express 4 & 5** — common patterns supported
16
+ - **Client-friendly output** — generate `generated-types.d.ts` and build any client wrapper
28
17
 
29
- ### Schema Compatibility
18
+ ---
30
19
 
31
- This library is schema-agnostic: it provides a validation plumbing that works with multiple popular schema libraries. Below are short examples showing how you can use different schema libraries with the router. The router expects a schema-like object that can validate input; most adapters are straightforward.
20
+ ## Install
32
21
 
33
- Example with Zod (v3 or v4):
34
-
35
- ```typescript
36
- import { z } from "zod"; // or "zod/v4" or "zod/v3" as needed
37
- const userSchema = z.object({ name: z.string() });
38
- router.post("/users", { bodySchema: userSchema }, handler);
39
- ```
40
-
41
- Example with Yup:
42
-
43
- ```javascript
44
- import * as yup from "yup";
45
- const userSchema = yup.object({ name: yup.string().required() });
46
- // pass the yup schema directly as bodySchema; the router will run validation
47
- router.post("/users", { bodySchema: userSchema }, handler);
48
- ```
49
-
50
- Example with Valibot (valibot):
51
-
52
- ```typescript
53
- import { object, string } from "valibot";
54
- const userSchema = object({ name: string() });
55
- router.post("/users", { bodySchema: userSchema }, handler);
56
- ```
57
-
58
- Example with Arktype:
59
-
60
- ```typescript
61
- import { object, string } from "arktype";
62
- const userSchema = object({ name: string() });
63
- router.post("/users", { bodySchema: userSchema }, handler);
22
+ ```bash
23
+ npm install @minisylar/express-typed-router
24
+ pnpm add @minisylar/express-typed-router
64
25
  ```
65
26
 
66
- If a schema library needs an adapter (for example to map its errors to the router's error format), add a small wrapper that runs validation and throws the expected error shape. See the project's examples for concrete adapter patterns.
67
-
68
- Note about Joi: Joi's TypeScript typings do not reliably infer the output type from the runtime schema shape. When using Joi you should either:
27
+ Requires Express 4.18+ or Express 5.
69
28
 
70
- - provide an explicit generic type for the schema (e.g. `Joi.object<MyType>(...)`),
71
- - add a variable type annotation (e.g. `const s: Joi.ObjectSchema<MyType> = Joi.object(...)`), or
72
- - write a small adapter that validates at runtime and exposes a typed result to TypeScript.
29
+ ---
73
30
 
74
- ## Quick Start
31
+ ## Quick start
75
32
 
76
- ```javascript
33
+ ```ts
77
34
  import express from "express";
78
35
  import { z } from "zod";
79
36
  import { createTypedRouter } from "@minisylar/express-typed-router";
@@ -81,854 +38,391 @@ import { createTypedRouter } from "@minisylar/express-typed-router";
81
38
  const app = express();
82
39
  app.use(express.json());
83
40
 
84
- // Create a typed router
85
41
  const router = createTypedRouter();
86
42
 
87
- // Define routes - parameters are automatically typed!
88
- router.get("/users/:userId", (req, res) => {
89
- // req.params.userId is automatically inferred as string
90
- res.json({ userId: req.params.userId });
43
+ router.get("/users/:id", (req, res) => {
44
+ res.json({ id: req.params.id }); // params.id: string
91
45
  });
92
46
 
93
- // Add validation with Zod schemas
94
47
  router.post(
95
48
  "/users",
96
- {
97
- bodySchema: z.object({
98
- name: z.string(),
99
- email: z.string().email(),
100
- }),
101
- },
49
+ { bodySchema: z.object({ name: z.string() }) },
102
50
  (req, res) => {
103
- // req.body is validated and typed automatically
104
- const { name, email } = req.body;
105
- res.json({ id: "123", name, email });
106
- }
51
+ res.json({ name: req.body.name }); // body.name: string
52
+ },
107
53
  );
108
54
 
109
- const expressRouter = router.getRouter();
110
- app.use("/api", expressRouter);
55
+ app.use("/api", router.getRouter());
56
+ app.use("/docs", router.docs({ title: "My API", version: "1.0.0" }));
57
+
111
58
  app.listen(3000);
59
+ // http://localhost:3000/docs → interactive API docs
112
60
  ```
113
61
 
114
- That's it! Your routes now have full type safety and validation.
115
-
116
- ## Works with Existing Express Routes
117
-
118
- The typed router seamlessly integrates with your existing Express application - no need to rewrite everything!
62
+ ---
119
63
 
120
- ```javascript
121
- import express from "express";
122
- import { createTypedRouter } from "@minisylar/express-typed-router";
64
+ ## Route typing
123
65
 
124
- const app = express();
125
- app.use(express.json());
66
+ Path params are inferred from the route string. No extra types needed.
126
67
 
127
- // Your existing Express routes continue to work
128
- app.get("/health", (req, res) => {
129
- res.json({ status: "ok" });
130
- });
131
-
132
- // Existing Express router
133
- const legacyRouter = express.Router();
134
- legacyRouter.get("/legacy/:id", (req, res) => {
135
- res.json({ id: req.params.id });
68
+ ```ts
69
+ router.get("/users/:id", (req, res) => {
70
+ req.params.id; // string
136
71
  });
137
72
 
138
- // New typed router with full type safety
139
- const typedRouter = createTypedRouter();
140
- typedRouter.get("/users/:userId", (req, res) => {
141
- // req.params.userId is automatically typed as string
142
- res.json({ userId: req.params.userId });
73
+ router.get("/flights/:from-:to", (req, res) => {
74
+ req.params.from; // string
75
+ req.params.to; // string
143
76
  });
144
77
 
145
- // Extract the Express router before using it
146
- const typedExpressRouter = typedRouter.getRouter();
147
-
148
- // Mix them all together
149
- app.use("/api/legacy", legacyRouter);
150
- app.use("/api/v2", typedExpressRouter);
151
-
152
- // Gradually migrate your routes to get type safety where you need it!
153
- app.listen(3000);
154
- ```
155
-
156
- ## The Main API: `createTypedRouter()`
157
-
158
- `createTypedRouter()` is the primary and most flexible way to create typed routers. It supports:
159
-
160
- - ✅ **Global middleware** with automatic type merging
161
- - ✅ **Per-route middleware** with type inference
162
- - ✅ **Zod validation** for params, body, and query
163
- - ✅ **Express 4 & 5 compatibility** with full route pattern support
164
- - ✅ **Chainable API** for easy configuration
165
-
166
- ### Global Middleware
167
-
168
- **Important**: Unlike Express, middleware must be applied using method chaining or capturing returned routers. See the [FAQ section](#faq-and-common-patterns) for details.
169
-
170
- ```typescript
171
- // Method chaining pattern (recommended)
172
- const router = createTypedRouter()
173
- .useMiddleware(authMiddleware)
174
- .useMiddleware(loggingMiddleware)
175
- .useMiddleware(timestampMiddleware);
176
-
177
- // All routes automatically get types from all middleware
178
- router.get("/protected", (req, res) => {
179
- // req.userId, req.requestId, req.timestamp all available and typed
78
+ router.get("/posts/:year/:month?", (req, res) => {
79
+ req.params.year; // string
80
+ req.params.month; // string | undefined
180
81
  });
181
-
182
- // Alternative: capturing returned router
183
- const baseRouter = createTypedRouter();
184
- const routerWithMiddleware = baseRouter
185
- .useMiddleware(authMiddleware)
186
- .useMiddleware(loggingMiddleware);
187
-
188
- // Use the router with middleware applied
189
- routerWithMiddleware.get("/users", handler);
190
82
  ```
191
83
 
192
- ### Per-Route Middleware
84
+ Schema options infer body and query:
193
85
 
194
- ```typescript
86
+ ```ts
195
87
  router.get(
196
- "/admin/:userId",
197
- {
198
- middleware: [adminMiddleware, auditMiddleware],
88
+ "/search",
89
+ { querySchema: z.object({ q: z.string() }) },
90
+ (req, res) => {
91
+ req.query.q; // string — validated at runtime, typed at compile time
199
92
  },
93
+ );
94
+
95
+ router.post(
96
+ "/users",
97
+ { bodySchema: z.object({ name: z.string(), email: z.string().email() }) },
200
98
  (req, res) => {
201
- // Types from both global AND per-route middleware are merged
202
- // req.userId (global), req.isAdmin (adminMiddleware), req.auditId (auditMiddleware)
203
- }
99
+ req.body.name; // string
100
+ req.body.email; // string
101
+ },
204
102
  );
205
103
  ```
206
104
 
207
- ### Express 4 & 5 Route Pattern Support
208
-
209
- Works with **all** Express routing patterns:
210
-
211
- ```typescript
212
- // Named parameters
213
- router.get("/users/:userId", handler); // { userId: string }
214
-
215
- // Multiple parameters
216
- router.get("/users/:userId/posts/:postId", handler); // { userId: string; postId: string }
105
+ <details>
106
+ <summary><strong>All supported route patterns</strong></summary>
217
107
 
218
- // Consecutive parameters with separators
108
+ ```ts
109
+ router.get("/users/:id", handler); // { id: string }
219
110
  router.get("/flights/:from-:to", handler); // { from: string; to: string }
220
111
  router.get("/files/:name.:ext", handler); // { name: string; ext: string }
221
-
222
- // Optional parameters (Express 4)
223
112
  router.get("/posts/:year/:month?", handler); // { year: string; month?: string }
224
-
225
- // Repeating parameters (Express 5)
226
113
  router.get("/files/:path+", handler); // { path: string[] }
227
-
228
- // Optional repeating (Express 5)
229
114
  router.get("/search/:terms*", handler); // { terms?: string[] }
230
-
231
- // Optional segments (Express 5)
232
115
  router.get("/api{/:version}/users", handler); // { version?: string }
233
-
234
- // Regex constraints
235
116
  router.get("/users/:id(\\d+)", handler); // { id: string }
236
-
237
- // Wildcards
238
117
  router.get("/static/*", handler); // { "0": string }
239
118
  ```
240
119
 
241
- ## Comprehensive Example
242
-
243
- ```javascript
244
- import express from "express";
245
- import { z } from "zod";
246
- import { createTypedRouter } from "@minisylar/express-typed-router";
247
-
248
- const app = express();
249
- app.use(express.json());
250
-
251
- // Create router and define middleware inline (automatically typed!)
252
- const router = createTypedRouter()
253
- .useMiddleware((req, res, next) => {
254
- const token = req.headers.authorization;
255
- req.userId = "user123";
256
- req.isAdmin = token?.includes("admin") || false;
257
- next();
258
- })
259
- .useMiddleware((req, res, next) => {
260
- req.requestId = Math.random().toString(36);
261
- console.log(`[${req.requestId}] ${req.method} ${req.path}`);
262
- next();
263
- });
264
-
265
- // Define schemas
266
- const CreateUserSchema = z.object({
267
- name: z.string().min(1),
268
- email: z.string().email(),
269
- role: z.enum(["user", "admin"]).optional(),
270
- });
271
-
272
- const UserQuerySchema = z.object({
273
- include: z.array(z.string()).optional(),
274
- limit: z.coerce.number().int().positive().max(100).default(10),
275
- });
276
-
277
- // Routes with full type safety
278
- router.get(
279
- "/users/:userId",
280
- {
281
- querySchema: UserQuerySchema,
282
- },
283
- (req, res) => {
284
- // All properties are automatically typed:
285
- const { userId } = req.params; // string (auto-inferred from route)
286
- const { include, limit } = req.query; // from schema validation
287
- const { userId: authUserId, isAdmin, requestId } = req; // from middleware
288
-
289
- res.json({
290
- id: userId,
291
- authUserId,
292
- isAdmin,
293
- requestId,
294
- include,
295
- limit,
296
- });
297
- }
298
- );
299
-
300
- router.post(
301
- "/users",
302
- {
303
- bodySchema: CreateUserSchema,
304
- },
305
- (req, res) => {
306
- const { name, email, role } = req.body; // Fully typed from schema
307
- const { userId, isAdmin, requestId } = req; // From middleware
308
-
309
- if (role === "admin" && !isAdmin) {
310
- return res.status(403).json({ error: "Insufficient permissions" });
311
- }
312
-
313
- res.status(201).json({
314
- id: "new-user-id",
315
- name,
316
- email,
317
- role: role || "user",
318
- createdBy: userId,
319
- requestId,
320
- });
321
- }
322
- );
323
-
324
- // Per-route middleware can also be inline
325
- router.delete(
326
- "/users/:userId",
327
- {
328
- middleware: [
329
- (req, res, next) => {
330
- if (!req.isAdmin) {
331
- return res.status(403).json({ error: "Admin required" });
332
- }
333
- req.hasAdminAccess = true;
334
- next();
335
- },
336
- ], // No need for 'as const' - middleware arrays are automatically typed
337
- },
338
- (req, res) => {
339
- // Types from BOTH global middleware AND per-route middleware
340
- const { userId } = req.params; // From route
341
- const { userId: authUserId, requestId } = req; // From global middleware
342
- const { hasAdminAccess } = req; // From per-route middleware
343
-
344
- res.json({
345
- deleted: userId,
346
- deletedBy: authUserId,
347
- requestId,
348
- hasAdminAccess,
349
- });
350
- }
351
- );
120
+ </details>
352
121
 
353
- app.use("/api", router.getRouter());
354
- app.listen(3000, () => {
355
- console.log("Server running on http://localhost:3000");
356
- });
357
- ```
122
+ ---
358
123
 
359
- ### With Explicit TypeScript Types
124
+ ## Middleware typing
360
125
 
361
- For TypeScript users who prefer explicit typing, you can define middleware with generics:
126
+ Declare what a middleware adds to `req`, and that type flows into every handler that uses it.
362
127
 
363
- ```typescript
364
- import { TypedMiddleware } from "@minisylar/express-typed-router";
128
+ ```ts
129
+ import type { TypedMiddleware } from "@minisylar/express-typed-router";
365
130
 
366
- // Define typed middleware explicitly
367
- const authMiddleware: TypedMiddleware<{ userId: string; isAdmin: boolean }> = (
131
+ const requireAuth: TypedMiddleware<{ userId: string; email: string }> = (
368
132
  req,
369
133
  res,
370
- next
134
+ next,
371
135
  ) => {
372
- const token = req.headers.authorization;
373
- req.userId = "user123";
374
- req.isAdmin = token?.includes("admin") || false;
136
+ const payload = jwt.verify(
137
+ req.headers.authorization!,
138
+ process.env.JWT_SECRET!,
139
+ );
140
+ req.userId = payload.userId;
141
+ req.email = payload.email;
375
142
  next();
376
143
  };
144
+ ```
377
145
 
378
- const loggingMiddleware: TypedMiddleware<{ requestId: string }> = (
379
- req,
380
- res,
381
- next
382
- ) => {
383
- req.requestId = Math.random().toString(36);
384
- console.log(`[${req.requestId}] ${req.method} ${req.path}`);
385
- next();
386
- };
146
+ **Global middleware** applied to all routes on the router:
387
147
 
388
- // Use the explicitly typed middleware
148
+ ```ts
389
149
  const router = createTypedRouter()
390
- .useMiddleware(authMiddleware)
150
+ .useMiddleware(requireAuth)
391
151
  .useMiddleware(loggingMiddleware);
392
152
 
393
- // Rest of the routes work the same way...
394
- ```
395
-
396
- ## TypeScript Features
397
-
398
- For TypeScript users, the library provides advanced type safety features:
399
-
400
- ### Typed Middleware
401
-
402
- Define middleware that extends the request object with typed properties:
403
-
404
- ```typescript
405
- import { TypedMiddleware } from "@minisylar/express-typed-router";
406
-
407
- const authMiddleware: TypedMiddleware<{ userId: string; isAdmin: boolean }> = (
408
- req,
409
- res,
410
- next
411
- ) => {
412
- req.userId = "user123";
413
- req.isAdmin = true;
414
- next();
415
- };
416
-
417
- // Add to router - types are automatically merged
418
- router.useMiddleware(authMiddleware);
419
-
420
- router.get("/protected", (req, res) => {
421
- // TypeScript knows about req.userId and req.isAdmin
422
- const { userId, isAdmin } = req;
423
- res.json({ userId, isAdmin });
153
+ router.get("/profile", (req, res) => {
154
+ req.userId; // string — from requireAuth
155
+ req.requestId; // string — from loggingMiddleware
424
156
  });
425
157
  ```
426
158
 
427
- ### Per-Route Middleware with Type Merging
428
-
429
- ```typescript
430
- const adminMiddleware: TypedMiddleware<{ hasAdminAccess: true }> = (
431
- req,
432
- res,
433
- next
434
- ) => {
435
- if (!req.isAdmin) {
436
- return res.status(403).json({ error: "Admin required" });
437
- }
438
- req.hasAdminAccess = true;
439
- next();
440
- };
441
-
442
- router.get(
443
- "/admin/:userId",
444
- {
445
- middleware: [adminMiddleware], // No need for 'as const' - arrays are automatically typed
446
- },
447
- (req, res) => {
448
- // Types from BOTH global and per-route middleware are available
449
- const { userId } = req.params; // From route params
450
- const { userId: authUserId } = req; // From global middleware
451
- const { hasAdminAccess } = req; // From per-route middleware
452
-
453
- res.json({ userId, authUserId, hasAdminAccess });
454
- }
455
- );
456
- ```
457
-
458
- ### Advanced Route Parameter Types
459
-
460
- The library automatically infers complex Express route patterns:
159
+ **Per-route middleware** scoped to one route, types still merge:
461
160
 
462
- ```typescript
463
- // Express 5 repeating parameters
464
- router.get("/files/:path+", (req, res) => {
465
- const { path } = req.params; // string[] - automatically inferred!
466
- });
467
-
468
- // Optional parameters
469
- router.get("/posts/:year/:month?", (req, res) => {
470
- const { year, month } = req.params; // { year: string; month?: string }
471
- });
472
-
473
- // Complex patterns with separators
474
- router.get("/flights/:from-:to", (req, res) => {
475
- const { from, to } = req.params; // { from: string; to: string }
161
+ ```ts
162
+ router.get("/admin", { middleware: [requireAdmin] }, (req, res) => {
163
+ req.userId; // from global middleware
164
+ req.isAdmin; // from requireAdmin
476
165
  });
477
166
  ```
478
167
 
479
- ## Alternative API Styles
480
-
481
- <details>
482
- <summary><strong>🎯 createTypedRouterWithMiddleware(...middleware)</strong> - Pre-configured with middleware</summary>
168
+ > **Note:** `useMiddleware` returns a new router instance. Use method chaining or capture the return value — see [Common Patterns](#common-patterns).
483
169
 
484
- For developers who prefer setting up all middleware upfront:
170
+ ---
485
171
 
486
- ```typescript
487
- import {
488
- createTypedRouterWithMiddleware,
489
- TypedMiddleware,
490
- } from "@minisylar/express-typed-router";
172
+ ## ✨ OpenAPI and docs
491
173
 
492
- const authMiddleware: TypedMiddleware<{ userId: string; isAdmin: boolean }> = (
493
- req,
494
- res,
495
- next
496
- ) => {
497
- req.userId = "user123";
498
- req.isAdmin = true;
499
- next();
500
- };
501
-
502
- const timestampMiddleware: TypedMiddleware<{ timestamp: Date }> = (
503
- req,
504
- res,
505
- next
506
- ) => {
507
- req.timestamp = new Date();
508
- next();
509
- };
174
+ Mount the docs endpoint and get a Scalar-based interactive UI plus raw OpenAPI JSON — all generated automatically from your routes.
510
175
 
511
- // Create router with middleware - types are automatically merged
512
- const router = createTypedRouterWithMiddleware(
513
- authMiddleware,
514
- timestampMiddleware
176
+ ```ts
177
+ app.use(
178
+ "/docs",
179
+ router.docs({
180
+ title: "My API",
181
+ version: "1.0.0",
182
+ description: "Public API docs",
183
+ specOutputPath: "./openapi.json", // write spec to disk (enables watch mode)
184
+ }),
515
185
  );
516
-
517
- router.get("/protected", (req, res) => {
518
- // req.userId, req.isAdmin, and req.timestamp are all typed correctly!
519
- res.json({
520
- userId: req.userId, // string
521
- isAdmin: req.isAdmin, // boolean
522
- timestamp: req.timestamp, // Date
523
- });
524
- });
525
- ```
526
-
527
- </details>
528
-
529
- <details>
530
- <summary><strong>⚙️ createTypedRouterWithConfig(config)</strong> - Custom configuration</summary>
531
-
532
- For applications that need custom error handling or configuration:
533
-
534
- ```typescript
535
- import { createTypedRouterWithConfig } from "@minisylar/express-typed-router";
536
-
537
- const router = createTypedRouterWithConfig({
538
- errorHandler: (error, req, res, next) => {
539
- if (error.name === "ZodError") {
540
- res.status(400).json({
541
- error: "Validation failed",
542
- details: error.errors,
543
- });
544
- } else {
545
- next(error);
546
- }
547
- },
548
- });
549
-
550
- // Use normally
551
- router.get("/users/:id", (req, res) => {
552
- // Custom error handling is automatically applied
553
- const { id } = req.params;
554
- res.json({ id });
555
- });
556
- ```
557
-
558
- </details>
559
-
560
- ## Express 4 & 5 Route Pattern Support
561
-
562
- This library provides **complete TypeScript inference** for all Express.js routing patterns across both Express 4 and 5:
563
-
564
- ### Basic Patterns (Express 4 & 5)
565
-
566
- ```typescript
567
- // Named parameters
568
- router.get("/users/:userId", handler);
569
- // → { userId: string }
570
-
571
- // Multiple parameters
572
- router.get("/users/:userId/posts/:postId", handler);
573
- // → { userId: string; postId: string }
574
-
575
- // Parameters with separators
576
- router.get("/flights/:from-:to", handler);
577
- // → { from: string; to: string }
578
-
579
- router.get("/files/:name.:ext", handler);
580
- // → { name: string; ext: string }
186
+ // GET /docs → Scalar UI
187
+ // GET /docs/openapi.json raw OpenAPI 3.1 spec
581
188
  ```
582
189
 
583
- ### Advanced Patterns (Express 4)
584
-
585
- ```typescript
586
- // Optional parameters
587
- router.get("/posts/:year/:month?", handler);
588
- // → { year: string; month?: string }
589
-
590
- // Regex constraints
591
- router.get("/users/:id(\\d+)", handler);
592
- // → { id: string }
593
-
594
- // Wildcards
595
- router.get("/files/*", handler);
596
- // → { "0": string }
597
-
598
- router.get("/api/*/files/*", handler);
599
- // → { "0": string; "1": string }
600
- ```
190
+ **What's generated automatically:**
601
191
 
602
- ### Express 5 Enhanced Patterns
192
+ - route paths, methods, and path parameters
193
+ - query and body schemas (from `querySchema` / `bodySchema`)
194
+ - response examples — captured from real `res.json()` calls, no manual input needed
195
+ - tags and summaries — inferred from route paths, or set manually
603
196
 
604
- ```typescript
605
- // Repeating parameters (one or more)
606
- router.get("/files/:path+", handler);
607
- // → { path: string[] }
197
+ **Custom route metadata:**
608
198
 
609
- // Optional repeating (zero or more)
610
- router.get("/search/:terms*", handler);
611
- // → { terms?: string[] }
612
-
613
- // Optional segments with braces
614
- router.get("/api{/:version}/users", handler);
615
- // → { version?: string }
616
-
617
- router.get("/files{/:category}/:filename", handler);
618
- // → { category?: string; filename: string }
619
- ```
620
-
621
- ### Real-World Examples
622
-
623
- ```typescript
624
- // E-commerce routes
625
- router.get("/products/:category/:subcategory?", handler);
626
- // → { category: string; subcategory?: string }
627
-
628
- // File serving with optional versioning
629
- router.get("/assets{/:version}/:filename.:ext", handler);
630
- // → { version?: string; filename: string; ext: string }
631
-
632
- // API versioning with wildcards
633
- router.get("/api/v:version/*", handler);
634
- // → { version: string; "0": string }
635
-
636
- // Multi-segment paths (Express 5)
637
- router.get("/docs/:sections+", handler);
638
- // → { sections: string[] }
639
- ```
640
-
641
- All patterns work seamlessly with Zod validation and middleware type inference!
642
-
643
- ## API Reference
644
-
645
- ### `createTypedRouter()` - Main API
646
-
647
- Creates a typed router instance with full flexibility for middleware and validation.
648
-
649
- ```typescript
650
- const router = createTypedRouter();
651
-
652
- // Add global middleware (chainable)
653
- router.useMiddleware(middleware1)
654
- .useMiddleware(middleware2);
655
-
656
- // All HTTP methods supported
657
- router.get(path, options?, handler)
658
- router.post(path, options?, handler)
659
- router.put(path, options?, handler)
660
- router.patch(path, options?, handler)
661
- router.delete(path, options?, handler)
662
- router.options(path, options?, handler)
663
- router.head(path, options?, handler)
664
- router.all(path, options?, handler)
665
- ```
666
-
667
- **Route Options:**
668
-
669
- - `bodySchema`: Zod schema for request body validation
670
- - `querySchema`: Zod schema for query parameter validation
671
- - `paramsSchema`: Zod schema for route parameter validation (optional - auto-inferred from path)
672
- - `middleware`: Array of typed middleware functions for this specific route
673
-
674
- **Examples:**
675
-
676
- ```typescript
677
- // Simple route with auto-inferred params
678
- router.get("/users/:id", (req, res) => {
679
- const { id } = req.params; // string
680
- });
681
-
682
- // With body validation
199
+ ```ts
683
200
  router.post(
684
201
  "/users",
685
202
  {
686
- bodySchema: z.object({ name: z.string() }),
687
- },
688
- (req, res) => {
689
- const { name } = req.body; // string
690
- }
691
- );
692
-
693
- // With per-route middleware
694
- router.get(
695
- "/admin",
696
- {
697
- middleware: [authMiddleware, adminMiddleware] as const,
203
+ bodySchema: CreateUserSchema,
204
+ responseSchema: UserSchema, // typed responses in the spec
205
+ tags: ["Users"],
206
+ summary: "Create a user",
207
+ description: "Creates a new account and returns the created user.",
698
208
  },
699
- (req, res) => {
700
- // Types from both middleware are available
701
- }
209
+ handler,
702
210
  );
703
211
  ```
704
212
 
705
- ### `TypedMiddleware<T>`
213
+ **Multi-router docs** — one `.docs()` call covers everything:
706
214
 
707
- Type for middleware functions that extend the request object.
215
+ ```ts
216
+ const api = createTypedRouter()
217
+ .use("/users", usersRouter)
218
+ .use("/auth", authRouter);
708
219
 
709
- ```typescript
710
- const authMiddleware: TypedMiddleware<{ userId: string }> = (
711
- req,
712
- res,
713
- next
714
- ) => {
715
- req.userId = "123";
716
- next();
717
- };
220
+ app.use("/docs", api.docs({ title: "My API", version: "1.0.0" }));
221
+ // Discovers all sub-routers and merges routes with correct prefixes
718
222
  ```
719
223
 
720
- <details>
721
- <summary><strong>Alternative APIs</strong></summary>
224
+ ### Schema library support for docs
722
225
 
723
- ### `createTypedRouterWithConfig(config)`
226
+ 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.
724
227
 
725
- ```typescript
726
- const router = createTypedRouterWithConfig({
727
- errorHandler: (error, req, res, next) => {
728
- // Custom error handling
729
- },
730
- });
731
- ```
228
+ | Library | Validation | Docs schema | Extra install |
229
+ |---|---|---|---|
230
+ | Zod 4 | | ✅ | none — built-in |
231
+ | Zod 3 | ✅ | ✅ | `zod-to-json-schema` |
232
+ | Valibot | ✅ | ✅ | `@valibot/to-json-schema` |
233
+ | ArkType | ✅ | ✅ | none — built-in |
234
+ | Effect | ✅ | ✅ | none — built-in |
235
+ | Yup | ✅ | ⚠️ | not supported — no official JSON Schema converter |
236
+ | Joi | ✅ | ⚠️ | not supported — no official JSON Schema converter |
237
+ | Decoders / ts.data.json / unhoax | ✅ | ⚠️ | not supported — no schema introspection |
732
238
 
733
- ### `createTypedRouterWithMiddleware(...middleware)`
239
+ > **⚠️ 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.
734
240
 
735
- ```typescript
736
- const router = createTypedRouterWithMiddleware(middleware1, middleware2);
737
- ```
241
+ ---
738
242
 
739
- </details>
243
+ ## Client types
740
244
 
741
- ## Development
245
+ The library generates an OpenAPI spec. Feed it to `openapi-typescript` to get a `.d.ts` file, then use it with any HTTP client.
742
246
 
743
- ```bash
744
- # Install dependencies
745
- pnpm install
247
+ ### Generate types
746
248
 
747
- # Build the library
748
- pnpm build
749
-
750
- # Run type checking
751
- pnpm type-check
249
+ One-time:
752
250
 
753
- # Build in watch mode
754
- pnpm build:watch
251
+ ```bash
252
+ npx openapi-typescript http://localhost:3000/docs/openapi.json -o ./generated-types.d.ts
755
253
  ```
756
254
 
757
- ## License
758
-
759
- ISC
760
-
761
- ## FAQ and Common Patterns
762
-
763
- ### Middleware Behavior Differences from Express
764
-
765
- #### IMPORTANT: Router Middleware and Route Registration
766
-
767
- When using middleware with `express-typed-router`, there's an important difference from standard Express behavior:
255
+ Watch mode — types regenerate automatically as routes change (requires `specOutputPath` set above):
768
256
 
769
- **In Express**, middleware added with `router.use()` applies to all routes registered _after_ it:
770
-
771
- ```javascript
772
- // Express middleware behavior
773
- const router = express.Router();
774
- router.use(authMiddleware); // Apply middleware
775
- router.get("/route1", handler1); // Has authMiddleware
776
- router.use(logMiddleware); // Apply another middleware
777
- router.get("/route2", handler2); // Has BOTH auth and log middleware
257
+ ```bash
258
+ npx openapi-typescript ./openapi.json -o ./generated-types.d.ts --watch
778
259
  ```
779
260
 
780
- **In express-typed-router**, `useMiddleware()` returns a _new router instance_ for type safety:
261
+ ### Use with `openapi-fetch`
781
262
 
782
- ```typescript
783
- // WON'T WORK - middleware not applied to route
784
- const router = createTypedRouter();
785
- router.useMiddleware(authMiddleware); // Returns new router that isn't captured
786
- router.get("/route", handler); // Original router without middleware!
263
+ ```ts
264
+ import createClient from "openapi-fetch";
265
+ import type { paths } from "./generated-types";
787
266
 
788
- // CORRECT - chain methods (recommended)
789
- const router = createTypedRouter()
790
- .useMiddleware(authMiddleware)
791
- .get("/route", handler);
267
+ const client = createClient<paths>({ baseUrl: "http://localhost:3000/api" });
792
268
 
793
- // CORRECT - chain directly from middleware call
794
- const router = createTypedRouter();
795
- router.useMiddleware(authMiddleware).get("/route", handler);
269
+ // Path, params, body, and response all typed from the spec
270
+ const { data } = await client.GET("/users/{id}", {
271
+ params: { path: { id: "123" } },
272
+ });
796
273
 
797
- // ALSO CORRECT - use per-route middleware
798
- const router = createTypedRouter();
799
- router.get("/route", { middleware: [authMiddleware] }, handler);
274
+ const { data: user } = await client.POST("/users", {
275
+ body: { name: "Alice", email: "alice@example.com" },
276
+ });
800
277
  ```
801
278
 
802
- This design is necessary for full type safety but requires a different pattern than standard Express.
279
+ ### Roll your own client
803
280
 
804
- ### Common Express Patterns vs express-typed-router
281
+ If you prefer not to add `openapi-fetch`, use the generated types directly with standard `fetch`:
805
282
 
806
- Here are common Express patterns and how to achieve them with express-typed-router:
283
+ ```ts
284
+ import type { paths } from "./generated-types";
807
285
 
808
- #### Pattern 1: Adding middleware to specific routes
286
+ type Body<
287
+ P extends keyof paths,
288
+ M extends keyof paths[P],
289
+ > = paths[P][M] extends {
290
+ requestBody?: { content: { "application/json": infer B } };
291
+ }
292
+ ? B
293
+ : never;
294
+
295
+ type Res<
296
+ P extends keyof paths,
297
+ M extends keyof paths[P],
298
+ > = paths[P][M] extends {
299
+ responses: { 200: { content: { "application/json": infer R } } };
300
+ }
301
+ ? R
302
+ : unknown;
303
+
304
+ async function apiFetch<P extends keyof paths, M extends keyof paths[P]>(
305
+ path: P,
306
+ options: {
307
+ method: M;
308
+ data?: Body<P, M>;
309
+ params?: {
310
+ path?: Record<string, string | number>;
311
+ query?: Record<string, string | number | boolean>;
312
+ };
313
+ },
314
+ ): Promise<Res<P, M>> {
315
+ const url = new URL(
316
+ String(path).replace(/\{([^}]+)\}/g, (_, key) =>
317
+ encodeURIComponent(String(options.params?.path?.[key] ?? "")),
318
+ ),
319
+ "/api",
320
+ );
321
+
322
+ for (const [k, v] of Object.entries(options.params?.query ?? {})) {
323
+ url.searchParams.set(k, String(v));
324
+ }
809
325
 
810
- **Express:**
326
+ const res = await fetch(url, {
327
+ method: String(options.method).toUpperCase(),
328
+ headers: options.data ? { "Content-Type": "application/json" } : undefined,
329
+ body: options.data ? JSON.stringify(options.data) : undefined,
330
+ });
811
331
 
812
- ```javascript
813
- const router = express.Router();
814
- router.get("/public", publicHandler);
815
- router.use(authMiddleware); // Only affects routes below
816
- router.get("/private", privateHandler); // Has authMiddleware
817
- ```
332
+ return res.json();
333
+ }
818
334
 
819
- **express-typed-router:**
335
+ // Path, method, body, and params all typed from the spec
336
+ await apiFetch("/users/{id}", {
337
+ method: "get",
338
+ params: { path: { id: "123" } },
339
+ });
340
+ await apiFetch("/users", {
341
+ method: "post",
342
+ data: { name: "Alice", email: "alice@example.com" },
343
+ });
344
+ await apiFetch("/search", { method: "get", params: { query: { q: "hello" } } });
345
+ ```
820
346
 
821
- ```typescript
822
- // Option 1: Separate routers
823
- const publicRouter = createTypedRouter();
824
- publicRouter.get("/public", publicHandler);
347
+ The same type utilities work with axios — swap `fetch` for `axios.request`.
825
348
 
826
- const privateRouter = createTypedRouter().useMiddleware(authMiddleware);
827
- privateRouter.get("/private", privateHandler);
349
+ ---
828
350
 
829
- // Combine in Express
830
- app.use(publicRouter.getRouter());
831
- app.use(privateRouter.getRouter());
351
+ ## Common patterns
832
352
 
833
- // Option 2: Per-route middleware
834
- const router = createTypedRouter();
835
- router.get("/public", publicHandler);
836
- router.get("/private", { middleware: [authMiddleware] }, privateHandler);
837
- ```
353
+ ### Migrate an existing Express app
838
354
 
839
- #### Pattern 2: Adding middleware for a group of routes
355
+ No rewrite required. Add typed routes alongside existing ones.
840
356
 
841
- **Express:**
357
+ ```diff
358
+ const app = express();
842
359
 
843
- ```javascript
844
- const router = express.Router();
845
- router.get("/public", handler);
360
+ + const typedRouter = createTypedRouter();
361
+ + typedRouter.get("/users/:id", (req, res) => {
362
+ + res.json({ id: req.params.id }); // typed
363
+ + });
846
364
 
847
- // Only admin routes have auth middleware
848
- const adminRouter = express.Router();
849
- adminRouter.use(authMiddleware);
850
- adminRouter.get("/users", adminHandler1);
851
- adminRouter.get("/settings", adminHandler2);
365
+ app.get("/health", (_req, res) => res.json({ ok: true })); // untouched
852
366
 
853
- router.use("/admin", adminRouter);
367
+ + app.use("/api", typedRouter.getRouter());
368
+ + app.use("/docs", typedRouter.docs());
854
369
  ```
855
370
 
856
- **express-typed-router:**
857
-
858
- ```typescript
859
- const publicRouter = createTypedRouter();
860
- publicRouter.get("/public", handler);
371
+ ### Middleware on a group of routes
861
372
 
862
- // Admin router with middleware
863
- const adminRouter = createTypedRouter().useMiddleware(authMiddleware);
864
- adminRouter.get("/users", adminHandler1);
865
- adminRouter.get("/settings", adminHandler2);
373
+ ```ts
374
+ // All admin routes share auth middleware and its types
375
+ const adminRouter = createTypedRouter()
376
+ .useMiddleware(requireAuth)
377
+ .get("/users", listUsersHandler)
378
+ .delete("/users/:id", deleteUserHandler);
866
379
 
867
- // Combine with Express
868
- app.use(publicRouter.getRouter());
869
380
  app.use("/admin", adminRouter.getRouter());
870
381
  ```
871
382
 
872
- #### Pattern 3: Middleware with dynamically added routes
383
+ ### Per-feature routers, one doc endpoint
873
384
 
874
- **Express:**
385
+ ```ts
386
+ import {usersRouter} from "./v1/usersRouter"
387
+ ....
875
388
 
876
- ```javascript
877
- const router = express.Router();
878
- router.use(middleware);
389
+ const api = createTypedRouter()
390
+ .use("/users", usersRouter)
391
+ .use("/orders", ordersRouter)
392
+ .use("/auth", authRouter);
879
393
 
880
- // Later, routes are added dynamically
881
- function addRoute(path, handler) {
882
- router.get(path, handler); // Has middleware
883
- }
394
+ app.use("/api/v1", api.getRouter());
395
+ app.use("/docs", api.docs({ title: "My API", version: "1.0.0" }));
884
396
  ```
885
397
 
886
- **express-typed-router:**
398
+ ---
887
399
 
888
- ```typescript
889
- // Option 1: Pass the router to the function
890
- const router = createTypedRouter().useMiddleware(middleware);
400
+ ## API surface
891
401
 
892
- function addRoute(router, path, handler) {
893
- router.get(path, handler);
894
- }
402
+ | | |
403
+ | ---------------------------------------- | ------------------------------------------------ |
404
+ | `createTypedRouter()` | Create a router |
405
+ | `createTypedRouterWithMiddleware(...mw)` | Create a router pre-configured with middleware |
406
+ | `createTypedRouterWithConfig(config)` | Create a router with custom error handling |
407
+ | `router.useMiddleware(mw)` | Add typed global middleware (returns new router) |
408
+ | `router.use(prefix, subRouter)` | Mount a sub-router |
409
+ | `router.getRouter()` | Get the underlying Express router |
410
+ | `router.docs(options)` | Get the docs + OpenAPI spec router |
411
+ | `TypedMiddleware<T>` | Type helper for middleware that extends `req` |
895
412
 
896
- // Option 2: Factory function
897
- function createRouteAdder(middleware) {
898
- const router = createTypedRouter().useMiddleware(middleware);
413
+ ---
899
414
 
900
- return {
901
- addRoute: (path, handler) => router.get(path, handler),
902
- getRouter: () => router.getRouter(),
903
- };
904
- }
415
+ ## Development
905
416
 
906
- const routeAdder = createRouteAdder(middleware);
907
- routeAdder.addRoute("/path", handler);
908
- app.use(routeAdder.getRouter());
417
+ ```bash
418
+ pnpm install
419
+ pnpm build
420
+ pnpm type-check
421
+ pnpm build:watch
909
422
  ```
910
423
 
911
- ### Using express-typed-router in JavaScript
912
-
913
- JavaScript users don't need to worry about TypeScript types but should still follow the middleware chaining pattern:
914
-
915
- ```javascript
916
- // JavaScript usage
917
- const { createTypedRouter } = require("@minisylar/express-typed-router");
424
+ ---
918
425
 
919
- const router = createTypedRouter().useMiddleware((req, res, next) => {
920
- req.user = { id: "user123" };
921
- next();
922
- });
923
-
924
- router.get("/users", (req, res) => {
925
- // req.user is available but not typed (JavaScript doesn't have types)
926
- res.json({ userId: req.user.id });
927
- });
928
-
929
- module.exports = router.getRouter();
930
- ```
931
-
932
- ## Contributing
426
+ ## License
933
427
 
934
- Contributions are welcome! Please feel free to submit a Pull Request.
428
+ ISC