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 +315 -154
- package/dist/helpers.d.ts +3 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +56 -0
- package/dist/middleware.d.ts +22 -0
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +3149 -347
- package/dist/prince.d.ts +28 -0
- package/dist/prince.d.ts.map +1 -1
- package/dist/prince.js +38 -0
- 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 **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
|
-
|
|
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,19 @@ app.listen(3000);
|
|
|
60
61
|
|
|
61
62
|
| Feature | Import |
|
|
62
63
|
|---------|--------|
|
|
63
|
-
| Routing | `princejs` |
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
| **
|
|
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
|
-
|
|
|
80
|
-
|
|
|
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
|
|
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
|
|
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,
|
|
108
|
+
maxAge: 3600, // 1 hour
|
|
109
109
|
path: "/",
|
|
110
|
-
httpOnly: true,
|
|
111
|
-
secure: true,
|
|
112
|
-
sameSite: "Strict"
|
|
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` —
|
|
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` —
|
|
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
|
|
137
|
+
- `X-Client-IP` — other proxy services
|
|
138
|
+
- Fallback — `127.0.0.1`
|
|
147
139
|
|
|
148
140
|
```ts
|
|
149
|
-
//
|
|
141
|
+
// IP-based rate limiting
|
|
150
142
|
app.use((req, next) => {
|
|
151
|
-
const
|
|
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
|
|
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.
|
|
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
|
|
358
|
+
- ✅ Auto-wires body validation — no separate middleware needed
|
|
207
359
|
- ✅ Writes the full OpenAPI spec entry
|
|
208
360
|
|
|
209
|
-
| `schema` key | Runtime | Scalar
|
|
361
|
+
| `schema` key | Runtime effect | Scalar docs |
|
|
210
362
|
|---|---|---|
|
|
211
|
-
| `body` | ✅ Validates
|
|
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
|
-
|
|
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(`🔍
|
|
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
|
|
271
|
-
console.log(`✅
|
|
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(`❌
|
|
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
|
-
**
|
|
284
|
-
1. `onRequest` —
|
|
285
|
-
2. `onBeforeHandle` — just before
|
|
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
|
|
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
|
|
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
|
|
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
|
|
490
|
+
app.get("/", () => ({ message: "Hello!" }));
|
|
352
491
|
|
|
353
|
-
// Native Node
|
|
354
|
-
|
|
355
|
-
server.listen(3000);
|
|
492
|
+
// Native Node http
|
|
493
|
+
createServer(toNode(app)).listen(3000);
|
|
356
494
|
|
|
357
|
-
// Or
|
|
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 {
|
|
371
|
-
|
|
372
|
-
|
|
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
|
|
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
|
-
|
|
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}
|
|
537
|
+
console.error(`❌ ${method} ${path}:`, err.message);
|
|
398
538
|
});
|
|
399
539
|
|
|
400
|
-
//
|
|
401
|
-
|
|
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(
|
|
406
|
-
app.use(
|
|
407
|
-
app.use(
|
|
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
|
-
|
|
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
|
|
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("./
|
|
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:
|
|
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"] }), (
|
|
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
|
-
//
|
|
451
|
-
app.
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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:
|
|
675
|
+
**PrinceJS: 6.3kB. Hono-speed. Everything included. 👑**
|
|
515
676
|
|
|
516
677
|
*Built with ❤️ in Nigeria*
|
|
517
678
|
|