@ndiinginc/router 4.13.0 → 4.13.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 +79 -38
- package/package.json +1 -1
- package/src/router.js +7 -9
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @ndiinginc/router
|
|
2
2
|
|
|
3
|
-
Router HTTP kecil, cepat, dan **zero-dependency** untuk Node.js. Dibangun langsung di atas `node:http`, dengan routing berbasis tree, nested router, middleware async, error handler, timeout per request, serta middleware `json` dan `cors` bawaan.
|
|
3
|
+
Router HTTP kecil, cepat, dan **zero-dependency** untuk Node.js. Dibangun langsung di atas `node:http`, dengan routing berbasis tree, nested router, middleware async, error handler, timeout per request, serta middleware `json`, `urlencoded`, dan `cors` bawaan.
|
|
4
4
|
|
|
5
5
|
## Fitur
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@ Router HTTP kecil, cepat, dan **zero-dependency** untuk Node.js. Dibangun langsu
|
|
|
13
13
|
- Timeout per request, dengan `req.controller` (`AbortController`) untuk pembatalan kooperatif
|
|
14
14
|
- `req.path`, `req.query`, `req.params`, `req.body`
|
|
15
15
|
- Helper `res.status()`, `res.send()`, dan `res.json()`
|
|
16
|
-
- Middleware bawaan: `json()` (dengan limit dan proteksi prototype pollution) dan `cors()`
|
|
16
|
+
- Middleware bawaan: `json()` (dengan limit dan proteksi prototype pollution), `urlencoded()`, dan `cors()`
|
|
17
17
|
- Semua HTTP method, termasuk `QUERY`
|
|
18
18
|
|
|
19
19
|
## Instalasi
|
|
@@ -48,21 +48,26 @@ app.post("/users", (req, res) => {
|
|
|
48
48
|
app.listen(3000);
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
Yang diekspor adalah class `App`, dengan
|
|
51
|
+
Yang diekspor adalah class `App`, dengan beberapa properti tambahan:
|
|
52
52
|
|
|
53
|
-
| Export
|
|
54
|
-
|
|
|
55
|
-
| `App`
|
|
53
|
+
| Export | Keterangan |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `App` | Router + HTTP server (`listen`) |
|
|
56
56
|
| `App.Router` | Router tanpa server, untuk nested router |
|
|
57
|
-
| `App.json`
|
|
58
|
-
| `App.
|
|
57
|
+
| `App.json` | Middleware JSON body parser |
|
|
58
|
+
| `App.urlencoded` | Middleware form body parser (`application/x-www-form-urlencoded`) |
|
|
59
|
+
| `App.cors` | Middleware CORS |
|
|
59
60
|
|
|
60
61
|
## Routing
|
|
61
62
|
|
|
62
63
|
Semua method mengembalikan `this`, jadi bisa di-chain.
|
|
63
64
|
|
|
64
65
|
```js
|
|
65
|
-
app.get("/users", handler)
|
|
66
|
+
app.get("/users", handler)
|
|
67
|
+
.post("/users", handler)
|
|
68
|
+
.put("/users/:id", handler)
|
|
69
|
+
.patch("/users/:id", handler)
|
|
70
|
+
.delete("/users/:id", handler);
|
|
66
71
|
```
|
|
67
72
|
|
|
68
73
|
Method yang tersedia: `get`, `post`, `put`, `patch`, `delete`, `head`, `options`, `trace`, `connect`, `query`, dan `use` (semua method).
|
|
@@ -232,12 +237,12 @@ app.get("/proxy", async (req, res) => {
|
|
|
232
237
|
|
|
233
238
|
## Request
|
|
234
239
|
|
|
235
|
-
| Properti
|
|
236
|
-
|
|
|
237
|
-
| `req.path`
|
|
238
|
-
| `req.query`
|
|
239
|
-
| `req.params`
|
|
240
|
-
| `req.body`
|
|
240
|
+
| Properti | Keterangan |
|
|
241
|
+
| --- | --- |
|
|
242
|
+
| `req.path` | Pathname tanpa query string |
|
|
243
|
+
| `req.query` | Object query string, di-parse secara lazy. Key yang berulang menjadi array, `+` di-decode menjadi spasi, dan key tanpa nilai (`?flag`) bernilai string kosong |
|
|
244
|
+
| `req.params` | Parameter dari route dinamis (sudah di-decode). Wildcard `*` ada di `req.params.wildcard` |
|
|
245
|
+
| `req.body` | Hasil parse body (butuh middleware `json()` atau `urlencoded()`) |
|
|
241
246
|
| `req.controller` | `AbortController` untuk request ini. Di-abort saat timeout atau error handler (jika `requestTimeout` aktif) |
|
|
242
247
|
|
|
243
248
|
```js
|
|
@@ -253,11 +258,11 @@ app.get("/search", (req, res) => {
|
|
|
253
258
|
|
|
254
259
|
## Response
|
|
255
260
|
|
|
256
|
-
| Method
|
|
257
|
-
|
|
|
258
|
-
| `res.status(code)` | Set status code, mengembalikan `res` sehingga bisa di-chain
|
|
259
|
-
| `res.send(chunk)`
|
|
260
|
-
| `res.json(value)`
|
|
261
|
+
| Method | Keterangan |
|
|
262
|
+
| --- | --- |
|
|
263
|
+
| `res.status(code)` | Set status code, mengembalikan `res` sehingga bisa di-chain |
|
|
264
|
+
| `res.send(chunk)` | Kirim response dan akhiri |
|
|
265
|
+
| `res.json(value)` | Set `Content-Type: application/json; charset=utf-8`, serialize, lalu kirim |
|
|
261
266
|
|
|
262
267
|
```js
|
|
263
268
|
res.status(201).json({ created: true });
|
|
@@ -273,8 +278,8 @@ res.status(201).json({ created: true });
|
|
|
273
278
|
app.use(App.json({ limit: 100 * 1024 })); // default 100 KB
|
|
274
279
|
```
|
|
275
280
|
|
|
276
|
-
| Opsi
|
|
277
|
-
|
|
|
281
|
+
| Opsi | Default | Keterangan |
|
|
282
|
+
| --- | --- | --- |
|
|
278
283
|
| `limit` | `102400` | Ukuran body maksimum dalam byte |
|
|
279
284
|
|
|
280
285
|
Perilaku:
|
|
@@ -285,6 +290,32 @@ Perilaku:
|
|
|
285
290
|
- Body kosong menghasilkan `req.body = null`
|
|
286
291
|
- Key `__proto__`, `constructor`, dan `prototype` dibuang untuk mencegah prototype pollution
|
|
287
292
|
|
|
293
|
+
### `urlencoded([options])`
|
|
294
|
+
|
|
295
|
+
```js
|
|
296
|
+
app.use(App.urlencoded({ limit: 100 * 1024 })); // default 100 KB
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
| Opsi | Default | Keterangan |
|
|
300
|
+
| --- | --- | --- |
|
|
301
|
+
| `limit` | `102400` | Ukuran body maksimum dalam byte |
|
|
302
|
+
|
|
303
|
+
Perilaku:
|
|
304
|
+
|
|
305
|
+
- Hanya memproses method `POST`, `PUT`, `PATCH`, dan `QUERY` dengan `Content-Type: application/x-www-form-urlencoded`
|
|
306
|
+
- Body melebihi `limit` menghasilkan `413` (dicek lewat `Content-Length` dan saat streaming)
|
|
307
|
+
- Body kosong menghasilkan `req.body = null`
|
|
308
|
+
- Hasilnya object tanpa prototype dengan aturan yang sama seperti `req.query`: key yang berulang menjadi array dan `+` menjadi spasi
|
|
309
|
+
|
|
310
|
+
```js
|
|
311
|
+
app.use(App.urlencoded());
|
|
312
|
+
|
|
313
|
+
app.post("/login", (req, res) => {
|
|
314
|
+
// body: user=ndiing&tag=a&tag=b
|
|
315
|
+
res.json(req.body); // { user: "ndiing", tag: ["a", "b"] }
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
288
319
|
### `cors([options])`
|
|
289
320
|
|
|
290
321
|
```js
|
|
@@ -299,13 +330,13 @@ app.use(
|
|
|
299
330
|
);
|
|
300
331
|
```
|
|
301
332
|
|
|
302
|
-
| Opsi
|
|
303
|
-
|
|
|
304
|
-
| `origin`
|
|
305
|
-
| `credentials` | `false` | Mengirim `Access-Control-Allow-Credentials: true`
|
|
306
|
-
| `headers`
|
|
307
|
-
| `methods`
|
|
308
|
-
| `maxAge`
|
|
333
|
+
| Opsi | Default | Keterangan |
|
|
334
|
+
| --- | --- | --- |
|
|
335
|
+
| `origin` | `"*"` | `"*"`, satu origin, atau array origin. `null` tidak mengirim header `Access-Control-Allow-Origin` sama sekali |
|
|
336
|
+
| `credentials` | `false` | Mengirim `Access-Control-Allow-Credentials: true` |
|
|
337
|
+
| `headers` | - | `"*"` atau array nama header (tidak membedakan huruf besar/kecil) |
|
|
338
|
+
| `methods` | - | `"*"` atau array nama method (tidak membedakan huruf besar/kecil) |
|
|
339
|
+
| `maxAge` | `0` | Lama cache preflight (detik). `0` berarti header tidak dikirim |
|
|
309
340
|
|
|
310
341
|
Perilaku:
|
|
311
342
|
|
|
@@ -325,9 +356,9 @@ Perilaku:
|
|
|
325
356
|
const app = new App({ requestTimeout: 5000 });
|
|
326
357
|
```
|
|
327
358
|
|
|
328
|
-
| Opsi
|
|
329
|
-
|
|
|
330
|
-
| `requestTimeout` | `0`
|
|
359
|
+
| Opsi | Default | Keterangan |
|
|
360
|
+
| --- | --- | --- |
|
|
361
|
+
| `requestTimeout` | `0` | Batas waktu handler dalam ms (khusus `App`). Lihat bagian [Timeout](#timeout) |
|
|
331
362
|
|
|
332
363
|
`Router` tidak memiliki opsi sendiri; `requestTimeout` hanya dipakai oleh `App`.
|
|
333
364
|
|
|
@@ -369,7 +400,10 @@ console.log(server.address().port);
|
|
|
369
400
|
```js
|
|
370
401
|
const https = require("https");
|
|
371
402
|
|
|
372
|
-
const server = https.createServer(
|
|
403
|
+
const server = https.createServer(
|
|
404
|
+
{ ...App.SERVER_OPTIONS, key, cert },
|
|
405
|
+
(req, res) => app.handle(req, res),
|
|
406
|
+
);
|
|
373
407
|
server.listen(443);
|
|
374
408
|
```
|
|
375
409
|
|
|
@@ -377,14 +411,21 @@ server.listen(443);
|
|
|
377
411
|
|
|
378
412
|
Hello world dengan satu route `GET /`, diuji memakai `autocannon -c 100 -d 40 -p 10` di mesin lokal.
|
|
379
413
|
|
|
380
|
-
|
|
|
381
|
-
|
|
|
382
|
-
| Fastify
|
|
383
|
-
| **@ndiinginc/router** | 29.735
|
|
384
|
-
| Express
|
|
414
|
+
| | Req/sec (avg) | Latency (avg) |
|
|
415
|
+
| --- | --- | --- |
|
|
416
|
+
| Fastify | 31.475 | 31,27 ms |
|
|
417
|
+
| **@ndiinginc/router** | 29.735 | 33,12 ms |
|
|
418
|
+
| Express | 20.211 | 48,93 ms |
|
|
385
419
|
|
|
386
420
|
Hasil bervariasi tergantung mesin, versi Node.js, dan jumlah route. Benchmark ini hanya menggambarkan overhead dasar, bukan performa aplikasi nyata.
|
|
387
421
|
|
|
422
|
+
Skrip benchmark ada di `benchmark/`. Tiap framework dijalankan di proses terpisah, dan hasilnya adalah median dari beberapa run:
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
npm i -D autocannon express fastify
|
|
426
|
+
node benchmark/run.js
|
|
427
|
+
```
|
|
428
|
+
|
|
388
429
|
## Testing
|
|
389
430
|
|
|
390
431
|
```bash
|
package/package.json
CHANGED
package/src/router.js
CHANGED
|
@@ -194,20 +194,18 @@ class Router {
|
|
|
194
194
|
}
|
|
195
195
|
if (node.params.size) {
|
|
196
196
|
for (const [, child] of node.params) {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
const res = this.search(method, segments, child, path, params, state, index + 1);
|
|
197
|
+
const next = { ...params, [child.name]: decodeParam(segment) };
|
|
198
|
+
const res = this.search(method, segments, child, path, next, state, index + 1);
|
|
200
199
|
if (res.matched) return res;
|
|
201
|
-
if (prev === undefined) {
|
|
202
|
-
const { [child.name]: _, ...rest } = params;
|
|
203
|
-
params = rest;
|
|
204
|
-
} else params[child.name] = prev;
|
|
205
200
|
}
|
|
206
201
|
}
|
|
207
202
|
if (node.wildcard && (node.wildcard.handlers.get(method) || node.wildcard.handlers.get("ALL"))) {
|
|
208
|
-
params["wildcard"] = segments.slice(index).map(decodeParam).join("/");
|
|
209
203
|
path.push(node.wildcard);
|
|
210
|
-
return {
|
|
204
|
+
return {
|
|
205
|
+
path,
|
|
206
|
+
params: { ...params, wildcard: segments.slice(index).map(decodeParam).join("/") },
|
|
207
|
+
matched: true,
|
|
208
|
+
};
|
|
211
209
|
}
|
|
212
210
|
if (path.length > state.path.length) {
|
|
213
211
|
state.path = path.slice();
|