@minisylar/express-typed-router 1.6.2 → 1.7.1
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 +269 -774
- package/dist/schema-router.cjs +12 -1
- package/dist/schema-router.d.cts +276 -145
- package/dist/{schema-router.d.ts → schema-router.d.mts} +276 -145
- package/dist/schema-router.mjs +12 -0
- package/package.json +18 -13
- package/dist/schema-router.js +0 -1
package/README.md
CHANGED
|
@@ -1,79 +1,36 @@
|
|
|
1
1
|
# @minisylar/express-typed-router
|
|
2
2
|
|
|
3
|
-
A strongly
|
|
3
|
+
A strongly typed Express router with **Standard Schema** validation, automatic type inference, and ✨ OpenAPI docs.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Define routes once, infer `params` / `body` / `query`, and generate a clean API spec for docs or client generation.
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
##
|
|
9
|
+
## What you get
|
|
18
10
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
18
|
+
---
|
|
30
19
|
|
|
31
|
-
|
|
20
|
+
## Install
|
|
32
21
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
31
|
+
## Quick start
|
|
75
32
|
|
|
76
|
-
```
|
|
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,392 @@ 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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
res.json({ id: "123", name, email });
|
|
106
|
-
}
|
|
51
|
+
res.json({ name: req.body.name }); // body.name: string
|
|
52
|
+
},
|
|
107
53
|
);
|
|
108
54
|
|
|
109
|
-
|
|
110
|
-
app.use("/
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
## Works with Existing Express Routes
|
|
62
|
+
---
|
|
117
63
|
|
|
118
|
-
|
|
64
|
+
## Route typing
|
|
119
65
|
|
|
120
|
-
|
|
121
|
-
import express from "express";
|
|
122
|
-
import { createTypedRouter } from "@minisylar/express-typed-router";
|
|
66
|
+
Path params are inferred from the route string. No extra types needed.
|
|
123
67
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
84
|
+
Schema options infer body and query:
|
|
193
85
|
|
|
194
|
-
```
|
|
86
|
+
```ts
|
|
195
87
|
router.get(
|
|
196
|
-
"/
|
|
197
|
-
{
|
|
198
|
-
|
|
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
|
-
//
|
|
202
|
-
|
|
203
|
-
}
|
|
99
|
+
req.body.name; // string
|
|
100
|
+
req.body.email; // string
|
|
101
|
+
},
|
|
204
102
|
);
|
|
205
103
|
```
|
|
206
104
|
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
354
|
-
app.listen(3000, () => {
|
|
355
|
-
console.log("Server running on http://localhost:3000");
|
|
356
|
-
});
|
|
357
|
-
```
|
|
122
|
+
---
|
|
358
123
|
|
|
359
|
-
|
|
124
|
+
## Middleware typing
|
|
360
125
|
|
|
361
|
-
|
|
126
|
+
Declare what a middleware adds to `req`, and that type flows into every handler that uses it.
|
|
362
127
|
|
|
363
|
-
```
|
|
364
|
-
import { TypedMiddleware } from "@minisylar/express-typed-router";
|
|
128
|
+
```ts
|
|
129
|
+
import type { TypedMiddleware } from "@minisylar/express-typed-router";
|
|
365
130
|
|
|
366
|
-
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
+
```ts
|
|
389
149
|
const router = createTypedRouter()
|
|
390
|
-
.useMiddleware(
|
|
150
|
+
.useMiddleware(requireAuth)
|
|
391
151
|
.useMiddleware(loggingMiddleware);
|
|
392
152
|
|
|
393
|
-
|
|
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
|
-
|
|
159
|
+
**Per-route middleware** — scoped to one route, types still merge:
|
|
428
160
|
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
req
|
|
432
|
-
|
|
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:
|
|
461
|
-
|
|
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
|
-
|
|
168
|
+
> **Note:** `useMiddleware` returns a new router instance. Use method chaining or capture the return value — see [Common Patterns](#common-patterns).
|
|
480
169
|
|
|
481
|
-
|
|
482
|
-
<summary><strong>🎯 createTypedRouterWithMiddleware(...middleware)</strong> - Pre-configured with middleware</summary>
|
|
170
|
+
---
|
|
483
171
|
|
|
484
|
-
|
|
172
|
+
## ✨ OpenAPI and docs
|
|
485
173
|
|
|
486
|
-
|
|
487
|
-
import {
|
|
488
|
-
createTypedRouterWithMiddleware,
|
|
489
|
-
TypedMiddleware,
|
|
490
|
-
} from "@minisylar/express-typed-router";
|
|
174
|
+
Mount the docs endpoint and get a Scalar-based interactive UI plus raw OpenAPI JSON — all generated automatically from your routes.
|
|
491
175
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const timestampMiddleware: TypedMiddleware<{ timestamp: Date }> = (
|
|
503
|
-
req,
|
|
504
|
-
res,
|
|
505
|
-
next
|
|
506
|
-
) => {
|
|
507
|
-
req.timestamp = new Date();
|
|
508
|
-
next();
|
|
509
|
-
};
|
|
510
|
-
|
|
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
|
-
|
|
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 }
|
|
581
|
-
```
|
|
582
|
-
|
|
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 }
|
|
186
|
+
// GET /docs → Scalar UI
|
|
187
|
+
// GET /docs/openapi.json → raw OpenAPI 3.1 spec
|
|
600
188
|
```
|
|
601
189
|
|
|
602
|
-
|
|
190
|
+
**What's generated automatically:**
|
|
603
191
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
|
608
196
|
|
|
609
|
-
|
|
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
|
-
```
|
|
197
|
+
**Custom route metadata:**
|
|
640
198
|
|
|
641
|
-
|
|
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:
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
700
|
-
// Types from both middleware are available
|
|
701
|
-
}
|
|
209
|
+
handler,
|
|
702
210
|
);
|
|
703
211
|
```
|
|
704
212
|
|
|
705
|
-
|
|
213
|
+
**Multi-router docs** — one `.docs()` call covers everything:
|
|
706
214
|
|
|
707
|
-
|
|
215
|
+
```ts
|
|
216
|
+
const api = createTypedRouter()
|
|
217
|
+
.use("/users", usersRouter)
|
|
218
|
+
.use("/auth", authRouter);
|
|
708
219
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
req,
|
|
712
|
-
res,
|
|
713
|
-
next
|
|
714
|
-
) => {
|
|
715
|
-
req.userId = "123";
|
|
716
|
-
next();
|
|
717
|
-
};
|
|
718
|
-
```
|
|
719
|
-
|
|
720
|
-
<details>
|
|
721
|
-
<summary><strong>Alternative APIs</strong></summary>
|
|
722
|
-
|
|
723
|
-
### `createTypedRouterWithConfig(config)`
|
|
220
|
+
app.use("/docs", api.docs({ title: "My API", version: "1.0.0" }));
|
|
221
|
+
// Discovers all sub-routers and merges routes with correct prefixes
|
|
724
222
|
|
|
725
|
-
```typescript
|
|
726
|
-
const router = createTypedRouterWithConfig({
|
|
727
|
-
errorHandler: (error, req, res, next) => {
|
|
728
|
-
// Custom error handling
|
|
729
|
-
},
|
|
730
|
-
});
|
|
731
223
|
```
|
|
732
224
|
|
|
733
|
-
###
|
|
225
|
+
### Schema library support for docs
|
|
734
226
|
|
|
735
|
-
|
|
736
|
-
const router = createTypedRouterWithMiddleware(middleware1, middleware2);
|
|
737
|
-
```
|
|
227
|
+
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.
|
|
738
228
|
|
|
739
|
-
|
|
229
|
+
| Library | Validation | Docs schema | Extra install |
|
|
230
|
+
|---|---|---|---|
|
|
231
|
+
| Zod 4 | ✅ | ✅ | none — built-in |
|
|
232
|
+
| Zod 3 | ✅ | ✅ | `zod-to-json-schema` |
|
|
233
|
+
| Valibot | ✅ | ✅ | `@valibot/to-json-schema` |
|
|
234
|
+
| ArkType | ✅ | ✅ | none — built-in |
|
|
235
|
+
| Effect | ✅ | ✅ | none — built-in |
|
|
236
|
+
| Yup | ✅ | ⚠️ | not supported — no official JSON Schema converter |
|
|
237
|
+
| Joi | ✅ | ⚠️ | not supported — no official JSON Schema converter |
|
|
238
|
+
| Decoders / ts.data.json / unhoax | ✅ | ⚠️ | not supported — no schema introspection |
|
|
740
239
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
```bash
|
|
744
|
-
# Install dependencies
|
|
745
|
-
pnpm install
|
|
746
|
-
|
|
747
|
-
# Build the library
|
|
748
|
-
pnpm build
|
|
749
|
-
|
|
750
|
-
# Run type checking
|
|
751
|
-
pnpm type-check
|
|
752
|
-
|
|
753
|
-
# Build in watch mode
|
|
754
|
-
pnpm build:watch
|
|
755
|
-
```
|
|
240
|
+
> **⚠️ 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.
|
|
756
241
|
|
|
757
|
-
|
|
242
|
+
---
|
|
758
243
|
|
|
759
|
-
|
|
244
|
+
## Client types
|
|
760
245
|
|
|
761
|
-
|
|
246
|
+
The library generates an OpenAPI spec. Feed it to `openapi-typescript` to get a `.d.ts` file, then use it with any HTTP client.
|
|
762
247
|
|
|
763
|
-
###
|
|
248
|
+
### Generate types
|
|
764
249
|
|
|
765
|
-
|
|
250
|
+
One-time:
|
|
766
251
|
|
|
767
|
-
|
|
252
|
+
```bash
|
|
253
|
+
npx openapi-typescript http://localhost:3000/docs/openapi.json -o ./generated-types.d.ts
|
|
254
|
+
```
|
|
768
255
|
|
|
769
|
-
|
|
256
|
+
Watch mode — types regenerate automatically as routes change (requires `specOutputPath` set above):
|
|
770
257
|
|
|
771
|
-
```
|
|
772
|
-
|
|
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
|
|
258
|
+
```bash
|
|
259
|
+
npx openapi-typescript ./openapi.json -o ./generated-types.d.ts --watch
|
|
778
260
|
```
|
|
779
261
|
|
|
780
|
-
|
|
262
|
+
### Use with `openapi-fetch`
|
|
781
263
|
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
router.useMiddleware(authMiddleware); // Returns new router that isn't captured
|
|
786
|
-
router.get("/route", handler); // Original router without middleware!
|
|
264
|
+
```ts
|
|
265
|
+
import createClient from "openapi-fetch";
|
|
266
|
+
import type { paths } from "./generated-types";
|
|
787
267
|
|
|
788
|
-
|
|
789
|
-
const router = createTypedRouter()
|
|
790
|
-
.useMiddleware(authMiddleware)
|
|
791
|
-
.get("/route", handler);
|
|
268
|
+
const client = createClient<paths>({ baseUrl: "http://localhost:3000/api" });
|
|
792
269
|
|
|
793
|
-
//
|
|
794
|
-
const
|
|
795
|
-
|
|
270
|
+
// Path, params, body, and response all typed from the spec
|
|
271
|
+
const { data } = await client.GET("/users/{id}", {
|
|
272
|
+
params: { path: { id: "123" } },
|
|
273
|
+
});
|
|
796
274
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
275
|
+
const { data: user } = await client.POST("/users", {
|
|
276
|
+
body: { name: "Alice", email: "alice@example.com" },
|
|
277
|
+
});
|
|
800
278
|
```
|
|
801
279
|
|
|
802
|
-
|
|
280
|
+
### Roll your own client
|
|
803
281
|
|
|
804
|
-
|
|
282
|
+
If you prefer not to add `openapi-fetch`, use the generated types directly with standard `fetch`:
|
|
805
283
|
|
|
806
|
-
|
|
284
|
+
```ts
|
|
285
|
+
import type { paths } from "./generated-types";
|
|
807
286
|
|
|
808
|
-
|
|
287
|
+
type Body<
|
|
288
|
+
P extends keyof paths,
|
|
289
|
+
M extends keyof paths[P],
|
|
290
|
+
> = paths[P][M] extends {
|
|
291
|
+
requestBody?: { content: { "application/json": infer B } };
|
|
292
|
+
}
|
|
293
|
+
? B
|
|
294
|
+
: never;
|
|
295
|
+
|
|
296
|
+
type Res<
|
|
297
|
+
P extends keyof paths,
|
|
298
|
+
M extends keyof paths[P],
|
|
299
|
+
> = paths[P][M] extends {
|
|
300
|
+
responses: { 200: { content: { "application/json": infer R } } };
|
|
301
|
+
}
|
|
302
|
+
? R
|
|
303
|
+
: unknown;
|
|
304
|
+
|
|
305
|
+
async function apiFetch<P extends keyof paths, M extends keyof paths[P]>(
|
|
306
|
+
path: P,
|
|
307
|
+
options: {
|
|
308
|
+
method: M;
|
|
309
|
+
data?: Body<P, M>;
|
|
310
|
+
params?: {
|
|
311
|
+
path?: Record<string, string | number>;
|
|
312
|
+
query?: Record<string, string | number | boolean>;
|
|
313
|
+
};
|
|
314
|
+
},
|
|
315
|
+
): Promise<Res<P, M>> {
|
|
316
|
+
const url = new URL(
|
|
317
|
+
String(path).replace(/\{([^}]+)\}/g, (_, key) =>
|
|
318
|
+
encodeURIComponent(String(options.params?.path?.[key] ?? "")),
|
|
319
|
+
),
|
|
320
|
+
"/api",
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
for (const [k, v] of Object.entries(options.params?.query ?? {})) {
|
|
324
|
+
url.searchParams.set(k, String(v));
|
|
325
|
+
}
|
|
809
326
|
|
|
810
|
-
|
|
327
|
+
const res = await fetch(url, {
|
|
328
|
+
method: String(options.method).toUpperCase(),
|
|
329
|
+
headers: options.data ? { "Content-Type": "application/json" } : undefined,
|
|
330
|
+
body: options.data ? JSON.stringify(options.data) : undefined,
|
|
331
|
+
});
|
|
811
332
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
router.get("/public", publicHandler);
|
|
815
|
-
router.use(authMiddleware); // Only affects routes below
|
|
816
|
-
router.get("/private", privateHandler); // Has authMiddleware
|
|
817
|
-
```
|
|
333
|
+
return res.json();
|
|
334
|
+
}
|
|
818
335
|
|
|
819
|
-
|
|
336
|
+
// Path, method, body, and params all typed from the spec
|
|
337
|
+
await apiFetch("/users/{id}", {
|
|
338
|
+
method: "get",
|
|
339
|
+
params: { path: { id: "123" } },
|
|
340
|
+
});
|
|
341
|
+
await apiFetch("/users", {
|
|
342
|
+
method: "post",
|
|
343
|
+
data: { name: "Alice", email: "alice@example.com" },
|
|
344
|
+
});
|
|
345
|
+
await apiFetch("/search", { method: "get", params: { query: { q: "hello" } } });
|
|
346
|
+
```
|
|
820
347
|
|
|
821
|
-
|
|
822
|
-
// Option 1: Separate routers
|
|
823
|
-
const publicRouter = createTypedRouter();
|
|
824
|
-
publicRouter.get("/public", publicHandler);
|
|
348
|
+
The same type utilities work with axios — swap `fetch` for `axios.request`.
|
|
825
349
|
|
|
826
|
-
|
|
827
|
-
privateRouter.get("/private", privateHandler);
|
|
350
|
+
---
|
|
828
351
|
|
|
829
|
-
|
|
830
|
-
app.use(publicRouter.getRouter());
|
|
831
|
-
app.use(privateRouter.getRouter());
|
|
352
|
+
## Common patterns
|
|
832
353
|
|
|
833
|
-
|
|
834
|
-
const router = createTypedRouter();
|
|
835
|
-
router.get("/public", publicHandler);
|
|
836
|
-
router.get("/private", { middleware: [authMiddleware] }, privateHandler);
|
|
837
|
-
```
|
|
354
|
+
### Migrate an existing Express app
|
|
838
355
|
|
|
839
|
-
|
|
356
|
+
No rewrite required. Add typed routes alongside existing ones.
|
|
840
357
|
|
|
841
|
-
|
|
358
|
+
```diff
|
|
359
|
+
const app = express();
|
|
842
360
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
361
|
+
+ const typedRouter = createTypedRouter();
|
|
362
|
+
+ typedRouter.get("/users/:id", (req, res) => {
|
|
363
|
+
+ res.json({ id: req.params.id }); // typed
|
|
364
|
+
+ });
|
|
846
365
|
|
|
847
|
-
|
|
848
|
-
const adminRouter = express.Router();
|
|
849
|
-
adminRouter.use(authMiddleware);
|
|
850
|
-
adminRouter.get("/users", adminHandler1);
|
|
851
|
-
adminRouter.get("/settings", adminHandler2);
|
|
366
|
+
app.get("/health", (_req, res) => res.json({ ok: true })); // untouched
|
|
852
367
|
|
|
853
|
-
|
|
368
|
+
+ app.use("/api", typedRouter.getRouter());
|
|
369
|
+
+ app.use("/docs", typedRouter.docs());
|
|
854
370
|
```
|
|
855
371
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
```typescript
|
|
859
|
-
const publicRouter = createTypedRouter();
|
|
860
|
-
publicRouter.get("/public", handler);
|
|
372
|
+
### Middleware on a group of routes
|
|
861
373
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
adminRouter
|
|
865
|
-
|
|
374
|
+
```ts
|
|
375
|
+
// All admin routes share auth middleware and its types
|
|
376
|
+
const adminRouter = createTypedRouter()
|
|
377
|
+
.useMiddleware(requireAuth)
|
|
378
|
+
.get("/users", listUsersHandler)
|
|
379
|
+
.delete("/users/:id", deleteUserHandler);
|
|
866
380
|
|
|
867
|
-
// Combine with Express
|
|
868
|
-
app.use(publicRouter.getRouter());
|
|
869
381
|
app.use("/admin", adminRouter.getRouter());
|
|
870
382
|
```
|
|
871
383
|
|
|
872
|
-
|
|
384
|
+
### Per-feature routers, one doc endpoint
|
|
873
385
|
|
|
874
|
-
|
|
386
|
+
```ts
|
|
387
|
+
import {usersRouter} from "./v1/usersRouter"
|
|
388
|
+
....
|
|
875
389
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
390
|
+
const api = createTypedRouter()
|
|
391
|
+
.use("/users", usersRouter)
|
|
392
|
+
.use("/orders", ordersRouter)
|
|
393
|
+
.use("/auth", authRouter);
|
|
879
394
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
router.get(path, handler); // Has middleware
|
|
883
|
-
}
|
|
395
|
+
app.use("/api/v1", api.getRouter());
|
|
396
|
+
app.use("/docs", api.docs({ title: "My API", version: "1.0.0" }));
|
|
884
397
|
```
|
|
885
398
|
|
|
886
|
-
|
|
399
|
+
---
|
|
887
400
|
|
|
888
|
-
|
|
889
|
-
// Option 1: Pass the router to the function
|
|
890
|
-
const router = createTypedRouter().useMiddleware(middleware);
|
|
401
|
+
## API surface
|
|
891
402
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
403
|
+
| | |
|
|
404
|
+
| ---------------------------------------- | ------------------------------------------------ |
|
|
405
|
+
| `createTypedRouter()` | Create a router |
|
|
406
|
+
| `createTypedRouterWithMiddleware(...mw)` | Create a router pre-configured with middleware |
|
|
407
|
+
| `createTypedRouterWithConfig(config)` | Create a router with custom error handling |
|
|
408
|
+
| `router.useMiddleware(mw)` | Add typed global middleware (returns new router) |
|
|
409
|
+
| `router.use(prefix, subRouter)` | Mount a sub-router |
|
|
410
|
+
| `router.getRouter()` | Get the underlying Express router |
|
|
411
|
+
| `router.docs(options)` | Get the docs + OpenAPI spec router |
|
|
412
|
+
| `TypedMiddleware<T>` | Type helper for middleware that extends `req` |
|
|
895
413
|
|
|
896
|
-
|
|
897
|
-
function createRouteAdder(middleware) {
|
|
898
|
-
const router = createTypedRouter().useMiddleware(middleware);
|
|
414
|
+
---
|
|
899
415
|
|
|
900
|
-
|
|
901
|
-
addRoute: (path, handler) => router.get(path, handler),
|
|
902
|
-
getRouter: () => router.getRouter(),
|
|
903
|
-
};
|
|
904
|
-
}
|
|
416
|
+
## Development
|
|
905
417
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
418
|
+
```bash
|
|
419
|
+
pnpm install
|
|
420
|
+
pnpm build
|
|
421
|
+
pnpm type-check
|
|
422
|
+
pnpm build:watch
|
|
909
423
|
```
|
|
910
424
|
|
|
911
|
-
|
|
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");
|
|
425
|
+
---
|
|
918
426
|
|
|
919
|
-
|
|
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
|
|
427
|
+
## License
|
|
933
428
|
|
|
934
|
-
|
|
429
|
+
ISC
|