princejs 2.1.5 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md CHANGED
@@ -20,23 +20,24 @@ Built by a 13-year-old Nigerian developer. Among the top three in performance.
20
20
 
21
21
  Benchmarked with `oha -c 100 -z 30s` on Windows 10:
22
22
 
23
- | Framework | Req/s | Total |
24
- |-----------|------:|------:|
25
- | Elysia | 25,312 | 759k |
26
- | Hono | 22,124 | 664k |
27
- | **PrinceJS** | **21,748** | **653k** |
28
- | Express | 9,325 | 280k |
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 |
29
30
 
30
- > PrinceJS is **2.3× faster than Express** and sits comfortably in the top 3 at just **4.4kB gzipped**.
31
+ > PrinceJS is **2.3× faster than Express**, matches Hono head-to-head, and sits at just **6.3kB gzipped** — loads in ~122ms on a slow 3G connection.
31
32
 
32
33
  ---
33
34
 
34
35
  ## 🚀 Quick Start
35
36
 
36
37
  ```bash
37
- npm install princejs
38
38
  bun add princejs
39
- yarn add princejs
39
+ # or
40
+ npm install princejs
40
41
  ```
41
42
 
42
43
  ```ts
@@ -49,7 +50,7 @@ app.use(cors());
49
50
  app.use(logger());
50
51
 
51
52
  app.get("/", () => ({ message: "Hello PrinceJS!" }));
52
- app.get("/users/:id", (req) => ({ id: req.params.id }));
53
+ app.get("/users/:id", (req) => ({ id: req.params?.id }));
53
54
 
54
55
  app.listen(3000);
55
56
  ```
@@ -60,24 +61,19 @@ app.listen(3000);
60
61
 
61
62
  | Feature | Import |
62
63
  |---------|--------|
63
- | Routing | `princejs` |
64
- | Middleware (CORS, Logger, Rate Limit, Auth, JWT) | `princejs/middleware` |
65
- | Zod Validation | `princejs/middleware` |
66
- | **Cookies & IP Detection** | `princejs` |
67
- | File Uploads | `princejs/helpers` |
68
- | WebSockets | `princejs` |
69
- | Server-Sent Events | `princejs/helpers` |
70
- | Sessions | `princejs/middleware` |
71
- | Response Compression | `princejs/middleware` |
72
- | In-memory Cache | `princejs/helpers` |
64
+ | Routing, WebSockets, OpenAPI, Plugins, Lifecycle Hooks, Cookies, IP | `princejs` |
65
+ | **Route Grouping** | `princejs` |
66
+ | CORS, Logger, JWT, Auth, Rate Limit, Validate, Compress, Session, API Key | `princejs/middleware` |
67
+ | **Secure Headers, Timeout, Request ID, IP Restriction, Static Files, JWKS** | `princejs/middleware` |
68
+ | File Uploads, SSE, In-memory Cache, **Streaming** | `princejs/helpers` |
73
69
  | Cron Scheduler | `princejs/scheduler` |
74
- | **OpenAPI + Scalar Docs** | `princejs` |
75
70
  | JSX / SSR | `princejs/jsx` |
76
71
  | SQLite Database | `princejs/db` |
77
- | Plugin System | `princejs` |
78
72
  | End-to-End Type Safety | `princejs/client` |
79
- | Deploy Adapters | `princejs/vercel` · `princejs/cloudflare` · `princejs/deno` · `princejs/node` |
80
- | Lifecycle Hooks | `princejs` |
73
+ | Vercel Edge adapter | `princejs/vercel` |
74
+ | Cloudflare Workers adapter | `princejs/cloudflare` |
75
+ | Deno Deploy adapter | `princejs/deno` |
76
+ | Node.js / Express adapter | `princejs/node` |
81
77
 
82
78
  ---
83
79
 
@@ -85,31 +81,35 @@ app.listen(3000);
85
81
 
86
82
  ### Reading Cookies
87
83
 
88
- Cookies are automatically parsed from the request:
84
+ Cookies are automatically parsed and available on every request:
89
85
 
