princejs 2.1.0 → 2.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -20
- package/{README.md → Readme.md} +517 -517
- package/dist/middleware.js +22 -22
- package/dist/prince.d.ts.map +1 -1
- package/dist/prince.js +14 -28
- package/package.json +13 -11
package/{README.md → Readme.md}
RENAMED
|
@@ -1,518 +1,518 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
|
|
3
|
-
# 👑 PrinceJS
|
|
4
|
-
|
|
5
|
-
**Ultra-clean, modern & minimal Bun web framework.**
|
|
6
|
-
Built by a 13-year-old Nigerian developer. Among the top three in performance.
|
|
7
|
-
|
|
8
|
-
[](https://www.npmjs.com/package/princejs)
|
|
9
|
-
[](https://github.com/MatthewTheCoder1218/princejs)
|
|
10
|
-
[](https://www.npmjs.com/package/princejs)
|
|
11
|
-
[](https://github.com/MatthewTheCoder1218/princejs/blob/main/LICENSE)
|
|
12
|
-
|
|
13
|
-
[**Website**](https://princejs.vercel.app) · [**npm**](https://www.npmjs.com/package/princejs) · [**GitHub**](https://github.com/MatthewTheCoder1218/princejs) · [**Twitter**](https://twitter.com/princejs_bun)
|
|
14
|
-
|
|
15
|
-
</div>
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## ⚡ Performance
|
|
20
|
-
|
|
21
|
-
Benchmarked with `oha -c 100 -z 30s` on Windows 10:
|
|
22
|
-
|
|
23
|
-
| Framework | Req/s | Total |
|
|
24
|
-
|-----------|------:|------:|
|
|
25
|
-
| Elysia | 25,312 | 759k |
|
|
26
|
-
| Hono | 22,124 | 664k |
|
|
27
|
-
| **PrinceJS** | **21,748** | **653k** |
|
|
28
|
-
| Express | 9,325 | 280k |
|
|
29
|
-
|
|
30
|
-
> PrinceJS is **2.3× faster than Express** and sits comfortably in the top 3 — at just **4.4kB gzipped**.
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
## 🚀 Quick Start
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
npm install princejs
|
|
38
|
-
bun add princejs
|
|
39
|
-
yarn add princejs
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
```ts
|
|
43
|
-
import { prince } from "princejs";
|
|
44
|
-
import { cors, logger } from "princejs/middleware";
|
|
45
|
-
|
|
46
|
-
const app = prince();
|
|
47
|
-
|
|
48
|
-
app.use(cors());
|
|
49
|
-
app.use(logger());
|
|
50
|
-
|
|
51
|
-
app.get("/", () => ({ message: "Hello PrinceJS!" }));
|
|
52
|
-
app.get("/users/:id", (req) => ({ id: req.params.id }));
|
|
53
|
-
|
|
54
|
-
app.listen(3000);
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
## 🧰 Features
|
|
60
|
-
|
|
61
|
-
| Feature | Import |
|
|
62
|
-
|---------|--------|
|
|
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` |
|
|
73
|
-
| Cron Scheduler | `princejs/scheduler` |
|
|
74
|
-
| **OpenAPI + Scalar Docs** | `princejs` |
|
|
75
|
-
| JSX / SSR | `princejs/jsx` |
|
|
76
|
-
| SQLite Database | `princejs/db` |
|
|
77
|
-
| Plugin System | `princejs` |
|
|
78
|
-
| End-to-End Type Safety | `princejs/client` |
|
|
79
|
-
| Deploy Adapters | `princejs/vercel` · `princejs/cloudflare` · `princejs/deno` · `princejs/node` |
|
|
80
|
-
| Lifecycle Hooks | `princejs` |
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## 🍪 Cookies & 🌐 IP Detection
|
|
85
|
-
|
|
86
|
-
### Reading Cookies
|
|
87
|
-
|
|
88
|
-
Cookies are automatically parsed from the request:
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
app.get("/profile", (req) => ({
|
|
92
|
-
sessionId: req.cookies?.sessionId,
|
|
93
|
-
theme: req.cookies?.theme,
|
|
94
|
-
allCookies: req.cookies // Record<string, string>
|
|
95
|
-
}));
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### Setting Cookies
|
|
99
|
-
|
|
100
|
-
Use the response builder to set cookies with full control:
|
|
101
|
-
|
|
102
|
-
```ts
|
|
103
|
-
app.get("/login", (req) =>
|
|
104
|
-
app.response()
|
|
105
|
-
.status(200)
|
|
106
|
-
.json({ ok: true })
|
|
107
|
-
.cookie("sessionId", "abc123", {
|
|
108
|
-
maxAge: 3600, // 1 hour
|
|
109
|
-
path: "/",
|
|
110
|
-
httpOnly: true, // not accessible from JS
|
|
111
|
-
secure: true, // HTTPS only
|
|
112
|
-
sameSite: "Strict" // CSRF protection
|
|
113
|
-
})
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
// Chain multiple cookies
|
|
117
|
-
app.response()
|
|
118
|
-
.json({ ok: true })
|
|
119
|
-
.cookie("session", "xyz")
|
|
120
|
-
.cookie("theme", "dark")
|
|
121
|
-
.cookie("lang", "en");
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### Client IP Detection
|
|
125
|
-
|
|
126
|
-
Automatically detect client IP from request headers:
|
|
127
|
-
|
|
128
|
-
```ts
|
|
129
|
-
app.get("/api/data", (req) => ({
|
|
130
|
-
clientIp: req.ip,
|
|
131
|
-
data: [...]
|
|
132
|
-
}));
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
**Supported headers** (in priority order):
|
|
136
|
-
- `X-Forwarded-For` — Load balancers, proxies (first IP in list)
|
|
137
|
-
- `X-Real-IP` — Nginx, Apache reverse proxy
|
|
138
|
-
- `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
|
|
147
|
-
|
|
148
|
-
```ts
|
|
149
|
-
// Rate limit by IP
|
|
150
|
-
app.use((req, next) => {
|
|
151
|
-
const ip = req.ip;
|
|
152
|
-
const count = ipTracker.getCount(ip) || 0;
|
|
153
|
-
if (count > 100) return new Response("Too many requests", { status: 429 });
|
|
154
|
-
ipTracker.increment(ip);
|
|
155
|
-
return next();
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// IP-based security
|
|
159
|
-
app.post("/admin", (req) => {
|
|
160
|
-
if (!ALLOWED_IPS.includes(req.ip!)) {
|
|
161
|
-
return new Response("Forbidden", { status: 403 });
|
|
162
|
-
}
|
|
163
|
-
return { authorized: true };
|
|
164
|
-
});
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
---
|
|
168
|
-
|
|
169
|
-
## 📖 OpenAPI + Scalar Docs ✨
|
|
170
|
-
|
|
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.
|
|
172
|
-
|
|
173
|
-
```ts
|
|
174
|
-
import { prince } from "princejs";
|
|
175
|
-
import { z } from "zod";
|
|
176
|
-
|
|
177
|
-
const app = prince();
|
|
178
|
-
|
|
179
|
-
const api = app.openapi({ title: "My API", version: "1.0.0" }, "/docs", { theme: "moon" });
|
|
180
|
-
|
|
181
|
-
api.route("GET", "/users/:id", {
|
|
182
|
-
summary: "Get user by ID",
|
|
183
|
-
tags: ["users"],
|
|
184
|
-
schema: {
|
|
185
|
-
response: z.object({ id: z.string(), name: z.string() }),
|
|
186
|
-
},
|
|
187
|
-
}, (req) => ({ id: req.params!.id, name: "Alice" }));
|
|
188
|
-
|
|
189
|
-
api.route("POST", "/users", {
|
|
190
|
-
summary: "Create user",
|
|
191
|
-
tags: ["users"],
|
|
192
|
-
schema: {
|
|
193
|
-
body: z.object({ name: z.string().min(2), email: z.string().email() }),
|
|
194
|
-
response: z.object({ id: z.string(), name: z.string(), email: z.string() }),
|
|
195
|
-
},
|
|
196
|
-
}, (req) => ({ id: crypto.randomUUID(), ...req.parsedBody }));
|
|
197
|
-
|
|
198
|
-
app.listen(3000);
|
|
199
|
-
// → GET /docs Scalar UI
|
|
200
|
-
// → GET /docs.json Raw OpenAPI JSON
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
`api.route()` does three things at once:
|
|
204
|
-
|
|
205
|
-
- ✅ Registers the route on PrinceJS
|
|
206
|
-
- ✅ Auto-wires `validate(schema.body)` — no separate import needed
|
|
207
|
-
- ✅ Writes the full OpenAPI spec entry
|
|
208
|
-
|
|
209
|
-
| `schema` key | Runtime | Scalar Docs |
|
|
210
|
-
|---|---|---|
|
|
211
|
-
| `body` | ✅ Validates request | ✅ requestBody model |
|
|
212
|
-
| `query` | — | ✅ Typed query params |
|
|
213
|
-
| `response` | — | ✅ 200 response model |
|
|
214
|
-
|
|
215
|
-
> Routes on `app.get()` / `app.post()` stay private — never appear in docs.
|
|
216
|
-
|
|
217
|
-
**Themes:** `default` · `moon` · `purple` · `solarized` · `bluePlanet` · `deepSpace` · `saturn` · `kepler` · `mars`
|
|
218
|
-
|
|
219
|
-
---
|
|
220
|
-
|
|
221
|
-
## 🔌 Plugin System
|
|
222
|
-
|
|
223
|
-
Share bundles of routes and middleware as reusable plugins:
|
|
224
|
-
|
|
225
|
-
```ts
|
|
226
|
-
import { prince, type PrincePlugin } from "princejs";
|
|
227
|
-
|
|
228
|
-
const usersPlugin: PrincePlugin<{ prefix?: string }> = (app, opts) => {
|
|
229
|
-
const base = opts?.prefix ?? "";
|
|
230
|
-
|
|
231
|
-
app.use((req, next) => {
|
|
232
|
-
(req as any).fromPlugin = true;
|
|
233
|
-
return next();
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
app.get(`${base}/users`, (req) => ({
|
|
237
|
-
ok: true,
|
|
238
|
-
fromPlugin: (req as any).fromPlugin,
|
|
239
|
-
}));
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
const app = prince();
|
|
243
|
-
app.plugin(usersPlugin, { prefix: "/api" });
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
---
|
|
247
|
-
|
|
248
|
-
## 🎣 Lifecycle Hooks
|
|
249
|
-
|
|
250
|
-
React to key moments in request processing with lifecycle hooks:
|
|
251
|
-
|
|
252
|
-
```ts
|
|
253
|
-
import { prince } from "princejs";
|
|
254
|
-
|
|
255
|
-
const app = prince();
|
|
256
|
-
|
|
257
|
-
// Called for every incoming request
|
|
258
|
-
app.onRequest((req) => {
|
|
259
|
-
console.log(`📥 Request received: ${req.method} ${req.url}`);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
// Called before handler execution
|
|
263
|
-
app.onBeforeHandle((req, path, method) => {
|
|
264
|
-
console.log(`🔍 About to handle: ${method} ${path}`);
|
|
265
|
-
(req as any).startTime = Date.now();
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// Called after successful handler execution
|
|
269
|
-
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)`);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// Called when handler throws an error
|
|
275
|
-
app.onError((err, req, path, method) => {
|
|
276
|
-
console.error(`❌ Error in ${method} ${path}:`, err.message);
|
|
277
|
-
// Send alert, log to monitoring service, etc.
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
app.get("/users", () => ({ users: [] }));
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
**Hook execution order:**
|
|
284
|
-
1. `onRequest` — early for request-wide setup
|
|
285
|
-
2. `onBeforeHandle` — just before route handler runs
|
|
286
|
-
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
|
|
296
|
-
|
|
297
|
-
---
|
|
298
|
-
|
|
299
|
-
## 🔒 End-to-End Type Safety
|
|
300
|
-
|
|
301
|
-
Define a contract once — your client gets full TypeScript autocompletion automatically:
|
|
302
|
-
|
|
303
|
-
```ts
|
|
304
|
-
type ApiContract = {
|
|
305
|
-
"GET /users/:id": {
|
|
306
|
-
params: { id: string };
|
|
307
|
-
response: { id: string; name: string };
|
|
308
|
-
};
|
|
309
|
-
"POST /users": {
|
|
310
|
-
body: { name: string };
|
|
311
|
-
response: { id: string; ok: boolean };
|
|
312
|
-
};
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
import { createClient } from "princejs/client";
|
|
316
|
-
|
|
317
|
-
const client = createClient<ApiContract>("http://localhost:3000");
|
|
318
|
-
|
|
319
|
-
const user = await client.get("/users/:id", { params: { id: "42" } });
|
|
320
|
-
console.log(user.name); // typed as string ✅
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
---
|
|
324
|
-
|
|
325
|
-
## 🌍 Deploy Adapters
|
|
326
|
-
|
|
327
|
-
**Vercel Edge** — `api/[[...route]].ts`
|
|
328
|
-
```ts
|
|
329
|
-
import { toVercel } from "princejs/vercel";
|
|
330
|
-
export default toVercel(app);
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
**Cloudflare Workers** — `src/index.ts`
|
|
334
|
-
```ts
|
|
335
|
-
import { toWorkers } from "princejs/cloudflare";
|
|
336
|
-
export default toWorkers(app);
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
**Deno Deploy** — `main.ts`
|
|
340
|
-
```ts
|
|
341
|
-
import { toDeno } from "princejs/deno";
|
|
342
|
-
Deno.serve(toDeno(app));
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
**Node Adapter** - `server.ts`
|
|
346
|
-
```ts
|
|
347
|
-
import { createServer } from "http";
|
|
348
|
-
import { toNode, toExpress } from "princejs/node";
|
|
349
|
-
|
|
350
|
-
const app = prince();
|
|
351
|
-
app.get("/", () => ({ message: "Hello from Node!" }));
|
|
352
|
-
|
|
353
|
-
// Native Node.js http
|
|
354
|
-
const server = createServer(toNode(app));
|
|
355
|
-
server.listen(3000);
|
|
356
|
-
|
|
357
|
-
// Or with Express
|
|
358
|
-
import express from "express";
|
|
359
|
-
const expressApp = express();
|
|
360
|
-
expressApp.all("*", toExpress(app));
|
|
361
|
-
expressApp.listen(3000);
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
---
|
|
365
|
-
|
|
366
|
-
## 🎯 Full Example
|
|
367
|
-
|
|
368
|
-
```ts
|
|
369
|
-
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";
|
|
373
|
-
import { cron } from "princejs/scheduler";
|
|
374
|
-
import { Html, Head, Body, H1, P, render } from "princejs/jsx";
|
|
375
|
-
import { db } from "princejs/db";
|
|
376
|
-
import { z } from "zod";
|
|
377
|
-
|
|
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
|
-
});
|
|
390
|
-
|
|
391
|
-
app.onAfterHandle((req, res, path, method) => {
|
|
392
|
-
const duration = Date.now() - (req as any).startTime;
|
|
393
|
-
console.log(`✅ ${method} ${path} → ${res.status} (${duration}ms)`);
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
app.onError((err, req, path, method) => {
|
|
397
|
-
console.error(`❌ ${method} ${path} failed:`, err.message);
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
// ==========================================
|
|
401
|
-
// GLOBAL MIDDLEWARE
|
|
402
|
-
// ==========================================
|
|
403
|
-
app.use(cors());
|
|
404
|
-
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" }));
|
|
409
|
-
app.use(compress());
|
|
410
|
-
|
|
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()));
|
|
418
|
-
|
|
419
|
-
// Cookies & IP Detection
|
|
420
|
-
app.post("/login", (req) =>
|
|
421
|
-
app.response()
|
|
422
|
-
.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
|
|
428
|
-
})
|
|
429
|
-
);
|
|
430
|
-
|
|
431
|
-
app.get("/profile", (req) => ({
|
|
432
|
-
sessionId: req.cookies?.sessionId,
|
|
433
|
-
clientIp: req.ip,
|
|
434
|
-
}));
|
|
435
|
-
|
|
436
|
-
// Database
|
|
437
|
-
const users = db.sqlite("./db.sqlite", "CREATE TABLE users...");
|
|
438
|
-
app.get("/users", () => users.query("SELECT * FROM users"));
|
|
439
|
-
|
|
440
|
-
// WebSockets
|
|
441
|
-
app.ws("/chat", {
|
|
442
|
-
open: (ws) => ws.send("Welcome!"),
|
|
443
|
-
message: (ws, msg) => ws.send(`Echo: ${msg}`),
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
// Auth
|
|
447
|
-
app.get("/protected", auth(), (req) => ({ user: req.user }));
|
|
448
|
-
app.get("/api", apiKey({ keys: ["key_123"] }), (req) => ({ ok: true }));
|
|
449
|
-
|
|
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);
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
// ==========================================
|
|
458
|
-
// CRON JOBS
|
|
459
|
-
// ==========================================
|
|
460
|
-
cron("*/1 * * * *", () => console.log("PrinceJS heartbeat"));
|
|
461
|
-
|
|
462
|
-
// ==========================================
|
|
463
|
-
// OPENAPI + SCALAR DOCS
|
|
464
|
-
// ==========================================
|
|
465
|
-
const api = app.openapi({ title: "PrinceJS App", version: "1.0.0" }, "/docs");
|
|
466
|
-
api.route("GET", "/items", {
|
|
467
|
-
summary: "List items",
|
|
468
|
-
tags: ["items"],
|
|
469
|
-
schema: {
|
|
470
|
-
query: z.object({ q: z.string().optional() }),
|
|
471
|
-
response: z.array(z.object({ id: z.string(), name: z.string() })),
|
|
472
|
-
},
|
|
473
|
-
}, () => [{ id: "1", name: "Widget" }]);
|
|
474
|
-
|
|
475
|
-
app.listen(3000);
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
---
|
|
479
|
-
|
|
480
|
-
## 📦 Installation
|
|
481
|
-
|
|
482
|
-
```bash
|
|
483
|
-
bun add princejs
|
|
484
|
-
# or
|
|
485
|
-
npm install princejs
|
|
486
|
-
# or
|
|
487
|
-
yarn add princejs
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
---
|
|
491
|
-
|
|
492
|
-
## 🤝 Contributing
|
|
493
|
-
|
|
494
|
-
```bash
|
|
495
|
-
git clone https://github.com/MatthewTheCoder1218/princejs
|
|
496
|
-
cd princejs
|
|
497
|
-
bun install
|
|
498
|
-
bun test
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
---
|
|
502
|
-
|
|
503
|
-
## 🔗 Links
|
|
504
|
-
|
|
505
|
-
- 🌐 Website: [princejs.vercel.app](https://princejs.vercel.app)
|
|
506
|
-
- 📦 npm: [npmjs.com/package/princejs](https://www.npmjs.com/package/princejs)
|
|
507
|
-
- 💻 GitHub: [github.com/MatthewTheCoder1218/princejs](https://github.com/MatthewTheCoder1218/princejs)
|
|
508
|
-
- 🐦 Twitter: [@princejs_bun](https://twitter.com/princejs_bun)
|
|
509
|
-
|
|
510
|
-
---
|
|
511
|
-
|
|
512
|
-
<div align="center">
|
|
513
|
-
|
|
514
|
-
**PrinceJS: Small in size. Giant in capability. 👑**
|
|
515
|
-
|
|
516
|
-
*Built with ❤️ in Nigeria*
|
|
517
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 👑 PrinceJS
|
|
4
|
+
|
|
5
|
+
**Ultra-clean, modern & minimal Bun web framework.**
|
|
6
|
+
Built by a 13-year-old Nigerian developer. Among the top three in performance.
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/princejs)
|
|
9
|
+
[](https://github.com/MatthewTheCoder1218/princejs)
|
|
10
|
+
[](https://www.npmjs.com/package/princejs)
|
|
11
|
+
[](https://github.com/MatthewTheCoder1218/princejs/blob/main/LICENSE)
|
|
12
|
+
|
|
13
|
+
[**Website**](https://princejs.vercel.app) · [**npm**](https://www.npmjs.com/package/princejs) · [**GitHub**](https://github.com/MatthewTheCoder1218/princejs) · [**Twitter**](https://twitter.com/princejs_bun)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## ⚡ Performance
|
|
20
|
+
|
|
21
|
+
Benchmarked with `oha -c 100 -z 30s` on Windows 10:
|
|
22
|
+
|
|
23
|
+
| Framework | Req/s | Total |
|
|
24
|
+
|-----------|------:|------:|
|
|
25
|
+
| Elysia | 25,312 | 759k |
|
|
26
|
+
| Hono | 22,124 | 664k |
|
|
27
|
+
| **PrinceJS** | **21,748** | **653k** |
|
|
28
|
+
| Express | 9,325 | 280k |
|
|
29
|
+
|
|
30
|
+
> PrinceJS is **2.3× faster than Express** and sits comfortably in the top 3 — at just **4.4kB gzipped**.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 🚀 Quick Start
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install princejs
|
|
38
|
+
bun add princejs
|
|
39
|
+
yarn add princejs
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { prince } from "princejs";
|
|
44
|
+
import { cors, logger } from "princejs/middleware";
|
|
45
|
+
|
|
46
|
+
const app = prince();
|
|
47
|
+
|
|
48
|
+
app.use(cors());
|
|
49
|
+
app.use(logger());
|
|
50
|
+
|
|
51
|
+
app.get("/", () => ({ message: "Hello PrinceJS!" }));
|
|
52
|
+
app.get("/users/:id", (req) => ({ id: req.params.id }));
|
|
53
|
+
|
|
54
|
+
app.listen(3000);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 🧰 Features
|
|
60
|
+
|
|
61
|
+
| Feature | Import |
|
|
62
|
+
|---------|--------|
|
|
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` |
|
|
73
|
+
| Cron Scheduler | `princejs/scheduler` |
|
|
74
|
+
| **OpenAPI + Scalar Docs** | `princejs` |
|
|
75
|
+
| JSX / SSR | `princejs/jsx` |
|
|
76
|
+
| SQLite Database | `princejs/db` |
|
|
77
|
+
| Plugin System | `princejs` |
|
|
78
|
+
| End-to-End Type Safety | `princejs/client` |
|
|
79
|
+
| Deploy Adapters | `princejs/vercel` · `princejs/cloudflare` · `princejs/deno` · `princejs/node` |
|
|
80
|
+
| Lifecycle Hooks | `princejs` |
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 🍪 Cookies & 🌐 IP Detection
|
|
85
|
+
|
|
86
|
+
### Reading Cookies
|
|
87
|
+
|
|
88
|
+
Cookies are automatically parsed from the request:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
app.get("/profile", (req) => ({
|
|
92
|
+
sessionId: req.cookies?.sessionId,
|
|
93
|
+
theme: req.cookies?.theme,
|
|
94
|
+
allCookies: req.cookies // Record<string, string>
|
|
95
|
+
}));
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Setting Cookies
|
|
99
|
+
|
|
100
|
+
Use the response builder to set cookies with full control:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
app.get("/login", (req) =>
|
|
104
|
+
app.response()
|
|
105
|
+
.status(200)
|
|
106
|
+
.json({ ok: true })
|
|
107
|
+
.cookie("sessionId", "abc123", {
|
|
108
|
+
maxAge: 3600, // 1 hour
|
|
109
|
+
path: "/",
|
|
110
|
+
httpOnly: true, // not accessible from JS
|
|
111
|
+
secure: true, // HTTPS only
|
|
112
|
+
sameSite: "Strict" // CSRF protection
|
|
113
|
+
})
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Chain multiple cookies
|
|
117
|
+
app.response()
|
|
118
|
+
.json({ ok: true })
|
|
119
|
+
.cookie("session", "xyz")
|
|
120
|
+
.cookie("theme", "dark")
|
|
121
|
+
.cookie("lang", "en");
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Client IP Detection
|
|
125
|
+
|
|
126
|
+
Automatically detect client IP from request headers:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
app.get("/api/data", (req) => ({
|
|
130
|
+
clientIp: req.ip,
|
|
131
|
+
data: [...]
|
|
132
|
+
}));
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Supported headers** (in priority order):
|
|
136
|
+
- `X-Forwarded-For` — Load balancers, proxies (first IP in list)
|
|
137
|
+
- `X-Real-IP` — Nginx, Apache reverse proxy
|
|
138
|
+
- `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
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
// Rate limit by IP
|
|
150
|
+
app.use((req, next) => {
|
|
151
|
+
const ip = req.ip;
|
|
152
|
+
const count = ipTracker.getCount(ip) || 0;
|
|
153
|
+
if (count > 100) return new Response("Too many requests", { status: 429 });
|
|
154
|
+
ipTracker.increment(ip);
|
|
155
|
+
return next();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// IP-based security
|
|
159
|
+
app.post("/admin", (req) => {
|
|
160
|
+
if (!ALLOWED_IPS.includes(req.ip!)) {
|
|
161
|
+
return new Response("Forbidden", { status: 403 });
|
|
162
|
+
}
|
|
163
|
+
return { authorized: true };
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 📖 OpenAPI + Scalar Docs ✨
|
|
170
|
+
|
|
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.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { prince } from "princejs";
|
|
175
|
+
import { z } from "zod";
|
|
176
|
+
|
|
177
|
+
const app = prince();
|
|
178
|
+
|
|
179
|
+
const api = app.openapi({ title: "My API", version: "1.0.0" }, "/docs", { theme: "moon" });
|
|
180
|
+
|
|
181
|
+
api.route("GET", "/users/:id", {
|
|
182
|
+
summary: "Get user by ID",
|
|
183
|
+
tags: ["users"],
|
|
184
|
+
schema: {
|
|
185
|
+
response: z.object({ id: z.string(), name: z.string() }),
|
|
186
|
+
},
|
|
187
|
+
}, (req) => ({ id: req.params!.id, name: "Alice" }));
|
|
188
|
+
|
|
189
|
+
api.route("POST", "/users", {
|
|
190
|
+
summary: "Create user",
|
|
191
|
+
tags: ["users"],
|
|
192
|
+
schema: {
|
|
193
|
+
body: z.object({ name: z.string().min(2), email: z.string().email() }),
|
|
194
|
+
response: z.object({ id: z.string(), name: z.string(), email: z.string() }),
|
|
195
|
+
},
|
|
196
|
+
}, (req) => ({ id: crypto.randomUUID(), ...req.parsedBody }));
|
|
197
|
+
|
|
198
|
+
app.listen(3000);
|
|
199
|
+
// → GET /docs Scalar UI
|
|
200
|
+
// → GET /docs.json Raw OpenAPI JSON
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`api.route()` does three things at once:
|
|
204
|
+
|
|
205
|
+
- ✅ Registers the route on PrinceJS
|
|
206
|
+
- ✅ Auto-wires `validate(schema.body)` — no separate import needed
|
|
207
|
+
- ✅ Writes the full OpenAPI spec entry
|
|
208
|
+
|
|
209
|
+
| `schema` key | Runtime | Scalar Docs |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `body` | ✅ Validates request | ✅ requestBody model |
|
|
212
|
+
| `query` | — | ✅ Typed query params |
|
|
213
|
+
| `response` | — | ✅ 200 response model |
|
|
214
|
+
|
|
215
|
+
> Routes on `app.get()` / `app.post()` stay private — never appear in docs.
|
|
216
|
+
|
|
217
|
+
**Themes:** `default` · `moon` · `purple` · `solarized` · `bluePlanet` · `deepSpace` · `saturn` · `kepler` · `mars`
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## 🔌 Plugin System
|
|
222
|
+
|
|
223
|
+
Share bundles of routes and middleware as reusable plugins:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import { prince, type PrincePlugin } from "princejs";
|
|
227
|
+
|
|
228
|
+
const usersPlugin: PrincePlugin<{ prefix?: string }> = (app, opts) => {
|
|
229
|
+
const base = opts?.prefix ?? "";
|
|
230
|
+
|
|
231
|
+
app.use((req, next) => {
|
|
232
|
+
(req as any).fromPlugin = true;
|
|
233
|
+
return next();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
app.get(`${base}/users`, (req) => ({
|
|
237
|
+
ok: true,
|
|
238
|
+
fromPlugin: (req as any).fromPlugin,
|
|
239
|
+
}));
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const app = prince();
|
|
243
|
+
app.plugin(usersPlugin, { prefix: "/api" });
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 🎣 Lifecycle Hooks
|
|
249
|
+
|
|
250
|
+
React to key moments in request processing with lifecycle hooks:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import { prince } from "princejs";
|
|
254
|
+
|
|
255
|
+
const app = prince();
|
|
256
|
+
|
|
257
|
+
// Called for every incoming request
|
|
258
|
+
app.onRequest((req) => {
|
|
259
|
+
console.log(`📥 Request received: ${req.method} ${req.url}`);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Called before handler execution
|
|
263
|
+
app.onBeforeHandle((req, path, method) => {
|
|
264
|
+
console.log(`🔍 About to handle: ${method} ${path}`);
|
|
265
|
+
(req as any).startTime = Date.now();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Called after successful handler execution
|
|
269
|
+
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)`);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Called when handler throws an error
|
|
275
|
+
app.onError((err, req, path, method) => {
|
|
276
|
+
console.error(`❌ Error in ${method} ${path}:`, err.message);
|
|
277
|
+
// Send alert, log to monitoring service, etc.
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
app.get("/users", () => ({ users: [] }));
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Hook execution order:**
|
|
284
|
+
1. `onRequest` — early for request-wide setup
|
|
285
|
+
2. `onBeforeHandle` — just before route handler runs
|
|
286
|
+
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
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 🔒 End-to-End Type Safety
|
|
300
|
+
|
|
301
|
+
Define a contract once — your client gets full TypeScript autocompletion automatically:
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
type ApiContract = {
|
|
305
|
+
"GET /users/:id": {
|
|
306
|
+
params: { id: string };
|
|
307
|
+
response: { id: string; name: string };
|
|
308
|
+
};
|
|
309
|
+
"POST /users": {
|
|
310
|
+
body: { name: string };
|
|
311
|
+
response: { id: string; ok: boolean };
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
import { createClient } from "princejs/client";
|
|
316
|
+
|
|
317
|
+
const client = createClient<ApiContract>("http://localhost:3000");
|
|
318
|
+
|
|
319
|
+
const user = await client.get("/users/:id", { params: { id: "42" } });
|
|
320
|
+
console.log(user.name); // typed as string ✅
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## 🌍 Deploy Adapters
|
|
326
|
+
|
|
327
|
+
**Vercel Edge** — `api/[[...route]].ts`
|
|
328
|
+
```ts
|
|
329
|
+
import { toVercel } from "princejs/vercel";
|
|
330
|
+
export default toVercel(app);
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Cloudflare Workers** — `src/index.ts`
|
|
334
|
+
```ts
|
|
335
|
+
import { toWorkers } from "princejs/cloudflare";
|
|
336
|
+
export default toWorkers(app);
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**Deno Deploy** — `main.ts`
|
|
340
|
+
```ts
|
|
341
|
+
import { toDeno } from "princejs/deno";
|
|
342
|
+
Deno.serve(toDeno(app));
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Node Adapter** - `server.ts`
|
|
346
|
+
```ts
|
|
347
|
+
import { createServer } from "http";
|
|
348
|
+
import { toNode, toExpress } from "princejs/node";
|
|
349
|
+
|
|
350
|
+
const app = prince();
|
|
351
|
+
app.get("/", () => ({ message: "Hello from Node!" }));
|
|
352
|
+
|
|
353
|
+
// Native Node.js http
|
|
354
|
+
const server = createServer(toNode(app));
|
|
355
|
+
server.listen(3000);
|
|
356
|
+
|
|
357
|
+
// Or with Express
|
|
358
|
+
import express from "express";
|
|
359
|
+
const expressApp = express();
|
|
360
|
+
expressApp.all("*", toExpress(app));
|
|
361
|
+
expressApp.listen(3000);
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## 🎯 Full Example
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
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";
|
|
373
|
+
import { cron } from "princejs/scheduler";
|
|
374
|
+
import { Html, Head, Body, H1, P, render } from "princejs/jsx";
|
|
375
|
+
import { db } from "princejs/db";
|
|
376
|
+
import { z } from "zod";
|
|
377
|
+
|
|
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
|
+
});
|
|
390
|
+
|
|
391
|
+
app.onAfterHandle((req, res, path, method) => {
|
|
392
|
+
const duration = Date.now() - (req as any).startTime;
|
|
393
|
+
console.log(`✅ ${method} ${path} → ${res.status} (${duration}ms)`);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
app.onError((err, req, path, method) => {
|
|
397
|
+
console.error(`❌ ${method} ${path} failed:`, err.message);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// ==========================================
|
|
401
|
+
// GLOBAL MIDDLEWARE
|
|
402
|
+
// ==========================================
|
|
403
|
+
app.use(cors());
|
|
404
|
+
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" }));
|
|
409
|
+
app.use(compress());
|
|
410
|
+
|
|
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()));
|
|
418
|
+
|
|
419
|
+
// Cookies & IP Detection
|
|
420
|
+
app.post("/login", (req) =>
|
|
421
|
+
app.response()
|
|
422
|
+
.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
|
|
428
|
+
})
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
app.get("/profile", (req) => ({
|
|
432
|
+
sessionId: req.cookies?.sessionId,
|
|
433
|
+
clientIp: req.ip,
|
|
434
|
+
}));
|
|
435
|
+
|
|
436
|
+
// Database
|
|
437
|
+
const users = db.sqlite("./db.sqlite", "CREATE TABLE users...");
|
|
438
|
+
app.get("/users", () => users.query("SELECT * FROM users"));
|
|
439
|
+
|
|
440
|
+
// WebSockets
|
|
441
|
+
app.ws("/chat", {
|
|
442
|
+
open: (ws) => ws.send("Welcome!"),
|
|
443
|
+
message: (ws, msg) => ws.send(`Echo: ${msg}`),
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Auth
|
|
447
|
+
app.get("/protected", auth(), (req) => ({ user: req.user }));
|
|
448
|
+
app.get("/api", apiKey({ keys: ["key_123"] }), (req) => ({ ok: true }));
|
|
449
|
+
|
|
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);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// ==========================================
|
|
458
|
+
// CRON JOBS
|
|
459
|
+
// ==========================================
|
|
460
|
+
cron("*/1 * * * *", () => console.log("PrinceJS heartbeat"));
|
|
461
|
+
|
|
462
|
+
// ==========================================
|
|
463
|
+
// OPENAPI + SCALAR DOCS
|
|
464
|
+
// ==========================================
|
|
465
|
+
const api = app.openapi({ title: "PrinceJS App", version: "1.0.0" }, "/docs");
|
|
466
|
+
api.route("GET", "/items", {
|
|
467
|
+
summary: "List items",
|
|
468
|
+
tags: ["items"],
|
|
469
|
+
schema: {
|
|
470
|
+
query: z.object({ q: z.string().optional() }),
|
|
471
|
+
response: z.array(z.object({ id: z.string(), name: z.string() })),
|
|
472
|
+
},
|
|
473
|
+
}, () => [{ id: "1", name: "Widget" }]);
|
|
474
|
+
|
|
475
|
+
app.listen(3000);
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## 📦 Installation
|
|
481
|
+
|
|
482
|
+
```bash
|
|
483
|
+
bun add princejs
|
|
484
|
+
# or
|
|
485
|
+
npm install princejs
|
|
486
|
+
# or
|
|
487
|
+
yarn add princejs
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## 🤝 Contributing
|
|
493
|
+
|
|
494
|
+
```bash
|
|
495
|
+
git clone https://github.com/MatthewTheCoder1218/princejs
|
|
496
|
+
cd princejs
|
|
497
|
+
bun install
|
|
498
|
+
bun test
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## 🔗 Links
|
|
504
|
+
|
|
505
|
+
- 🌐 Website: [princejs.vercel.app](https://princejs.vercel.app)
|
|
506
|
+
- 📦 npm: [npmjs.com/package/princejs](https://www.npmjs.com/package/princejs)
|
|
507
|
+
- 💻 GitHub: [github.com/MatthewTheCoder1218/princejs](https://github.com/MatthewTheCoder1218/princejs)
|
|
508
|
+
- 🐦 Twitter: [@princejs_bun](https://twitter.com/princejs_bun)
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
<div align="center">
|
|
513
|
+
|
|
514
|
+
**PrinceJS: Small in size. Giant in capability. 👑**
|
|
515
|
+
|
|
516
|
+
*Built with ❤️ in Nigeria*
|
|
517
|
+
|
|
518
518
|
</div>
|