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.
@@ -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
- [![npm version](https://img.shields.io/npm/v/princejs?style=flat-square)](https://www.npmjs.com/package/princejs)
9
- [![GitHub stars](https://img.shields.io/github/stars/MatthewTheCoder1218/princejs?style=flat-square)](https://github.com/MatthewTheCoder1218/princejs)
10
- [![npm downloads](https://img.shields.io/npm/dt/princejs?style=flat-square)](https://www.npmjs.com/package/princejs)
11
- [![license](https://img.shields.io/github/license/MatthewTheCoder1218/princejs?style=flat-square)](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
- ## 🛡️ Secure Headers
197
-
198
- One call sets all the security headers your production app needs:
199
-
200
- ```ts
201
- import { secureHeaders } from "princejs/middleware";
202
-
203
- app.use(secureHeaders());
204
- // Sets: X-Frame-Options, X-Content-Type-Options, X-XSS-Protection,
205
- // Strict-Transport-Security, Referrer-Policy
206
-
207
- // Custom options
208
- app.use(secureHeaders({
209
- xFrameOptions: "DENY",
210
- contentSecurityPolicy: "default-src 'self'",
211
- permissionsPolicy: "camera=(), microphone=()",
212
- strictTransportSecurity: "max-age=63072000; includeSubDomains; preload",
213
- }));
214
- ```
215
-
216
- ---
217
-
218
- ## ⏱️ Request Timeout
219
-
220
- Kill hanging requests before they pile up:
221
-
222
- ```ts
223
- import { timeout } from "princejs/middleware";
224
-
225
- app.use(timeout(5000)); // 5 second global timeout → 408
226
- app.use(timeout(3000, "Slow!")); // custom message
227
-
228
- // Per-route timeout
229
- app.get("/heavy", timeout(10000), (req) => heavyOperation());
230
- ```
231
-
232
- ---
233
-
234
- ## 🏷️ Request ID
235
-
236
- Attach a unique ID to every request for distributed tracing and log correlation:
237
-
238
- ```ts
239
- import { requestId } from "princejs/middleware";
240
-
241
- app.use(requestId());
242
- // → sets req.id and X-Request-ID response header
243
-
244
- // Custom header name
245
- app.use(requestId({ header: "X-Trace-ID" }));
246
-
247
- // Custom generator
248
- app.use(requestId({ generator: () => `req-${Date.now()}` }));
249
-
250
- app.get("/", (req) => ({ requestId: req.id }));
251
- ```
252
-
253
- ---
254
-
255
- ## 🚫 IP Restriction
256
-
257
- Allow or block specific IPs:
258
-
259
- ```ts
260
- import { ipRestriction } from "princejs/middleware";
261
-
262
- // Only allow these IPs
263
- app.use(ipRestriction({ allowList: ["192.168.1.1", "10.0.0.1"] }));
264
-
265
- // Block these IPs
266
- app.use(ipRestriction({ denyList: ["1.2.3.4"] }));
267
- ```
268
-
269
- ---
270
-
271
- ## ✂️ Trim Trailing Slash
272
-
273
- Automatically redirect `/users/` `/users` so you never get mysterious 404s from a stray trailing slash:
274
-
275
- ```ts
276
- import { trimTrailingSlash } from "princejs/middleware";
277
-
278
- app.use(trimTrailingSlash()); // 301 by default
279
- app.use(trimTrailingSlash(302)); // or 302 temporary redirect
280
- ```
281
-
282
- Root `/` is never redirected. Query strings are preserved — `/search/?q=bun` → `/search?q=bun`.
283
-
284
- ---
285
-
286
- ## 🔀 Middleware Combinators
287
-
288
- Compose complex auth rules in a single readable line.
289
-
290
- ### `every()` — all must pass
291
-
292
- ```ts
293
- import { every } from "princejs/middleware";
294
-
295
- const isAdmin = async (req, next) => {
296
- if (req.user?.role !== "admin")
297
- return new Response(JSON.stringify({ error: "Forbidden" }), { status: 403 });
298
- return next();
299
- };
300
-
301
- app.get("/admin", every(auth(), isAdmin), () => ({ ok: true }));
302
- // short-circuits on first rejection — isAdmin never runs if auth() fails
303
- ```
304
-
305
- ### `some()` — either must pass
306
-
307
- ```ts
308
- import { some } from "princejs/middleware";
309
-
310
- // Accept a JWT token OR an API key — whichever the client sends
311
- app.get("/resource", some(auth(), apiKey({ keys: ["key_123"] })), () => ({ ok: true }));
312
- ```
313
-
314
- ### `except()` skip middleware for certain paths
315
-
316
- ```ts
317
- import { except } from "princejs/middleware";
318
-
319
- // Apply auth everywhere except /health and /
320
- app.use(except(["/health", "/"], auth()));
321
-
322
- app.get("/health", () => ({ ok: true })); // no auth
323
- app.get("/private", (req) => ({ user: req.user })); // auth required
324
- ```
325
-
326
- ---
327
-
328
- ## 🛡️ guard()
329
-
330
- Apply a validation schema to every route in a group at once — no need to repeat `validate()` on each handler:
331
-
332
- ```ts
333
- import { guard } from "princejs/middleware";
334
- import { z } from "zod";
335
-
336
- app.group("/users", guard({ body: z.object({ name: z.string().min(1) }) }), (r) => {
337
- r.post("/", (req) => ({ created: req.parsedBody.name })); // auto-validated
338
- r.put("/:id", (req) => ({ updated: req.parsedBody.name })); // auto-validated
339
- });
340
- // Bad body → 400 { error: "Validation failed", details: [...] }
341
- ```
342
-
343
- Also works as standalone route middleware:
344
-
345
- ```ts
346
- app.post(
347
- "/items",
348
- guard({ body: z.object({ name: z.string(), price: z.number() }) }),
349
- (req) => ({ created: req.parsedBody })
350
- );
351
- ```
352
-
353
- ---
354
-
355
- ## 📁 Static Files
356
-
357
- Serve a directory of static files. Falls through to your routes if the file doesn't exist:
358
-
359
- ```ts
360
- import { serveStatic } from "princejs/middleware";
361
-
362
- app.use(serveStatic("./public"));
363
- // → GET /logo.png serves ./public/logo.png
364
- // GET / serves ./public/index.html
365
- // GET /api/users falls through to your route handler
366
- ```
367
-
368
- ---
369
-
370
- ## 🌊 Streaming
371
-
372
- Stream chunked responses for AI/LLM output, large payloads, or anything that generates data over time:
373
-
374
- ```ts
375
- import { stream } from "princejs/helpers";
376
-
377
- // Async generator — cleanest for AI token streaming
378
- app.get("/ai", stream(async function*(req) {
379
- yield "Hello ";
380
- await delay(100);
381
- yield "from ";
382
- yield "PrinceJS!";
383
- }));
384
-
385
- // Async callback
386
- app.get("/data", stream(async (req) => {
387
- req.streamSend("chunk 1");
388
- await fetchMoreData();
389
- req.streamSend("chunk 2");
390
- }));
391
-
392
- // Custom content type for binary or JSON streams
393
- app.get("/events", stream(async function*(req) {
394
- for (const item of items) {
395
- yield JSON.stringify(item) + "\n";
396
- }
397
- }, { contentType: "application/x-ndjson" }));
398
- ```
399
-
400
- ---
401
-
402
- ## 🔑 JWKS / Third-Party Auth
403
-
404
- Verify JWTs from Auth0, Clerk, Supabase, or any JWKS endpoint no symmetric key needed:
405
-
406
- ```ts
407
- import { jwks } from "princejs/middleware";
408
-
409
- // Auth0
410
- app.use(jwks("https://your-domain.auth0.com/.well-known/jwks.json"));
411
-
412
- // Clerk
413
- app.use(jwks("https://your-clerk-domain.clerk.accounts.dev/.well-known/jwks.json"));
414
-
415
- // Supabase
416
- app.use(jwks("https://your-project.supabase.co/auth/v1/.well-known/jwks.json"));
417
-
418
- // req.user is set after verification, same as jwt()
419
- app.get("/protected", auth(), (req) => ({ user: req.user }));
420
- ```
421
-
422
- ---
423
-
424
- ## 📖 OpenAPI + Scalar Docs
425
-
426
- Auto-generate an OpenAPI 3.0 spec and serve a beautiful [Scalar](https://scalar.com) UI — all from a single `app.openapi()` call.
427
-
428
- ```ts
429
- import { prince } from "princejs";
430
- import { z } from "zod";
431
-
432
- const app = prince();
433
-
434
- const api = app.openapi({ title: "My API", version: "1.0.0" }, "/docs", { theme: "moon" });
435
-
436
- api.route("GET", "/users/:id", {
437
- summary: "Get user by ID",
438
- tags: ["users"],
439
- schema: {
440
- response: z.object({ id: z.string(), name: z.string() }),
441
- },
442
- }, (req) => ({ id: req.params!.id, name: "Alice" }));
443
-
444
- api.route("POST", "/users", {
445
- summary: "Create user",
446
- tags: ["users"],
447
- schema: {
448
- body: z.object({ name: z.string().min(2), email: z.string().email() }),
449
- response: z.object({ id: z.string(), name: z.string(), email: z.string() }),
450
- },
451
- }, (req) => ({ id: crypto.randomUUID(), ...req.parsedBody }));
452
-
453
- app.listen(3000);
454
- // → GET /docs Scalar UI
455
- // → GET /docs.json Raw OpenAPI JSON
456
- ```
457
-
458
- `api.route()` does three things at once:
459
-
460
- - ✅ Registers the route on PrinceJS
461
- - ✅ Auto-wires body validation — no separate middleware needed
462
- - Writes the full OpenAPI spec entry
463
-
464
- | `schema` key | Runtime effect | Scalar docs |
465
- |---|---|---|
466
- | `body` | Validates & rejects bad requests | ✅ requestBody model |
467
- | `query` | | Typed query params |
468
- | `response` | — | ✅ 200 response model |
469
-
470
- > Routes on `app.get()` / `app.post()` stay private — they never appear in the docs.
471
-
472
- **Themes:** `default` · `moon` · `purple` · `solarized` · `bluePlanet` · `deepSpace` · `saturn` · `kepler` · `mars`
473
-
474
- ---
475
-
476
- ## 🔌 Plugin System
477
-
478
- ```ts
479
- import { prince, type PrincePlugin } from "princejs";
480
-
481
- const usersPlugin: PrincePlugin<{ prefix?: string }> = (app, opts) => {
482
- const base = opts?.prefix ?? "";
483
-
484
- app.use((req, next) => {
485
- (req as any).fromPlugin = true;
486
- return next();
487
- });
488
-
489
- app.get(`${base}/users`, (req) => ({
490
- ok: true,
491
- fromPlugin: (req as any).fromPlugin,
492
- }));
493
- };
494
-
495
- const app = prince();
496
- app.plugin(usersPlugin, { prefix: "/api" });
497
- app.listen(3000);
498
- ```
499
-
500
- ---
501
-
502
- ## 🎣 Lifecycle Hooks
503
-
504
- ```ts
505
- import { prince } from "princejs";
506
-
507
- const app = prince();
508
-
509
- app.onRequest((req) => {
510
- (req as any).startTime = Date.now();
511
- });
512
-
513
- app.onBeforeHandle((req, path, method) => {
514
- console.log(`🔍 ${method} ${path}`);
515
- });
516
-
517
- app.onAfterHandle((req, res, path, method) => {
518
- const ms = Date.now() - (req as any).startTime;
519
- console.log(`✅ ${method} ${path} ${res.status} (${ms}ms)`);
520
- });
521
-
522
- app.onError((err, req, path, method) => {
523
- console.error(`❌ ${method} ${path}:`, err.message);
524
- });
525
-
526
- app.get("/users", () => ({ users: [] }));
527
- app.listen(3000);
528
- ```
529
-
530
- **Execution order:**
531
- 1. `onRequest` runs before routing, good for setup
532
- 2. `onBeforeHandle` just before the handler
533
- 3. Handler executes
534
- 4. `onAfterHandle` after success (skipped on error)
535
- 5. `onError` — only when handler throws
536
-
537
- ---
538
-
539
- ## 🔒 End-to-End Type Safety
540
-
541
- ```ts
542
- import { createClient, type PrinceApiContract } from "princejs/client";
543
-
544
- type ApiContract = {
545
- "GET /users/:id": {
546
- params: { id: string };
547
- response: { id: string; name: string };
548
- };
549
- "POST /users": {
550
- body: { name: string };
551
- response: { id: string; ok: boolean };
552
- };
553
- };
554
-
555
- const client = createClient<ApiContract>("http://localhost:3000");
556
-
557
- const user = await client.get("/users/:id", { params: { id: "42" } });
558
- console.log(user.name); // typed as string ✅
559
-
560
- const created = await client.post("/users", { body: { name: "Alice" } });
561
- console.log(created.id); // typed as string ✅
562
- ```
563
-
564
- ---
565
-
566
- ## 🌍 Deploy Adapters
567
-
568
- **Vercel Edge** `api/[[...route]].ts`
569
- ```ts
570
- import { toVercel } from "princejs/vercel";
571
- export default toVercel(app);
572
- ```
573
-
574
- **Cloudflare Workers** `src/index.ts`
575
- ```ts
576
- import { toWorkers } from "princejs/cloudflare";
577
- export default toWorkers(app);
578
- ```
579
-
580
- **Deno Deploy** — `main.ts`
581
- ```ts
582
- import { toDeno } from "princejs/deno";
583
- Deno.serve(toDeno(app));
584
- ```
585
-
586
- **Node.js** `server.ts`
587
- ```ts
588
- import { createServer } from "http";
589
- import { toNode, toExpress } from "princejs/node";
590
- import express from "express";
591
-
592
- const app = prince();
593
- app.get("/", () => ({ message: "Hello!" }));
594
-
595
- // Native Node http
596
- createServer(toNode(app)).listen(3000);
597
-
598
- // Or drop into Express
599
- const expressApp = express();
600
- expressApp.all("*", toExpress(app));
601
- expressApp.listen(3000);
602
- ```
603
-
604
- ---
605
-
606
- ## 🎯 Full Example
607
-
608
- ```ts
609
- import { prince } from "princejs";
610
- import {
611
- cors,
612
- logger,
613
- rateLimit,
614
- auth,
615
- apiKey,
616
- jwt,
617
- signJWT,
618
- session,
619
- compress,
620
- validate,
621
- secureHeaders,
622
- timeout,
623
- requestId,
624
- trimTrailingSlash,
625
- every,
626
- some,
627
- except,
628
- guard,
629
- } from "princejs/middleware";
630
- import { cache, upload, sse, stream } from "princejs/helpers";
631
- import { cron } from "princejs/scheduler";
632
- import { Html, Head, Body, H1, P, render } from "princejs/jsx";
633
- import { db } from "princejs/db";
634
- import { z } from "zod";
635
-
636
- const SECRET = new TextEncoder().encode("your-secret");
637
- const app = prince();
638
-
639
- // ── Lifecycle hooks ───────────────────────────────────────
640
- app.onRequest((req) => { (req as any).t = Date.now(); });
641
- app.onAfterHandle((req, res, path, method) => {
642
- console.log(`✅ ${method} ${path} ${res.status} (${Date.now() - (req as any).t}ms)`);
643
- });
644
- app.onError((err, req, path, method) => {
645
- console.error(`❌ ${method} ${path}:`, err.message);
646
- });
647
-
648
- // ── Global middleware ─────────────────────────────────────
649
- app.use(secureHeaders());
650
- app.use(requestId());
651
- app.use(trimTrailingSlash());
652
- app.use(timeout(10000));
653
- app.use(cors());
654
- app.use(logger());
655
- app.use(rateLimit(100, 60));
656
- app.use(jwt(SECRET));
657
- app.use(session({ secret: "session-secret" }));
658
- app.use(compress());
659
-
660
- // ── JSX SSR ───────────────────────────────────────────────
661
- const Page = () => Html(Head("Home"), Body(H1("Hello World"), P("Welcome!")));
662
- app.get("/", () => render(Page()));
663
-
664
- // ── Cookies & IP ──────────────────────────────────────────
665
- app.post("/login", (req) =>
666
- app.response()
667
- .json({ ok: true, ip: req.ip })
668
- .cookie("sessionId", "user_123", {
669
- httpOnly: true, secure: true, sameSite: "Strict", maxAge: 86400,
670
- })
671
- );
672
- app.get("/profile", (req) => ({
673
- sessionId: req.cookies?.sessionId,
674
- clientIp: req.ip,
675
- }));
676
-
677
- // ── Database ──────────────────────────────────────────────
678
- const users = db.sqlite("./app.sqlite", `
679
- CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT NOT NULL)
680
- `);
681
- app.get("/users", () => users.query("SELECT * FROM users"));
682
-
683
- // ── WebSockets ────────────────────────────────────────────
684
- app.ws("/chat", {
685
- open: (ws) => ws.send("Welcome!"),
686
- message: (ws, msg) => ws.send(`Echo: ${msg}`),
687
- close: (ws) => console.log("disconnected"),
688
- });
689
-
690
- // ── Auth & API keys ───────────────────────────────────────
691
- app.get("/protected", auth(), (req) => ({ user: req.user }));
692
- app.get("/api", apiKey({ keys: ["key_123"] }), () => ({ ok: true }));
693
- app.get("/admin", every(auth(), async (req, next) => {
694
- if (req.user?.role !== "admin")
695
- return new Response(JSON.stringify({ error: "Forbidden" }), { status: 403 });
696
- return next();
697
- }), () => ({ admin: true }));
698
-
699
- // ── Validated route group ─────────────────────────────────
700
- app.group("/items", guard({ body: z.object({ name: z.string().min(1) }) }), (r) => {
701
- r.post("/", (req) => ({ created: req.parsedBody.name }));
702
- });
703
-
704
- // ── Helpers ───────────────────────────────────────────────
705
- app.get("/cached", cache(60)(() => ({ time: Date.now() })));
706
- app.post("/upload", upload());
707
- app.get("/events", sse(), (req) => {
708
- let i = 0;
709
- const id = setInterval(() => {
710
- req.sseSend({ count: i++ });
711
- if (i >= 10) clearInterval(id);
712
- }, 1000);
713
- });
714
-
715
- // ── Validation ────────────────────────────────────────────
716
- app.post(
717
- "/items",
718
- validate(z.object({ name: z.string().min(1), price: z.number().positive() })),
719
- (req) => ({ created: req.parsedBody })
720
- );
721
-
722
- // ── Cron ──────────────────────────────────────────────────
723
- cron("* * * * *", () => console.log("💓 heartbeat"));
724
-
725
- // ── OpenAPI + Scalar ──────────────────────────────────────
726
- const api = app.openapi({ title: "PrinceJS App", version: "1.0.0" }, "/docs");
727
-
728
- api.route("GET", "/items", {
729
- summary: "List items",
730
- tags: ["items"],
731
- schema: {
732
- query: z.object({ q: z.string().optional() }),
733
- response: z.array(z.object({ id: z.string(), name: z.string() })),
734
- },
735
- }, () => [{ id: "1", name: "Widget" }]);
736
-
737
- api.route("POST", "/items", {
738
- summary: "Create item",
739
- tags: ["items"],
740
- schema: {
741
- body: z.object({ name: z.string().min(1), price: z.number().positive() }),
742
- response: z.object({ id: z.string(), name: z.string() }),
743
- },
744
- }, (req) => ({ id: crypto.randomUUID(), name: req.parsedBody.name }));
745
-
746
- app.listen(3000);
747
- ```
748
-
749
- ---
750
-
751
- ## 📦 Installation
752
-
753
- ```bash
754
- bun add princejs
755
- # or
756
- npm install princejs
757
- ```
758
-
759
- ---
760
-
761
- ## 🤝 Contributing
762
-
763
- ```bash
764
- git clone https://github.com/MatthewTheCoder1218/princejs
765
- cd princejs
766
- bun install
767
- bun test
768
- ```
769
-
770
- ---
771
-
772
- ## 🔗 Links
773
-
774
- - 🌐 Website: [princejs.vercel.app](https://princejs.vercel.app)
775
- - 📦 npm: [npmjs.com/package/princejs](https://www.npmjs.com/package/princejs)
776
- - 💻 GitHub: [github.com/MatthewTheCoder1218/princejs](https://github.com/MatthewTheCoder1218/princejs)
777
- - 🐦 Twitter: [@princejs_bun](https://twitter.com/princejs_bun)
778
-
779
- ---
780
-
781
- <div align="center">
782
-
783
- **PrinceJS: ~5kB. Hono-speed. Everything included. 👑**
784
-
785
- *Built with ❤️ in Nigeria*
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
+ [![npm version](https://img.shields.io/npm/v/princejs?style=flat-square)](https://www.npmjs.com/package/princejs)
9
+ [![GitHub stars](https://img.shields.io/github/stars/MatthewTheCoder1218/princejs?style=flat-square)](https://github.com/MatthewTheCoder1218/princejs)
10
+ [![npm downloads](https://img.shields.io/npm/dt/princejs?style=flat-square)](https://www.npmjs.com/package/princejs)
11
+ [![license](https://img.shields.io/github/license/MatthewTheCoder1218/princejs?style=flat-square)](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>