90
86
  ```ts
87
+ import { prince } from "princejs";
88
+
89
+ const app = prince();
90
+
91
91
  app.get("/profile", (req) => ({
92
92
  sessionId: req.cookies?.sessionId,
93
93
  theme: req.cookies?.theme,
94
- allCookies: req.cookies // Record<string, string>
94
+ allCookies: req.cookies, // Record<string, string>
95
95
  }));
96
96
  ```
97
97
 
98
98
  ### Setting Cookies
99
99
 
100
- Use the response builder to set cookies with full control:
100
+ Use the response builder for full cookie control:
101
101
 
102
102
  ```ts
103
- app.get("/login", (req) =>
103
+ app.get("/login", (req) =>
104
104
  app.response()
105
105
  .status(200)
106
106
  .json({ ok: true })
107
107
  .cookie("sessionId", "abc123", {
108
- maxAge: 3600, // 1 hour
108
+ maxAge: 3600, // 1 hour
109
109
  path: "/",
110
- httpOnly: true, // not accessible from JS
111
- secure: true, // HTTPS only
112
- sameSite: "Strict" // CSRF protection
110
+ httpOnly: true, // not accessible from JS
111
+ secure: true, // HTTPS only
112
+ sameSite: "Strict", // CSRF protection
113
113
  })
114
114
  );
115
115
 
@@ -123,39 +123,30 @@ app.response()
123
123
 
124
124
  ### Client IP Detection
125
125
 
126
- Automatically detect client IP from request headers:
127
-
128
126
  ```ts
129
127
  app.get("/api/data", (req) => ({
130
128
  clientIp: req.ip,
131
- data: [...]
129
+ data: [],
132
130
  }));
133
131
  ```
134
132
 
135
133
  **Supported headers** (in priority order):
136
- - `X-Forwarded-For` — Load balancers, proxies (first IP in list)
134
+ - `X-Forwarded-For` — load balancers, proxies (first IP in list)
137
135
  - `X-Real-IP` — Nginx, Apache reverse proxy
138
136
  - `CF-Connecting-IP` — Cloudflare
139
- - `X-Client-IP` — Other proxy services
140
- - Fallback — `127.0.0.1` (localhost)
141
-
142
- **Use cases:**
143
- - 🔒 Rate limiting per IP
144
- - 📊 Geolocation analytics
145
- - 🚨 IP-based access control
146
- - 👥 User tracking & fraud detection
137
+ - `X-Client-IP` — other proxy services
138
+ - Fallback — `127.0.0.1`
147
139
 
148
140
  ```ts
149
- // Rate limit by IP
141
+ // IP-based rate limiting
150
142
  app.use((req, next) => {
151
- const ip = req.ip;
152
- const count = ipTracker.getCount(ip) || 0;
143
+ const count = ipTracker.getCount(req.ip) || 0;
153
144
  if (count > 100) return new Response("Too many requests", { status: 429 });
154
- ipTracker.increment(ip);
145
+ ipTracker.increment(req.ip);
155
146
  return next();
156
147
  });
157
148
 
