vibe-gx 4.2.0 → 4.2.1
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 +10 -756
- package/package.json +1 -1
- package/utils/core/response.js +42 -0
- package/utils/core/server.js +19 -0
- package/vibe.d.ts +37 -0
package/README.md
CHANGED
|
@@ -1,770 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
<img src="https://github.com/thesixers/vibe/blob/808b45722a0b3ca0d266215bbe4cc074f62283e5/assets/vlogo.png?raw=true" alt="Vibe Logo" width="180" />
|
|
3
|
-
<h1>Vibe</h1>
|
|
4
|
-
<p>
|
|
5
|
-
<b>The fastest Node.js web framework with the simplest syntax.</b>
|
|
6
|
-
</p>
|
|
7
|
-
<p>
|
|
8
|
-
<img src="https://img.shields.io/badge/performance-11,472_RPS-brightgreen" alt="Performance" />
|
|
9
|
-
<img src="https://img.shields.io/badge/vs_Express-4.7x_faster-blue" alt="vs Express" />
|
|
10
|
-
<img src="https://img.shields.io/badge/vs_Fastify-faster-orange" alt="vs Fastify" />
|
|
11
|
-
<img src="https://img.shields.io/badge/license-MIT-green" alt="License" />
|
|
12
|
-
</p>
|
|
13
|
-
</div>
|
|
1
|
+
# Vibe
|
|
14
2
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
## 📦 Installation
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
npm install vibe-gx
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
> Pure JavaScript — no native dependencies, no build steps, just install and go.
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
## 🏆 Why Vibe?
|
|
28
|
-
|
|
29
|
-
| Metric | Vibe | Express | Fastify |
|
|
30
|
-
| :------------------------- | :------------: | :-------: | :--------: |
|
|
31
|
-
| **JSON Performance** | **11,472 RPS** | 2,421 RPS | 11,334 RPS |
|
|
32
|
-
| **Install Size** | **~280 KB** | ~5 MB | ~4 MB |
|
|
33
|
-
| **Lines for Hello World** | 3 | 5 | 6 |
|
|
34
|
-
| **Dependencies** | 1 | 30+ | 15+ |
|
|
35
|
-
| **Built-in Clustering** | ✅ | ❌ | ❌ |
|
|
36
|
-
| **Built-in Caching** | ✅ | ❌ | ❌ |
|
|
37
|
-
| **Code-Gen Serialization** | ✅ | ❌ | ✅ |
|
|
38
|
-
|
|
39
|
-
> **Vibe is faster than Fastify, simpler than Express, and 14-18x smaller than both.**
|
|
40
|
-
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
## ⚡ Features
|
|
44
|
-
|
|
45
|
-
| Feature | Description |
|
|
46
|
-
| :---------------------------- | :--------------------------------------------------------- |
|
|
47
|
-
| 🚀 **Code-Gen Serialization** | Schema-compiled JSON serializers via `new Function()` |
|
|
48
|
-
| 🎯 **Hybrid Router** | O(1) static + O(log n) Trie routing |
|
|
49
|
-
| 🔌 **Plugin System** | Encapsulated `register()` with optional route prefixes |
|
|
50
|
-
| 🎨 **Decorators** | Extend app, request, and response |
|
|
51
|
-
| ⚡ **Cluster Mode** | Built-in multi-process scaling |
|
|
52
|
-
| 💾 **LRU Cache** | Built-in response caching with ETag |
|
|
53
|
-
| 🛡️ **Rate Limiting** | Built-in sliding window rate limiter — no dependencies |
|
|
54
|
-
| 🌐 **CORS** | Built-in CORS with preflight handling — no dependencies |
|
|
55
|
-
| 🔗 **Connection Pool** | Generic pool for databases |
|
|
56
|
-
| 📂 **File Uploads** | Multipart uploads with size/type validation |
|
|
57
|
-
| 🌊 **Streaming** | Large file uploads without buffering |
|
|
58
|
-
| 🔒 **Security** | Path traversal protection, body limits, error sanitization |
|
|
59
|
-
| 🔄 **Express Adapter** | Use any Express middleware with `adapt()` |
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## 🚀 Quick Start
|
|
64
|
-
|
|
65
|
-
```javascript
|
|
66
|
-
import vibe from "vibe-gx";
|
|
67
|
-
|
|
68
|
-
const app = vibe();
|
|
69
|
-
|
|
70
|
-
// Direct value - no callback needed!
|
|
71
|
-
app.get("/", "Hello Vibe!");
|
|
72
|
-
|
|
73
|
-
// Auto JSON response - just return an object
|
|
74
|
-
app.get("/users/:id", (req) => ({ userId: req.params.id }));
|
|
75
|
-
|
|
76
|
-
app.listen(3000);
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
**That's it.** No `res.send()`, no `res.json()` - just return data.
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## 📖 Core API
|
|
84
|
-
|
|
85
|
-
### Routes
|
|
86
|
-
|
|
87
|
-
Vibe supports all standard HTTP methods with a clean, flexible syntax:
|
|
88
|
-
|
|
89
|
-
```javascript
|
|
90
|
-
// String response
|
|
91
|
-
app.get("/", "Hello World");
|
|
92
|
-
|
|
93
|
-
// JSON response (just return an object)
|
|
94
|
-
app.get("/json", { message: "Hello" });
|
|
95
|
-
|
|
96
|
-
// Handler function with request access
|
|
97
|
-
app.get("/users/:id", (req) => ({ id: req.params.id }));
|
|
98
|
-
|
|
99
|
-
// Multiple route parameters
|
|
100
|
-
app.get("/posts/:postId/comments/:commentId", (req) => ({
|
|
101
|
-
postId: req.params.postId,
|
|
102
|
-
commentId: req.params.commentId,
|
|
103
|
-
}));
|
|
104
|
-
|
|
105
|
-
// With options (interceptors, file uploads)
|
|
106
|
-
app.post("/protected", { intercept: authCheck }, handler);
|
|
107
|
-
|
|
108
|
-
// All HTTP methods
|
|
109
|
-
app.get("/");
|
|
110
|
-
app.post("/");
|
|
111
|
-
app.put("/");
|
|
112
|
-
app.del("/"); // DELETE
|
|
113
|
-
app.patch("/");
|
|
114
|
-
app.head("/");
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### Query Parameters
|
|
118
|
-
|
|
119
|
-
```javascript
|
|
120
|
-
// GET /search?q=hello&page=2
|
|
121
|
-
app.get("/search", (req) => ({
|
|
122
|
-
query: req.query.q, // "hello"
|
|
123
|
-
page: req.query.page, // "2"
|
|
124
|
-
}));
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### Request Body
|
|
128
|
-
|
|
129
|
-
```javascript
|
|
130
|
-
app.post("/users", (req) => {
|
|
131
|
-
const { name, email } = req.body;
|
|
132
|
-
return { created: { name, email } };
|
|
133
|
-
});
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
## 📝 Logging & Error Handling
|
|
139
|
-
|
|
140
|
-
Vibe ships with a structured JSON logger (Pino-compatible) and a powerful error interception system. Errors thrown, returned, or sent from any route are automatically caught and routed through a central error handler.
|
|
141
|
-
|
|
142
|
-
### JSON Structured Logging
|
|
143
|
-
|
|
144
|
-
Initialize the app with `logger: { lifecycle: true }` or add `prettyPrint: true` for development to get beautiful, human-readable terminal output.
|
|
145
|
-
|
|
146
|
-
```javascript
|
|
147
|
-
const app = vibe({
|
|
148
|
-
logger: {
|
|
149
|
-
lifecycle: true,
|
|
150
|
-
prettyPrint: process.env.NODE_ENV !== "production",
|
|
151
|
-
},
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// JSON native bindings
|
|
155
|
-
app.log.info({ database: "online" }, "System booting...");
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
### Contextual Sub-Loggers
|
|
159
|
-
|
|
160
|
-
Every incoming request dynamically extracts a fast UUID exposed securely on `req.id` natively piping through to `req.log`.
|
|
161
|
-
|
|
162
|
-
```javascript
|
|
163
|
-
app.get("/users/:id", (req) => {
|
|
164
|
-
req.log.warn("Database lookup constraint fired");
|
|
165
|
-
// Production Output -> {"level":40,"time":123,"reqId":"abcd-123", "msg":"..."}
|
|
166
|
-
|
|
167
|
-
return { success: true };
|
|
168
|
-
});
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### Central Error Abstraction
|
|
172
|
-
|
|
173
|
-
To route an error into the central handler without halting execution via `throw`, simply return an `Error` object from your handler — Vibe intercepts it automatically:
|
|
174
|
-
|
|
175
|
-
```javascript
|
|
176
|
-
app.get("/test", (req, res) => {
|
|
177
|
-
return new Error("Something went wrong");
|
|
178
|
-
});
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
---
|
|
182
|
-
|
|
183
|
-
## 🔌 Plugin System
|
|
184
|
-
|
|
185
|
-
Plugins provide encapsulated route groups with optional prefixes:
|
|
186
|
-
|
|
187
|
-
```javascript
|
|
188
|
-
// Register a plugin with prefix
|
|
189
|
-
await app.register(
|
|
190
|
-
async (api) => {
|
|
191
|
-
api.get("/status", { status: "ok" }); // GET /api/status
|
|
192
|
-
api.get("/health", { healthy: true }); // GET /api/health
|
|
193
|
-
|
|
194
|
-
// Plugins can have their own interceptors
|
|
195
|
-
api.plugin((req, res) => {
|
|
196
|
-
console.log(`[API] ${req.method} ${req.url}`);
|
|
197
|
-
});
|
|
198
|
-
},
|
|
199
|
-
{ prefix: "/api" },
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
// Nested plugins
|
|
203
|
-
await app.register(
|
|
204
|
-
async (v1) => {
|
|
205
|
-
v1.get("/users", { version: 1 }); // GET /api/v1/users
|
|
206
|
-
},
|
|
207
|
-
{ prefix: "/api/v1" },
|
|
208
|
-
);
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
---
|
|
212
|
-
|
|
213
|
-
## 🛡️ Interceptors (Middleware)
|
|
214
|
-
|
|
215
|
-
Interceptors run before your handler. Return `false` to stop execution.
|
|
216
|
-
|
|
217
|
-
### Single Interceptor
|
|
218
|
-
|
|
219
|
-
```javascript
|
|
220
|
-
const authCheck = (req, res) => {
|
|
221
|
-
if (!req.headers.authorization) {
|
|
222
|
-
res.unauthorized("Token required");
|
|
223
|
-
return false; // Stop execution
|
|
224
|
-
}
|
|
225
|
-
req.user = { id: 1 };
|
|
226
|
-
return true; // Continue to handler
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
app.get("/protected", { intercept: authCheck }, (req) => {
|
|
230
|
-
return { user: req.user };
|
|
231
|
-
});
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
### Multiple Interceptors
|
|
235
|
-
|
|
236
|
-
```javascript
|
|
237
|
-
app.get(
|
|
238
|
-
"/admin",
|
|
239
|
-
{
|
|
240
|
-
intercept: [authCheck, adminCheck, rateLimiter],
|
|
241
|
-
},
|
|
242
|
-
handler,
|
|
243
|
-
);
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
### Global Interceptors
|
|
247
|
-
|
|
248
|
-
```javascript
|
|
249
|
-
// Applies to ALL routes
|
|
250
|
-
app.plugin((req, res) => {
|
|
251
|
-
console.log(`${req.method} ${req.url}`);
|
|
252
|
-
});
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
---
|
|
256
|
-
|
|
257
|
-
## 🎨 Decorators
|
|
258
|
-
|
|
259
|
-
Extend app, request, or response with custom properties:
|
|
260
|
-
|
|
261
|
-
```javascript
|
|
262
|
-
// App decorator - shared config
|
|
263
|
-
app.decorate("config", { env: "production", version: "1.0.0" });
|
|
264
|
-
|
|
265
|
-
// Access directly on the app
|
|
266
|
-
app.get("/version", () => ({ version: app.config.version }));
|
|
267
|
-
|
|
268
|
-
// Same in plugins — decorators are spread directly
|
|
269
|
-
app.register(
|
|
270
|
-
async (api) => {
|
|
271
|
-
api.get("/env", () => ({ env: api.config.env }));
|
|
272
|
-
},
|
|
273
|
-
{ prefix: "/api" },
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
// Request decorator - add to all requests
|
|
277
|
-
app.decorateRequest("timestamp", () => Date.now());
|
|
278
|
-
|
|
279
|
-
app.get("/time", (req) => ({ timestamp: req.timestamp }));
|
|
280
|
-
|
|
281
|
-
// Reply decorator - add methods to response
|
|
282
|
-
app.decorateReply("sendSuccess", function (data) {
|
|
283
|
-
this.success(data);
|
|
284
|
-
});
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
---
|
|
288
|
-
|
|
289
|
-
## 📂 File Uploads
|
|
290
|
-
|
|
291
|
-
Vibe supports multipart file uploads with built-in validation and security.
|
|
292
|
-
|
|
293
|
-
> **🔒 Security**: File uploads are **disabled by default**. You must explicitly configure `media` options to accept uploads.
|
|
294
|
-
|
|
295
|
-
### Basic Upload
|
|
296
|
-
|
|
297
|
-
```javascript
|
|
298
|
-
app.post("/upload", { media: { dest: "uploads" } }, (req) => {
|
|
299
|
-
return { files: req.files, body: req.body };
|
|
300
|
-
});
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
### Media Options
|
|
304
|
-
|
|
305
|
-
```javascript
|
|
306
|
-
app.post(
|
|
307
|
-
"/upload",
|
|
308
|
-
{
|
|
309
|
-
media: {
|
|
310
|
-
dest: "uploads", // Subfolder destination
|
|
311
|
-
public: true, // Save in public folder (default: true)
|
|
312
|
-
maxSize: 5 * 1024 * 1024, // Max file size: 5MB
|
|
313
|
-
allowedTypes: ["image/jpeg", "image/png", "image/*"], // Wildcards supported
|
|
314
|
-
},
|
|
315
|
-
},
|
|
316
|
-
handler,
|
|
317
|
-
);
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
### Public vs Private Uploads
|
|
321
|
-
|
|
322
|
-
**Public uploads** (web-accessible):
|
|
323
|
-
|
|
324
|
-
```javascript
|
|
325
|
-
app.post(
|
|
326
|
-
"/upload/avatar",
|
|
327
|
-
{
|
|
328
|
-
media: {
|
|
329
|
-
public: true, // ✅ Files accessible via HTTP
|
|
330
|
-
dest: "avatars", // Saved to: public/avatars/
|
|
331
|
-
},
|
|
332
|
-
},
|
|
333
|
-
handler,
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
// Files accessible at: http://yourapp.com/avatars/filename.jpg
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
**Private uploads** (server-only access):
|
|
340
|
-
|
|
341
|
-
```javascript
|
|
342
|
-
app.post(
|
|
343
|
-
"/upload/documents",
|
|
344
|
-
{
|
|
345
|
-
media: {
|
|
346
|
-
public: false, // 🔒 Files NOT web-accessible
|
|
347
|
-
dest: "documents", // Saved to: private/documents/
|
|
348
|
-
},
|
|
349
|
-
},
|
|
350
|
-
handler,
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
// Files only accessible via your backend code (e.g., sendAbsoluteFile)
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
### Uploaded File Object
|
|
3
|
+
**The fastest Node.js web framework with the simplest syntax.**
|
|
357
4
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
[
|
|
361
|
-
|
|
362
|
-
filename: "image-a7x92b.png", // Saved filename (safe)
|
|
363
|
-
originalName: "photo.png", // Original filename
|
|
364
|
-
type: "image/png", // MIME type
|
|
365
|
-
filePath: "/uploads/image-a7x92b.png", // Full path
|
|
366
|
-
size: 102400, // Size in bytes
|
|
367
|
-
},
|
|
368
|
-
];
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
### Streaming Uploads (Large Files)
|
|
372
|
-
|
|
373
|
-
For large files, use streaming mode to avoid buffering in memory:
|
|
374
|
-
|
|
375
|
-
```javascript
|
|
376
|
-
import fs from "fs";
|
|
377
|
-
|
|
378
|
-
app.post("/upload-large", { media: { streaming: true } }, (req) => {
|
|
379
|
-
req.on("file", (name, stream, info) => {
|
|
380
|
-
stream.pipe(fs.createWriteStream(`/uploads/${info.filename}`));
|
|
381
|
-
});
|
|
382
|
-
return { status: "uploading" };
|
|
383
|
-
});
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
### Error Handling
|
|
387
|
-
|
|
388
|
-
- **413 Payload Too Large** - File exceeds `maxSize`
|
|
389
|
-
- **415 Unsupported Media Type** - File type not in `allowedTypes`
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
390
9
|
|
|
391
10
|
---
|
|
392
11
|
|
|
393
|
-
##
|
|
394
|
-
|
|
395
|
-
### Cluster Mode
|
|
396
|
-
|
|
397
|
-
Scale across all CPU cores automatically:
|
|
398
|
-
|
|
399
|
-
```javascript
|
|
400
|
-
import vibe, { clusterize, isPrimary, getWorkerId } from "vibe-gx";
|
|
401
|
-
|
|
402
|
-
clusterize(
|
|
403
|
-
() => {
|
|
404
|
-
const app = vibe();
|
|
405
|
-
app.get("/", `Hello from worker ${getWorkerId()}!`);
|
|
406
|
-
app.listen(3000);
|
|
407
|
-
},
|
|
408
|
-
{
|
|
409
|
-
workers: 4, // Number of workers (default: CPU count)
|
|
410
|
-
restart: true, // Auto-restart crashed workers
|
|
411
|
-
restartDelay: 1000, // Delay before restart (ms)
|
|
412
|
-
},
|
|
413
|
-
);
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
### LRU Cache
|
|
417
|
-
|
|
418
|
-
Built-in response caching with ETag support:
|
|
419
|
-
|
|
420
|
-
```javascript
|
|
421
|
-
import vibe, { LRUCache, cacheMiddleware } from "vibe-gx";
|
|
422
|
-
|
|
423
|
-
const cache = new LRUCache({
|
|
424
|
-
max: 1000, // Maximum entries
|
|
425
|
-
ttl: 60000, // TTL in milliseconds (60 seconds)
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
app.get("/expensive", { intercept: cacheMiddleware(cache) }, async () => {
|
|
429
|
-
// This only runs on cache MISS
|
|
430
|
-
return await expensiveOperation();
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
// Manual cache operations
|
|
434
|
-
cache.set("key", { data: "value" });
|
|
435
|
-
cache.get("key"); // { value, expires, etag }
|
|
436
|
-
cache.delete("key");
|
|
437
|
-
cache.clear();
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
### Connection Pool
|
|
441
|
-
|
|
442
|
-
Generic connection pool for databases:
|
|
443
|
-
|
|
444
|
-
```javascript
|
|
445
|
-
import vibe, { createPool } from "vibe-gx";
|
|
446
|
-
|
|
447
|
-
const dbPool = createPool({
|
|
448
|
-
create: async () => await connectToDatabase(),
|
|
449
|
-
destroy: async (conn) => await conn.close(),
|
|
450
|
-
validate: (conn) => conn.isAlive(),
|
|
451
|
-
min: 2, // Minimum connections
|
|
452
|
-
max: 10, // Maximum connections
|
|
453
|
-
acquireTimeout: 30000, // Timeout to acquire (ms)
|
|
454
|
-
idleTimeout: 60000, // Idle timeout (ms)
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
app.get("/users", async () => {
|
|
458
|
-
return await dbPool.use(async (conn) => {
|
|
459
|
-
return await conn.query("SELECT * FROM users");
|
|
460
|
-
});
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
// Pool statistics
|
|
464
|
-
console.log(dbPool.stats);
|
|
465
|
-
// { available: 5, inUse: 2, waiting: 0, max: 10 }
|
|
466
|
-
|
|
467
|
-
// Cleanup on shutdown
|
|
468
|
-
process.on("SIGTERM", () => dbPool.close());
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
---
|
|
472
|
-
|
|
473
|
-
## 🔄 Express Middleware Adapter
|
|
474
|
-
|
|
475
|
-
Use any Express middleware with the adapter:
|
|
476
|
-
|
|
477
|
-
```javascript
|
|
478
|
-
import vibe, { adapt } from "vibe-gx";
|
|
479
|
-
import helmet from "helmet";
|
|
480
|
-
import compression from "compression";
|
|
481
|
-
|
|
482
|
-
app.plugin(adapt(helmet()));
|
|
483
|
-
app.plugin(adapt(compression()));
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
> **Note:** Vibe ships with a native `cors()` helper. No need to adapt the `cors` npm package.
|
|
487
|
-
|
|
488
|
-
---
|
|
489
|
-
|
|
490
|
-
## 🛡️ Rate Limiting
|
|
491
|
-
|
|
492
|
-
Built-in sliding window rate limiter — no external dependencies:
|
|
493
|
-
|
|
494
|
-
```javascript
|
|
495
|
-
import vibe, { rateLimit } from "vibe-gx";
|
|
496
|
-
|
|
497
|
-
const app = vibe();
|
|
498
|
-
|
|
499
|
-
// Global limit: 100 requests per minute per IP
|
|
500
|
-
app.plugin(rateLimit({ max: 100, window: 60_000 }));
|
|
501
|
-
|
|
502
|
-
// Per-route: tight limit on login (brute force protection)
|
|
503
|
-
app.post(
|
|
504
|
-
"/auth/login",
|
|
505
|
-
{ intercept: rateLimit({ max: 5, window: 60_000 }) },
|
|
506
|
-
handler,
|
|
507
|
-
);
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
Sets `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers automatically.
|
|
511
|
-
|
|
512
|
-
---
|
|
513
|
-
|
|
514
|
-
## 🌐 CORS
|
|
515
|
-
|
|
516
|
-
Built-in CORS with automatic preflight handling — no external dependencies:
|
|
517
|
-
|
|
518
|
-
```javascript
|
|
519
|
-
import vibe, { cors } from "vibe-gx";
|
|
520
|
-
|
|
521
|
-
const app = vibe();
|
|
522
|
-
|
|
523
|
-
app.plugin(
|
|
524
|
-
cors({
|
|
525
|
-
origin: "https://myapp.com",
|
|
526
|
-
credentials: true,
|
|
527
|
-
maxAge: 86_400, // cache preflight for 24 hours
|
|
528
|
-
}),
|
|
529
|
-
);
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
Supports wildcard, single origin, array of origins, and dynamic origin functions.
|
|
533
|
-
|
|
534
|
-
---
|
|
535
|
-
|
|
536
|
-
## 🔒 Security
|
|
537
|
-
|
|
538
|
-
Built-in protections:
|
|
539
|
-
|
|
540
|
-
| Feature | Status |
|
|
541
|
-
| :--------------------------------------- | :----: |
|
|
542
|
-
| **File upload protection** (opt-in only) | ✅ |
|
|
543
|
-
| Path traversal protection | ✅ |
|
|
544
|
-
| File type validation | ✅ |
|
|
545
|
-
| Body size limits (1MB JSON, 10MB files) | ✅ |
|
|
546
|
-
| Error sanitization (production mode) | ✅ |
|
|
547
|
-
| Safe filename generation | ✅ |
|
|
548
|
-
| Port validation | ✅ |
|
|
549
|
-
|
|
550
|
-
### File Upload Security
|
|
551
|
-
|
|
552
|
-
Routes **reject multipart uploads by default** unless `media` is explicitly configured:
|
|
553
|
-
|
|
554
|
-
```javascript
|
|
555
|
-
// ❌ This will reject file uploads with 400 Bad Request
|
|
556
|
-
app.post("/api/data", (req) => ({ data: req.body }));
|
|
557
|
-
|
|
558
|
-
// ✅ This accepts file uploads (explicit opt-in)
|
|
559
|
-
app.post(
|
|
560
|
-
"/upload",
|
|
561
|
-
{
|
|
562
|
-
media: {
|
|
563
|
-
dest: "uploads",
|
|
564
|
-
maxSize: 5 * 1024 * 1024,
|
|
565
|
-
allowedTypes: ["image/*", "application/pdf"],
|
|
566
|
-
},
|
|
567
|
-
},
|
|
568
|
-
handler,
|
|
569
|
-
);
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
This prevents attackers from uploading malicious files to unintended routes.
|
|
573
|
-
|
|
574
|
-
Set `NODE_ENV=production` for secure error handling (stack traces hidden).
|
|
575
|
-
|
|
576
|
-
---
|
|
577
|
-
|
|
578
|
-
## ⚡ Schema-Based Serialization
|
|
579
|
-
|
|
580
|
-
**Optional** performance boost: Pre-compile JSON serializers for 2-3x faster responses.
|
|
581
|
-
|
|
582
|
-
```javascript
|
|
583
|
-
app.get(
|
|
584
|
-
"/users/:id",
|
|
585
|
-
{
|
|
586
|
-
schema: {
|
|
587
|
-
response: {
|
|
588
|
-
type: "object",
|
|
589
|
-
properties: {
|
|
590
|
-
id: { type: "number" },
|
|
591
|
-
name: { type: "string" },
|
|
592
|
-
email: { type: "string" },
|
|
593
|
-
active: { type: "boolean" },
|
|
594
|
-
},
|
|
595
|
-
},
|
|
596
|
-
},
|
|
597
|
-
},
|
|
598
|
-
async (req) => {
|
|
599
|
-
const user = await db.getUser(req.params.id);
|
|
600
|
-
return user; // Uses pre-compiled serializer (2-3x faster than JSON.stringify)
|
|
601
|
-
},
|
|
602
|
-
);
|
|
603
|
-
```
|
|
604
|
-
|
|
605
|
-
**Benefits:**
|
|
606
|
-
|
|
607
|
-
- ✅ 2-3x faster JSON serialization
|
|
608
|
-
- ✅ Zero-loop code generation via `new Function()`
|
|
609
|
-
- ✅ No `Object.keys()` enumeration
|
|
610
|
-
- ✅ Zero runtime type checking
|
|
611
|
-
- ✅ Completely optional (routes work without schemas)
|
|
612
|
-
|
|
613
|
-
---
|
|
614
|
-
|
|
615
|
-
### Route Options
|
|
616
|
-
|
|
617
|
-
```javascript
|
|
618
|
-
app.post(
|
|
619
|
-
"/path",
|
|
620
|
-
{
|
|
621
|
-
intercept: authMiddleware, // Middleware function(s)
|
|
622
|
-
media: {
|
|
623
|
-
// File upload config
|
|
624
|
-
dest: "uploads",
|
|
625
|
-
maxSize: 10 * 1024 * 1024,
|
|
626
|
-
allowedTypes: ["image/*"],
|
|
627
|
-
},
|
|
628
|
-
},
|
|
629
|
-
handler,
|
|
630
|
-
);
|
|
631
|
-
```
|
|
632
|
-
|
|
633
|
-
## 🛠️ API Reference
|
|
634
|
-
|
|
635
|
-
### Application
|
|
636
|
-
|
|
637
|
-
| Method | Description |
|
|
638
|
-
| :----------------------------------------------- | :--------------------- |
|
|
639
|
-
| `vibe({ logger?: LoggerConfig })` | Initialize app |
|
|
640
|
-
| `app.setErrorHandler(fn)` | Override error handler |
|
|
641
|
-
| `app.get/post/put/del/patch/head(path, handler)` | Register route |
|
|
642
|
-
| `app.listen(port, host?, callback?)` | Start server |
|
|
643
|
-
| `app.register(fn, { prefix })` | Register plugin |
|
|
644
|
-
| `app.plugin(fn)` | Global interceptor |
|
|
645
|
-
| `app.decorate(name, value)` | Add app property |
|
|
646
|
-
| `app.decorateRequest(name, value)` | Add to all requests |
|
|
647
|
-
| `app.decorateReply(name, value)` | Add to all responses |
|
|
648
|
-
| `app.setPublicFolder(path)` | Set static folder |
|
|
649
|
-
| `app.logRoutes()` | Log all routes |
|
|
650
|
-
|
|
651
|
-
### Request (`req`)
|
|
652
|
-
|
|
653
|
-
| Property | Description |
|
|
654
|
-
| :------------ | :------------------------------------------------------- |
|
|
655
|
-
| `req.id` | Lazy UUID — generated only on first access |
|
|
656
|
-
| `req.log` | Lazy context-bound logger — created only on first access |
|
|
657
|
-
| `req.params` | Route parameters (`:id`) |
|
|
658
|
-
| `req.query` | Query string (`?page=1`) |
|
|
659
|
-
| `req.body` | Parsed JSON/form body |
|
|
660
|
-
| `req.files` | Uploaded files (multipart) |
|
|
661
|
-
| `req.ip` | Real client IP — proxy-aware (`x-forwarded-for` first) |
|
|
662
|
-
| `req.method` | HTTP method |
|
|
663
|
-
| `req.url` | Request URL (pathname only, query stripped) |
|
|
664
|
-
| `req.headers` | Request headers |
|
|
665
|
-
|
|
666
|
-
### Response (`res`)
|
|
667
|
-
|
|
668
|
-
| Method | Description |
|
|
669
|
-
| :---------------------------------- | :--------------------------- |
|
|
670
|
-
| `res.json(data)` | Send JSON |
|
|
671
|
-
| `res.send(data)` | Send any response |
|
|
672
|
-
| `res.status(code)` | Set status (chainable) |
|
|
673
|
-
| `res.redirect(url, code?)` | Redirect (302) |
|
|
674
|
-
| `res.sendFile(path)` | Send file from public folder |
|
|
675
|
-
| `res.sendAbsoluteFile(path, opts?)` | Send file from any path |
|
|
676
|
-
| `res.sendHtml(filename)` | Send HTML file |
|
|
677
|
-
| `res.success(data?, msg?)` | 200 OK |
|
|
678
|
-
| `res.created(data?, msg?)` | 201 Created |
|
|
679
|
-
| `res.badRequest(msg?, errors?)` | 400 Bad Request |
|
|
680
|
-
| `res.unauthorized(msg?)` | 401 Unauthorized |
|
|
681
|
-
| `res.forbidden(msg?)` | 403 Forbidden |
|
|
682
|
-
| `res.notFound(msg?)` | 404 Not Found |
|
|
683
|
-
| `res.conflict(msg?)` | 409 Conflict |
|
|
684
|
-
| `res.serverError(err?)` | 500 Server Error |
|
|
685
|
-
|
|
686
|
-
### Cluster Utilities
|
|
687
|
-
|
|
688
|
-
| Function | Description |
|
|
689
|
-
| :--------------------- | :---------------------------- |
|
|
690
|
-
| `clusterize(fn, opts)` | Start in cluster mode |
|
|
691
|
-
| `isPrimary()` | Check if primary process |
|
|
692
|
-
| `isWorker()` | Check if worker process |
|
|
693
|
-
| `getWorkerId()` | Get worker ID (0 for primary) |
|
|
694
|
-
| `getWorkerCount()` | Get number of active workers |
|
|
695
|
-
|
|
696
|
-
### Cache Utilities
|
|
697
|
-
|
|
698
|
-
| Class/Function | Description |
|
|
699
|
-
| :-------------------------- | :------------------------ |
|
|
700
|
-
| `new LRUCache(opts)` | Create LRU cache instance |
|
|
701
|
-
| `cacheMiddleware(cache)` | Create cache interceptor |
|
|
702
|
-
| `LRUCache.key(method, url)` | Generate cache key |
|
|
703
|
-
| `LRUCache.etag(value)` | Generate ETag |
|
|
704
|
-
|
|
705
|
-
### Pool Utilities
|
|
706
|
-
|
|
707
|
-
| Class/Function | Description |
|
|
708
|
-
| :----------------- | :--------------------- |
|
|
709
|
-
| `createPool(opts)` | Create connection pool |
|
|
710
|
-
| `pool.acquire()` | Acquire resource |
|
|
711
|
-
| `pool.release(r)` | Release resource |
|
|
712
|
-
| `pool.use(fn)` | Use with auto-release |
|
|
713
|
-
| `pool.close()` | Close pool |
|
|
714
|
-
| `pool.stats` | Get pool statistics |
|
|
715
|
-
|
|
716
|
-
### Rate Limit Utilities
|
|
717
|
-
|
|
718
|
-
| Function | Description |
|
|
719
|
-
| :---------------- | :----------------------------------- |
|
|
720
|
-
| `rateLimit(opts)` | Create a sliding window rate limiter |
|
|
721
|
-
|
|
722
|
-
### CORS Utilities
|
|
723
|
-
|
|
724
|
-
| Function | Description |
|
|
725
|
-
| :------------ | :------------------------ |
|
|
726
|
-
| `cors(opts?)` | Create a CORS interceptor |
|
|
727
|
-
|
|
728
|
-
---
|
|
729
|
-
|
|
730
|
-
## 📊 Benchmarks
|
|
731
|
-
|
|
732
|
-
Run benchmarks yourself:
|
|
733
|
-
|
|
734
|
-
```bash
|
|
735
|
-
npm run benchmark
|
|
736
|
-
```
|
|
737
|
-
|
|
738
|
-
Tested under overload (20,000 requests × 200 concurrent):
|
|
739
|
-
|
|
740
|
-
```
|
|
741
|
-
Framework | JSON RPS | vs Express | vs Fastify
|
|
742
|
-
-------------|-------------|------------|------------
|
|
743
|
-
Vibe | 11,472 | 4.7x ✅ | 1.01x ✅
|
|
744
|
-
Fastify | 11,334 | 4.7x | baseline
|
|
745
|
-
Hono | 7,351 | 3.0x | 0.6x
|
|
746
|
-
Express | 2,421 | baseline | 0.2x
|
|
747
|
-
```
|
|
748
|
-
|
|
749
|
-
---
|
|
750
|
-
|
|
751
|
-
## 🧪 Testing
|
|
752
|
-
|
|
753
|
-
```bash
|
|
754
|
-
# Run all tests
|
|
755
|
-
npm test
|
|
12
|
+
## 📖 Complete Documentation
|
|
756
13
|
|
|
757
|
-
|
|
758
|
-
npm run test:all
|
|
14
|
+
Vibe-GX features, API references, tutorials, and guidelines are available on the official documentation website:
|
|
759
15
|
|
|
760
|
-
|
|
761
|
-
npm run benchmark
|
|
762
|
-
```
|
|
16
|
+
👉 **[genesix.hkai.site/vibegx](https://genesix.hkai.site/vibegx)**
|
|
763
17
|
|
|
764
18
|
---
|
|
765
19
|
|
|
766
20
|
## 📝 License
|
|
767
21
|
|
|
768
|
-
Part of the **GeNeSix** brand. Created by **
|
|
22
|
+
Part of the **GeNeSix** brand. Created by **NA**.
|
|
769
23
|
|
|
770
24
|
MIT License.
|
package/package.json
CHANGED
package/utils/core/response.js
CHANGED
|
@@ -253,6 +253,48 @@ const vibeResponseMethods = {
|
|
|
253
253
|
this.end(RESPONSES.serverError);
|
|
254
254
|
},
|
|
255
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Sets a cookie on the response.
|
|
258
|
+
* Chainable — supports multiple cookies: res.setCookie("a","1").setCookie("b","2")
|
|
259
|
+
* @param {string} name - Cookie name
|
|
260
|
+
* @param {string} value - Cookie value (will be URI-encoded)
|
|
261
|
+
* @param {Object} [options]
|
|
262
|
+
* @param {number} [options.maxAge] - Max age in seconds
|
|
263
|
+
* @param {Date} [options.expires] - Expiry date
|
|
264
|
+
* @param {string} [options.path="/"] - Cookie path
|
|
265
|
+
* @param {string} [options.domain] - Cookie domain
|
|
266
|
+
* @param {boolean} [options.secure] - HTTPS only
|
|
267
|
+
* @param {boolean} [options.httpOnly] - Inaccessible to JS
|
|
268
|
+
* @param {"Strict"|"Lax"|"None"} [options.sameSite] - SameSite policy
|
|
269
|
+
* @returns {this}
|
|
270
|
+
*/
|
|
271
|
+
setCookie(name, value, options = {}) {
|
|
272
|
+
let cookie = `${name}=${encodeURIComponent(value)}`;
|
|
273
|
+
if (options.maxAge != null) cookie += `; Max-Age=${options.maxAge}`;
|
|
274
|
+
if (options.expires instanceof Date) cookie += `; Expires=${options.expires.toUTCString()}`;
|
|
275
|
+
cookie += `; Path=${options.path ?? "/"}`;
|
|
276
|
+
if (options.domain) cookie += `; Domain=${options.domain}`;
|
|
277
|
+
if (options.secure) cookie += "; Secure";
|
|
278
|
+
if (options.httpOnly) cookie += "; HttpOnly";
|
|
279
|
+
if (options.sameSite) cookie += `; SameSite=${options.sameSite}`;
|
|
280
|
+
|
|
281
|
+
const existing = this.getHeader("Set-Cookie");
|
|
282
|
+
if (Array.isArray(existing)) this.setHeader("Set-Cookie", [...existing, cookie]);
|
|
283
|
+
else if (existing) this.setHeader("Set-Cookie", [existing, cookie]);
|
|
284
|
+
else this.setHeader("Set-Cookie", cookie);
|
|
285
|
+
return this;
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Clears a cookie by immediately expiring it.
|
|
290
|
+
* @param {string} name - Cookie name
|
|
291
|
+
* @param {Object} [options] - Same options as setCookie (except maxAge/expires)
|
|
292
|
+
* @returns {this}
|
|
293
|
+
*/
|
|
294
|
+
clearCookie(name, options = {}) {
|
|
295
|
+
return this.setCookie(name, "", { ...options, maxAge: 0, expires: new Date(0) });
|
|
296
|
+
},
|
|
297
|
+
|
|
256
298
|
/**
|
|
257
299
|
* Redirects the client to another URL.
|
|
258
300
|
* @param {string} url
|
package/utils/core/server.js
CHANGED
|
@@ -110,6 +110,25 @@ async function server(options, port, host, callback) {
|
|
|
110
110
|
configurable: true,
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
// Lazy req.cookies — parsed once on first access, zero cost if unused
|
|
114
|
+
Object.defineProperty(req, "cookies", {
|
|
115
|
+
get() {
|
|
116
|
+
if (this._parsedCookies !== undefined) return this._parsedCookies;
|
|
117
|
+
const header = this.headers["cookie"];
|
|
118
|
+
if (!header) return (this._parsedCookies = {});
|
|
119
|
+
const cookies = {};
|
|
120
|
+
for (const pair of header.split(";")) {
|
|
121
|
+
const idx = pair.indexOf("=");
|
|
122
|
+
if (idx < 0) continue;
|
|
123
|
+
const key = pair.slice(0, idx).trim();
|
|
124
|
+
const val = pair.slice(idx + 1).trim();
|
|
125
|
+
if (key) cookies[key] = decodeURIComponent(val);
|
|
126
|
+
}
|
|
127
|
+
return (this._parsedCookies = cookies);
|
|
128
|
+
},
|
|
129
|
+
configurable: true,
|
|
130
|
+
});
|
|
131
|
+
|
|
113
132
|
if (options.loggerConfig && options.loggerConfig.lifecycle) {
|
|
114
133
|
req.startTime = Date.now();
|
|
115
134
|
|
package/vibe.d.ts
CHANGED
|
@@ -162,6 +162,26 @@ export interface RouteOptions {
|
|
|
162
162
|
schema?: SchemaOptions;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Options for cookie serialization.
|
|
167
|
+
*/
|
|
168
|
+
export interface CookieOptions {
|
|
169
|
+
/** Max age in seconds. Takes priority over expires. */
|
|
170
|
+
maxAge?: number;
|
|
171
|
+
/** Expiry date */
|
|
172
|
+
expires?: Date;
|
|
173
|
+
/** Cookie path. Default: "/" */
|
|
174
|
+
path?: string;
|
|
175
|
+
/** Cookie domain */
|
|
176
|
+
domain?: string;
|
|
177
|
+
/** Send only over HTTPS */
|
|
178
|
+
secure?: boolean;
|
|
179
|
+
/** Inaccessible to client-side JavaScript */
|
|
180
|
+
httpOnly?: boolean;
|
|
181
|
+
/** SameSite policy */
|
|
182
|
+
sameSite?: "Strict" | "Lax" | "None";
|
|
183
|
+
}
|
|
184
|
+
|
|
165
185
|
/**
|
|
166
186
|
* Options for registering a plugin.
|
|
167
187
|
*/
|
|
@@ -239,6 +259,8 @@ export interface VibeRequest extends IncomingMessage {
|
|
|
239
259
|
files?: UploadedFile[];
|
|
240
260
|
/** Real client IP — first entry from x-forwarded-for, x-real-ip, or socket address */
|
|
241
261
|
ip?: string;
|
|
262
|
+
/** Parsed cookies from the Cookie header (lazily evaluated on first access) */
|
|
263
|
+
cookies: Record<string, string>;
|
|
242
264
|
/** Automatically generated UUID for the request lifecycle */
|
|
243
265
|
id: string;
|
|
244
266
|
/** Context-bound logger automatically stamped with the req.id constraint */
|
|
@@ -264,6 +286,21 @@ export interface VibeResponse extends ServerResponse {
|
|
|
264
286
|
sendHtml: (filename: string) => void;
|
|
265
287
|
redirect: (url: string, code?: number) => void;
|
|
266
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Sets a cookie on the response. Chainable.
|
|
291
|
+
* @example
|
|
292
|
+
* res.setCookie("token", "abc", { httpOnly: true, secure: true, maxAge: 3600 });
|
|
293
|
+
* res.setCookie("a", "1").setCookie("b", "2"); // multiple cookies
|
|
294
|
+
*/
|
|
295
|
+
setCookie(name: string, value: string, options?: CookieOptions): VibeResponse;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Clears a cookie by expiring it immediately.
|
|
299
|
+
* @example
|
|
300
|
+
* res.clearCookie("token");
|
|
301
|
+
*/
|
|
302
|
+
clearCookie(name: string, options?: Omit<CookieOptions, "maxAge" | "expires">): VibeResponse;
|
|
303
|
+
|
|
267
304
|
/** Sends a 200 OK response with a success message */
|
|
268
305
|
success: (data?: any, message?: string) => void;
|
|
269
306
|
/** Sends a 201 Created response */
|