princejs 2.1.5 → 2.1.6
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 +135 -154
- package/package.json +1 -1
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 |
|
|
24
|
-
|
|
25
|
-
| Elysia |
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
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
|
|
31
|
+
> PrinceJS is **2.3× faster than Express**, matches Hono head-to-head, and sits at just **5.1kB gzipped** — loads in ~101ms 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
|
-
|
|
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
|
|
53
|
+
app.get("/users/:id", (req) => ({ id: req.params?.id }));
|
|
53
54
|
|
|
54
55
|
app.listen(3000);
|
|
55
56
|
```
|
|
@@ -60,24 +61,17 @@ app.listen(3000);
|
|
|
60
61
|
|
|
61
62
|
| Feature | Import |
|
|
62
63
|
|---------|--------|
|
|
63
|
-
| Routing | `princejs` |
|
|
64
|
-
|
|
|
65
|
-
|
|
|
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
|
+
| CORS, Logger, JWT, Auth, Rate Limit, Validate, Compress, Session, API Key | `princejs/middleware` |
|
|
66
|
+
| File Uploads, SSE, In-memory Cache | `princejs/helpers` |
|
|
73
67
|
| Cron Scheduler | `princejs/scheduler` |
|
|
74
|
-
| **OpenAPI + Scalar Docs** | `princejs` |
|
|
75
68
|
| JSX / SSR | `princejs/jsx` |
|
|
76
69
|
| SQLite Database | `princejs/db` |
|
|
77
|
-
| Plugin System | `princejs` |
|
|
78
70
|
| End-to-End Type Safety | `princejs/client` |
|
|
79
|
-
|
|
|
80
|
-
|
|
|
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` |
|
|
81
75
|
|
|
82
76
|
---
|
|
83
77
|
|
|
@@ -85,31 +79,35 @@ app.listen(3000);
|
|
|
85
79
|
|
|
86
80
|
### Reading Cookies
|
|
87
81
|
|
|
88
|
-
Cookies are automatically parsed
|
|
82
|
+
Cookies are automatically parsed and available on every request:
|
|
89
83
|
|
|
90
84
|
```ts
|
|
85
|
+
import { prince } from "princejs";
|
|
86
|
+
|
|
87
|
+
const app = prince();
|
|
88
|
+
|
|
91
89
|
app.get("/profile", (req) => ({
|
|
92
90
|
sessionId: req.cookies?.sessionId,
|
|
93
91
|
theme: req.cookies?.theme,
|
|
94
|
-
allCookies: req.cookies // Record<string, string>
|
|
92
|
+
allCookies: req.cookies, // Record<string, string>
|
|
95
93
|
}));
|
|
96
94
|
```
|
|
97
95
|
|
|
98
96
|
### Setting Cookies
|
|
99
97
|
|
|
100
|
-
Use the response builder
|
|
98
|
+
Use the response builder for full cookie control:
|
|
101
99
|
|
|
102
100
|
```ts
|
|
103
|
-
app.get("/login", (req) =>
|
|
101
|
+
app.get("/login", (req) =>
|
|
104
102
|
app.response()
|
|
105
103
|
.status(200)
|
|
106
104
|
.json({ ok: true })
|
|
107
105
|
.cookie("sessionId", "abc123", {
|
|
108
|
-
maxAge: 3600,
|
|
106
|
+
maxAge: 3600, // 1 hour
|
|
109
107
|
path: "/",
|
|
110
|
-
httpOnly: true,
|
|
111
|
-
secure: true,
|
|
112
|
-
sameSite: "Strict"
|
|
108
|
+
httpOnly: true, // not accessible from JS
|
|
109
|
+
secure: true, // HTTPS only
|
|
110
|
+
sameSite: "Strict", // CSRF protection
|
|
113
111
|
})
|
|
114
112
|
);
|
|
115
113
|
|
|
@@ -123,39 +121,30 @@ app.response()
|
|
|
123
121
|
|
|
124
122
|
### Client IP Detection
|
|
125
123
|
|
|
126
|
-
Automatically detect client IP from request headers:
|
|
127
|
-
|
|
128
124
|
```ts
|
|
129
125
|
app.get("/api/data", (req) => ({
|
|
130
126
|
clientIp: req.ip,
|
|
131
|
-
data: [
|
|
127
|
+
data: [],
|
|
132
128
|
}));
|
|
133
129
|
```
|
|
134
130
|
|
|
135
131
|
**Supported headers** (in priority order):
|
|
136
|
-
- `X-Forwarded-For` —
|
|
132
|
+
- `X-Forwarded-For` — load balancers, proxies (first IP in list)
|
|
137
133
|
- `X-Real-IP` — Nginx, Apache reverse proxy
|
|
138
134
|
- `CF-Connecting-IP` — Cloudflare
|
|
139
|
-
- `X-Client-IP` —
|
|
140
|
-
- Fallback — `127.0.0.1`
|
|
141
|
-
|
|
142
|
-
**Use cases:**
|
|
143
|
-
- 🔒 Rate limiting per IP
|
|
144
|
-
- 📊 Geolocation analytics
|
|
145
|
-
- 🚨 IP-based access control
|
|
146
|
-
- 👥 User tracking & fraud detection
|
|
135
|
+
- `X-Client-IP` — other proxy services
|
|
136
|
+
- Fallback — `127.0.0.1`
|
|
147
137
|
|
|
148
138
|
```ts
|
|
149
|
-
//
|
|
139
|
+
// IP-based rate limiting
|
|
150
140
|
app.use((req, next) => {
|
|
151
|
-
const
|
|
152
|
-
const count = ipTracker.getCount(ip) || 0;
|
|
141
|
+
const count = ipTracker.getCount(req.ip) || 0;
|
|
153
142
|
if (count > 100) return new Response("Too many requests", { status: 429 });
|
|
154
|
-
ipTracker.increment(ip);
|
|
143
|
+
ipTracker.increment(req.ip);
|
|
155
144
|
return next();
|
|
156
145
|
});
|
|
157
146
|
|
|
158
|
-
// IP
|
|
147
|
+
// IP allowlist
|
|
159
148
|
app.post("/admin", (req) => {
|
|
160
149
|
if (!ALLOWED_IPS.includes(req.ip!)) {
|
|
161
150
|
return new Response("Forbidden", { status: 403 });
|
|
@@ -168,7 +157,7 @@ app.post("/admin", (req) => {
|
|
|
168
157
|
|
|
169
158
|
## 📖 OpenAPI + Scalar Docs ✨
|
|
170
159
|
|
|
171
|
-
Auto-generate an OpenAPI 3.0 spec and serve a beautiful [Scalar](https://scalar.com) UI — all from a single `app.openapi()` call.
|
|
160
|
+
Auto-generate an OpenAPI 3.0 spec and serve a beautiful [Scalar](https://scalar.com) UI — all from a single `app.openapi()` call.
|
|
172
161
|
|
|
173
162
|
```ts
|
|
174
163
|
import { prince } from "princejs";
|
|
@@ -203,16 +192,16 @@ app.listen(3000);
|
|
|
203
192
|
`api.route()` does three things at once:
|
|
204
193
|
|
|
205
194
|
- ✅ Registers the route on PrinceJS
|
|
206
|
-
- ✅ Auto-wires
|
|
195
|
+
- ✅ Auto-wires body validation — no separate middleware needed
|
|
207
196
|
- ✅ Writes the full OpenAPI spec entry
|
|
208
197
|
|
|
209
|
-
| `schema` key | Runtime | Scalar
|
|
198
|
+
| `schema` key | Runtime effect | Scalar docs |
|
|
210
199
|
|---|---|---|
|
|
211
|
-
| `body` | ✅ Validates
|
|
200
|
+
| `body` | ✅ Validates & rejects bad requests | ✅ requestBody model |
|
|
212
201
|
| `query` | — | ✅ Typed query params |
|
|
213
202
|
| `response` | — | ✅ 200 response model |
|
|
214
203
|
|
|
215
|
-
> Routes on `app.get()` / `app.post()` stay private — never appear in docs.
|
|
204
|
+
> Routes on `app.get()` / `app.post()` stay private — they never appear in the docs.
|
|
216
205
|
|
|
217
206
|
**Themes:** `default` · `moon` · `purple` · `solarized` · `bluePlanet` · `deepSpace` · `saturn` · `kepler` · `mars`
|
|
218
207
|
|
|
@@ -220,8 +209,6 @@ app.listen(3000);
|
|
|
220
209
|
|
|
221
210
|
## 🔌 Plugin System
|
|
222
211
|
|
|
223
|
-
Share bundles of routes and middleware as reusable plugins:
|
|
224
|
-
|
|
225
212
|
```ts
|
|
226
213
|
import { prince, type PrincePlugin } from "princejs";
|
|
227
214
|
|
|
@@ -241,66 +228,53 @@ const usersPlugin: PrincePlugin<{ prefix?: string }> = (app, opts) => {
|
|
|
241
228
|
|
|
242
229
|
const app = prince();
|
|
243
230
|
app.plugin(usersPlugin, { prefix: "/api" });
|
|
231
|
+
app.listen(3000);
|
|
244
232
|
```
|
|
245
233
|
|
|
246
234
|
---
|
|
247
235
|
|
|
248
236
|
## 🎣 Lifecycle Hooks
|
|
249
237
|
|
|
250
|
-
React to key moments in request processing with lifecycle hooks:
|
|
251
|
-
|
|
252
238
|
```ts
|
|
253
239
|
import { prince } from "princejs";
|
|
254
240
|
|
|
255
241
|
const app = prince();
|
|
256
242
|
|
|
257
|
-
// Called for every incoming request
|
|
258
243
|
app.onRequest((req) => {
|
|
259
|
-
|
|
244
|
+
(req as any).startTime = Date.now();
|
|
260
245
|
});
|
|
261
246
|
|
|
262
|
-
// Called before handler execution
|
|
263
247
|
app.onBeforeHandle((req, path, method) => {
|
|
264
|
-
console.log(`🔍
|
|
265
|
-
(req as any).startTime = Date.now();
|
|
248
|
+
console.log(`🔍 ${method} ${path}`);
|
|
266
249
|
});
|
|
267
250
|
|
|
268
|
-
// Called after successful handler execution
|
|
269
251
|
app.onAfterHandle((req, res, path, method) => {
|
|
270
|
-
const
|
|
271
|
-
console.log(`✅
|
|
252
|
+
const ms = Date.now() - (req as any).startTime;
|
|
253
|
+
console.log(`✅ ${method} ${path} ${res.status} (${ms}ms)`);
|
|
272
254
|
});
|
|
273
255
|
|
|
274
|
-
// Called when handler throws an error
|
|
275
256
|
app.onError((err, req, path, method) => {
|
|
276
|
-
console.error(`❌
|
|
277
|
-
// Send alert, log to monitoring service, etc.
|
|
257
|
+
console.error(`❌ ${method} ${path}:`, err.message);
|
|
278
258
|
});
|
|
279
259
|
|
|
280
260
|
app.get("/users", () => ({ users: [] }));
|
|
261
|
+
app.listen(3000);
|
|
281
262
|
```
|
|
282
263
|
|
|
283
|
-
**
|
|
284
|
-
1. `onRequest` —
|
|
285
|
-
2. `onBeforeHandle` — just before
|
|
264
|
+
**Execution order:**
|
|
265
|
+
1. `onRequest` — runs before routing, good for setup
|
|
266
|
+
2. `onBeforeHandle` — just before the handler
|
|
286
267
|
3. Handler executes
|
|
287
|
-
4. `onAfterHandle` — after success (on error
|
|
288
|
-
5. `onError` — only
|
|
289
|
-
|
|
290
|
-
**Use cases:**
|
|
291
|
-
- 📊 Metrics & observability
|
|
292
|
-
- 🔍 Request inspection & debugging
|
|
293
|
-
- ⏱️ Timing & performance monitoring
|
|
294
|
-
- 🚨 Error tracking & alerting
|
|
295
|
-
- 🔐 Security audits & compliance logging
|
|
268
|
+
4. `onAfterHandle` — after success (skipped on error)
|
|
269
|
+
5. `onError` — only when handler throws
|
|
296
270
|
|
|
297
271
|
---
|
|
298
272
|
|
|
299
273
|
## 🔒 End-to-End Type Safety
|
|
300
274
|
|
|
301
|
-
Define a contract once — your client gets full TypeScript autocompletion automatically:
|
|
302
|
-
|
|
303
275
|
```ts
|
|
276
|
+
import { createClient, type PrinceApiContract } from "princejs/client";
|
|
277
|
+
|
|
304
278
|
type ApiContract = {
|
|
305
279
|
"GET /users/:id": {
|
|
306
280
|
params: { id: string };
|
|
@@ -312,12 +286,13 @@ type ApiContract = {
|
|
|
312
286
|
};
|
|
313
287
|
};
|
|
314
288
|
|
|
315
|
-
import { createClient } from "princejs/client";
|
|
316
|
-
|
|
317
289
|
const client = createClient<ApiContract>("http://localhost:3000");
|
|
318
290
|
|
|
319
291
|
const user = await client.get("/users/:id", { params: { id: "42" } });
|
|
320
292
|
console.log(user.name); // typed as string ✅
|
|
293
|
+
|
|
294
|
+
const created = await client.post("/users", { body: { name: "Alice" } });
|
|
295
|
+
console.log(created.id); // typed as string ✅
|
|
321
296
|
```
|
|
322
297
|
|
|
323
298
|
---
|
|
@@ -342,20 +317,19 @@ import { toDeno } from "princejs/deno";
|
|
|
342
317
|
Deno.serve(toDeno(app));
|
|
343
318
|
```
|
|
344
319
|
|
|
345
|
-
**Node
|
|
320
|
+
**Node.js** — `server.ts`
|
|
346
321
|
```ts
|
|
347
322
|
import { createServer } from "http";
|
|
348
323
|
import { toNode, toExpress } from "princejs/node";
|
|
324
|
+
import express from "express";
|
|
349
325
|
|
|
350
326
|
const app = prince();
|
|
351
|
-
app.get("/", () => ({ message: "Hello
|
|
327
|
+
app.get("/", () => ({ message: "Hello!" }));
|
|
352
328
|
|
|
353
|
-
// Native Node
|
|
354
|
-
|
|
355
|
-
server.listen(3000);
|
|
329
|
+
// Native Node http
|
|
330
|
+
createServer(toNode(app)).listen(3000);
|
|
356
331
|
|
|
357
|
-
// Or
|
|
358
|
-
import express from "express";
|
|
332
|
+
// Or drop into Express
|
|
359
333
|
const expressApp = express();
|
|
360
334
|
expressApp.all("*", toExpress(app));
|
|
361
335
|
expressApp.listen(3000);
|
|
@@ -367,102 +341,102 @@ expressApp.listen(3000);
|
|
|
367
341
|
|
|
368
342
|
```ts
|
|
369
343
|
import { prince } from "princejs";
|
|
370
|
-
import {
|
|
371
|
-
|
|
344
|
+
import {
|
|
345
|
+
cors,
|
|
346
|
+
logger,
|
|
347
|
+
rateLimit,
|
|
348
|
+
auth,
|
|
349
|
+
apiKey,
|
|
350
|
+
jwt,
|
|
351
|
+
signJWT,
|
|
352
|
+
session,
|
|
353
|
+
compress,
|
|
354
|
+
validate,
|
|
355
|
+
} from "princejs/middleware";
|
|
372
356
|
import { cache, upload, sse } from "princejs/helpers";
|
|
373
357
|
import { cron } from "princejs/scheduler";
|
|
374
358
|
import { Html, Head, Body, H1, P, render } from "princejs/jsx";
|
|
375
359
|
import { db } from "princejs/db";
|
|
376
360
|
import { z } from "zod";
|
|
377
361
|
|
|
378
|
-
const
|
|
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
|
-
});
|
|
362
|
+
const SECRET = new TextEncoder().encode("your-secret");
|
|
363
|
+
const app = prince();
|
|
390
364
|
|
|
365
|
+
// ── Lifecycle hooks ───────────────────────────────────────
|
|
366
|
+
app.onRequest((req) => { (req as any).t = Date.now(); });
|
|
391
367
|
app.onAfterHandle((req, res, path, method) => {
|
|
392
|
-
|
|
393
|
-
console.log(`✅ ${method} ${path} → ${res.status} (${duration}ms)`);
|
|
368
|
+
console.log(`✅ ${method} ${path} ${res.status} (${Date.now() - (req as any).t}ms)`);
|
|
394
369
|
});
|
|
395
|
-
|
|
396
370
|
app.onError((err, req, path, method) => {
|
|
397
|
-
console.error(`❌ ${method} ${path}
|
|
371
|
+
console.error(`❌ ${method} ${path}:`, err.message);
|
|
398
372
|
});
|
|
399
373
|
|
|
400
|
-
//
|
|
401
|
-
// GLOBAL MIDDLEWARE
|
|
402
|
-
// ==========================================
|
|
374
|
+
// ── Global middleware ─────────────────────────────────────
|
|
403
375
|
app.use(cors());
|
|
404
376
|
app.use(logger());
|
|
405
|
-
app.use(rateLimit(
|
|
406
|
-
app.use(
|
|
407
|
-
app.use(
|
|
408
|
-
app.use(session({ secret: "key" }));
|
|
377
|
+
app.use(rateLimit(100, 60));
|
|
378
|
+
app.use(jwt(SECRET));
|
|
379
|
+
app.use(session({ secret: "session-secret" }));
|
|
409
380
|
app.use(compress());
|
|
410
381
|
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
|
|
382
|
+
// ── JSX SSR ───────────────────────────────────────────────
|
|
383
|
+
const Page = () => Html(Head("Home"), Body(H1("Hello World"), P("Welcome!")));
|
|
384
|
+
app.get("/", () => render(Page()));
|
|
414
385
|
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
app.get("/jsx", () => render(Page()));
|
|
418
|
-
|
|
419
|
-
// Cookies & IP Detection
|
|
420
|
-
app.post("/login", (req) =>
|
|
386
|
+
// ── Cookies & IP ──────────────────────────────────────────
|
|
387
|
+
app.post("/login", (req) =>
|
|
421
388
|
app.response()
|
|
422
389
|
.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
|
|
390
|
+
.cookie("sessionId", "user_123", {
|
|
391
|
+
httpOnly: true, secure: true, sameSite: "Strict", maxAge: 86400,
|
|
428
392
|
})
|
|
429
393
|
);
|
|
430
|
-
|
|
431
394
|
app.get("/profile", (req) => ({
|
|
432
395
|
sessionId: req.cookies?.sessionId,
|
|
433
396
|
clientIp: req.ip,
|
|
434
397
|
}));
|
|
435
398
|
|
|
436
|
-
// Database
|
|
437
|
-
const users = db.sqlite("./
|
|
399
|
+
// ── Database ──────────────────────────────────────────────
|
|
400
|
+
const users = db.sqlite("./app.sqlite", `
|
|
401
|
+
CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT NOT NULL)
|
|
402
|
+
`);
|
|
438
403
|
app.get("/users", () => users.query("SELECT * FROM users"));
|
|
439
404
|
|
|
440
|
-
// WebSockets
|
|
405
|
+
// ── WebSockets ────────────────────────────────────────────
|
|
441
406
|
app.ws("/chat", {
|
|
442
|
-
open:
|
|
407
|
+
open: (ws) => ws.send("Welcome!"),
|
|
443
408
|
message: (ws, msg) => ws.send(`Echo: ${msg}`),
|
|
409
|
+
close: (ws) => console.log("disconnected"),
|
|
444
410
|
});
|
|
445
411
|
|
|
446
|
-
// Auth
|
|
412
|
+
// ── Auth & API keys ───────────────────────────────────────
|
|
447
413
|
app.get("/protected", auth(), (req) => ({ user: req.user }));
|
|
448
|
-
app.get("/api", apiKey({ keys: ["key_123"] }), (
|
|
449
|
-
|
|
450
|
-
// Helpers
|
|
451
|
-
app.get("/
|
|
452
|
-
app.post("/upload", upload()
|
|
453
|
-
app.get("/events",
|
|
454
|
-
|
|
414
|
+
app.get("/api", apiKey({ keys: ["key_123"] }), () => ({ ok: true }));
|
|
415
|
+
|
|
416
|
+
// ── Helpers ───────────────────────────────────────────────
|
|
417
|
+
app.get("/cached", cache(60)(() => ({ time: Date.now() })));
|
|
418
|
+
app.post("/upload", upload());
|
|
419
|
+
app.get("/events", sse(), (req) => {
|
|
420
|
+
let i = 0;
|
|
421
|
+
const id = setInterval(() => {
|
|
422
|
+
req.sseSend({ count: i++ });
|
|
423
|
+
if (i >= 10) clearInterval(id);
|
|
424
|
+
}, 1000);
|
|
455
425
|
});
|
|
456
426
|
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
427
|
+
// ── Validation ────────────────────────────────────────────
|
|
428
|
+
app.post(
|
|
429
|
+
"/items",
|
|
430
|
+
validate(z.object({ name: z.string().min(1), price: z.number().positive() })),
|
|
431
|
+
(req) => ({ created: req.parsedBody })
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
// ── Cron ──────────────────────────────────────────────────
|
|
435
|
+
cron("* * * * *", () => console.log("💓 heartbeat"));
|
|
461
436
|
|
|
462
|
-
//
|
|
463
|
-
// OPENAPI + SCALAR DOCS
|
|
464
|
-
// ==========================================
|
|
437
|
+
// ── OpenAPI + Scalar ──────────────────────────────────────
|
|
465
438
|
const api = app.openapi({ title: "PrinceJS App", version: "1.0.0" }, "/docs");
|
|
439
|
+
|
|
466
440
|
api.route("GET", "/items", {
|
|
467
441
|
summary: "List items",
|
|
468
442
|
tags: ["items"],
|
|
@@ -472,6 +446,15 @@ api.route("GET", "/items", {
|
|
|
472
446
|
},
|
|
473
447
|
}, () => [{ id: "1", name: "Widget" }]);
|
|
474
448
|
|
|
449
|
+
api.route("POST", "/items", {
|
|
450
|
+
summary: "Create item",
|
|
451
|
+
tags: ["items"],
|
|
452
|
+
schema: {
|
|
453
|
+
body: z.object({ name: z.string().min(1), price: z.number().positive() }),
|
|
454
|
+
response: z.object({ id: z.string(), name: z.string() }),
|
|
455
|
+
},
|
|
456
|
+
}, (req) => ({ id: crypto.randomUUID(), name: req.parsedBody.name }));
|
|
457
|
+
|
|
475
458
|
app.listen(3000);
|
|
476
459
|
```
|
|
477
460
|
|
|
@@ -483,8 +466,6 @@ app.listen(3000);
|
|
|
483
466
|
bun add princejs
|
|
484
467
|
# or
|
|
485
468
|
npm install princejs
|
|
486
|
-
# or
|
|
487
|
-
yarn add princejs
|
|
488
469
|
```
|
|
489
470
|
|
|
490
471
|
---
|
|
@@ -511,7 +492,7 @@ bun test
|
|
|
511
492
|
|
|
512
493
|
<div align="center">
|
|
513
494
|
|
|
514
|
-
**PrinceJS:
|
|
495
|
+
**PrinceJS: 5.1kB. Hono-speed. Everything included. 👑**
|
|
515
496
|
|
|
516
497
|
*Built with ❤️ in Nigeria*
|
|
517
498
|
|
package/package.json
CHANGED