158
- // IP-based security
149
+ // IP allowlist
159
150
  app.post("/admin", (req) => {
160
151
  if (!ALLOWED_IPS.includes(req.ip!)) {
161
152
  return new Response("Forbidden", { status: 403 });
@@ -166,9 +157,170 @@ app.post("/admin", (req) => {
166
157
 
167
158
  ---
168
159
 
160
+ ---
161
+
162
+ ## 📁 Route Grouping
163
+
164
+ Namespace routes under a shared prefix with optional shared middleware. Zero overhead at request time — just registers prefixed routes in the trie.
165
+
166
+ ```ts
167
+ // Basic grouping
168
+ app.group("/api", (r) => {
169
+ r.get("/users", () => ({ users: [] })); // → GET /api/users
170
+ r.post("/users", (req) => req.parsedBody); // → POST /api/users
171
+ r.get("/users/:id", (req) => ({ id: req.params?.id })); // → GET /api/users/:id
172
+ });
173
+
174
+ // With shared middleware — applies to every route in the group
175
+ import { auth } from "princejs/middleware";
176
+
177
+ app.group("/admin", auth(), (r) => {
178
+ r.get("/stats", () => ({ ok: true })); // → GET /admin/stats
179
+ r.delete("/user", () => ({ deleted: true })); // → DELETE /admin/user
180
+ });
181
+
182
+ // Chainable
183
+ app
184
+ .group("/v1", (r) => { r.get("/ping", () => ({ v: 1 })); })
185
+ .group("/v2", (r) => { r.get("/ping", () => ({ v: 2 })); });
186
+ ```
187
+
188
+ ---
189
+
190
+ ## 🔐 Security Middleware
191
+
192
+ ### Secure Headers
193
+
194
+ One call sets X-Frame-Options, HSTS, X-Content-Type-Options, X-XSS-Protection, and Referrer-Policy:
195
+
196
+ ```ts
197
+ import { secureHeaders } from "princejs/middleware";
198
+
199
+ app.use(secureHeaders());
200
+
201
+ // Custom overrides
202
+ app.use(secureHeaders({
203
+ xFrameOptions: "DENY",
204
+ contentSecurityPolicy: "default-src 'self'",
205
+ permissionsPolicy: "camera=(), microphone=()",
206
+ strictTransportSecurity: "max-age=63072000; includeSubDomains; preload",
207
+ }));
208
+ ```
209
+
210
+ ### Request Timeout
211
+
212
+ Kill hanging requests before they pile up:
213
+
214
+ ```ts
215
+ import { timeout } from "princejs/middleware";
216
+
217
+ app.use(timeout(5000)); // 408 after 5s
218
+ app.use(timeout(3000, "Gateway Timeout")); // custom message
219
+ ```
220
+
221
+ ### Request ID
222
+
223
+ Attach a unique ID to every request for distributed tracing and log correlation:
224
+
225
+ ```ts
226
+ import { requestId } from "princejs/middleware";
227
+
228
+ app.use(requestId());
229
+ // → sets req.id and X-Request-ID response header
230
+
231
+ // Custom header or generator
232
+ app.use(requestId({
233
+ header: "X-Trace-ID",
234
+ generator: () => `req-${Date.now()}`,
235
+ }));
236
+
237
+ app.get("/", (req) => ({ requestId: req.id }));
238
+ ```
239
+
240
+ ### IP Restriction
241
+
242
+ Allow or deny specific IPs:
243
+
244
+ ```ts
245
+ import { ipRestriction } from "princejs/middleware";
246
+
247
+ // Only allow these IPs
248
+ app.use(ipRestriction({ allowList: ["192.168.1.1", "10.0.0.1"] }));
249
+
250
+ // Block specific IPs
251
+ app.use(ipRestriction({ denyList: ["1.2.3.4"] }));
252
+ ```
253
+
254
+ ### JWKS — Auth0, Clerk, Supabase
255
+
256
+ Verify JWTs against a remote JWKS endpoint — no symmetric key needed:
257
+
258
+ ```ts
259
+ import { jwks } from "princejs/middleware";
260
+
261
+ // Auth0
262
+ app.use(jwks("https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json"));
263
+
264
+ // Clerk
265
+ app.use(jwks("https://YOUR_CLERK_DOMAIN/.well-known/jwks.json"));
266
+
267
+ // Keys are cached automatically — only fetched on rotation
268
+ app.get("/protected", auth(), (req) => ({ user: req.user }));
269
+ ```
270
+
271
+ ---
272
+
273
+ ## 📂 Static Files
274
+
275
+ Serve a directory of static files. Falls through to your routes if the file doesn't exist:
276
+
277
+ ```ts
278
+ import { serveStatic } from "princejs/middleware";
279
+
280
+ app.use(serveStatic("./public"));
281
+
282
+ // Your API routes still work normally
283
+ app.get("/api/users", () => ({ users: [] }));
284
+ ```
285
+
286
+ ---
287
+
288
+ ## 🌊 Streaming
289
+
290
+ Stream responses chunk by chunk — perfect for AI/LLM token output:
291
+
292
+ ```ts
293
+ import { stream } from "princejs/helpers";
294
+
295
+ // Async generator (cleanest for AI output)
296
+ app.get("/ai", stream(async function*(req) {
297
+ yield "Hello ";
298
+ yield "from ";
299
+ yield "PrinceJS!";
300
+ }));
301
+
302
+ // Callback style
303
+ app.get("/stream", stream((req) => {
304
+ req.streamSend("chunk 1");
305
+ req.streamSend("chunk 2");
306
+ }));
307
+
308
+ // Async callback
309
+ app.get("/slow-stream", stream(async (req) => {
310
+ req.streamSend("Starting...");
311
+ await fetch("https://api.openai.com/v1/chat/completions", { /* ... */ })
312
+ .then(res => res.body?.pipeTo(new WritableStream({
313
+ write(chunk) { req.streamSend(chunk); }
314
+ })));
315
+ }));
316
+
317
+ // Custom content type
318
+ app.get("/binary", stream(() => { /* ... */ }, { contentType: "application/octet-stream" }));
319
+ ```
320
+
169
321
  ## 📖 OpenAPI + Scalar Docs ✨
170
322
 
171
- Auto-generate an OpenAPI 3.0 spec and serve a beautiful [Scalar](https://scalar.com) UI — all from a single `app.openapi()` call. Routes, validation, and docs stay in sync automatically.
323
+ Auto-generate an OpenAPI 3.0 spec and serve a beautiful [Scalar](https://scalar.com) UI — all from a single `app.openapi()` call.
172
324
 
173
325
  ```ts
174
326
  import { prince } from "princejs";
@@ -203,16 +355,16 @@ app.listen(3000);
203
355
  `api.route()` does three things at once:
204
356
 
205
357
  - ✅ Registers the route on PrinceJS
206
- - ✅ Auto-wires `validate(schema.body)` — no separate import needed
358
+ - ✅ Auto-wires body validation — no separate middleware needed
207
359
  - ✅ Writes the full OpenAPI spec entry
208
360
 
209
- | `schema` key | Runtime | Scalar Docs |
361
+ | `schema` key | Runtime effect | Scalar docs |
210
362
  |---|---|---|
211
- | `body` | ✅ Validates request | ✅ requestBody model |
363
+ | `body` | ✅ Validates & rejects bad requests | ✅ requestBody model |
212
364
  | `query` | — | ✅ Typed query params |
213
365
  | `response` | — | ✅ 200 response model |
214
366
 
215
- > Routes on `app.get()` / `app.post()` stay private — never appear in docs.
367
+ > Routes on `app.get()` / `app.post()` stay private — they never appear in the docs.
216
368
 
217
369
  **Themes:** `default` · `moon` · `purple` · `solarized` · `bluePlanet` · `deepSpace` · `saturn` · `kepler` · `mars`
218
370
 
@@ -220,8 +372,6 @@ app.listen(3000);
220
372
 
221
373
  ## 🔌 Plugin System
222
374
 
223
- Share bundles of routes and middleware as reusable plugins:
224
-
225
375
  ```ts
226
376
  import { prince, type PrincePlugin } from "princejs";
227
377
 
@@ -241,66 +391,53 @@ const usersPlugin: PrincePlugin<{ prefix?: string }> = (app, opts) => {
241
391
 
242
392
  const app = prince();
243
393
  app.plugin(usersPlugin, { prefix: "/api" });
394
+ app.listen(3000);
244
395
  ```
245
396
 
246
397
  ---
247
398
 
248
399
  ## 🎣 Lifecycle Hooks
249
400
 
250
- React to key moments in request processing with lifecycle hooks:
251
-
252
401
  ```ts
253
402
  import { prince } from "princejs";
254
403
 
255
404
  const app = prince();
256
405
 
257
- // Called for every incoming request
258
406
  app.onRequest((req) => {
259
- console.log(`📥 Request received: ${req.method} ${req.url}`);
407
+ (req as any).startTime = Date.now();
260
408
  });
261
409
 
262
- // Called before handler execution
263
410
  app.onBeforeHandle((req, path, method) => {
264
- console.log(`🔍 About to handle: ${method} ${path}`);
265
- (req as any).startTime = Date.now();
411
+ console.log(`🔍 ${method} ${path}`);
266
412
  });
267
413
 
268
- // Called after successful handler execution
269
414
  app.onAfterHandle((req, res, path, method) => {
270
- const duration = Date.now() - (req as any).startTime;
271
- console.log(`✅ Response: ${method} ${path} ${res.status} (${duration}ms)`);
415
+ const ms = Date.now() - (req as any).startTime;
416
+ console.log(`✅ ${method} ${path} ${res.status} (${ms}ms)`);
272
417
  });
273
418
 
274
- // Called when handler throws an error
275
419
  app.onError((err, req, path, method) => {
276
- console.error(`❌ Error in ${method} ${path}:`, err.message);
277
- // Send alert, log to monitoring service, etc.
420
+ console.error(`❌ ${method} ${path}:`, err.message);
278
421
  });
279
422
 
280
423
  app.get("/users", () => ({ users: [] }));
424
+ app.listen(3000);
281
425
  ```
282
426
 
283
- **Hook execution order:**
284
- 1. `onRequest` — early for request-wide setup
285
- 2. `onBeforeHandle` — just before route handler runs
427
+ **Execution order:**
428
+ 1. `onRequest` — runs before routing, good for setup
429
+ 2. `onBeforeHandle` — just before the handler
286
430
  3. Handler executes
287
- 4. `onAfterHandle` — after success (on error, skipped)
288
- 5. `onError` — only if handler throws (skips onAfterHandle)
289
-
290
- **Use cases:**
291
- - 📊 Metrics & observability
292
- - 🔍 Request inspection & debugging
293
- - ⏱️ Timing & performance monitoring
294
- - 🚨 Error tracking & alerting
295
- - 🔐 Security audits & compliance logging
431
+ 4. `onAfterHandle` — after success (skipped on error)
432
+ 5. `onError` — only when handler throws
296
433
 
297
434
  ---
298
435
 
299
436
  ## 🔒 End-to-End Type Safety
300
437
 
301
- Define a contract once — your client gets full TypeScript autocompletion automatically:
302
-
303
438
  ```ts
439
+ import { createClient, type PrinceApiContract } from "princejs/client";
440
+
304
441
  type ApiContract = {
305
442
  "GET /users/:id": {
306
443
  params: { id: string };
@@ -312,12 +449,13 @@ type ApiContract = {
312
449
  };
313
450
  };
314
451
 
315
- import { createClient } from "princejs/client";
316
-
317
452
  const client = createClient<ApiContract>("http://localhost:3000");
318
453
 
319
454
  const user = await client.get("/users/:id", { params: { id: "42" } });
320
455
  console.log(user.name); // typed as string ✅
456
+
457
+ const created = await client.post("/users", { body: { name: "Alice" } });
458
+ console.log(created.id); // typed as string ✅
321
459
  ```
322
460
 
323
461
  ---
@@ -342,20 +480,19 @@ import { toDeno } from "princejs/deno";
342
480
  Deno.serve(toDeno(app));
343
481
  ```
344
482
 
345
- **Node Adapter** - `server.ts`
483
+ **Node.js** `server.ts`
346
484
  ```ts
347
485
  import { createServer } from "http";
348
486
  import { toNode, toExpress } from "princejs/node";
487
+ import express from "express";
349
488
 
350
489
  const app = prince();
351
- app.get("/", () => ({ message: "Hello from Node!" }));
490
+ app.get("/", () => ({ message: "Hello!" }));
352
491
 
353
- // Native Node.js http
354
- const server = createServer(toNode(app));
355
- server.listen(3000);
492
+ // Native Node http
493
+ createServer(toNode(app)).listen(3000);
356
494
 
357
- // Or with Express
358
- import express from "express";
495
+ // Or drop into Express
359
496
  const expressApp = express();
360
497
  expressApp.all("*", toExpress(app));
361
498
  expressApp.listen(3000);
@@ -367,102 +504,119 @@ expressApp.listen(3000);
367
504
 
368
505
  ```ts
369
506
  import { prince } from "princejs";
370
- import { cors, logger, rateLimit, auth, apiKey, jwt, session, compress, serve } from "princejs/middleware";
371
- import { validate } from "princejs/validation";
372
- import { cache, upload, sse } from "princejs/helpers";
507
+ import {
508
+ cors,
509
+ logger,
510
+ rateLimit,
511
+ auth,
512
+ apiKey,
513
+ jwt,
514
+ signJWT,
515
+ session,
516
+ compress,
517
+ validate,
518
+ secureHeaders,
519
+ timeout,
520
+ requestId,
521
+ } from "princejs/middleware";
522
+ import { cache, upload, sse, stream } from "princejs/helpers";
373
523
  import { cron } from "princejs/scheduler";
374
524
  import { Html, Head, Body, H1, P, render } from "princejs/jsx";
375
525
  import { db } from "princejs/db";
376
526
  import { z } from "zod";
377
527
 
378
- const app = prince(true);
379
-
380
- // ==========================================
381
- // LIFECYCLE HOOKS - Timing & Observability
382
- // ==========================================
383
- app.onRequest((req) => {
384
- (req as any).startTime = Date.now();
385
- });
386
-
387
- app.onBeforeHandle((req, path, method) => {
388
- console.log(`🔍 Handling: ${method} ${path}`);
389
- });
528
+ const SECRET = new TextEncoder().encode("your-secret");
529
+ const app = prince();
390
530
 
531
+ // ── Lifecycle hooks ───────────────────────────────────────
532
+ app.onRequest((req) => { (req as any).t = Date.now(); });
391
533
  app.onAfterHandle((req, res, path, method) => {
392
- const duration = Date.now() - (req as any).startTime;
393
- console.log(`✅ ${method} ${path} → ${res.status} (${duration}ms)`);
534
+ console.log(`✅ ${method} ${path} ${res.status} (${Date.now() - (req as any).t}ms)`);
394
535
  });
395
-
396
536
  app.onError((err, req, path, method) => {
397
- console.error(`❌ ${method} ${path} failed:`, err.message);
537
+ console.error(`❌ ${method} ${path}:`, err.message);
398
538
  });
399
539
 
400
- // ==========================================
401
- // GLOBAL MIDDLEWARE
402
- // ==========================================
540
+ // ── Global middleware ─────────────────────────────────────
541
+ app.use(secureHeaders());
542
+ app.use(requestId());
543
+ app.use(timeout(10000));
403
544
  app.use(cors());
404
545
  app.use(logger());
405
- app.use(rateLimit({ max: 100, window: 60 }));
406
- app.use(serve({ root: "./public" }));
407
- app.use(jwt(key));
408
- app.use(session({ secret: "key" }));
546
+ app.use(rateLimit(100, 60));
547
+ app.use(jwt(SECRET));
548
+ app.use(session({ secret: "session-secret" }));
409
549
  app.use(compress());
410
550
 
411
- // ==========================================
412
- // ROUTES
413
- // ==========================================
414
-
415
- // JSX
416
- const Page = () => Html(Head("Test Page"), Body(H1("Hello World"), P("This is a test")));
417
- app.get("/jsx", () => render(Page()));
551
+ // ── JSX SSR ───────────────────────────────────────────────
552
+ const Page = () => Html(Head("Home"), Body(H1("Hello World"), P("Welcome!")));
553
+ app.get("/", () => render(Page()));
418
554
 
419
- // Cookies & IP Detection
420
- app.post("/login", (req) =>
555
+ // ── Cookies & IP ──────────────────────────────────────────
556
+ app.post("/login", (req) =>
421
557
  app.response()
422
558
  .json({ ok: true, ip: req.ip })
423
- .cookie("sessionId", "user_123", {
424
- httpOnly: true,
425
- secure: true,
426
- sameSite: "Strict",
427
- maxAge: 86400 // 24 hours
559
+ .cookie("sessionId", "user_123", {
560
+ httpOnly: true, secure: true, sameSite: "Strict", maxAge: 86400,
428
561
  })
429
562
  );
430
-
431
563
  app.get("/profile", (req) => ({
432
564
  sessionId: req.cookies?.sessionId,
433
565
  clientIp: req.ip,
434
566
  }));
435
567
 
436
- // Database
437
- const users = db.sqlite("./db.sqlite", "CREATE TABLE users...");
568
+ // ── Database ──────────────────────────────────────────────
569
+ const users = db.sqlite("./app.sqlite", `
570
+ CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT NOT NULL)
571
+ `);
438
572
  app.get("/users", () => users.query("SELECT * FROM users"));
439
573
 
440
- // WebSockets
574
+ // ── WebSockets ────────────────────────────────────────────
441
575
  app.ws("/chat", {
442
- open: (ws) => ws.send("Welcome!"),
576
+ open: (ws) => ws.send("Welcome!"),
443
577
  message: (ws, msg) => ws.send(`Echo: ${msg}`),
578
+ close: (ws) => console.log("disconnected"),
444
579
  });
445
580
 
446
- // Auth
581
+ // ── Auth & API keys ───────────────────────────────────────
447
582
  app.get("/protected", auth(), (req) => ({ user: req.user }));
448
- app.get("/api", apiKey({ keys: ["key_123"] }), (req) => ({ ok: true }));
583
+ app.get("/api", apiKey({ keys: ["key_123"] }), () => ({ ok: true }));
584
+
585
+ // ── Helpers ───────────────────────────────────────────────
586
+ app.get("/cached", cache(60)(() => ({ time: Date.now() })));
587
+ app.post("/upload", upload());
588
+ app.get("/events", sse(), (req) => {
589
+ let i = 0;
590
+ const id = setInterval(() => {
591
+ req.sseSend({ count: i++ });
592
+ if (i >= 10) clearInterval(id);
593
+ }, 1000);
594
+ });
449
595
 
450
- // Helpers
451
- app.get("/data", cache(60)(() => ({ time: Date.now() })));
452
- app.post("/upload", upload(), (req) => ({ files: Object.keys(req.files || {}) }));
453
- app.get("/events", sse(), (req) => {
454
- setInterval(() => req.sseSend({ time: Date.now() }), 1000);
596
+ // ── Validation ────────────────────────────────────────────
597
+ app.post(
598
+ "/items",
599
+ validate(z.object({ name: z.string().min(1), price: z.number().positive() })),
600
+ (req) => ({ created: req.parsedBody })
601
+ );
602
+
603
+ // ── Route groups ─────────────────────────────────────────
604
+ app.group("/v1", (r) => {
605
+ r.get("/status", () => ({ version: 1, ok: true }));
455
606
  });
456
607
 
457
- // ==========================================
458
- // CRON JOBS
459
- // ==========================================
460
- cron("*/1 * * * *", () => console.log("PrinceJS heartbeat"));
608
+ // ── Streaming ─────────────────────────────────────────────
609
+ app.get("/ai", stream(async function*() {
610
+ yield "Hello ";
611
+ yield "World!";
612
+ }));
613
+
614
+ // ── Cron ──────────────────────────────────────────────────
615
+ cron("* * * * *", () => console.log("💓 heartbeat"));
461
616
 
462
- // ==========================================
463
- // OPENAPI + SCALAR DOCS
464
- // ==========================================
617
+ // ── OpenAPI + Scalar ──────────────────────────────────────
465
618
  const api = app.openapi({ title: "PrinceJS App", version: "1.0.0" }, "/docs");
619
+
466
620
  api.route("GET", "/items", {
467
621
  summary: "List items",
468
622
  tags: ["items"],
@@ -472,6 +626,15 @@ api.route("GET", "/items", {
472
626
  },
473
627
  }, () => [{ id: "1", name: "Widget" }]);
474
628
 
629
+ api.route("POST", "/items", {
630
+ summary: "Create item",
631
+ tags: ["items"],
632
+ schema: {
633
+ body: z.object({ name: z.string().min(1), price: z.number().positive() }),
634
+ response: z.object({ id: z.string(), name: z.string() }),
635
+ },
636
+ }, (req) => ({ id: crypto.randomUUID(), name: req.parsedBody.name }));
637
+
475
638
  app.listen(3000);
476
639
  ```
477
640
 
@@ -483,8 +646,6 @@ app.listen(3000);
483
646
  bun add princejs
484
647
  # or
485
648
  npm install princejs
486
- # or
487
- yarn add princejs
488
649
  ```
489
650
 
490
651
  ---
@@ -511,7 +672,7 @@ bun test
511
672
 
512
673
  <div align="center">
513
674
 
514
- **PrinceJS: Small in size. Giant in capability. 👑**
675
+ **PrinceJS: 6.3kB. Hono-speed. Everything included. 👑**
515
676
 
516
677
  *Built with ❤️ in Nigeria*
517
678