@lengkapp/edge 0.0.14 → 0.0.15
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/dist/edge-client.js +2 -2
- package/dist/edge-server.d.ts +4 -73
- package/dist/edge-server.js +2 -2
- package/package.json +1 -1
- package/readme.md +513 -672
package/readme.md
CHANGED
|
@@ -1,673 +1,514 @@
|
|
|
1
|
-
# @lengkapp/edge
|
|
2
|
-
|
|
3
|
-
A minimal, high-performance framework for Cloudflare Workers with built-in server-side rendering, routing, auth, rate limiting, caching, and a declarative client-side partial-update library.
|
|
4
|
-
|
|
5
|
-
Inspired by [Hono](https://hono.dev), @lengkapp/edge aims for the same class of performance while shipping with **zero runtime dependencies**. See [Performance](#performance) for a local benchmark against Hono, `hono/tiny`, and a plain native Workers handler.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
####
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
|
266
|
-
|
|
|
267
|
-
|
|
|
268
|
-
|
|
|
269
|
-
|
|
|
270
|
-
|
|
|
271
|
-
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
<
|
|
368
|
-
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
ctx.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
[
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
</details>
|
|
515
|
-
|
|
516
|
-
> Wrangler supports both `wrangler.jsonc` and `wrangler.toml` — they configure the same fields, just in different syntax. Don't keep both in the same project. Keep `compatibility_date` current (Cloudflare recommends staying within the last 30 days); see the [compatibility dates docs](https://developers.cloudflare.com/workers/configuration/compatibility-dates/).
|
|
517
|
-
|
|
518
|
-
### Environment Variables
|
|
519
|
-
|
|
520
|
-
The KV binding names are configurable on the Edge instance:
|
|
521
|
-
|
|
522
|
-
```ts
|
|
523
|
-
app.authKvBinding = 'CUSTOM_AUTH_KV';
|
|
524
|
-
app.rateLimitKvBinding = 'CUSTOM_RATE_KV';
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
### Recommended CSP
|
|
528
|
-
|
|
529
|
-
The client and JSX runtime are compatible with a strict Content Security Policy. A reasonable starting point:
|
|
530
|
-
|
|
531
|
-
```text
|
|
532
|
-
Content-Security-Policy:
|
|
533
|
-
default-src 'self';
|
|
534
|
-
script-src 'self';
|
|
535
|
-
style-src 'self';
|
|
536
|
-
img-src 'self' data:;
|
|
537
|
-
require-trusted-types-for 'script';
|
|
538
|
-
frame-ancestors 'none';
|
|
539
|
-
base-uri 'self';
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
Set this via `app.security.extraHeaders` or at your edge/CDN layer.
|
|
543
|
-
|
|
544
|
-
---
|
|
545
|
-
|
|
546
|
-
## Full Example
|
|
547
|
-
|
|
548
|
-
```ts
|
|
549
|
-
import { Edge, renderToString } from '@lengkapp/edge';
|
|
550
|
-
import { HomePage } from './Page.jsx';
|
|
551
|
-
|
|
552
|
-
const app = new Edge();
|
|
553
|
-
|
|
554
|
-
// Strict CORS allowlist for production
|
|
555
|
-
app.defaults.cors.origin = ['https://app.example.com'];
|
|
556
|
-
|
|
557
|
-
// HSTS + isolation headers
|
|
558
|
-
app.security.extraHeaders = {
|
|
559
|
-
'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
|
|
560
|
-
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
561
|
-
'Cross-Origin-Resource-Policy': 'same-origin',
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
// Basic text
|
|
565
|
-
app.get('/', (ctx) => ctx.text('Hello from Edge!'));
|
|
566
|
-
|
|
567
|
-
// JSX component
|
|
568
|
-
app.get('/home', (ctx) => ctx.html(renderToString(<HomePage />)));
|
|
569
|
-
|
|
570
|
-
// JSON with route params
|
|
571
|
-
app.get('/users/:id', (ctx) => ctx.json({ userId: ctx.params.id }));
|
|
572
|
-
|
|
573
|
-
// POST JSON
|
|
574
|
-
app.post('/users', async (ctx) => {
|
|
575
|
-
const body = await ctx.req.json();
|
|
576
|
-
return ctx.json({ created: true, user: body }, 201);
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
// Auth
|
|
580
|
-
app.get('/protected', { auth: true }, (ctx) => ctx.text('Authenticated'));
|
|
581
|
-
|
|
582
|
-
// Rate limiting
|
|
583
|
-
app.get('/limited', { rateLimit: { max: 5, window: 60 } }, (ctx) => ctx.text('Limited'));
|
|
584
|
-
|
|
585
|
-
// Caching
|
|
586
|
-
app.get('/cached', { cache: { ttl: 60 } }, (ctx) => ctx.text('Cached'));
|
|
587
|
-
|
|
588
|
-
// Cookies
|
|
589
|
-
app.get('/set-cookie', (ctx) => {
|
|
590
|
-
ctx.setCookie('session', 'abc123', { httpOnly: true, secure: true, sameSite: 'Lax', path: '/' });
|
|
591
|
-
return ctx.text('Cookie set');
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
app.get('/get-cookie', (ctx) => {
|
|
595
|
-
const session = ctx.getCookie('session');
|
|
596
|
-
return ctx.text(`Cookie: ${session}`);
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
// Scheduled
|
|
600
|
-
app.scheduled(async (event, env, ctx) => {
|
|
601
|
-
console.log('Cron:', event.cron);
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
export default {
|
|
605
|
-
fetch: (req, env, ctx) => app.fetch(req, env, ctx),
|
|
606
|
-
scheduled: (event, env, ctx) => app.scheduledHandler?.(event, env, ctx)
|
|
607
|
-
};
|
|
608
|
-
```
|
|
609
|
-
|
|
610
|
-
---
|
|
611
|
-
|
|
612
|
-
## Performance
|
|
613
|
-
|
|
614
|
-
@lengkapp/edge is inspired by Hono — one of the fastest Workers frameworks around — but ships with zero dependencies. A quick local benchmark shows it holding its own against both Hono builds (`hono` and `hono/tiny`) and a plain native `fetch` handler with no framework at all.
|
|
615
|
-
|
|
616
|
-
### Test setup
|
|
617
|
-
|
|
618
|
-
- **Tool:** autocannon — 10 connections, 10s run per route
|
|
619
|
-
- **Target:** local `wrangler dev` server
|
|
620
|
-
- **Routes:** `GET /text` (plain text) and `GET /json` (JSON)
|
|
621
|
-
- **Contenders:** @lengkapp/edge, hono, hono/tiny, and a native Workers handler with no framework
|
|
622
|
-
|
|
623
|
-
> This is a single local run, not a formal benchmark suite — treat the numbers as directional. Latency stdev was ±3–4 ms across all four, so differences smaller than that are within noise.
|
|
624
|
-
|
|
625
|
-
### Results
|
|
626
|
-
|
|
627
|
-
| Framework | Route | Avg Req/sec | Avg Latency | Requests | Data Read |
|
|
628
|
-
|---|---|---|---|---|---|
|
|
629
|
-
| @lengkapp/edge | `/text` | 503.1 | 19.38 ms | 5,000 / 10.03s | 1.38 MB |
|
|
630
|
-
| @lengkapp/edge | `/json` | 500.4 | 19.47 ms | 5,000 / 10.03s | 1.42 MB |
|
|
631
|
-
| Hono | `/text` | 506.0 | 19.27 ms | 5,000 / 10.03s | 455 kB |
|
|
632
|
-
| Hono | `/json` | 495.2 | 19.69 ms | 5,000 / 10.03s | 426 kB |
|
|
633
|
-
| Hono (`hono/tiny`) | `/text` | 505.5 | 19.28 ms | 5,000 / 10.03s | 455 kB |
|
|
634
|
-
| Hono (`hono/tiny`) | `/json` | 497.9 | 19.57 ms | 5,000 / 10.03s | 428 kB |
|
|
635
|
-
| Native Workers (no framework) | `/text` | 498.4 | 19.55 ms | 5,000 / 10.02s | 379 kB |
|
|
636
|
-
| Native Workers (no framework) | `/json` | 499.7 | 19.51 ms | 5,000 / 10.03s | 430 kB |
|
|
637
|
-
|
|
638
|
-
### Takeaways
|
|
639
|
-
|
|
640
|
-
- On `/json`, @lengkapp/edge posted the highest average throughput of the four, ahead of both Hono builds and the native handler.
|
|
641
|
-
- On `/text`, it landed within ~1% of `hono`/`hono/tiny` and ahead of the native baseline — effectively a tie once you account for run-to-run variance.
|
|
642
|
-
- All four sit in the same performance tier; the practical difference is that @lengkapp/edge gets there with zero runtime dependencies, matching `hono/tiny`'s footprint without having to choose a "tiny" build.
|
|
643
|
-
- @lengkapp/edge read more bytes per request than the other three in this run — worth profiling if minimal payload size matters for your use case.
|
|
644
|
-
- The 2025 security hardening adds only cheap operations to the request path: prototype-safe objects (`Object.create(null)`), two regex checks per JSX attribute, and a throttled logger that skips duplicate events within 60 seconds. No new async boundaries, no hashing on the hot path (`hashAuthTokens` defaults to `false`), and no added dependencies.
|
|
645
|
-
|
|
646
|
-
---
|
|
647
|
-
|
|
648
|
-
## Migration Notes
|
|
649
|
-
|
|
650
|
-
### Upgrading from ≤ 0.x
|
|
651
|
-
|
|
652
|
-
The security release is backward-compatible. Existing apps continue to work without code changes. New defaults:
|
|
653
|
-
|
|
654
|
-
- Every response now includes `X-Content-Type-Options: nosniff` and `Referrer-Policy: strict-origin-when-cross-origin`.
|
|
655
|
-
- Rate-limited responses include `Retry-After: 60`.
|
|
656
|
-
- Route params, cookie stores, and JSON payloads use prototype-safe objects. If you were relying on `ctx.params.hasOwnProperty(...)`, use `Object.prototype.hasOwnProperty.call(ctx.params, ...)` or `'x' in ctx.params`.
|
|
657
|
-
- The JSX renderer now drops `on*` attributes. Handlers relying on server-rendered inline event handlers must be moved to the client.
|
|
658
|
-
- Security events are logged as JSON. To disable: `app.security.logSecurityEvents = false`.
|
|
659
|
-
|
|
660
|
-
### Client upgrade
|
|
661
|
-
|
|
662
|
-
If you were using the old client that executed `<script>` tags in responses, replace those scripts with either:
|
|
663
|
-
|
|
664
|
-
- DOM-based initialization (event delegation, `IntersectionObserver`, etc.), or
|
|
665
|
-
- An explicit `<script src="...">` injected via `x-js-required` on the response.
|
|
666
|
-
|
|
667
|
-
The new client refuses to run inline scripts for security reasons and will not execute `<script>` elements found in response bodies.
|
|
668
|
-
|
|
669
|
-
---
|
|
670
|
-
|
|
671
|
-
## License
|
|
672
|
-
|
|
1
|
+
# @lengkapp/edge
|
|
2
|
+
|
|
3
|
+
A minimal, high-performance framework for Cloudflare Workers with built-in server-side rendering, routing, auth, rate limiting, caching, and a declarative client-side partial-update library.
|
|
4
|
+
|
|
5
|
+
Inspired by [Hono](https://hono.dev), @lengkapp/edge aims for the same class of performance while shipping with **zero runtime dependencies**. See [Performance](#performance) for a local benchmark against Hono, `hono/tiny`, and a plain native Workers handler.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Trie-based routing** – static & dynamic routes (`/users/:id`) with fast lookups.
|
|
12
|
+
- **JSX support** – render JSX components to HTML without a build step (using `jsx` and `renderToString`).
|
|
13
|
+
- **Middleware options** – authentication, rate limiting, CORS, logging, caching, compression, and custom validation.
|
|
14
|
+
- **Cookie helpers** – set, get, and delete cookies with flexible options.
|
|
15
|
+
- **Declarative client** – enhance HTML with `_get`, `_post`, `_target`, `_trigger` attributes to fetch and replace content dynamically, with skeleton loaders and retry buttons.
|
|
16
|
+
- **Resource injection** – automatically load CSS/JS files from response headers.
|
|
17
|
+
- **Scheduled tasks** – built-in support for Cron triggers.
|
|
18
|
+
- **Zero dependencies** – lightweight and fast.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @lengkapp/edge
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
**1. Create a worker script (`worker.js` or `src/index.ts`):**
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { Edge, renderToString } from '@lengkapp/edge';
|
|
36
|
+
|
|
37
|
+
const app = new Edge();
|
|
38
|
+
|
|
39
|
+
app.get('/', (ctx) => ctx.text('Hello World!'));
|
|
40
|
+
|
|
41
|
+
app.get('/users/:id', (ctx) => {
|
|
42
|
+
return ctx.json({ id: ctx.params.id });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export default {
|
|
46
|
+
fetch: (request, env, ctx) => app.fetch(request, env, ctx),
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**2. Create `wrangler.jsonc`:**
|
|
51
|
+
|
|
52
|
+
Cloudflare recommends `wrangler.jsonc` for new projects (TOML is still supported — see [Configuration](#configuration)).
|
|
53
|
+
|
|
54
|
+
```jsonc
|
|
55
|
+
{
|
|
56
|
+
"$schema": "./node_modules/wrangler/config-schema.json",
|
|
57
|
+
"name": "my-edge-app",
|
|
58
|
+
"main": "worker.js",
|
|
59
|
+
// Set this to today's date
|
|
60
|
+
"compatibility_date": "2026-09-06",
|
|
61
|
+
"observability": {
|
|
62
|
+
"enabled": true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**3. Deploy:**
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
wrangler deploy
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> `wrangler publish` was removed in favor of `wrangler deploy`. If you're on an older Wrangler version, run `npm install -g wrangler@latest` first.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Server API
|
|
78
|
+
|
|
79
|
+
### Creating an App
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { Edge } from '@lengkapp/edge';
|
|
83
|
+
const app = new Edge();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Routing
|
|
87
|
+
|
|
88
|
+
Register routes using HTTP method helpers. Both static and dynamic paths are supported.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// Static
|
|
92
|
+
app.get('/home', handler);
|
|
93
|
+
app.post('/submit', handler);
|
|
94
|
+
|
|
95
|
+
// Dynamic (colon-prefixed segments)
|
|
96
|
+
app.get('/users/:id', (ctx) => {
|
|
97
|
+
const userId = ctx.params.id;
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Supported methods: `get`, `post`, `put`, `delete`, `patch`, `options`, `head`.
|
|
102
|
+
|
|
103
|
+
### Context Object
|
|
104
|
+
|
|
105
|
+
Each handler receives a `Context` object (`ctx`) with the following members:
|
|
106
|
+
|
|
107
|
+
| Property / Method | Description |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `ctx.req` | The original `Request` object |
|
|
110
|
+
| `ctx.env` | The environment bindings (KV, secrets, etc.) |
|
|
111
|
+
| `ctx.executionCtx` | The `ExecutionContext` (for `waitUntil`) |
|
|
112
|
+
| `ctx.params` | Route parameters (e.g., `{ id: '123' }`) |
|
|
113
|
+
| `ctx.status` | HTTP status code (default `200`) |
|
|
114
|
+
| `ctx.headers` | `Headers` object for the response |
|
|
115
|
+
| `ctx.query` | `URLSearchParams` (lazy) |
|
|
116
|
+
| `ctx.getCookie(name)` | Get a cookie value |
|
|
117
|
+
| `ctx.setCookie(name, value, options)` | Set a cookie |
|
|
118
|
+
| `ctx.deleteCookie(name, options)` | Delete a cookie (expires immediately) |
|
|
119
|
+
| `ctx.text(data, status?, headers?)` | Return plain text |
|
|
120
|
+
| `ctx.json(data, status?, headers?)` | Return JSON |
|
|
121
|
+
| `ctx.html(data, status?, headers?)` | Return HTML |
|
|
122
|
+
|
|
123
|
+
Cookie options: `path`, `domain`, `maxAge`, `expires`, `secure`, `httpOnly`, `sameSite`.
|
|
124
|
+
|
|
125
|
+
### Returning Responses
|
|
126
|
+
|
|
127
|
+
You can return a `Response` object, a string (becomes text), or an object (automatically JSON).
|
|
128
|
+
If the handler returns a JSX element, it will be rendered to HTML automatically (see [JSX Support](#jsx-support)).
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
app.get('/json', (ctx) => {
|
|
132
|
+
return { hello: 'world' }; // → ctx.json()
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
app.get('/html', (ctx) => {
|
|
136
|
+
return '<h1>Hi</h1>'; // → ctx.html()
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Route Options
|
|
141
|
+
|
|
142
|
+
Pass an options object as the second argument (before the handler) to enable middleware.
|
|
143
|
+
|
|
144
|
+
#### Authentication (`auth`)
|
|
145
|
+
|
|
146
|
+
Requires an `AUTH_KV` KV binding. Valid tokens are stored as keys with a JSON payload.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
app.get('/protected', { auth: true }, handler);
|
|
150
|
+
app.get('/admin', { auth: { role: 'admin' } }, handler);
|
|
151
|
+
app.get('/scoped', { auth: { scopes: ['read', 'write'] } }, handler);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The client must send the token via `Authorization: Bearer <token>` or a cookie named `auth_token`.
|
|
155
|
+
|
|
156
|
+
#### Rate Limiting (`rateLimit`)
|
|
157
|
+
|
|
158
|
+
Requires a `RATE_LIMIT_KV` binding.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
app.get('/limited', { rateLimit: { max: 100, window: 60 } }, handler);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### Caching (`cache`)
|
|
165
|
+
|
|
166
|
+
Caches successful GET responses in the Cloudflare cache.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
app.get('/cached', { cache: { ttl: 60, staleWhileRevalidate: 30 } }, handler);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### CORS (`cors`)
|
|
173
|
+
|
|
174
|
+
Enables CORS headers.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
app.get('/api', { cors: true }, handler);
|
|
178
|
+
|
|
179
|
+
// Custom origin / methods
|
|
180
|
+
app.get('/api', { cors: { origin: 'https://example.com', methods: 'GET,POST' } }, handler);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### Logging (`log`)
|
|
184
|
+
|
|
185
|
+
Logs method, URL, and status to the console.
|
|
186
|
+
|
|
187
|
+
#### Compression (`compress`)
|
|
188
|
+
|
|
189
|
+
Compresses the response using gzip/deflate/br if the client supports it.
|
|
190
|
+
|
|
191
|
+
#### Custom Validation (`validate`)
|
|
192
|
+
|
|
193
|
+
A function returning `true`/`false` (or a `Promise`) to allow/deny the request.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
app.post('/submit', {
|
|
197
|
+
validate: (ctx) => ctx.req.headers.get('Authorization') === 'Bearer secret'
|
|
198
|
+
}, handler);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## JSX Support
|
|
204
|
+
|
|
205
|
+
The package includes a minimal JSX runtime. Write components as functions returning JSX.
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import { jsx, renderToString, Fragment } from '@lengkapp/edge';
|
|
209
|
+
|
|
210
|
+
function Card({ title }) {
|
|
211
|
+
return (
|
|
212
|
+
<div class="card">
|
|
213
|
+
<h2>{title}</h2>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
app.get('/card', (ctx) => {
|
|
219
|
+
return ctx.html(renderToString(<Card title="Hello" />));
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
If your handler returns a JSX element directly, Edge will automatically render it:
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
app.get('/auto', (ctx) => <h1>Auto rendered</h1>);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Scheduled Tasks
|
|
232
|
+
|
|
233
|
+
Register a scheduled handler for Cron triggers.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
app.scheduled(async (event, env, ctx) => {
|
|
237
|
+
console.log('Cron executed:', event.cron);
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Then export it in your worker:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
export default {
|
|
245
|
+
fetch: (req, env, ctx) => app.fetch(req, env, ctx),
|
|
246
|
+
scheduled: (event, env, ctx) => app.scheduledHandler?.(event, env, ctx)
|
|
247
|
+
};
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Client (Declarative Partial Updates)
|
|
253
|
+
|
|
254
|
+
Add the client script to your page (or include it via a CDN):
|
|
255
|
+
|
|
256
|
+
```html
|
|
257
|
+
<script src="https://cdn.example.com/edge-client.min.js"></script>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Then use HTML attributes to make elements fetch content asynchronously.
|
|
261
|
+
|
|
262
|
+
### Attributes
|
|
263
|
+
|
|
264
|
+
| Attribute | Description |
|
|
265
|
+
| --- | --- |
|
|
266
|
+
| `_get` | URL to fetch via GET |
|
|
267
|
+
| `_post` | URL to fetch via POST |
|
|
268
|
+
| `_target` | CSS selector for the container to replace. Use `"this"` to replace the element itself. |
|
|
269
|
+
| `_trigger` | Comma-separated list of events that trigger the fetch (default: `click`; `load` if `_target="this"`). Supported: `click`, `load`, `visible`, `intersect`, `submit`, etc. |
|
|
270
|
+
| `_form` | (with `_post`) ID of a form to serialize as the POST body (URL-encoded). |
|
|
271
|
+
| `_json` | (with `_post`) Comma-separated names of input fields to send as JSON. |
|
|
272
|
+
|
|
273
|
+
### Examples
|
|
274
|
+
|
|
275
|
+
**Load content on click:**
|
|
276
|
+
|
|
277
|
+
```html
|
|
278
|
+
<button _get="/more-posts" _target="#posts" _trigger="click">Load More</button>
|
|
279
|
+
<div id="posts"></div>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Auto-load on page load:**
|
|
283
|
+
|
|
284
|
+
```html
|
|
285
|
+
<div _get="/user-profile" _target="this"></div>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Post a form via AJAX:**
|
|
289
|
+
|
|
290
|
+
```html
|
|
291
|
+
<form id="contact-form">
|
|
292
|
+
<input name="email" />
|
|
293
|
+
<button _post="/submit" _form="contact-form" _target="#result">Submit</button>
|
|
294
|
+
</form>
|
|
295
|
+
<div id="result"></div>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Send JSON data:**
|
|
299
|
+
|
|
300
|
+
```html
|
|
301
|
+
<input name="username" />
|
|
302
|
+
<input name="password" />
|
|
303
|
+
<button _post="/login" _json="username,password" _target="#status">Login</button>
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Load when element becomes visible (IntersectionObserver):**
|
|
307
|
+
|
|
308
|
+
```html
|
|
309
|
+
<div _get="/lazy-content" _target="this" _trigger="visible"></div>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Skeleton Loading
|
|
313
|
+
|
|
314
|
+
While the request is in progress, the target container is filled with a skeleton loader (three shimmer bars). On error, a "Retry" button is shown.
|
|
315
|
+
|
|
316
|
+
### Resource Injection
|
|
317
|
+
|
|
318
|
+
If the server response includes headers `x-css-required` or `x-js-required`, the client will automatically inject those resources (once per URL) into the page.
|
|
319
|
+
|
|
320
|
+
Example server route:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
app.get('/widget', (ctx) => {
|
|
324
|
+
ctx.headers.set('x-css-required', '["/widget.css"]');
|
|
325
|
+
ctx.headers.set('x-js-required', '["/widget.js"]');
|
|
326
|
+
return ctx.html('<div class="widget">...</div>');
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Configuration
|
|
333
|
+
|
|
334
|
+
### `wrangler.jsonc`
|
|
335
|
+
|
|
336
|
+
If you use authentication or rate limiting, you need KV namespaces. Create them first:
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
wrangler kv namespace create AUTH_KV
|
|
340
|
+
wrangler kv namespace create RATE_LIMIT_KV
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Then wire up the returned IDs in your config:
|
|
344
|
+
|
|
345
|
+
```jsonc
|
|
346
|
+
{
|
|
347
|
+
"$schema": "./node_modules/wrangler/config-schema.json",
|
|
348
|
+
"name": "my-edge-app",
|
|
349
|
+
"main": "worker.js",
|
|
350
|
+
// Set this to today's date
|
|
351
|
+
"compatibility_date": "2026-09-06",
|
|
352
|
+
"observability": {
|
|
353
|
+
"enabled": true
|
|
354
|
+
},
|
|
355
|
+
"kv_namespaces": [
|
|
356
|
+
{ "binding": "AUTH_KV", "id": "your-auth-kv-id" },
|
|
357
|
+
{ "binding": "RATE_LIMIT_KV", "id": "your-ratelimit-kv-id" }
|
|
358
|
+
],
|
|
359
|
+
// Optional Cron triggers
|
|
360
|
+
"triggers": {
|
|
361
|
+
"crons": ["*/5 * * * *"]
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
<details>
|
|
367
|
+
<summary>Equivalent <code>wrangler.toml</code></summary>
|
|
368
|
+
|
|
369
|
+
```toml
|
|
370
|
+
name = "my-edge-app"
|
|
371
|
+
main = "worker.js"
|
|
372
|
+
compatibility_date = "2026-09-06"
|
|
373
|
+
|
|
374
|
+
[observability]
|
|
375
|
+
enabled = true
|
|
376
|
+
|
|
377
|
+
[[kv_namespaces]]
|
|
378
|
+
binding = "AUTH_KV"
|
|
379
|
+
id = "your-auth-kv-id"
|
|
380
|
+
|
|
381
|
+
[[kv_namespaces]]
|
|
382
|
+
binding = "RATE_LIMIT_KV"
|
|
383
|
+
id = "your-ratelimit-kv-id"
|
|
384
|
+
|
|
385
|
+
# Optional Cron triggers
|
|
386
|
+
[triggers]
|
|
387
|
+
crons = ["*/5 * * * *"]
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
</details>
|
|
391
|
+
|
|
392
|
+
Wrangler supports both `wrangler.jsonc` and `wrangler.toml` — they configure the same fields, just in different syntax. Don't keep both in the same project. Keep `compatibility_date` current (Cloudflare recommends staying within the last 30 days); see the [compatibility dates docs](https://developers.cloudflare.com/workers/configuration/compatibility-dates/).
|
|
393
|
+
|
|
394
|
+
### Environment Variables
|
|
395
|
+
|
|
396
|
+
The KV binding names are configurable on the Edge instance:
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
app.authKvBinding = 'CUSTOM_AUTH_KV';
|
|
400
|
+
app.rateLimitKvBinding = 'CUSTOM_RATE_KV';
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Full Example
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
import { Edge, renderToString } from '@lengkapp/edge';
|
|
409
|
+
import { HomePage } from './Page.jsx';
|
|
410
|
+
|
|
411
|
+
const app = new Edge();
|
|
412
|
+
|
|
413
|
+
// Basic text
|
|
414
|
+
app.get('/', (ctx) => ctx.text('Hello from Edge!'));
|
|
415
|
+
|
|
416
|
+
// JSX component
|
|
417
|
+
app.get('/home', (ctx) => ctx.html(renderToString(<HomePage />)));
|
|
418
|
+
|
|
419
|
+
// JSON with route params
|
|
420
|
+
app.get('/users/:id', (ctx) => ctx.json({ userId: ctx.params.id }));
|
|
421
|
+
|
|
422
|
+
// POST JSON
|
|
423
|
+
app.post('/users', async (ctx) => {
|
|
424
|
+
const body = await ctx.req.json();
|
|
425
|
+
return ctx.json({ created: true, user: body }, 201);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Auth
|
|
429
|
+
app.get('/protected', { auth: true }, (ctx) => ctx.text('Authenticated'));
|
|
430
|
+
|
|
431
|
+
// Rate limiting
|
|
432
|
+
app.get('/limited', { rateLimit: { max: 5, window: 60 } }, (ctx) => ctx.text('Limited'));
|
|
433
|
+
|
|
434
|
+
// Caching
|
|
435
|
+
app.get('/cached', { cache: { ttl: 60 } }, (ctx) => ctx.text('Cached'));
|
|
436
|
+
|
|
437
|
+
// Cookies
|
|
438
|
+
app.get('/set-cookie', (ctx) => {
|
|
439
|
+
ctx.setCookie('session', 'abc123', { httpOnly: true, path: '/' });
|
|
440
|
+
return ctx.text('Cookie set');
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
app.get('/get-cookie', (ctx) => {
|
|
444
|
+
const session = ctx.getCookie('session');
|
|
445
|
+
return ctx.text(`Cookie: ${session}`);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Scheduled
|
|
449
|
+
app.scheduled(async (event, env, ctx) => {
|
|
450
|
+
console.log('Cron:', event.cron);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
export default {
|
|
454
|
+
fetch: (req, env, ctx) => app.fetch(req, env, ctx),
|
|
455
|
+
scheduled: (event, env, ctx) => app.scheduledHandler?.(event, env, ctx)
|
|
456
|
+
};
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## Performance
|
|
462
|
+
|
|
463
|
+
@lengkapp/edge is inspired by [Hono](https://hono.dev) — one of the fastest Workers frameworks around — but ships with **zero dependencies**. A quick local benchmark shows it holding its own against both Hono builds (`hono` and `hono/tiny`) and a plain native `fetch` handler with no framework at all.
|
|
464
|
+
|
|
465
|
+
### Test setup
|
|
466
|
+
|
|
467
|
+
- **Tool:** `autocannon` — 10 connections, 10s run per route
|
|
468
|
+
- **Target:** local `wrangler dev` server
|
|
469
|
+
- **Routes:** `GET /text` (plain text) and `GET /json` (JSON)
|
|
470
|
+
- **Contenders:** `@lengkapp/edge`, `hono`, `hono/tiny`, and a native Workers handler with no framework
|
|
471
|
+
|
|
472
|
+
> This is a single local run, not a formal benchmark suite — treat the numbers as directional. Latency stdev was ±3–4 ms across all four, so differences smaller than that are within noise.
|
|
473
|
+
|
|
474
|
+
### Results
|
|
475
|
+
|
|
476
|
+
| Framework | Route | Avg Req/sec | Avg Latency | Requests | Data Read |
|
|
477
|
+
| --- | --- | --- | --- | --- | --- |
|
|
478
|
+
| **@lengkapp/edge** | `/text` | 503.1 | 19.38 ms | 5,000 / 10.03s | 1.38 MB |
|
|
479
|
+
| **@lengkapp/edge** | `/json` | **500.4** | 19.47 ms | 5,000 / 10.03s | 1.42 MB |
|
|
480
|
+
| Hono | `/text` | **506.0** | 19.27 ms | 5,000 / 10.03s | 455 kB |
|
|
481
|
+
| Hono | `/json` | 495.2 | 19.69 ms | 5,000 / 10.03s | 426 kB |
|
|
482
|
+
| Hono (`hono/tiny`) | `/text` | 505.5 | 19.28 ms | 5,000 / 10.03s | 455 kB |
|
|
483
|
+
| Hono (`hono/tiny`) | `/json` | 497.9 | 19.57 ms | 5,000 / 10.03s | 428 kB |
|
|
484
|
+
| Native Workers (no framework) | `/text` | 498.4 | 19.55 ms | 5,000 / 10.02s | 379 kB |
|
|
485
|
+
| Native Workers (no framework) | `/json` | 499.7 | 19.51 ms | 5,000 / 10.03s | 430 kB |
|
|
486
|
+
|
|
487
|
+
```mermaid
|
|
488
|
+
xychart-beta
|
|
489
|
+
title "Avg requests/sec — GET /text"
|
|
490
|
+
x-axis ["@lengkapp/edge", "Hono", "Hono (tiny)", "Native Workers"]
|
|
491
|
+
y-axis "Req/sec" 490 --> 510
|
|
492
|
+
bar [503.1, 506.0, 505.5, 498.4]
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
```mermaid
|
|
496
|
+
xychart-beta
|
|
497
|
+
title "Avg requests/sec — GET /json"
|
|
498
|
+
x-axis ["@lengkapp/edge", "Hono", "Hono (tiny)", "Native Workers"]
|
|
499
|
+
y-axis "Req/sec" 490 --> 505
|
|
500
|
+
bar [500.4, 495.2, 497.9, 499.7]
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Takeaways
|
|
504
|
+
|
|
505
|
+
- On `/json`, @lengkapp/edge posted the highest average throughput of the four, ahead of both Hono builds and the native handler.
|
|
506
|
+
- On `/text`, it landed within ~1% of `hono`/`hono/tiny` and ahead of the native baseline — effectively a tie once you account for run-to-run variance.
|
|
507
|
+
- All four sit in the same performance tier; the practical difference is that @lengkapp/edge gets there with **zero runtime dependencies**, matching `hono/tiny`'s footprint without having to choose a "tiny" build.
|
|
508
|
+
- @lengkapp/edge read more bytes per request than the other three in this run — worth profiling if minimal payload size matters for your use case.
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## License
|
|
513
|
+
|
|
673
514
|
See [LICENSE](./LICENSE). © LengkApp — Yasir Haris
|