princejs 2.2.3 → 2.2.4
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/LICENSE +20 -20
- package/{Readme.md → README.md} +893 -786
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +10 -4
- package/dist/helpers.d.ts +9 -2
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +94 -7
- package/dist/middleware.d.ts +9 -2
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +107 -84
- package/package.json +10 -11
package/{Readme.md → README.md}
RENAMED
|
@@ -1,787 +1,894 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
|
|
3
|
-
# 👑 PrinceJS
|
|
4
|
-
|
|
5
|
-
**Ultra-clean, modern & minimal Bun web framework.**
|
|
6
|
-
Built by a 13-year-old Nigerian developer. Among the top three in performance.
|
|
7
|
-
|
|
8
|
-
[](https://www.npmjs.com/package/princejs)
|
|
9
|
-
[](https://github.com/MatthewTheCoder1218/princejs)
|
|
10
|
-
[](https://www.npmjs.com/package/princejs)
|
|
11
|
-
[](https://github.com/MatthewTheCoder1218/princejs/blob/main/LICENSE)
|
|
12
|
-
|
|
13
|
-
[**Website**](https://princejs.vercel.app) · [**npm**](https://www.npmjs.com/package/princejs) · [**GitHub**](https://github.com/MatthewTheCoder1218/princejs) · [**Twitter**](https://twitter.com/princejs_bun)
|
|
14
|
-
|
|
15
|
-
</div>
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## ⚡ Performance
|
|
20
|
-
|
|
21
|
-
Benchmarked with `oha -c 100 -z 30s` on Windows 10:
|
|
22
|
-
|
|
23
|
-
| Framework | Avg Req/s | Peak Req/s |
|
|
24
|
-
|-----------|----------:|-----------:|
|
|
25
|
-
| Elysia | 27,606 | 27,834 |
|
|
26
|
-
| **PrinceJS** | **17,985** | **18,507** |
|
|
27
|
-
| Hono | 17,914 | 18,826 |
|
|
28
|
-
| Fastify | 15,519 | 16,434 |
|
|
29
|
-
| Express | 13,138 | 13,458 |
|
|
30
|
-
|
|
31
|
-
> PrinceJS is **2.3× faster than Express**, matches Hono head-to-head, and sits at approximately 5kB gzipped — loads in approximately 100ms on a slow 3G connection.
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
## 🚀 Quick Start
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
bun add princejs
|
|
39
|
-
# or
|
|
40
|
-
npm install princejs
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
```ts
|
|
44
|
-
import { prince } from "princejs";
|
|
45
|
-
import { cors, logger } from "princejs/middleware";
|
|
46
|
-
|
|
47
|
-
const app = prince();
|
|
48
|
-
|
|
49
|
-
app.use(cors());
|
|
50
|
-
app.use(logger());
|
|
51
|
-
|
|
52
|
-
app.get("/", () => ({ message: "Hello PrinceJS!" }));
|
|
53
|
-
app.get("/users/:id", (req) => ({ id: req.params?.id }));
|
|
54
|
-
|
|
55
|
-
app.listen(3000);
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
## 🧰 Features
|
|
61
|
-
|
|
62
|
-
| Feature | Import |
|
|
63
|
-
|---------|--------|
|
|
64
|
-
| Routing, Route Grouping, WebSockets, OpenAPI, Plugins, Lifecycle Hooks, Cookies, IP | `princejs` |
|
|
65
|
-
| CORS, Logger, JWT, JWKS, Auth, Rate Limit, Validate, Compress, Session, API Key, Secure Headers, Timeout, Request ID, IP Restriction, Static Files, Trim Trailing Slash, Middleware Combinators (`every`, `some`, `except`), `guard()` | `princejs/middleware` |
|
|
66
|
-
| File Uploads, SSE, Streaming, In-memory Cache | `princejs/helpers` |
|
|
67
|
-
| Cron Scheduler | `princejs/scheduler` |
|
|
68
|
-
| JSX / SSR | `princejs/jsx` |
|
|
69
|
-
| SQLite Database | `princejs/db` |
|
|
70
|
-
| End-to-End Type Safety | `princejs/client` |
|
|
71
|
-
| Vercel Edge adapter | `princejs/vercel` |
|
|
72
|
-
| Cloudflare Workers adapter | `princejs/cloudflare` |
|
|
73
|
-
| Deno Deploy adapter | `princejs/deno` |
|
|
74
|
-
| Node.js / Express adapter | `princejs/node` |
|
|
75
|
-
|
|
76
|
-
---
|
|
77
|
-
|
|
78
|
-
## 🍪 Cookies & 🌐 IP Detection
|
|
79
|
-
|
|
80
|
-
### Reading Cookies
|
|
81
|
-
|
|
82
|
-
Cookies are automatically parsed and available on every request:
|
|
83
|
-
|
|
84
|
-
```ts
|
|
85
|
-
import { prince } from "princejs";
|
|
86
|
-
|
|
87
|
-
const app = prince();
|
|
88
|
-
|
|
89
|
-
app.get("/profile", (req) => ({
|
|
90
|
-
sessionId: req.cookies?.sessionId,
|
|
91
|
-
theme: req.cookies?.theme,
|
|
92
|
-
allCookies: req.cookies, // Record<string, string>
|
|
93
|
-
}));
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### Setting Cookies
|
|
97
|
-
|
|
98
|
-
Use the response builder for full cookie control:
|
|
99
|
-
|
|
100
|
-
```ts
|
|
101
|
-
app.get("/login", (req) =>
|
|
102
|
-
app.response()
|
|
103
|
-
.status(200)
|
|
104
|
-
.json({ ok: true })
|
|
105
|
-
.cookie("sessionId", "abc123", {
|
|
106
|
-
maxAge: 3600, // 1 hour
|
|
107
|
-
path: "/",
|
|
108
|
-
httpOnly: true, // not accessible from JS
|
|
109
|
-
secure: true, // HTTPS only
|
|
110
|
-
sameSite: "Strict", // CSRF protection
|
|
111
|
-
})
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
// Chain multiple cookies
|
|
115
|
-
app.response()
|
|
116
|
-
.json({ ok: true })
|
|
117
|
-
.cookie("session", "xyz")
|
|
118
|
-
.cookie("theme", "dark")
|
|
119
|
-
.cookie("lang", "en");
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### Client IP Detection
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
app.get("/api/data", (req) => ({
|
|
126
|
-
clientIp: req.ip,
|
|
127
|
-
data: [],
|
|
128
|
-
}));
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
**Supported headers** (in priority order):
|
|
132
|
-
- `X-Forwarded-For` — load balancers, proxies (first IP in list)
|
|
133
|
-
- `X-Real-IP` — Nginx, Apache reverse proxy
|
|
134
|
-
- `CF-Connecting-IP` — Cloudflare
|
|
135
|
-
- `X-Client-IP` — other proxy services
|
|
136
|
-
- Fallback — `127.0.0.1`
|
|
137
|
-
|
|
138
|
-
```ts
|
|
139
|
-
// IP-based rate limiting
|
|
140
|
-
app.use((req, next) => {
|
|
141
|
-
const count = ipTracker.getCount(req.ip) || 0;
|
|
142
|
-
if (count > 100) return new Response("Too many requests", { status: 429 });
|
|
143
|
-
ipTracker.increment(req.ip);
|
|
144
|
-
return next();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// IP allowlist
|
|
148
|
-
app.post("/admin", (req) => {
|
|
149
|
-
if (!ALLOWED_IPS.includes(req.ip!)) {
|
|
150
|
-
return new Response("Forbidden", { status: 403 });
|
|
151
|
-
}
|
|
152
|
-
return { authorized: true };
|
|
153
|
-
});
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
---
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
## 🗂️ Route Grouping
|
|
160
|
-
|
|
161
|
-
Group routes under a shared prefix with optional shared middleware. Zero overhead at request time — purely a registration convenience.
|
|
162
|
-
|
|
163
|
-
```ts
|
|
164
|
-
import { prince } from "princejs";
|
|
165
|
-
|
|
166
|
-
const app = prince();
|
|
167
|
-
|
|
168
|
-
// Basic grouping
|
|
169
|
-
app.group("/api", (r) => {
|
|
170
|
-
r.get("/users", () => ({ users: [] }));
|
|
171
|
-
r.post("/users", (req) => ({ created: req.parsedBody }));
|
|
172
|
-
r.get("/users/:id", (req) => ({ id: req.params?.id }));
|
|
173
|
-
});
|
|
174
|
-
// → GET /api/users
|
|
175
|
-
// → POST /api/users
|
|
176
|
-
// → GET /api/users/:id
|
|
177
|
-
|
|
178
|
-
// With shared middleware — applies to every route in the group
|
|
179
|
-
import { auth } from "princejs/middleware";
|
|
180
|
-
|
|
181
|
-
app.group("/admin", auth(), (r) => {
|
|
182
|
-
r.get("/stats", () => ({ stats: {} }));
|
|
183
|
-
r.delete("/users/:id", (req) => ({ deleted: req.params?.id }));
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// Chainable
|
|
187
|
-
app
|
|
188
|
-
.group("/v1", (r) => { r.get("/ping", () => ({ v: 1 })); })
|
|
189
|
-
.group("/v2", (r) => { r.get("/ping", () => ({ v: 2 })); });
|
|
190
|
-
|
|
191
|
-
app.listen(3000);
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
---
|
|
195
|
-
|
|
196
|
-
## 🛡️
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
```ts
|
|
201
|
-
import {
|
|
202
|
-
|
|
203
|
-
app
|
|
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
|
-
```ts
|
|
276
|
-
import {
|
|
277
|
-
|
|
278
|
-
app.
|
|
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
|
-
app.
|
|
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
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
body:
|
|
551
|
-
response: { id: string
|
|
552
|
-
}
|
|
553
|
-
};
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
**
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
//
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
app.
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 👑 PrinceJS
|
|
4
|
+
|
|
5
|
+
**Ultra-clean, modern & minimal Bun web framework.**
|
|
6
|
+
Built by a 13-year-old Nigerian developer. Among the top three in performance.
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/princejs)
|
|
9
|
+
[](https://github.com/MatthewTheCoder1218/princejs)
|
|
10
|
+
[](https://www.npmjs.com/package/princejs)
|
|
11
|
+
[](https://github.com/MatthewTheCoder1218/princejs/blob/main/LICENSE)
|
|
12
|
+
|
|
13
|
+
[**Website**](https://princejs.vercel.app) · [**npm**](https://www.npmjs.com/package/princejs) · [**GitHub**](https://github.com/MatthewTheCoder1218/princejs) · [**Twitter**](https://twitter.com/princejs_bun)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## ⚡ Performance
|
|
20
|
+
|
|
21
|
+
Benchmarked with `oha -c 100 -z 30s` on Windows 10:
|
|
22
|
+
|
|
23
|
+
| Framework | Avg Req/s | Peak Req/s |
|
|
24
|
+
|-----------|----------:|-----------:|
|
|
25
|
+
| Elysia | 27,606 | 27,834 |
|
|
26
|
+
| **PrinceJS** | **17,985** | **18,507** |
|
|
27
|
+
| Hono | 17,914 | 18,826 |
|
|
28
|
+
| Fastify | 15,519 | 16,434 |
|
|
29
|
+
| Express | 13,138 | 13,458 |
|
|
30
|
+
|
|
31
|
+
> PrinceJS is **2.3× faster than Express**, matches Hono head-to-head, and sits at approximately 5kB gzipped — loads in approximately 100ms on a slow 3G connection.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 🚀 Quick Start
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bun add princejs
|
|
39
|
+
# or
|
|
40
|
+
npm install princejs
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { prince } from "princejs";
|
|
45
|
+
import { cors, logger } from "princejs/middleware";
|
|
46
|
+
|
|
47
|
+
const app = prince();
|
|
48
|
+
|
|
49
|
+
app.use(cors());
|
|
50
|
+
app.use(logger());
|
|
51
|
+
|
|
52
|
+
app.get("/", () => ({ message: "Hello PrinceJS!" }));
|
|
53
|
+
app.get("/users/:id", (req) => ({ id: req.params?.id }));
|
|
54
|
+
|
|
55
|
+
app.listen(3000);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 🧰 Features
|
|
61
|
+
|
|
62
|
+
| Feature | Import |
|
|
63
|
+
|---------|--------|
|
|
64
|
+
| Routing, Route Grouping, WebSockets, OpenAPI, Plugins, Lifecycle Hooks, Cookies, IP | `princejs` |
|
|
65
|
+
| CORS, Logger, JWT, JWKS, Auth, Rate Limit, Validate, Compress, Session, API Key, Secure Headers, CSRF Protection, Timeout, Request ID, IP Restriction, Static Files, Trim Trailing Slash, Middleware Combinators (`every`, `some`, `except`), `guard()` | `princejs/middleware` |
|
|
66
|
+
| File Uploads, SSE, Streaming, In-memory Cache, Input Sanitization, Environment Validation, Response Helpers | `princejs/helpers` |
|
|
67
|
+
| Cron Scheduler | `princejs/scheduler` |
|
|
68
|
+
| JSX / SSR | `princejs/jsx` |
|
|
69
|
+
| SQLite Database | `princejs/db` |
|
|
70
|
+
| End-to-End Type Safety | `princejs/client` |
|
|
71
|
+
| Vercel Edge adapter | `princejs/vercel` |
|
|
72
|
+
| Cloudflare Workers adapter | `princejs/cloudflare` |
|
|
73
|
+
| Deno Deploy adapter | `princejs/deno` |
|
|
74
|
+
| Node.js / Express adapter | `princejs/node` |
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 🍪 Cookies & 🌐 IP Detection
|
|
79
|
+
|
|
80
|
+
### Reading Cookies
|
|
81
|
+
|
|
82
|
+
Cookies are automatically parsed and available on every request:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { prince } from "princejs";
|
|
86
|
+
|
|
87
|
+
const app = prince();
|
|
88
|
+
|
|
89
|
+
app.get("/profile", (req) => ({
|
|
90
|
+
sessionId: req.cookies?.sessionId,
|
|
91
|
+
theme: req.cookies?.theme,
|
|
92
|
+
allCookies: req.cookies, // Record<string, string>
|
|
93
|
+
}));
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Setting Cookies
|
|
97
|
+
|
|
98
|
+
Use the response builder for full cookie control:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
app.get("/login", (req) =>
|
|
102
|
+
app.response()
|
|
103
|
+
.status(200)
|
|
104
|
+
.json({ ok: true })
|
|
105
|
+
.cookie("sessionId", "abc123", {
|
|
106
|
+
maxAge: 3600, // 1 hour
|
|
107
|
+
path: "/",
|
|
108
|
+
httpOnly: true, // not accessible from JS
|
|
109
|
+
secure: true, // HTTPS only
|
|
110
|
+
sameSite: "Strict", // CSRF protection
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Chain multiple cookies
|
|
115
|
+
app.response()
|
|
116
|
+
.json({ ok: true })
|
|
117
|
+
.cookie("session", "xyz")
|
|
118
|
+
.cookie("theme", "dark")
|
|
119
|
+
.cookie("lang", "en");
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Client IP Detection
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
app.get("/api/data", (req) => ({
|
|
126
|
+
clientIp: req.ip,
|
|
127
|
+
data: [],
|
|
128
|
+
}));
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Supported headers** (in priority order):
|
|
132
|
+
- `X-Forwarded-For` — load balancers, proxies (first IP in list)
|
|
133
|
+
- `X-Real-IP` — Nginx, Apache reverse proxy
|
|
134
|
+
- `CF-Connecting-IP` — Cloudflare
|
|
135
|
+
- `X-Client-IP` — other proxy services
|
|
136
|
+
- Fallback — `127.0.0.1`
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// IP-based rate limiting
|
|
140
|
+
app.use((req, next) => {
|
|
141
|
+
const count = ipTracker.getCount(req.ip) || 0;
|
|
142
|
+
if (count > 100) return new Response("Too many requests", { status: 429 });
|
|
143
|
+
ipTracker.increment(req.ip);
|
|
144
|
+
return next();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// IP allowlist
|
|
148
|
+
app.post("/admin", (req) => {
|
|
149
|
+
if (!ALLOWED_IPS.includes(req.ip!)) {
|
|
150
|
+
return new Response("Forbidden", { status: 403 });
|
|
151
|
+
}
|
|
152
|
+
return { authorized: true };
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
## 🗂️ Route Grouping
|
|
160
|
+
|
|
161
|
+
Group routes under a shared prefix with optional shared middleware. Zero overhead at request time — purely a registration convenience.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { prince } from "princejs";
|
|
165
|
+
|
|
166
|
+
const app = prince();
|
|
167
|
+
|
|
168
|
+
// Basic grouping
|
|
169
|
+
app.group("/api", (r) => {
|
|
170
|
+
r.get("/users", () => ({ users: [] }));
|
|
171
|
+
r.post("/users", (req) => ({ created: req.parsedBody }));
|
|
172
|
+
r.get("/users/:id", (req) => ({ id: req.params?.id }));
|
|
173
|
+
});
|
|
174
|
+
// → GET /api/users
|
|
175
|
+
// → POST /api/users
|
|
176
|
+
// → GET /api/users/:id
|
|
177
|
+
|
|
178
|
+
// With shared middleware — applies to every route in the group
|
|
179
|
+
import { auth } from "princejs/middleware";
|
|
180
|
+
|
|
181
|
+
app.group("/admin", auth(), (r) => {
|
|
182
|
+
r.get("/stats", () => ({ stats: {} }));
|
|
183
|
+
r.delete("/users/:id", (req) => ({ deleted: req.params?.id }));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Chainable
|
|
187
|
+
app
|
|
188
|
+
.group("/v1", (r) => { r.get("/ping", () => ({ v: 1 })); })
|
|
189
|
+
.group("/v2", (r) => { r.get("/ping", () => ({ v: 2 })); });
|
|
190
|
+
|
|
191
|
+
app.listen(3000);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 🛡️ CSRF Protection
|
|
197
|
+
|
|
198
|
+
Protect your forms from Cross-Site Request Forgery attacks with built-in token generation and validation:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { csrf } from "princejs/middleware";
|
|
202
|
+
|
|
203
|
+
const app = prince();
|
|
204
|
+
app.use(csrf());
|
|
205
|
+
|
|
206
|
+
// GET form page — token is auto-included in response headers
|
|
207
|
+
app.get("/form", (req) => ({
|
|
208
|
+
form: `<form method="post" action="/submit">
|
|
209
|
+
<input type="hidden" name="csrf" value="${req.csrfToken}">
|
|
210
|
+
<input type="text" name="message">
|
|
211
|
+
<button>Submit</button>
|
|
212
|
+
</form>`,
|
|
213
|
+
}));
|
|
214
|
+
|
|
215
|
+
// POST handler — CSRF token is automatically validated
|
|
216
|
+
app.post("/submit", (req) => ({
|
|
217
|
+
ok: true,
|
|
218
|
+
message: req.parsedBody?.message,
|
|
219
|
+
}));
|
|
220
|
+
// Missing or invalid token → 403 Forbidden
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Tokens are stored in `HttpOnly` secure cookies with `SameSite=Strict` to prevent token theft.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 🧹 Input Sanitization
|
|
228
|
+
|
|
229
|
+
Prevent XSS attacks by sanitizing user input before rendering to HTML:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { sanitize } from "princejs/helpers";
|
|
233
|
+
|
|
234
|
+
const userComment = `<img src=x onerror="alert('xss')">Looks great!`;
|
|
235
|
+
|
|
236
|
+
// Remove script tags and event handlers
|
|
237
|
+
const safe = sanitize(userComment, 'text');
|
|
238
|
+
// → "Looks great!"
|
|
239
|
+
|
|
240
|
+
// Allow basic HTML but no scripts
|
|
241
|
+
const htmlSafe = sanitize(userComment, 'html');
|
|
242
|
+
// → "<img src=x>Looks great!"
|
|
243
|
+
|
|
244
|
+
// Validate URLs are safe
|
|
245
|
+
const url = "javascript:alert('xss')";
|
|
246
|
+
const safeUrl = sanitize(url, 'url');
|
|
247
|
+
// → "" (invalid URL removed)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## ✅ Environment Validation
|
|
253
|
+
|
|
254
|
+
Fail fast at startup if required environment variables are missing:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
import { validateEnv } from "princejs/helpers";
|
|
258
|
+
|
|
259
|
+
// Throws error immediately if any variable is missing
|
|
260
|
+
const env = validateEnv(["DATABASE_URL", "JWT_SECRET", "API_KEY"]);
|
|
261
|
+
|
|
262
|
+
const db = connect(env.DATABASE_URL);
|
|
263
|
+
const secret = new TextEncoder().encode(env.JWT_SECRET);
|
|
264
|
+
const apiKey = env.API_KEY;
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Prevents runtime errors from missing config in production deployments.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## 📨 Response Helpers
|
|
272
|
+
|
|
273
|
+
Consistent error and success response formatting:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
import { errorResponse, successResponse } from "princejs/helpers";
|
|
277
|
+
|
|
278
|
+
app.get("/users/:id", async (req) => {
|
|
279
|
+
try {
|
|
280
|
+
const user = await findUser(req.params!.id);
|
|
281
|
+
if (!user) {
|
|
282
|
+
return errorResponse("User not found", 404);
|
|
283
|
+
}
|
|
284
|
+
return successResponse(user);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
return errorResponse("Internal server error", 500);
|
|
287
|
+
// Note: error details are hidden from client for security
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Responses are JSON with consistent structure:
|
|
292
|
+
// Success: { ok: true, data: {...}, statusCode: 200 }
|
|
293
|
+
// Error: { ok: false, error: "...", statusCode: 400 }
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## 🛡️ Secure Headers
|
|
299
|
+
|
|
300
|
+
One call sets all the security headers your production app needs:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
import { secureHeaders } from "princejs/middleware";
|
|
304
|
+
|
|
305
|
+
app.use(secureHeaders());
|
|
306
|
+
// Sets: X-Frame-Options, X-Content-Type-Options, X-XSS-Protection,
|
|
307
|
+
// Strict-Transport-Security, Referrer-Policy
|
|
308
|
+
|
|
309
|
+
// Custom options
|
|
310
|
+
app.use(secureHeaders({
|
|
311
|
+
xFrameOptions: "DENY",
|
|
312
|
+
contentSecurityPolicy: "default-src 'self'",
|
|
313
|
+
permissionsPolicy: "camera=(), microphone=()",
|
|
314
|
+
strictTransportSecurity: "max-age=63072000; includeSubDomains; preload",
|
|
315
|
+
}));
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## ⏱️ Request Timeout
|
|
321
|
+
|
|
322
|
+
Kill hanging requests before they pile up:
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import { timeout } from "princejs/middleware";
|
|
326
|
+
|
|
327
|
+
app.use(timeout(5000)); // 5 second global timeout → 408
|
|
328
|
+
app.use(timeout(3000, "Slow!")); // custom message
|
|
329
|
+
|
|
330
|
+
// Per-route timeout
|
|
331
|
+
app.get("/heavy", timeout(10000), (req) => heavyOperation());
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## 🏷️ Request ID
|
|
337
|
+
|
|
338
|
+
Attach a unique ID to every request for distributed tracing and log correlation:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
import { requestId } from "princejs/middleware";
|
|
342
|
+
|
|
343
|
+
app.use(requestId());
|
|
344
|
+
// → sets req.id and X-Request-ID response header
|
|
345
|
+
|
|
346
|
+
// Custom header name
|
|
347
|
+
app.use(requestId({ header: "X-Trace-ID" }));
|
|
348
|
+
|
|
349
|
+
// Custom generator
|
|
350
|
+
app.use(requestId({ generator: () => `req-${Date.now()}` }));
|
|
351
|
+
|
|
352
|
+
app.get("/", (req) => ({ requestId: req.id }));
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## 🚫 IP Restriction
|
|
358
|
+
|
|
359
|
+
Allow or block specific IPs:
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
import { ipRestriction } from "princejs/middleware";
|
|
363
|
+
|
|
364
|
+
// Only allow these IPs
|
|
365
|
+
app.use(ipRestriction({ allowList: ["192.168.1.1", "10.0.0.1"] }));
|
|
366
|
+
|
|
367
|
+
// Block these IPs
|
|
368
|
+
app.use(ipRestriction({ denyList: ["1.2.3.4"] }));
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## ✂️ Trim Trailing Slash
|
|
374
|
+
|
|
375
|
+
Automatically redirect `/users/` → `/users` so you never get mysterious 404s from a stray trailing slash:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import { trimTrailingSlash } from "princejs/middleware";
|
|
379
|
+
|
|
380
|
+
app.use(trimTrailingSlash()); // 301 by default
|
|
381
|
+
app.use(trimTrailingSlash(302)); // or 302 temporary redirect
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Root `/` is never redirected. Query strings are preserved — `/search/?q=bun` → `/search?q=bun`.
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## 🔀 Middleware Combinators
|
|
389
|
+
|
|
390
|
+
Compose complex auth rules in a single readable line.
|
|
391
|
+
|
|
392
|
+
### `every()` — all must pass
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
import { every } from "princejs/middleware";
|
|
396
|
+
|
|
397
|
+
const isAdmin = async (req, next) => {
|
|
398
|
+
if (req.user?.role !== "admin")
|
|
399
|
+
return new Response(JSON.stringify({ error: "Forbidden" }), { status: 403 });
|
|
400
|
+
return next();
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
app.get("/admin", every(auth(), isAdmin), () => ({ ok: true }));
|
|
404
|
+
// short-circuits on first rejection — isAdmin never runs if auth() fails
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### `some()` — either must pass
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
import { some } from "princejs/middleware";
|
|
411
|
+
|
|
412
|
+
// Accept a JWT token OR an API key — whichever the client sends
|
|
413
|
+
app.get("/resource", some(auth(), apiKey({ keys: ["key_123"] })), () => ({ ok: true }));
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### `except()` — skip middleware for certain paths
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
import { except } from "princejs/middleware";
|
|
420
|
+
|
|
421
|
+
// Apply auth everywhere except /health and /
|
|
422
|
+
app.use(except(["/health", "/"], auth()));
|
|
423
|
+
|
|
424
|
+
app.get("/health", () => ({ ok: true })); // no auth
|
|
425
|
+
app.get("/private", (req) => ({ user: req.user })); // auth required
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## 🛡️ guard()
|
|
431
|
+
|
|
432
|
+
Apply a validation schema to every route in a group at once — no need to repeat `validate()` on each handler:
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
import { guard } from "princejs/middleware";
|
|
436
|
+
import { z } from "zod";
|
|
437
|
+
|
|
438
|
+
app.group("/users", guard({ body: z.object({ name: z.string().min(1) }) }), (r) => {
|
|
439
|
+
r.post("/", (req) => ({ created: req.parsedBody.name })); // auto-validated
|
|
440
|
+
r.put("/:id", (req) => ({ updated: req.parsedBody.name })); // auto-validated
|
|
441
|
+
});
|
|
442
|
+
// Bad body → 400 { error: "Validation failed", details: [...] }
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Also works as standalone route middleware:
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
app.post(
|
|
449
|
+
"/items",
|
|
450
|
+
guard({ body: z.object({ name: z.string(), price: z.number() }) }),
|
|
451
|
+
(req) => ({ created: req.parsedBody })
|
|
452
|
+
);
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## 📁 Static Files
|
|
458
|
+
|
|
459
|
+
Serve a directory of static files. Falls through to your routes if the file doesn't exist:
|
|
460
|
+
|
|
461
|
+
```ts
|
|
462
|
+
import { serveStatic } from "princejs/middleware";
|
|
463
|
+
|
|
464
|
+
app.use(serveStatic("./public"));
|
|
465
|
+
// → GET /logo.png serves ./public/logo.png
|
|
466
|
+
// → GET / serves ./public/index.html
|
|
467
|
+
// → GET /api/users falls through to your route handler
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## 🌊 Streaming
|
|
473
|
+
|
|
474
|
+
Stream chunked responses for AI/LLM output, large payloads, or anything that generates data over time:
|
|
475
|
+
|
|
476
|
+
```ts
|
|
477
|
+
import { stream } from "princejs/helpers";
|
|
478
|
+
|
|
479
|
+
// Async generator — cleanest for AI token streaming
|
|
480
|
+
app.get("/ai", stream(async function*(req) {
|
|
481
|
+
yield "Hello ";
|
|
482
|
+
await delay(100);
|
|
483
|
+
yield "from ";
|
|
484
|
+
yield "PrinceJS!";
|
|
485
|
+
}));
|
|
486
|
+
|
|
487
|
+
// Async callback
|
|
488
|
+
app.get("/data", stream(async (req) => {
|
|
489
|
+
req.streamSend("chunk 1");
|
|
490
|
+
await fetchMoreData();
|
|
491
|
+
req.streamSend("chunk 2");
|
|
492
|
+
}));
|
|
493
|
+
|
|
494
|
+
// Custom content type for binary or JSON streams
|
|
495
|
+
app.get("/events", stream(async function*(req) {
|
|
496
|
+
for (const item of items) {
|
|
497
|
+
yield JSON.stringify(item) + "\n";
|
|
498
|
+
}
|
|
499
|
+
}, { contentType: "application/x-ndjson" }));
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## 🔑 JWKS / Third-Party Auth
|
|
505
|
+
|
|
506
|
+
Verify JWTs from Auth0, Clerk, Supabase, or any JWKS endpoint — no symmetric key needed:
|
|
507
|
+
|
|
508
|
+
```ts
|
|
509
|
+
import { jwks } from "princejs/middleware";
|
|
510
|
+
|
|
511
|
+
// Auth0
|
|
512
|
+
app.use(jwks("https://your-domain.auth0.com/.well-known/jwks.json"));
|
|
513
|
+
|
|
514
|
+
// Clerk
|
|
515
|
+
app.use(jwks("https://your-clerk-domain.clerk.accounts.dev/.well-known/jwks.json"));
|
|
516
|
+
|
|
517
|
+
// Supabase
|
|
518
|
+
app.use(jwks("https://your-project.supabase.co/auth/v1/.well-known/jwks.json"));
|
|
519
|
+
|
|
520
|
+
// req.user is set after verification, same as jwt()
|
|
521
|
+
app.get("/protected", auth(), (req) => ({ user: req.user }));
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## 📖 OpenAPI + Scalar Docs ✨
|
|
527
|
+
|
|
528
|
+
Auto-generate an OpenAPI 3.0 spec and serve a beautiful [Scalar](https://scalar.com) UI — all from a single `app.openapi()` call.
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
import { prince } from "princejs";
|
|
532
|
+
import { z } from "zod";
|
|
533
|
+
|
|
534
|
+
const app = prince();
|
|
535
|
+
|
|
536
|
+
const api = app.openapi({ title: "My API", version: "1.0.0" }, "/docs", { theme: "moon" });
|
|
537
|
+
|
|
538
|
+
api.route("GET", "/users/:id", {
|
|
539
|
+
summary: "Get user by ID",
|
|
540
|
+
tags: ["users"],
|
|
541
|
+
schema: {
|
|
542
|
+
response: z.object({ id: z.string(), name: z.string() }),
|
|
543
|
+
},
|
|
544
|
+
}, (req) => ({ id: req.params!.id, name: "Alice" }));
|
|
545
|
+
|
|
546
|
+
api.route("POST", "/users", {
|
|
547
|
+
summary: "Create user",
|
|
548
|
+
tags: ["users"],
|
|
549
|
+
schema: {
|
|
550
|
+
body: z.object({ name: z.string().min(2), email: z.string().email() }),
|
|
551
|
+
response: z.object({ id: z.string(), name: z.string(), email: z.string() }),
|
|
552
|
+
},
|
|
553
|
+
}, (req) => ({ id: crypto.randomUUID(), ...req.parsedBody }));
|
|
554
|
+
|
|
555
|
+
app.listen(3000);
|
|
556
|
+
// → GET /docs Scalar UI
|
|
557
|
+
// → GET /docs.json Raw OpenAPI JSON
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
`api.route()` does three things at once:
|
|
561
|
+
|
|
562
|
+
- ✅ Registers the route on PrinceJS
|
|
563
|
+
- ✅ Auto-wires body validation — no separate middleware needed
|
|
564
|
+
- ✅ Writes the full OpenAPI spec entry
|
|
565
|
+
|
|
566
|
+
| `schema` key | Runtime effect | Scalar docs |
|
|
567
|
+
|---|---|---|
|
|
568
|
+
| `body` | ✅ Validates & rejects bad requests | ✅ requestBody model |
|
|
569
|
+
| `query` | — | ✅ Typed query params |
|
|
570
|
+
| `response` | — | ✅ 200 response model |
|
|
571
|
+
|
|
572
|
+
> Routes on `app.get()` / `app.post()` stay private — they never appear in the docs.
|
|
573
|
+
|
|
574
|
+
**Themes:** `default` · `moon` · `purple` · `solarized` · `bluePlanet` · `deepSpace` · `saturn` · `kepler` · `mars`
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## 🔌 Plugin System
|
|
579
|
+
|
|
580
|
+
```ts
|
|
581
|
+
import { prince, type PrincePlugin } from "princejs";
|
|
582
|
+
|
|
583
|
+
const usersPlugin: PrincePlugin<{ prefix?: string }> = (app, opts) => {
|
|
584
|
+
const base = opts?.prefix ?? "";
|
|
585
|
+
|
|
586
|
+
app.use((req, next) => {
|
|
587
|
+
(req as any).fromPlugin = true;
|
|
588
|
+
return next();
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
app.get(`${base}/users`, (req) => ({
|
|
592
|
+
ok: true,
|
|
593
|
+
fromPlugin: (req as any).fromPlugin,
|
|
594
|
+
}));
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const app = prince();
|
|
598
|
+
app.plugin(usersPlugin, { prefix: "/api" });
|
|
599
|
+
app.listen(3000);
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
|
|
604
|
+
## 🎣 Lifecycle Hooks
|
|
605
|
+
|
|
606
|
+
```ts
|
|
607
|
+
import { prince } from "princejs";
|
|
608
|
+
|
|
609
|
+
const app = prince();
|
|
610
|
+
|
|
611
|
+
app.onRequest((req) => {
|
|
612
|
+
(req as any).startTime = Date.now();
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
app.onBeforeHandle((req, path, method) => {
|
|
616
|
+
console.log(`🔍 ${method} ${path}`);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
app.onAfterHandle((req, res, path, method) => {
|
|
620
|
+
const ms = Date.now() - (req as any).startTime;
|
|
621
|
+
console.log(`✅ ${method} ${path} ${res.status} (${ms}ms)`);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
app.onError((err, req, path, method) => {
|
|
625
|
+
console.error(`❌ ${method} ${path}:`, err.message);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
app.get("/users", () => ({ users: [] }));
|
|
629
|
+
app.listen(3000);
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
**Execution order:**
|
|
633
|
+
1. `onRequest` — runs before routing, good for setup
|
|
634
|
+
2. `onBeforeHandle` — just before the handler
|
|
635
|
+
3. Handler executes
|
|
636
|
+
4. `onAfterHandle` — after success (skipped on error)
|
|
637
|
+
5. `onError` — only when handler throws
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## 🔒 End-to-End Type Safety
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
import { createClient, type PrinceApiContract } from "princejs/client";
|
|
645
|
+
|
|
646
|
+
type ApiContract = {
|
|
647
|
+
"GET /users/:id": {
|
|
648
|
+
params: { id: string };
|
|
649
|
+
response: { id: string; name: string };
|
|
650
|
+
};
|
|
651
|
+
"POST /users": {
|
|
652
|
+
body: { name: string };
|
|
653
|
+
response: { id: string; ok: boolean };
|
|
654
|
+
};
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const client = createClient<ApiContract>("http://localhost:3000");
|
|
658
|
+
|
|
659
|
+
const user = await client.get("/users/:id", { params: { id: "42" } });
|
|
660
|
+
console.log(user.name); // typed as string ✅
|
|
661
|
+
|
|
662
|
+
const created = await client.post("/users", { body: { name: "Alice" } });
|
|
663
|
+
console.log(created.id); // typed as string ✅
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
## 🌍 Deploy Adapters
|
|
669
|
+
|
|
670
|
+
**Vercel Edge** — `api/[[...route]].ts`
|
|
671
|
+
```ts
|
|
672
|
+
import { toVercel } from "princejs/vercel";
|
|
673
|
+
export default toVercel(app);
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
**Cloudflare Workers** — `src/index.ts`
|
|
677
|
+
```ts
|
|
678
|
+
import { toWorkers } from "princejs/cloudflare";
|
|
679
|
+
export default toWorkers(app);
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
**Deno Deploy** — `main.ts`
|
|
683
|
+
```ts
|
|
684
|
+
import { toDeno } from "princejs/deno";
|
|
685
|
+
Deno.serve(toDeno(app));
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
**Node.js** — `server.ts`
|
|
689
|
+
```ts
|
|
690
|
+
import { createServer } from "http";
|
|
691
|
+
import { toNode, toExpress } from "princejs/node";
|
|
692
|
+
import express from "express";
|
|
693
|
+
|
|
694
|
+
const app = prince();
|
|
695
|
+
app.get("/", () => ({ message: "Hello!" }));
|
|
696
|
+
|
|
697
|
+
// Native Node http
|
|
698
|
+
createServer(toNode(app)).listen(3000);
|
|
699
|
+
|
|
700
|
+
// Or drop into Express
|
|
701
|
+
const expressApp = express();
|
|
702
|
+
expressApp.all("*", toExpress(app));
|
|
703
|
+
expressApp.listen(3000);
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
---
|
|
707
|
+
|
|
708
|
+
## 🎯 Full Example
|
|
709
|
+
|
|
710
|
+
```ts
|
|
711
|
+
import { prince } from "princejs";
|
|
712
|
+
import {
|
|
713
|
+
cors,
|
|
714
|
+
logger,
|
|
715
|
+
rateLimit,
|
|
716
|
+
auth,
|
|
717
|
+
apiKey,
|
|
718
|
+
jwt,
|
|
719
|
+
signJWT,
|
|
720
|
+
session,
|
|
721
|
+
compress,
|
|
722
|
+
validate,
|
|
723
|
+
secureHeaders,
|
|
724
|
+
timeout,
|
|
725
|
+
requestId,
|
|
726
|
+
trimTrailingSlash,
|
|
727
|
+
csrf,
|
|
728
|
+
every,
|
|
729
|
+
some,
|
|
730
|
+
except,
|
|
731
|
+
guard,
|
|
732
|
+
} from "princejs/middleware";
|
|
733
|
+
import { cache, upload, sse, stream, sanitize, validateEnv, errorResponse, successResponse } from "princejs/helpers";
|
|
734
|
+
import { cron } from "princejs/scheduler";
|
|
735
|
+
import { Html, Head, Body, H1, P, render } from "princejs/jsx";
|
|
736
|
+
import { db } from "princejs/db";
|
|
737
|
+
import { z } from "zod";
|
|
738
|
+
|
|
739
|
+
// ── Environment Validation ────────────────────────────────
|
|
740
|
+
const env = validateEnv(["DATABASE_URL", "JWT_SECRET", "API_KEY"]);
|
|
741
|
+
|
|
742
|
+
const SECRET = new TextEncoder().encode(env.JWT_SECRET);
|
|
743
|
+
const app = prince();
|
|
744
|
+
|
|
745
|
+
// ── Lifecycle hooks ───────────────────────────────────────
|
|
746
|
+
app.onRequest((req) => { (req as any).t = Date.now(); });
|
|
747
|
+
app.onAfterHandle((req, res, path, method) => {
|
|
748
|
+
console.log(`✅ ${method} ${path} ${res.status} (${Date.now() - (req as any).t}ms)`);
|
|
749
|
+
});
|
|
750
|
+
app.onError((err, req, path, method) => {
|
|
751
|
+
console.error(`❌ ${method} ${path}:`, err.message);
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
// ── Global middleware ─────────────────────────────────────
|
|
755
|
+
app.use(secureHeaders());
|
|
756
|
+
app.use(requestId());
|
|
757
|
+
app.use(trimTrailingSlash());
|
|
758
|
+
app.use(timeout(10000));
|
|
759
|
+
app.use(cors());
|
|
760
|
+
app.use(logger());
|
|
761
|
+
app.use(rateLimit(100, 60));
|
|
762
|
+
app.use(jwt(SECRET));
|
|
763
|
+
app.use(session({ secret: "session-secret" }));
|
|
764
|
+
app.use(compress());
|
|
765
|
+
app.use(csrf());
|
|
766
|
+
|
|
767
|
+
// ── JSX SSR ───────────────────────────────────────────────
|
|
768
|
+
const Page = () => Html(Head("Home"), Body(H1("Hello World"), P("Welcome!")));
|
|
769
|
+
app.get("/", () => render(Page()));
|
|
770
|
+
|
|
771
|
+
// ── Cookies & IP ──────────────────────────────────────────
|
|
772
|
+
app.post("/login", (req) =>
|
|
773
|
+
app.response()
|
|
774
|
+
.json({ ok: true, ip: req.ip })
|
|
775
|
+
.cookie("sessionId", "user_123", {
|
|
776
|
+
httpOnly: true, secure: true, sameSite: "Strict", maxAge: 86400,
|
|
777
|
+
})
|
|
778
|
+
);
|
|
779
|
+
app.get("/profile", (req) => ({
|
|
780
|
+
sessionId: req.cookies?.sessionId,
|
|
781
|
+
clientIp: req.ip,
|
|
782
|
+
}));
|
|
783
|
+
|
|
784
|
+
// ── Database ──────────────────────────────────────────────
|
|
785
|
+
const users = db.sqlite("./app.sqlite", `
|
|
786
|
+
CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT NOT NULL)
|
|
787
|
+
`);
|
|
788
|
+
app.get("/users", () => users.query("SELECT * FROM users"));
|
|
789
|
+
|
|
790
|
+
// ── WebSockets ────────────────────────────────────────────
|
|
791
|
+
app.ws("/chat", {
|
|
792
|
+
open: (ws) => ws.send("Welcome!"),
|
|
793
|
+
message: (ws, msg) => ws.send(`Echo: ${sanitize(msg as string, 'text')}`),
|
|
794
|
+
close: (ws) => console.log("disconnected"),
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// ── Auth & API keys ───────────────────────────────────────
|
|
798
|
+
app.get("/protected", auth(), (req) => ({ user: req.user }));
|
|
799
|
+
app.get("/api", apiKey({ keys: ["key_123"] }), () => ({ ok: true }));
|
|
800
|
+
app.get("/admin", every(auth(), async (req, next) => {
|
|
801
|
+
if (req.user?.role !== "admin")
|
|
802
|
+
return errorResponse("Forbidden", 403);
|
|
803
|
+
return next();
|
|
804
|
+
}), () => ({ admin: true }));
|
|
805
|
+
|
|
806
|
+
// ── Validated route group ─────────────────────────────────
|
|
807
|
+
app.group("/items", guard({ body: z.object({ name: z.string().min(1) }) }), (r) => {
|
|
808
|
+
r.post("/", (req) => successResponse({ created: req.parsedBody.name }));
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
// ── Helpers ───────────────────────────────────────────────
|
|
812
|
+
app.get("/cached", cache(60)(() => ({ time: Date.now() })));
|
|
813
|
+
app.post("/upload", upload());
|
|
814
|
+
app.get("/events", sse(), (req) => {
|
|
815
|
+
let i = 0;
|
|
816
|
+
const id = setInterval(() => {
|
|
817
|
+
req.sseSend({ count: i++ });
|
|
818
|
+
if (i >= 10) clearInterval(id);
|
|
819
|
+
}, 1000);
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// ── Validation ────────────────────────────────────────────
|
|
823
|
+
app.post(
|
|
824
|
+
"/items",
|
|
825
|
+
validate(z.object({ name: z.string().min(1), price: z.number().positive() })),
|
|
826
|
+
(req) => successResponse({ created: req.parsedBody })
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
// ── Cron ──────────────────────────────────────────────────
|
|
830
|
+
cron("* * * * *", () => console.log("💓 heartbeat"));
|
|
831
|
+
|
|
832
|
+
// ── OpenAPI + Scalar ──────────────────────────────────────
|
|
833
|
+
const api = app.openapi({ title: "PrinceJS App", version: "1.0.0" }, "/docs");
|
|
834
|
+
|
|
835
|
+
api.route("GET", "/items", {
|
|
836
|
+
summary: "List items",
|
|
837
|
+
tags: ["items"],
|
|
838
|
+
schema: {
|
|
839
|
+
query: z.object({ q: z.string().optional() }),
|
|
840
|
+
response: z.array(z.object({ id: z.string(), name: z.string() })),
|
|
841
|
+
},
|
|
842
|
+
}, () => [{ id: "1", name: "Widget" }]);
|
|
843
|
+
|
|
844
|
+
api.route("POST", "/items", {
|
|
845
|
+
summary: "Create item",
|
|
846
|
+
tags: ["items"],
|
|
847
|
+
schema: {
|
|
848
|
+
body: z.object({ name: z.string().min(1), price: z.number().positive() }),
|
|
849
|
+
response: z.object({ id: z.string(), name: z.string() }),
|
|
850
|
+
},
|
|
851
|
+
}, (req) => ({ id: crypto.randomUUID(), name: req.parsedBody.name }));
|
|
852
|
+
|
|
853
|
+
app.listen(3000);
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
---
|
|
857
|
+
|
|
858
|
+
## 📦 Installation
|
|
859
|
+
|
|
860
|
+
```bash
|
|
861
|
+
bun add princejs
|
|
862
|
+
# or
|
|
863
|
+
npm install princejs
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
---
|
|
867
|
+
|
|
868
|
+
## 🤝 Contributing
|
|
869
|
+
|
|
870
|
+
```bash
|
|
871
|
+
git clone https://github.com/MatthewTheCoder1218/princejs
|
|
872
|
+
cd princejs
|
|
873
|
+
bun install
|
|
874
|
+
bun test
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
---
|
|
878
|
+
|
|
879
|
+
## 🔗 Links
|
|
880
|
+
|
|
881
|
+
- 🌐 Website: [princejs.vercel.app](https://princejs.vercel.app)
|
|
882
|
+
- 📦 npm: [npmjs.com/package/princejs](https://www.npmjs.com/package/princejs)
|
|
883
|
+
- 💻 GitHub: [github.com/MatthewTheCoder1218/princejs](https://github.com/MatthewTheCoder1218/princejs)
|
|
884
|
+
- 🐦 Twitter: [@princejs_bun](https://twitter.com/princejs_bun)
|
|
885
|
+
|
|
886
|
+
---
|
|
887
|
+
|
|
888
|
+
<div align="center">
|
|
889
|
+
|
|
890
|
+
**PrinceJS: ~5kB. Hono-speed. Everything included. 👑**
|
|
891
|
+
|
|
892
|
+
*Built with ❤️ in Nigeria*
|
|
893
|
+
|
|
787
894
|
</div>
|