@minisylar/express-typed-router 1.0.0 → 1.1.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 +702 -164
- package/dist/zod-router.cjs +4 -56
- package/dist/zod-router.cjs.map +1 -1
- package/dist/zod-router.d.cts +90 -68
- package/dist/zod-router.d.ts +90 -68
- package/dist/zod-router.js +2 -52
- package/dist/zod-router.js.map +1 -1
- package/package.json +6 -5
- package/dist/zod-router.d.cts.map +0 -1
- package/dist/zod-router.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,164 +1,702 @@
|
|
|
1
|
-
# @minisylar/express-typed-router
|
|
2
|
-
|
|
3
|
-
A strongly-typed Express router with Zod validation and automatic type inference for params, body, query, and middleware.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- 🚀 **Full TypeScript support** with automatic type inference for route parameters
|
|
8
|
-
- 🛡️ **Zod validation** for request body, query parameters, and route params
|
|
9
|
-
- 🔗 **Express.js compatibility** - works with Express 4 and Express 5
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
router
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
- **
|
|
116
|
-
- **
|
|
117
|
-
- **
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
1
|
+
# @minisylar/express-typed-router
|
|
2
|
+
|
|
3
|
+
A strongly-typed Express router with Zod validation and automatic type inference for params, body, query, and middleware.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🚀 **Full TypeScript support** with automatic type inference for route parameters
|
|
8
|
+
- 🛡️ **Zod validation** for request body, query parameters, and route params
|
|
9
|
+
- 🔗 **Express.js compatibility** - works with Express 4 and Express 5
|
|
10
|
+
- 🤝 **Mix with existing Express routes** - seamlessly integrates with your current codebase
|
|
11
|
+
- 📝 **JSDoc documentation** with comprehensive examples
|
|
12
|
+
- 📦 **ES Modules** and CommonJS support
|
|
13
|
+
- 🎯 **Zero runtime overhead** for type checking
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @minisylar/express-typed-router
|
|
19
|
+
# or
|
|
20
|
+
pnpm add @minisylar/express-typed-router
|
|
21
|
+
# or
|
|
22
|
+
yarn add @minisylar/express-typed-router
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
> **Note:** This package requires Express 4.18.0+ or Express 5.0.0+
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
import express from "express";
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
import { createTypedRouter } from "@minisylar/express-typed-router";
|
|
33
|
+
|
|
34
|
+
const app = express();
|
|
35
|
+
app.use(express.json());
|
|
36
|
+
|
|
37
|
+
// Create a typed router
|
|
38
|
+
const router = createTypedRouter();
|
|
39
|
+
|
|
40
|
+
// Define routes - parameters are automatically typed!
|
|
41
|
+
router.get("/users/:userId", (req, res) => {
|
|
42
|
+
// req.params.userId is automatically inferred as string
|
|
43
|
+
res.json({ userId: req.params.userId });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Add validation with Zod schemas
|
|
47
|
+
router.post(
|
|
48
|
+
"/users",
|
|
49
|
+
{
|
|
50
|
+
bodySchema: z.object({
|
|
51
|
+
name: z.string(),
|
|
52
|
+
email: z.string().email(),
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
(req, res) => {
|
|
56
|
+
// req.body is validated and typed automatically
|
|
57
|
+
const { name, email } = req.body;
|
|
58
|
+
res.json({ id: "123", name, email });
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const expressRouter = router.getRouter();
|
|
63
|
+
app.use("/api", expressRouter);
|
|
64
|
+
app.listen(3000);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
That's it! Your routes now have full type safety and validation.
|
|
68
|
+
|
|
69
|
+
## Works with Existing Express Routes
|
|
70
|
+
|
|
71
|
+
The typed router seamlessly integrates with your existing Express application - no need to rewrite everything!
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
import express from "express";
|
|
75
|
+
import { createTypedRouter } from "@minisylar/express-typed-router";
|
|
76
|
+
|
|
77
|
+
const app = express();
|
|
78
|
+
app.use(express.json());
|
|
79
|
+
|
|
80
|
+
// Your existing Express routes continue to work
|
|
81
|
+
app.get("/health", (req, res) => {
|
|
82
|
+
res.json({ status: "ok" });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Existing Express router
|
|
86
|
+
const legacyRouter = express.Router();
|
|
87
|
+
legacyRouter.get("/legacy/:id", (req, res) => {
|
|
88
|
+
res.json({ id: req.params.id });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// New typed router with full type safety
|
|
92
|
+
const typedRouter = createTypedRouter();
|
|
93
|
+
typedRouter.get("/users/:userId", (req, res) => {
|
|
94
|
+
// req.params.userId is automatically typed as string
|
|
95
|
+
res.json({ userId: req.params.userId });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Extract the Express router before using it
|
|
99
|
+
const typedExpressRouter = typedRouter.getRouter();
|
|
100
|
+
|
|
101
|
+
// Mix them all together
|
|
102
|
+
app.use("/api/legacy", legacyRouter);
|
|
103
|
+
app.use("/api/v2", typedExpressRouter);
|
|
104
|
+
|
|
105
|
+
// Gradually migrate your routes to get type safety where you need it!
|
|
106
|
+
app.listen(3000);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## The Main API: `createTypedRouter()`
|
|
110
|
+
|
|
111
|
+
`createTypedRouter()` is the primary and most flexible way to create typed routers. It supports:
|
|
112
|
+
|
|
113
|
+
- ✅ **Global middleware** with automatic type merging
|
|
114
|
+
- ✅ **Per-route middleware** with type inference
|
|
115
|
+
- ✅ **Zod validation** for params, body, and query
|
|
116
|
+
- ✅ **Express 4 & 5 compatibility** with full route pattern support
|
|
117
|
+
- ✅ **Chainable API** for easy configuration
|
|
118
|
+
|
|
119
|
+
### Global Middleware
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
const router = createTypedRouter()
|
|
123
|
+
.useTypedMiddleware(authMiddleware)
|
|
124
|
+
.useTypedMiddleware(loggingMiddleware)
|
|
125
|
+
.useTypedMiddleware(timestampMiddleware);
|
|
126
|
+
|
|
127
|
+
// All routes automatically get types from all middleware
|
|
128
|
+
router.get("/protected", (req, res) => {
|
|
129
|
+
// req.userId, req.requestId, req.timestamp all available and typed
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Per-Route Middleware
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
router.get(
|
|
137
|
+
"/admin/:userId",
|
|
138
|
+
{
|
|
139
|
+
middleware: [adminMiddleware, auditMiddleware] as const,
|
|
140
|
+
},
|
|
141
|
+
(req, res) => {
|
|
142
|
+
// Types from both global AND per-route middleware are merged
|
|
143
|
+
// req.userId (global), req.isAdmin (adminMiddleware), req.auditId (auditMiddleware)
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Express 4 & 5 Route Pattern Support
|
|
149
|
+
|
|
150
|
+
Works with **all** Express routing patterns:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Named parameters
|
|
154
|
+
router.get("/users/:userId", handler); // { userId: string }
|
|
155
|
+
|
|
156
|
+
// Multiple parameters
|
|
157
|
+
router.get("/users/:userId/posts/:postId", handler); // { userId: string; postId: string }
|
|
158
|
+
|
|
159
|
+
// Consecutive parameters with separators
|
|
160
|
+
router.get("/flights/:from-:to", handler); // { from: string; to: string }
|
|
161
|
+
router.get("/files/:name.:ext", handler); // { name: string; ext: string }
|
|
162
|
+
|
|
163
|
+
// Optional parameters (Express 4)
|
|
164
|
+
router.get("/posts/:year/:month?", handler); // { year: string; month?: string }
|
|
165
|
+
|
|
166
|
+
// Repeating parameters (Express 5)
|
|
167
|
+
router.get("/files/:path+", handler); // { path: string[] }
|
|
168
|
+
|
|
169
|
+
// Optional repeating (Express 5)
|
|
170
|
+
router.get("/search/:terms*", handler); // { terms?: string[] }
|
|
171
|
+
|
|
172
|
+
// Optional segments (Express 5)
|
|
173
|
+
router.get("/api{/:version}/users", handler); // { version?: string }
|
|
174
|
+
|
|
175
|
+
// Regex constraints
|
|
176
|
+
router.get("/users/:id(\\d+)", handler); // { id: string }
|
|
177
|
+
|
|
178
|
+
// Wildcards
|
|
179
|
+
router.get("/static/*", handler); // { "0": string }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Comprehensive Example
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
import express from "express";
|
|
186
|
+
import { z } from "zod";
|
|
187
|
+
import { createTypedRouter } from "@minisylar/express-typed-router";
|
|
188
|
+
|
|
189
|
+
const app = express();
|
|
190
|
+
app.use(express.json());
|
|
191
|
+
|
|
192
|
+
// Create router and define middleware inline (automatically typed!)
|
|
193
|
+
const router = createTypedRouter()
|
|
194
|
+
.useTypedMiddleware((req, res, next) => {
|
|
195
|
+
const token = req.headers.authorization;
|
|
196
|
+
req.userId = "user123";
|
|
197
|
+
req.isAdmin = token?.includes("admin") || false;
|
|
198
|
+
next();
|
|
199
|
+
})
|
|
200
|
+
.useTypedMiddleware((req, res, next) => {
|
|
201
|
+
req.requestId = Math.random().toString(36);
|
|
202
|
+
console.log(`[${req.requestId}] ${req.method} ${req.path}`);
|
|
203
|
+
next();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Define schemas
|
|
207
|
+
const CreateUserSchema = z.object({
|
|
208
|
+
name: z.string().min(1),
|
|
209
|
+
email: z.string().email(),
|
|
210
|
+
role: z.enum(["user", "admin"]).optional(),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const UserQuerySchema = z.object({
|
|
214
|
+
include: z.array(z.string()).optional(),
|
|
215
|
+
limit: z.coerce.number().int().positive().max(100).default(10),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Routes with full type safety
|
|
219
|
+
router.get(
|
|
220
|
+
"/users/:userId",
|
|
221
|
+
{
|
|
222
|
+
querySchema: UserQuerySchema,
|
|
223
|
+
},
|
|
224
|
+
(req, res) => {
|
|
225
|
+
// All properties are automatically typed:
|
|
226
|
+
const { userId } = req.params; // string (auto-inferred from route)
|
|
227
|
+
const { include, limit } = req.query; // from schema validation
|
|
228
|
+
const { userId: authUserId, isAdmin, requestId } = req; // from middleware
|
|
229
|
+
|
|
230
|
+
res.json({
|
|
231
|
+
id: userId,
|
|
232
|
+
authUserId,
|
|
233
|
+
isAdmin,
|
|
234
|
+
requestId,
|
|
235
|
+
include,
|
|
236
|
+
limit,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
router.post(
|
|
242
|
+
"/users",
|
|
243
|
+
{
|
|
244
|
+
bodySchema: CreateUserSchema,
|
|
245
|
+
},
|
|
246
|
+
(req, res) => {
|
|
247
|
+
const { name, email, role } = req.body; // Fully typed from schema
|
|
248
|
+
const { userId, isAdmin, requestId } = req; // From middleware
|
|
249
|
+
|
|
250
|
+
if (role === "admin" && !isAdmin) {
|
|
251
|
+
return res.status(403).json({ error: "Insufficient permissions" });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
res.status(201).json({
|
|
255
|
+
id: "new-user-id",
|
|
256
|
+
name,
|
|
257
|
+
email,
|
|
258
|
+
role: role || "user",
|
|
259
|
+
createdBy: userId,
|
|
260
|
+
requestId,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// Per-route middleware can also be inline
|
|
266
|
+
router.delete(
|
|
267
|
+
"/users/:userId",
|
|
268
|
+
{
|
|
269
|
+
middleware: [(req, res, next) => {
|
|
270
|
+
if (!req.isAdmin) {
|
|
271
|
+
return res.status(403).json({ error: "Admin required" });
|
|
272
|
+
}
|
|
273
|
+
req.hasAdminAccess = true;
|
|
274
|
+
next();
|
|
275
|
+
}] as const,
|
|
276
|
+
},
|
|
277
|
+
(req, res) => {
|
|
278
|
+
// Types from BOTH global middleware AND per-route middleware
|
|
279
|
+
const { userId } = req.params; // From route
|
|
280
|
+
const { userId: authUserId, requestId } = req; // From global middleware
|
|
281
|
+
const { hasAdminAccess } = req; // From per-route middleware
|
|
282
|
+
|
|
283
|
+
res.json({
|
|
284
|
+
deleted: userId,
|
|
285
|
+
deletedBy: authUserId,
|
|
286
|
+
requestId,
|
|
287
|
+
hasAdminAccess,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
app.use("/api", router.getRouter());
|
|
293
|
+
app.listen(3000, () => {
|
|
294
|
+
console.log("Server running on http://localhost:3000");
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### With Explicit TypeScript Types
|
|
299
|
+
|
|
300
|
+
For TypeScript users who prefer explicit typing, you can define middleware with generics:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { TypedMiddleware } from "@minisylar/express-typed-router";
|
|
304
|
+
|
|
305
|
+
// Define typed middleware explicitly
|
|
306
|
+
const authMiddleware: TypedMiddleware<{ userId: string; isAdmin: boolean }> = (
|
|
307
|
+
req,
|
|
308
|
+
res,
|
|
309
|
+
next
|
|
310
|
+
) => {
|
|
311
|
+
const token = req.headers.authorization;
|
|
312
|
+
req.userId = "user123";
|
|
313
|
+
req.isAdmin = token?.includes("admin") || false;
|
|
314
|
+
next();
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const loggingMiddleware: TypedMiddleware<{ requestId: string }> = (
|
|
318
|
+
req,
|
|
319
|
+
res,
|
|
320
|
+
next
|
|
321
|
+
) => {
|
|
322
|
+
req.requestId = Math.random().toString(36);
|
|
323
|
+
console.log(`[${req.requestId}] ${req.method} ${req.path}`);
|
|
324
|
+
next();
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Use the explicitly typed middleware
|
|
328
|
+
const router = createTypedRouter()
|
|
329
|
+
.useTypedMiddleware(authMiddleware)
|
|
330
|
+
.useTypedMiddleware(loggingMiddleware);
|
|
331
|
+
|
|
332
|
+
// Rest of the routes work the same way...
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## TypeScript Features
|
|
336
|
+
|
|
337
|
+
For TypeScript users, the library provides advanced type safety features:
|
|
338
|
+
|
|
339
|
+
### Typed Middleware
|
|
340
|
+
|
|
341
|
+
Define middleware that extends the request object with typed properties:
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
import { TypedMiddleware } from "@minisylar/express-typed-router";
|
|
345
|
+
|
|
346
|
+
const authMiddleware: TypedMiddleware<{ userId: string; isAdmin: boolean }> = (
|
|
347
|
+
req,
|
|
348
|
+
res,
|
|
349
|
+
next
|
|
350
|
+
) => {
|
|
351
|
+
req.userId = "user123";
|
|
352
|
+
req.isAdmin = true;
|
|
353
|
+
next();
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// Add to router - types are automatically merged
|
|
357
|
+
router.useTypedMiddleware(authMiddleware);
|
|
358
|
+
|
|
359
|
+
router.get("/protected", (req, res) => {
|
|
360
|
+
// TypeScript knows about req.userId and req.isAdmin
|
|
361
|
+
const { userId, isAdmin } = req;
|
|
362
|
+
res.json({ userId, isAdmin });
|
|
363
|
+
});
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Per-Route Middleware with Type Merging
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
const adminMiddleware: TypedMiddleware<{ hasAdminAccess: true }> = (
|
|
370
|
+
req,
|
|
371
|
+
res,
|
|
372
|
+
next
|
|
373
|
+
) => {
|
|
374
|
+
if (!req.isAdmin) {
|
|
375
|
+
return res.status(403).json({ error: "Admin required" });
|
|
376
|
+
}
|
|
377
|
+
req.hasAdminAccess = true;
|
|
378
|
+
next();
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
router.get(
|
|
382
|
+
"/admin/:userId",
|
|
383
|
+
{
|
|
384
|
+
middleware: [adminMiddleware] as const,
|
|
385
|
+
},
|
|
386
|
+
(req, res) => {
|
|
387
|
+
// Types from BOTH global and per-route middleware are available
|
|
388
|
+
const { userId } = req.params; // From route params
|
|
389
|
+
const { userId: authUserId } = req; // From global middleware
|
|
390
|
+
const { hasAdminAccess } = req; // From per-route middleware
|
|
391
|
+
|
|
392
|
+
res.json({ userId, authUserId, hasAdminAccess });
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Advanced Route Parameter Types
|
|
398
|
+
|
|
399
|
+
The library automatically infers complex Express route patterns:
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
// Express 5 repeating parameters
|
|
403
|
+
router.get("/files/:path+", (req, res) => {
|
|
404
|
+
const { path } = req.params; // string[] - automatically inferred!
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Optional parameters
|
|
408
|
+
router.get("/posts/:year/:month?", (req, res) => {
|
|
409
|
+
const { year, month } = req.params; // { year: string; month?: string }
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Complex patterns with separators
|
|
413
|
+
router.get("/flights/:from-:to", (req, res) => {
|
|
414
|
+
const { from, to } = req.params; // { from: string; to: string }
|
|
415
|
+
});
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Alternative API Styles
|
|
419
|
+
|
|
420
|
+
<details>
|
|
421
|
+
<summary><strong>🎯 createTypedRouterWithMiddleware(...middleware)</strong> - Pre-configured with middleware</summary>
|
|
422
|
+
|
|
423
|
+
For developers who prefer setting up all middleware upfront:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
import {
|
|
427
|
+
createTypedRouterWithMiddleware,
|
|
428
|
+
TypedMiddleware,
|
|
429
|
+
} from "@minisylar/express-typed-router";
|
|
430
|
+
|
|
431
|
+
const authMiddleware: TypedMiddleware<{ userId: string; isAdmin: boolean }> = (
|
|
432
|
+
req,
|
|
433
|
+
res,
|
|
434
|
+
next
|
|
435
|
+
) => {
|
|
436
|
+
req.userId = "user123";
|
|
437
|
+
req.isAdmin = true;
|
|
438
|
+
next();
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const timestampMiddleware: TypedMiddleware<{ timestamp: Date }> = (
|
|
442
|
+
req,
|
|
443
|
+
res,
|
|
444
|
+
next
|
|
445
|
+
) => {
|
|
446
|
+
req.timestamp = new Date();
|
|
447
|
+
next();
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// Create router with middleware - types are automatically merged
|
|
451
|
+
const router = createTypedRouterWithMiddleware(
|
|
452
|
+
authMiddleware,
|
|
453
|
+
timestampMiddleware
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
router.get("/protected", (req, res) => {
|
|
457
|
+
// req.userId, req.isAdmin, and req.timestamp are all typed correctly!
|
|
458
|
+
res.json({
|
|
459
|
+
userId: req.userId, // string
|
|
460
|
+
isAdmin: req.isAdmin, // boolean
|
|
461
|
+
timestamp: req.timestamp, // Date
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
</details>
|
|
467
|
+
|
|
468
|
+
<details>
|
|
469
|
+
<summary><strong>⚙️ createTypedRouterWithConfig(config)</strong> - Custom configuration</summary>
|
|
470
|
+
|
|
471
|
+
For applications that need custom error handling or configuration:
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
import { createTypedRouterWithConfig } from "@minisylar/express-typed-router";
|
|
475
|
+
|
|
476
|
+
const router = createTypedRouterWithConfig({
|
|
477
|
+
errorHandler: (error, req, res, next) => {
|
|
478
|
+
if (error.name === "ZodError") {
|
|
479
|
+
res.status(400).json({
|
|
480
|
+
error: "Validation failed",
|
|
481
|
+
details: error.errors,
|
|
482
|
+
});
|
|
483
|
+
} else {
|
|
484
|
+
next(error);
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// Use normally
|
|
490
|
+
router.get("/users/:id", (req, res) => {
|
|
491
|
+
// Custom error handling is automatically applied
|
|
492
|
+
const { id } = req.params;
|
|
493
|
+
res.json({ id });
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
</details>
|
|
498
|
+
|
|
499
|
+
## Express 4 & 5 Route Pattern Support
|
|
500
|
+
|
|
501
|
+
This library provides **complete TypeScript inference** for all Express.js routing patterns across both Express 4 and 5:
|
|
502
|
+
|
|
503
|
+
### Basic Patterns (Express 4 & 5)
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
// Named parameters
|
|
507
|
+
router.get("/users/:userId", handler);
|
|
508
|
+
// → { userId: string }
|
|
509
|
+
|
|
510
|
+
// Multiple parameters
|
|
511
|
+
router.get("/users/:userId/posts/:postId", handler);
|
|
512
|
+
// → { userId: string; postId: string }
|
|
513
|
+
|
|
514
|
+
// Parameters with separators
|
|
515
|
+
router.get("/flights/:from-:to", handler);
|
|
516
|
+
// → { from: string; to: string }
|
|
517
|
+
|
|
518
|
+
router.get("/files/:name.:ext", handler);
|
|
519
|
+
// → { name: string; ext: string }
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Advanced Patterns (Express 4)
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
// Optional parameters
|
|
526
|
+
router.get("/posts/:year/:month?", handler);
|
|
527
|
+
// → { year: string; month?: string }
|
|
528
|
+
|
|
529
|
+
// Regex constraints
|
|
530
|
+
router.get("/users/:id(\\d+)", handler);
|
|
531
|
+
// → { id: string }
|
|
532
|
+
|
|
533
|
+
// Wildcards
|
|
534
|
+
router.get("/files/*", handler);
|
|
535
|
+
// → { "0": string }
|
|
536
|
+
|
|
537
|
+
router.get("/api/*/files/*", handler);
|
|
538
|
+
// → { "0": string; "1": string }
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### Express 5 Enhanced Patterns
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
// Repeating parameters (one or more)
|
|
545
|
+
router.get("/files/:path+", handler);
|
|
546
|
+
// → { path: string[] }
|
|
547
|
+
|
|
548
|
+
// Optional repeating (zero or more)
|
|
549
|
+
router.get("/search/:terms*", handler);
|
|
550
|
+
// → { terms?: string[] }
|
|
551
|
+
|
|
552
|
+
// Optional segments with braces
|
|
553
|
+
router.get("/api{/:version}/users", handler);
|
|
554
|
+
// → { version?: string }
|
|
555
|
+
|
|
556
|
+
router.get("/files{/:category}/:filename", handler);
|
|
557
|
+
// → { category?: string; filename: string }
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### Real-World Examples
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
// E-commerce routes
|
|
564
|
+
router.get("/products/:category/:subcategory?", handler);
|
|
565
|
+
// → { category: string; subcategory?: string }
|
|
566
|
+
|
|
567
|
+
// File serving with optional versioning
|
|
568
|
+
router.get("/assets{/:version}/:filename.:ext", handler);
|
|
569
|
+
// → { version?: string; filename: string; ext: string }
|
|
570
|
+
|
|
571
|
+
// API versioning with wildcards
|
|
572
|
+
router.get("/api/v:version/*", handler);
|
|
573
|
+
// → { version: string; "0": string }
|
|
574
|
+
|
|
575
|
+
// Multi-segment paths (Express 5)
|
|
576
|
+
router.get("/docs/:sections+", handler);
|
|
577
|
+
// → { sections: string[] }
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
All patterns work seamlessly with Zod validation and middleware type inference!
|
|
581
|
+
|
|
582
|
+
## API Reference
|
|
583
|
+
|
|
584
|
+
### `createTypedRouter()` - Main API
|
|
585
|
+
|
|
586
|
+
Creates a typed router instance with full flexibility for middleware and validation.
|
|
587
|
+
|
|
588
|
+
```typescript
|
|
589
|
+
const router = createTypedRouter();
|
|
590
|
+
|
|
591
|
+
// Add global middleware (chainable)
|
|
592
|
+
router.useTypedMiddleware(middleware1)
|
|
593
|
+
.useTypedMiddleware(middleware2);
|
|
594
|
+
|
|
595
|
+
// All HTTP methods supported
|
|
596
|
+
router.get(path, options?, handler)
|
|
597
|
+
router.post(path, options?, handler)
|
|
598
|
+
router.put(path, options?, handler)
|
|
599
|
+
router.patch(path, options?, handler)
|
|
600
|
+
router.delete(path, options?, handler)
|
|
601
|
+
router.options(path, options?, handler)
|
|
602
|
+
router.head(path, options?, handler)
|
|
603
|
+
router.all(path, options?, handler)
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
**Route Options:**
|
|
607
|
+
|
|
608
|
+
- `bodySchema`: Zod schema for request body validation
|
|
609
|
+
- `querySchema`: Zod schema for query parameter validation
|
|
610
|
+
- `paramsSchema`: Zod schema for route parameter validation (optional - auto-inferred from path)
|
|
611
|
+
- `middleware`: Array of typed middleware functions for this specific route
|
|
612
|
+
|
|
613
|
+
**Examples:**
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
// Simple route with auto-inferred params
|
|
617
|
+
router.get("/users/:id", (req, res) => {
|
|
618
|
+
const { id } = req.params; // string
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// With body validation
|
|
622
|
+
router.post(
|
|
623
|
+
"/users",
|
|
624
|
+
{
|
|
625
|
+
bodySchema: z.object({ name: z.string() }),
|
|
626
|
+
},
|
|
627
|
+
(req, res) => {
|
|
628
|
+
const { name } = req.body; // string
|
|
629
|
+
}
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
// With per-route middleware
|
|
633
|
+
router.get(
|
|
634
|
+
"/admin",
|
|
635
|
+
{
|
|
636
|
+
middleware: [authMiddleware, adminMiddleware] as const,
|
|
637
|
+
},
|
|
638
|
+
(req, res) => {
|
|
639
|
+
// Types from both middleware are available
|
|
640
|
+
}
|
|
641
|
+
);
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### `TypedMiddleware<T>`
|
|
645
|
+
|
|
646
|
+
Type for middleware functions that extend the request object.
|
|
647
|
+
|
|
648
|
+
```typescript
|
|
649
|
+
const authMiddleware: TypedMiddleware<{ userId: string }> = (
|
|
650
|
+
req,
|
|
651
|
+
res,
|
|
652
|
+
next
|
|
653
|
+
) => {
|
|
654
|
+
req.userId = "123";
|
|
655
|
+
next();
|
|
656
|
+
};
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
<details>
|
|
660
|
+
<summary><strong>Alternative APIs</strong></summary>
|
|
661
|
+
|
|
662
|
+
### `createTypedRouterWithConfig(config)`
|
|
663
|
+
|
|
664
|
+
```typescript
|
|
665
|
+
const router = createTypedRouterWithConfig({
|
|
666
|
+
errorHandler: (error, req, res, next) => {
|
|
667
|
+
// Custom error handling
|
|
668
|
+
},
|
|
669
|
+
});
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
### `createTypedRouterWithMiddleware(...middleware)`
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
const router = createTypedRouterWithMiddleware(middleware1, middleware2);
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
</details>
|
|
679
|
+
|
|
680
|
+
## Development
|
|
681
|
+
|
|
682
|
+
```bash
|
|
683
|
+
# Install dependencies
|
|
684
|
+
pnpm install
|
|
685
|
+
|
|
686
|
+
# Build the library
|
|
687
|
+
pnpm build
|
|
688
|
+
|
|
689
|
+
# Run type checking
|
|
690
|
+
pnpm type-check
|
|
691
|
+
|
|
692
|
+
# Build in watch mode
|
|
693
|
+
pnpm build:watch
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
## License
|
|
697
|
+
|
|
698
|
+
ISC
|
|
699
|
+
|
|
700
|
+
## Contributing
|
|
701
|
+
|
|
702
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|