@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 +268 -774
- package/dist/schema-router.cjs +12 -1
- package/dist/schema-router.d.cts +274 -146
- package/dist/{schema-router.d.ts → schema-router.d.mts} +274 -146
- 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,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
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
The typed router seamlessly integrates with your existing Express application - no need to rewrite everything!
|
|
62
|
+
---
|
|
119
63
|
|
|
120
|
-
|
|
121
|
-
import express from "express";
|
|
122
|
-
import { createTypedRouter } from "@minisylar/express-typed-router";
|
|
64
|
+
## Route typing
|
|
123
65
|
|
|
124
|
-
|
|
125
|
-
app.use(express.json());
|
|
66
|
+
Path params are inferred from the route string. No extra types needed.
|
|
126
67
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
+
---
|
|
485
171
|
|
|
486
|
-
|
|
487
|
-
import {
|
|
488
|
-
createTypedRouterWithMiddleware,
|
|
489
|
-
TypedMiddleware,
|
|
490
|
-
} from "@minisylar/express-typed-router";
|
|
172
|
+
## ✨ OpenAPI and docs
|
|
491
173
|
|
|
492
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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 }
|
|
186
|
+
// GET /docs → Scalar UI
|
|
187
|
+
// GET /docs/openapi.json → raw OpenAPI 3.1 spec
|
|
581
188
|
```
|
|
582
189
|
|
|
583
|
-
|
|
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
|
-
|
|
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
|
-
|
|
605
|
-
// Repeating parameters (one or more)
|
|
606
|
-
router.get("/files/:path+", handler);
|
|
607
|
-
// → { path: string[] }
|
|
197
|
+
**Custom route metadata:**
|
|
608
198
|
|
|
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
|
-
```
|
|
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:
|
|
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
|
-
};
|
|
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
|
-
|
|
721
|
-
<summary><strong>Alternative APIs</strong></summary>
|
|
224
|
+
### Schema library support for docs
|
|
722
225
|
|
|
723
|
-
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
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
|
-
|
|
736
|
-
const router = createTypedRouterWithMiddleware(middleware1, middleware2);
|
|
737
|
-
```
|
|
241
|
+
---
|
|
738
242
|
|
|
739
|
-
|
|
243
|
+
## Client types
|
|
740
244
|
|
|
741
|
-
|
|
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
|
-
|
|
744
|
-
# Install dependencies
|
|
745
|
-
pnpm install
|
|
247
|
+
### Generate types
|
|
746
248
|
|
|
747
|
-
|
|
748
|
-
pnpm build
|
|
749
|
-
|
|
750
|
-
# Run type checking
|
|
751
|
-
pnpm type-check
|
|
249
|
+
One-time:
|
|
752
250
|
|
|
753
|
-
|
|
754
|
-
|
|
251
|
+
```bash
|
|
252
|
+
npx openapi-typescript http://localhost:3000/docs/openapi.json -o ./generated-types.d.ts
|
|
755
253
|
```
|
|
756
254
|
|
|
757
|
-
|
|
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
|
-
|
|
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
|
-
|
|
261
|
+
### Use with `openapi-fetch`
|
|
781
262
|
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
789
|
-
const router = createTypedRouter()
|
|
790
|
-
.useMiddleware(authMiddleware)
|
|
791
|
-
.get("/route", handler);
|
|
267
|
+
const client = createClient<paths>({ baseUrl: "http://localhost:3000/api" });
|
|
792
268
|
|
|
793
|
-
//
|
|
794
|
-
const
|
|
795
|
-
|
|
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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
274
|
+
const { data: user } = await client.POST("/users", {
|
|
275
|
+
body: { name: "Alice", email: "alice@example.com" },
|
|
276
|
+
});
|
|
800
277
|
```
|
|
801
278
|
|
|
802
|
-
|
|
279
|
+
### Roll your own client
|
|
803
280
|
|
|
804
|
-
|
|
281
|
+
If you prefer not to add `openapi-fetch`, use the generated types directly with standard `fetch`:
|
|
805
282
|
|
|
806
|
-
|
|
283
|
+
```ts
|
|
284
|
+
import type { paths } from "./generated-types";
|
|
807
285
|
|
|
808
|
-
|
|
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
|
-
|
|
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
|
-
|
|
813
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
827
|
-
privateRouter.get("/private", privateHandler);
|
|
349
|
+
---
|
|
828
350
|
|
|
829
|
-
|
|
830
|
-
app.use(publicRouter.getRouter());
|
|
831
|
-
app.use(privateRouter.getRouter());
|
|
351
|
+
## Common patterns
|
|
832
352
|
|
|
833
|
-
|
|
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
|
-
|
|
355
|
+
No rewrite required. Add typed routes alongside existing ones.
|
|
840
356
|
|
|
841
|
-
|
|
357
|
+
```diff
|
|
358
|
+
const app = express();
|
|
842
359
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
360
|
+
+ const typedRouter = createTypedRouter();
|
|
361
|
+
+ typedRouter.get("/users/:id", (req, res) => {
|
|
362
|
+
+ res.json({ id: req.params.id }); // typed
|
|
363
|
+
+ });
|
|
846
364
|
|
|
847
|
-
|
|
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
|
-
|
|
367
|
+
+ app.use("/api", typedRouter.getRouter());
|
|
368
|
+
+ app.use("/docs", typedRouter.docs());
|
|
854
369
|
```
|
|
855
370
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
```typescript
|
|
859
|
-
const publicRouter = createTypedRouter();
|
|
860
|
-
publicRouter.get("/public", handler);
|
|
371
|
+
### Middleware on a group of routes
|
|
861
372
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
adminRouter
|
|
865
|
-
|
|
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
|
-
|
|
383
|
+
### Per-feature routers, one doc endpoint
|
|
873
384
|
|
|
874
|
-
|
|
385
|
+
```ts
|
|
386
|
+
import {usersRouter} from "./v1/usersRouter"
|
|
387
|
+
....
|
|
875
388
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
389
|
+
const api = createTypedRouter()
|
|
390
|
+
.use("/users", usersRouter)
|
|
391
|
+
.use("/orders", ordersRouter)
|
|
392
|
+
.use("/auth", authRouter);
|
|
879
393
|
|
|
880
|
-
|
|
881
|
-
|
|
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
|
-
|
|
398
|
+
---
|
|
887
399
|
|
|
888
|
-
|
|
889
|
-
// Option 1: Pass the router to the function
|
|
890
|
-
const router = createTypedRouter().useMiddleware(middleware);
|
|
400
|
+
## API surface
|
|
891
401
|
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
|
|
897
|
-
function createRouteAdder(middleware) {
|
|
898
|
-
const router = createTypedRouter().useMiddleware(middleware);
|
|
413
|
+
---
|
|
899
414
|
|
|
900
|
-
|
|
901
|
-
addRoute: (path, handler) => router.get(path, handler),
|
|
902
|
-
getRouter: () => router.getRouter(),
|
|
903
|
-
};
|
|
904
|
-
}
|
|
415
|
+
## Development
|
|
905
416
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
417
|
+
```bash
|
|
418
|
+
pnpm install
|
|
419
|
+
pnpm build
|
|
420
|
+
pnpm type-check
|
|
421
|
+
pnpm build:watch
|
|
909
422
|
```
|
|
910
423
|
|
|
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");
|
|
424
|
+
---
|
|
918
425
|
|
|
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
|
|
426
|
+
## License
|
|
933
427
|
|
|
934
|
-
|
|
428
|
+
ISC
|