@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.
Files changed (3) hide show
  1. package/README.md +79 -38
  2. package/package.json +1 -1
  3. 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 tiga properti tambahan:
51
+ Yang diekspor adalah class `App`, dengan beberapa properti tambahan:
52
52
 
53
- | Export | Keterangan |
54
- | ------------ | ---------------------------------------- |
55
- | `App` | Router + HTTP server (`listen`) |
53
+ | Export | Keterangan |
54
+ | --- | --- |
55
+ | `App` | Router + HTTP server (`listen`) |
56
56
  | `App.Router` | Router tanpa server, untuk nested router |
57
- | `App.json` | Middleware JSON body parser |
58
- | `App.cors` | Middleware CORS |
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).post("/users", handler).put("/users/:id", handler).patch("/users/:id", handler).delete("/users/:id", 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 | Keterangan |
236
- | ---------------- | ----------------------------------------------------------------------------------------------------------- |
237
- | `req.path` | Pathname tanpa query string |
238
- | `req.query` | Object query string, di-parse secara lazy. Key yang berulang menjadi array, dan `+` di-decode menjadi spasi |
239
- | `req.params` | Parameter dari route dinamis (sudah di-decode). Wildcard `*` ada di `req.params.wildcard` |
240
- | `req.body` | Hasil parse body (butuh middleware `json()`) |
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 | Keterangan |
257
- | ------------------ | -------------------------------------------------------------------------- |
258
- | `res.status(code)` | Set status code, mengembalikan `res` sehingga bisa di-chain |
259
- | `res.send(chunk)` | Kirim response dan akhiri |
260
- | `res.json(value)` | Set `Content-Type: application/json; charset=utf-8`, serialize, lalu kirim |
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 | Default | Keterangan |
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 | Default | Keterangan |
303
- | ------------- | ------- | ------------------------------------------------------------------------------------------------------------- |
304
- | `origin` | `"*"` | `"*"`, satu origin, atau array origin. `null` tidak mengirim header `Access-Control-Allow-Origin` sama sekali |
305
- | `credentials` | `false` | Mengirim `Access-Control-Allow-Credentials: true` |
306
- | `headers` | - | `"*"` atau array nama header (tidak membedakan huruf besar/kecil) |
307
- | `methods` | - | `"*"` atau array nama method (tidak membedakan huruf besar/kecil) |
308
- | `maxAge` | `0` | Lama cache preflight (detik). `0` berarti header tidak dikirim |
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 | Default | Keterangan |
329
- | ---------------- | ------- | ----------------------------------------------------------------------------- |
330
- | `requestTimeout` | `0` | Batas waktu handler dalam ms (khusus `App`). Lihat bagian [Timeout](#timeout) |
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({ ...App.SERVER_OPTIONS, key, cert }, (req, res) => app.handle(req, res));
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
- | | Req/sec (avg) | Latency (avg) |
381
- | --------------------- | ------------- | ------------- |
382
- | Fastify | 31.475 | 31,27 ms |
383
- | **@ndiinginc/router** | 29.735 | 33,12 ms |
384
- | Express | 20.211 | 48,93 ms |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ndiinginc/router",
3
- "version": "4.13.0",
3
+ "version": "4.13.1",
4
4
  "description": "",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
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 prev = params[child.name];
198
- params[child.name] = decodeParam(segment);
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 { path, params, matched: true };
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();