@visulima/connect 1.0.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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +607 -0
- package/dist/index.d.ts +141 -0
- package/dist/index.js +335 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +335 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +126 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## @visulima/connect 1.0.0 (2022-10-25)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* added connect as new package for Next.js, Micro, Vercel, or Node.js ([0d8a646](https://github.com/visulima/visulima/commit/0d8a646c6cbfffe884aabdaf3ed7c0a6087db1f1))
|
|
7
|
+
* **connect:** added connect as new package for Next.js, Micro, Vercel, or Node.js (public) ([bdeac5c](https://github.com/visulima/visulima/commit/bdeac5cf65750af44217c6a6496898a31097185e))
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 visulima
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h3>Visulima connect</h3>
|
|
3
|
+
<p>
|
|
4
|
+
The promise-based method routing and middleware layer with zod validation for <a href="https://nextjs.org/" title="Next.js">Next.js</a> (API Routes, Edge API Routes, getServerSideProps, Middleware) and many other frameworks is built on top of
|
|
5
|
+
|
|
6
|
+
[next-connect](https://github.com/hoangvvo/next-connect),
|
|
7
|
+
[http-errors](https://github.com/jshttp/http-errors),
|
|
8
|
+
[regexparam](https://github.com/lukeed/regexparam)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<br />
|
|
15
|
+
|
|
16
|
+
<div align="center">
|
|
17
|
+
|
|
18
|
+
[![typescript-image]][typescript-url] [![npm-image]][npm-url] [![license-image]][license-url] [![synk-image]][synk-url]
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div align="center">
|
|
23
|
+
<sub>Built with ❤︎ by <a href="https://twitter.com/_prisis_">Daniel Bannert</a></sub>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- Async middleware
|
|
29
|
+
- [Lightweight](https://bundlephobia.com/scan-results?packages=express,@visulima/connect,koa,micro) => Suitable for serverless environment
|
|
30
|
+
- [way faster](https://github.com/visulima/packages/connect/tree/main/bench) than Express.js. Compatible with Express.js via [a wrapper](#expressjs-compatibility).
|
|
31
|
+
- Works with async handlers (with error catching)
|
|
32
|
+
- TypeScript support
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm install @visulima/connect
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
yarn add @visulima/connect
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
pnpm add @visulima/connect
|
|
46
|
+
```
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
> **Note**
|
|
50
|
+
>
|
|
51
|
+
> Although `@visulima/connect` is initially written for Next.js, it can be used in [http server](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener), [Vercel](https://vercel.com/docs/concepts/functions/serverless-functions). See [Examples](./examples/) for more integrations.
|
|
52
|
+
|
|
53
|
+
Below are some use cases.
|
|
54
|
+
|
|
55
|
+
### Next.js API Routes
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// pages/api/hello.js
|
|
59
|
+
import type { NextApiRequest, NextApiResponse } from "next";
|
|
60
|
+
import { createRouter, expressWrapper } from "@visulima/connect";
|
|
61
|
+
import cors from "cors";
|
|
62
|
+
|
|
63
|
+
// Default Req and Res are IncomingMessage and ServerResponse
|
|
64
|
+
// You may want to pass in NextApiRequest and NextApiResponse
|
|
65
|
+
const router = createRouter<NextApiRequest, NextApiResponse>({
|
|
66
|
+
onError: (err, req, res) => {
|
|
67
|
+
console.error(err.stack);
|
|
68
|
+
res.status(500).end("Something broke!");
|
|
69
|
+
},
|
|
70
|
+
onNoMatch: (req, res) => {
|
|
71
|
+
res.status(404).end("Page is not found");
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
router
|
|
76
|
+
.use(expressWrapper(cors())) // express middleware are supported if you wrap it with expressWrapper
|
|
77
|
+
.use(async (req, res, next) => {
|
|
78
|
+
const start = Date.now();
|
|
79
|
+
await next(); // call next in chain
|
|
80
|
+
const end = Date.now();
|
|
81
|
+
console.log(`Request took ${end - start}ms`);
|
|
82
|
+
})
|
|
83
|
+
.get((req, res) => {
|
|
84
|
+
res.send("Hello world");
|
|
85
|
+
})
|
|
86
|
+
.post(async (req, res) => {
|
|
87
|
+
// use async/await
|
|
88
|
+
const user = await insertUser(req.body.user);
|
|
89
|
+
res.json({ user });
|
|
90
|
+
})
|
|
91
|
+
.put(
|
|
92
|
+
async (req, res, next) => {
|
|
93
|
+
// You may want to pass in NextApiRequest & { isLoggedIn: true }
|
|
94
|
+
// in createRouter generics to define this extra property
|
|
95
|
+
if (!req.isLoggedIn) throw new Error("thrown stuff will be caught");
|
|
96
|
+
// go to the next in chain
|
|
97
|
+
return next();
|
|
98
|
+
},
|
|
99
|
+
async (req, res) => {
|
|
100
|
+
const user = await updateUser(req.body.user);
|
|
101
|
+
res.json({ user });
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// create a handler from router with custom
|
|
106
|
+
// onError and onNoMatch
|
|
107
|
+
export default router.handler();
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Next.js getServerSideProps
|
|
111
|
+
|
|
112
|
+
```jsx
|
|
113
|
+
// page/users/[id].js
|
|
114
|
+
import { createRouter } from "@visulima/connect";
|
|
115
|
+
|
|
116
|
+
export default function Page({ user, updated }) {
|
|
117
|
+
return (
|
|
118
|
+
<div>
|
|
119
|
+
{updated && <p>User has been updated</p>}
|
|
120
|
+
<div>{JSON.stringify(user)}</div>
|
|
121
|
+
<form method="POST">{/* User update form */}</form>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const router = createRouter()
|
|
127
|
+
.use(async (req, res, next) => {
|
|
128
|
+
// this serve as the error handling middleware
|
|
129
|
+
try {
|
|
130
|
+
return await next();
|
|
131
|
+
} catch (e) {
|
|
132
|
+
return {
|
|
133
|
+
props: { error: e.message },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
.use(async (req, res, next) => {
|
|
138
|
+
logRequest(req);
|
|
139
|
+
return next();
|
|
140
|
+
})
|
|
141
|
+
.get(async (req, res) => {
|
|
142
|
+
const user = await getUser(req.params.id);
|
|
143
|
+
if (!user) {
|
|
144
|
+
// https://nextjs.org/docs/api-reference/data-fetching/get-server-side-props#notfound
|
|
145
|
+
return { props: { notFound: true } };
|
|
146
|
+
}
|
|
147
|
+
return { props: { user } };
|
|
148
|
+
})
|
|
149
|
+
.post(async (req, res) => {
|
|
150
|
+
const user = await updateUser(req);
|
|
151
|
+
return { props: { user, updated: true } };
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
export async function getServerSideProps({ req, res }) {
|
|
155
|
+
return router.run(req, res);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Next.js Edge API Routes (Beta)
|
|
160
|
+
|
|
161
|
+
Edge Router can be used in [Edge Runtime](https://nextjs.org/docs/api-reference/edge-runtime)
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import type { NextFetchEvent, NextRequest } from "next/server";
|
|
165
|
+
import { createEdgeRouter } from "@visulima/connect";
|
|
166
|
+
import cors from "cors";
|
|
167
|
+
|
|
168
|
+
// Default Req and Evt are Request and unknown
|
|
169
|
+
// You may want to pass in NextRequest and NextFetchEvent
|
|
170
|
+
const router = createEdgeRouter<NextRequest, NextFetchEvent>({
|
|
171
|
+
onError: (err, req, evt) => {
|
|
172
|
+
console.error(err.stack);
|
|
173
|
+
return new NextResponse("Something broke!", {
|
|
174
|
+
status: 500,
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
onNoMatch: (req, res) => {
|
|
178
|
+
return new NextResponse("Page is not found", {
|
|
179
|
+
status: 404,
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
router
|
|
185
|
+
.use(expressWrapper(cors())) // express middleware are supported if you wrap it with expressWrapper
|
|
186
|
+
.use(async (req, evt, next) => {
|
|
187
|
+
const start = Date.now();
|
|
188
|
+
await next(); // call next in chain
|
|
189
|
+
const end = Date.now();
|
|
190
|
+
console.log(`Request took ${end - start}ms`);
|
|
191
|
+
})
|
|
192
|
+
.get((req, res) => {
|
|
193
|
+
return new Response("Hello world");
|
|
194
|
+
})
|
|
195
|
+
.post(async (req, res) => {
|
|
196
|
+
// use async/await
|
|
197
|
+
const user = await insertUser(req.body.user);
|
|
198
|
+
res.json({ user });
|
|
199
|
+
return new Response(JSON.stringify({ user }), {
|
|
200
|
+
status: 200,
|
|
201
|
+
headers: {
|
|
202
|
+
"content-type": "application/json",
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
})
|
|
206
|
+
.put(async (req, res) => {
|
|
207
|
+
const user = await updateUser(req.body.user);
|
|
208
|
+
return new Response(JSON.stringify({ user }), {
|
|
209
|
+
status: 200,
|
|
210
|
+
headers: {
|
|
211
|
+
"content-type": "application/json",
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
export default router.handler();
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Next.js Middleware
|
|
220
|
+
|
|
221
|
+
Edge Router can be used in [Next.js Middleware](https://nextjs.org/docs/advanced-features/middleware)
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
// middleware.ts
|
|
225
|
+
import { NextResponse } from "next/server";
|
|
226
|
+
import type { NextRequest, NextFetchEvent } from "next/server";
|
|
227
|
+
import { createEdgeRouter } from "@visulima/connect";
|
|
228
|
+
|
|
229
|
+
// This function can be marked `async` if using `await` inside
|
|
230
|
+
|
|
231
|
+
const router = createEdgeRouter<NextRequest, NextFetchEvent>();
|
|
232
|
+
|
|
233
|
+
router.use(async (request, _, next) => {
|
|
234
|
+
await logRequest(request);
|
|
235
|
+
return next();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
router.get("/about", (request) => {
|
|
239
|
+
return NextResponse.redirect(new URL("/about-2", request.url));
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
router.use("/dashboard", (request) => {
|
|
243
|
+
if (!isAuthenticated(request)) {
|
|
244
|
+
return NextResponse.redirect(new URL("/login", request.url));
|
|
245
|
+
}
|
|
246
|
+
return NextResponse.next();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
router.all((request) => {
|
|
250
|
+
// default if none of the above matches
|
|
251
|
+
return NextResponse.next();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
export function middleware(request: NextRequest) {
|
|
255
|
+
return NextResponse.redirect(new URL("/about-2", request.url));
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## API
|
|
260
|
+
|
|
261
|
+
The following APIs are rewritten in terms of `NodeRouter` (`createRouter`), but they apply to `EdgeRouter` (`createEdgeRouter`) as well.
|
|
262
|
+
|
|
263
|
+
### router = createRouter()
|
|
264
|
+
|
|
265
|
+
Create an instance Node.js router.
|
|
266
|
+
|
|
267
|
+
### router.use(base, ...fn)
|
|
268
|
+
|
|
269
|
+
`base` (optional) - match all routes to the right of `base` or match all if omitted. (Note: If used in Next.js, this is often omitted)
|
|
270
|
+
|
|
271
|
+
`fn`(s) can either be:
|
|
272
|
+
|
|
273
|
+
- functions of `(req, res[, next])`
|
|
274
|
+
- **or** a router instance
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
// Mount a middleware function
|
|
278
|
+
router1.use(async (req, res, next) => {
|
|
279
|
+
req.hello = "world";
|
|
280
|
+
await next(); // call to proceed to the next in chain
|
|
281
|
+
console.log("request is done"); // call after all downstream handler has run
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Or include a base
|
|
285
|
+
router2.use("/foo", fn); // Only run in /foo/**
|
|
286
|
+
|
|
287
|
+
// mount an instance of router
|
|
288
|
+
const sub1 = createRouter().use(fn1, fn2);
|
|
289
|
+
const sub2 = createRouter().use("/dashboard", auth);
|
|
290
|
+
const sub3 = createRouter()
|
|
291
|
+
.use("/waldo", subby)
|
|
292
|
+
.get(getty)
|
|
293
|
+
.post("/baz", posty)
|
|
294
|
+
.put("/", putty);
|
|
295
|
+
router3
|
|
296
|
+
// - fn1 and fn2 always run
|
|
297
|
+
// - auth runs only on /dashboard
|
|
298
|
+
.use(sub1, sub2)
|
|
299
|
+
// `subby` runs on ANY /foo/waldo?/*
|
|
300
|
+
// `getty` runs on GET /foo/*
|
|
301
|
+
// `posty` runs on POST /foo/baz
|
|
302
|
+
// `putty` runs on PUT /foo
|
|
303
|
+
.use("/foo", sub3);
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### router.METHOD(pattern, ...fns)
|
|
307
|
+
|
|
308
|
+
`METHOD` is an HTTP method (`GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`, `TRACE`) in lowercase.
|
|
309
|
+
|
|
310
|
+
`pattern` (optional) - match routes based on [supported pattern](https://github.com/lukeed/regexparam#regexparam-) or match any if omitted.
|
|
311
|
+
|
|
312
|
+
`fn`(s) are functions of `(req, res[, next])`.
|
|
313
|
+
|
|
314
|
+
```javascript
|
|
315
|
+
router.get("/api/user", (req, res, next) => {
|
|
316
|
+
res.json(req.user);
|
|
317
|
+
});
|
|
318
|
+
router.post("/api/users", (req, res, next) => {
|
|
319
|
+
res.end("User created");
|
|
320
|
+
});
|
|
321
|
+
router.put("/api/user/:id", (req, res, next) => {
|
|
322
|
+
// https://nextjs.org/docs/routing/dynamic-routes
|
|
323
|
+
res.end(`User ${req.params.id} updated`);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Next.js already handles routing (including dynamic routes), we often
|
|
327
|
+
// omit `pattern` in `.METHOD`
|
|
328
|
+
router.get((req, res, next) => {
|
|
329
|
+
res.end("This matches whatever route");
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
> **Note**
|
|
334
|
+
> You should understand Next.js [file-system based routing](https://nextjs.org/docs/routing/introduction). For example, having a `router.put("/api/foo", handler)` inside `page/api/index.js` _does not_ serve that handler at `/api/foo`.
|
|
335
|
+
|
|
336
|
+
### router.all(pattern, ...fns)
|
|
337
|
+
|
|
338
|
+
Same as [.METHOD](#methodpattern-fns) but accepts _any_ methods.
|
|
339
|
+
|
|
340
|
+
### router.handler(options)
|
|
341
|
+
|
|
342
|
+
Create a handler to handle incoming requests.
|
|
343
|
+
|
|
344
|
+
**options.onError**
|
|
345
|
+
|
|
346
|
+
Accepts a function as a catch-all error handler; executed whenever a handler throws an error.
|
|
347
|
+
By default, it responds with a generic `500 Internal Server Error` while logging the error to `console`.
|
|
348
|
+
|
|
349
|
+
```javascript
|
|
350
|
+
function onError(err, req, res) {
|
|
351
|
+
logger.log(err);
|
|
352
|
+
// OR: console.error(err);
|
|
353
|
+
|
|
354
|
+
res.status(500).end("Internal server error");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const router = createRouter({ onError });
|
|
358
|
+
|
|
359
|
+
export default router.handler();
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**options.onNoMatch**
|
|
363
|
+
|
|
364
|
+
Accepts a function of `(req, res)` as a handler when no route is matched.
|
|
365
|
+
By default, it responds with a `404` status and a `Route [Method] [Url] not found` body.
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
function onNoMatch(req, res) {
|
|
369
|
+
res.status(404).end("page is not found... or is it!?");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const router = createRouter({ onNoMatch });
|
|
373
|
+
|
|
374
|
+
export default router.handler();
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### router.run(req, res)
|
|
378
|
+
|
|
379
|
+
Runs `req` and `res` through the middleware chain and returns a **promise**. It resolves with the value returned from handlers.
|
|
380
|
+
|
|
381
|
+
```js
|
|
382
|
+
router
|
|
383
|
+
.use(async (req, res, next) => {
|
|
384
|
+
return (await next()) + 1;
|
|
385
|
+
})
|
|
386
|
+
.use(async () => {
|
|
387
|
+
return (await next()) + 2;
|
|
388
|
+
})
|
|
389
|
+
.use(async () => {
|
|
390
|
+
return 3;
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
console.log(await router.run(req, res));
|
|
394
|
+
// The above will print "6"
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
If an error in thrown within the chain, `router.run` will reject. You can also add a try-catch in the first middleware to catch the error before it rejects the `.run()` call:
|
|
398
|
+
|
|
399
|
+
```js
|
|
400
|
+
router
|
|
401
|
+
.use(async (req, res, next) => {
|
|
402
|
+
return next().catch(errorHandler);
|
|
403
|
+
})
|
|
404
|
+
.use(thisMiddlewarewareMightThrow);
|
|
405
|
+
|
|
406
|
+
await router.run(req, res);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Common errors
|
|
410
|
+
|
|
411
|
+
There are some pitfalls in using `@visulima/connect`. Below are things to keep in mind to use it correctly.
|
|
412
|
+
|
|
413
|
+
1. **Always** `await next()`
|
|
414
|
+
|
|
415
|
+
If `next()` is not awaited, errors will not be caught if they are thrown in async handlers, leading to `UnhandledPromiseRejection`.
|
|
416
|
+
|
|
417
|
+
```javascript
|
|
418
|
+
// OK: we don't use async so no need to await
|
|
419
|
+
router
|
|
420
|
+
.use((req, res, next) => {
|
|
421
|
+
next();
|
|
422
|
+
})
|
|
423
|
+
.use((req, res, next) => {
|
|
424
|
+
next();
|
|
425
|
+
})
|
|
426
|
+
.use(() => {
|
|
427
|
+
throw new Error("💥");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// BAD: This will lead to UnhandledPromiseRejection
|
|
431
|
+
router
|
|
432
|
+
.use(async (req, res, next) => {
|
|
433
|
+
next();
|
|
434
|
+
})
|
|
435
|
+
.use(async (req, res, next) => {
|
|
436
|
+
next();
|
|
437
|
+
})
|
|
438
|
+
.use(async () => {
|
|
439
|
+
throw new Error("💥");
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// GOOD
|
|
443
|
+
router
|
|
444
|
+
.use(async (req, res, next) => {
|
|
445
|
+
await next(); // next() is awaited, so errors are caught properly
|
|
446
|
+
})
|
|
447
|
+
.use((req, res, next) => {
|
|
448
|
+
return next(); // this works as well since we forward the rejected promise
|
|
449
|
+
})
|
|
450
|
+
.use(async () => {
|
|
451
|
+
throw new Error("💥");
|
|
452
|
+
// return new Promise.reject("💥");
|
|
453
|
+
});
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Another issue is that the handler would resolve before all the code in each layer runs.
|
|
457
|
+
|
|
458
|
+
```javascript
|
|
459
|
+
const handler = router
|
|
460
|
+
.use(async (req, res, next) => {
|
|
461
|
+
next(); // this is not returned or await
|
|
462
|
+
})
|
|
463
|
+
.get(async () => {
|
|
464
|
+
// simulate a long task
|
|
465
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
466
|
+
res.send("ok");
|
|
467
|
+
console.log("request is completed");
|
|
468
|
+
})
|
|
469
|
+
.handler();
|
|
470
|
+
|
|
471
|
+
await handler(req, res);
|
|
472
|
+
console.log("finally"); // this will run before the get layer gets to finish
|
|
473
|
+
|
|
474
|
+
// This will result in:
|
|
475
|
+
// 1) "finally"
|
|
476
|
+
// 2) "request is completed"
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
2. **DO NOT** reuse the same instance of `router` like the below pattern:
|
|
480
|
+
|
|
481
|
+
```javascript
|
|
482
|
+
// api-libs/base.js
|
|
483
|
+
export default createRouter().use(a).use(b);
|
|
484
|
+
|
|
485
|
+
// api/foo.js
|
|
486
|
+
import router from "api-libs/base";
|
|
487
|
+
export default router.get(x).handler();
|
|
488
|
+
|
|
489
|
+
// api/bar.js
|
|
490
|
+
import router from "api-libs/base";
|
|
491
|
+
export default router.get(y).handler();
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
This is because, in each API Route, the same router instance is mutated, leading to undefined behaviors.
|
|
495
|
+
If you want to achieve something like that, you can use `router.clone` to return different instances with the same routes populated.
|
|
496
|
+
|
|
497
|
+
```javascript
|
|
498
|
+
// api-libs/base.js
|
|
499
|
+
export default createRouter().use(a).use(b);
|
|
500
|
+
|
|
501
|
+
// api/foo.js
|
|
502
|
+
import router from "api-libs/base";
|
|
503
|
+
export default router.clone().get(x).handler();
|
|
504
|
+
|
|
505
|
+
// api/bar.js
|
|
506
|
+
import router from "api-libs/base";
|
|
507
|
+
export default router.clone().get(y).handler();
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
3. **DO NOT** use response function like `res.(s)end` or `res.redirect` inside `getServerSideProps`.
|
|
511
|
+
|
|
512
|
+
```javascript
|
|
513
|
+
// page/index.js
|
|
514
|
+
const handler = createRouter()
|
|
515
|
+
.use((req, res) => {
|
|
516
|
+
// BAD: res.redirect is not a function (not defined in `getServerSideProps`)
|
|
517
|
+
// See https://github.com/hoangvvo/@visulima/connect/issues/194#issuecomment-1172961741 for a solution
|
|
518
|
+
res.redirect("foo");
|
|
519
|
+
})
|
|
520
|
+
.use((req, res) => {
|
|
521
|
+
// BAD: `getServerSideProps` gives undefined behavior if we try to send a response
|
|
522
|
+
res.end("bar");
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
export async function getServerSideProps({ req, res }) {
|
|
526
|
+
await router.run(req, res);
|
|
527
|
+
return {
|
|
528
|
+
props: {},
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
3. **DO NOT** use `handler()` directly in `getServerSideProps`.
|
|
534
|
+
|
|
535
|
+
```javascript
|
|
536
|
+
// page/index.js
|
|
537
|
+
const router = createRouter().use(foo).use(bar);
|
|
538
|
+
const handler = router.handler();
|
|
539
|
+
|
|
540
|
+
export async function getServerSideProps({ req, res }) {
|
|
541
|
+
await handler(req, res); // BAD: You should call router.run(req, res);
|
|
542
|
+
return {
|
|
543
|
+
props: {},
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
## Recipes
|
|
549
|
+
|
|
550
|
+
### Next.js
|
|
551
|
+
|
|
552
|
+
<details>
|
|
553
|
+
<summary>Match multiple routes</summary>
|
|
554
|
+
|
|
555
|
+
If you created the file `/api/<specific route>.js` folder, the handler will only run on that specific route.
|
|
556
|
+
|
|
557
|
+
If you need to create all handlers for all routes in one file (similar to `Express.js`). You can use [Optional catch-all API routes](https://nextjs.org/docs/api-routes/dynamic-api-routes#optional-catch-all-api-routes).
|
|
558
|
+
|
|
559
|
+
```javascript
|
|
560
|
+
// pages/api/[[...slug]].js
|
|
561
|
+
import { createRouter } from "@visulima/connect";
|
|
562
|
+
|
|
563
|
+
const router = createRouter()
|
|
564
|
+
.use("/api/hello", someMiddleware())
|
|
565
|
+
.get("/api/user/:userId", (req, res) => {
|
|
566
|
+
res.send(`Hello ${req.params.userId}`);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
export default router.handler();
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
While this allows quick migration from Express.js, consider seperating routes into different files (`/api/user/[userId].js`, `/api/hello.js`) in the future.
|
|
573
|
+
|
|
574
|
+
</details>
|
|
575
|
+
|
|
576
|
+
### Express.js Compatibility
|
|
577
|
+
|
|
578
|
+
<details>
|
|
579
|
+
<summary>Middleware wrapper</summary>
|
|
580
|
+
|
|
581
|
+
Express middleware is not built around promises but callbacks. This prevents it from playing well in the `@visulima/connect` model. Understanding the way express middleware works, we can build a wrapper like the below:
|
|
582
|
+
|
|
583
|
+
```js
|
|
584
|
+
import { expressWrapper } from "@visulima/connect";
|
|
585
|
+
import someExpressMiddleware from "some-express-middleware";
|
|
586
|
+
|
|
587
|
+
router.use(expressWrapper(someExpressMiddleware));
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
</details>
|
|
591
|
+
|
|
592
|
+
## Contributing
|
|
593
|
+
|
|
594
|
+
Please see my [contributing.md](https://github.com/visulima/visulima/blob/main/.github/CONTRIBUTING.md).
|
|
595
|
+
|
|
596
|
+
## License
|
|
597
|
+
|
|
598
|
+
[MIT][license-url]
|
|
599
|
+
|
|
600
|
+
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
|
|
601
|
+
[typescript-url]: "typescript"
|
|
602
|
+
[license-image]: https://img.shields.io/npm/l/@visulima/connect?color=blueviolet&style=for-the-badge
|
|
603
|
+
[license-url]: LICENSE.md "license"
|
|
604
|
+
[npm-image]: https://img.shields.io/npm/v/@visulima/connect/alpha.svg?style=for-the-badge&logo=npm
|
|
605
|
+
[npm-url]: https://www.npmjs.com/package/@visulima/connect/v/alpha "npm"
|
|
606
|
+
[synk-image]: https://img.shields.io/snyk/vulnerabilities/github/visulima/connect?label=Synk%20Vulnerabilities&style=for-the-badge
|
|
607
|
+
[synk-url]: https://snyk.io/test/github/visulima/connect?targetFile=package.json "synk"
|