vibe-gx 4.1.4 → 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 CHANGED
@@ -1,770 +1,24 @@
1
- <div align="center">
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
- ```javascript
359
- // req.files contains:
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
+ ![Performance](https://img.shields.io/badge/performance-11,472_RPS-brightgreen)
6
+ ![vs Express](https://img.shields.io/badge/vs_Express-4.7x_faster-blue)
7
+ ![vs Fastify](https://img.shields.io/badge/vs_Fastify-faster-orange)
8
+ ![License](https://img.shields.io/badge/license-MIT-green)
390
9
 
391
10
  ---
392
11
 
393
- ## 🔥 Scalability
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
- # Run comprehensive tests
758
- npm run test:all
14
+ Vibe-GX features, API references, tutorials, and guidelines are available on the official documentation website:
759
15
 
760
- # Run benchmarks
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 **Nnamdi "Joe" Amaga**.
22
+ Part of the **GeNeSix** brand. Created by **NA**.
769
23
 
770
24
  MIT License.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-gx",
3
- "version": "4.1.4",
3
+ "version": "4.2.1",
4
4
  "description": "A lightweight, high-performance Node.js web framework.",
5
5
  "type": "module",
6
6
  "main": "vibe.js",
@@ -9,6 +9,7 @@ const LOG_LEVELS = {
9
9
  warn: 40,
10
10
  error: 50,
11
11
  fatal: 60,
12
+ silent: 100, // Higher than all levels — suppresses all output (logger: false)
12
13
  };
13
14
 
14
15
  const LEVEL_NAMES = {
@@ -136,45 +137,59 @@ export class Logger {
136
137
  _printPretty(log) {
137
138
  const time = new Date(log.time).toLocaleTimeString();
138
139
  const lvlName = LEVEL_NAMES[log.level] || "INFO";
139
- let prefixC = color.cyan;
140
- if (log.level >= 50) prefixC = color.red;
141
- else if (log.level === 40) prefixC = color.yellow;
142
- else if (log.level <= 20) prefixC = color.dim;
143
-
144
- const prefix = prefixC(`[VIBE ${lvlName} ${time}]`);
145
- let context = "";
146
- if (log.reqId) {
147
- context = `\x1b[90m[${log.reqId}]\x1b[0m `;
148
- }
149
140
 
150
- let content = log.msg || "";
151
- if (log.color && color[log.color]) {
152
- content = color[log.color](content);
153
- }
141
+ const isError = log.level >= 50;
142
+ const isWarn = log.level === 40;
143
+ const isDebug = log.level <= 20;
144
+
145
+ // Build context tag (reqId)
146
+ const context = log.reqId ? `[${log.reqId}] ` : "";
154
147
 
148
+ // Build message content
149
+ let content = log.msg || "";
155
150
  if (log.err && log.err.stack) {
156
- content += "\n" + prefixC(log.err.stack);
151
+ content += "\n" + log.err.stack;
157
152
  }
158
153
 
159
- // Attempt to print remaining metadata if it's not standard
154
+ // Build metadata string (skip standard keys)
160
155
  const skipKeys = [
161
- "level",
162
- "time",
163
- "pid",
164
- "hostname",
165
- "reqId",
166
- "msg",
167
- "err",
168
- "color",
156
+ "level", "time", "pid", "hostname", "reqId", "msg", "err", "color",
169
157
  ];
170
158
  let metaStr = "";
171
159
  for (const key of Object.keys(log)) {
172
160
  if (!skipKeys.includes(key)) {
173
- metaStr += ` \x1b[90m${key}=${JSON.stringify(log[key])}\x1b[0m`;
161
+ metaStr += ` ${key}=${JSON.stringify(log[key])}`;
174
162
  }
175
163
  }
176
164
 
177
- this.stream.write(`${prefix} ${context}${content}${metaStr}\n`);
165
+ const rawPrefix = `[VIBE ${lvlName} ${time}]`;
166
+
167
+ if (isError) {
168
+ // Entire line is red — prefix, context, message, stack, metadata
169
+ const fullLine = `${rawPrefix} ${context}${content}${metaStr}`;
170
+ this.stream.write(color.red(fullLine) + "\n");
171
+ } else if (isWarn) {
172
+ // Yellow prefix, bright content
173
+ const coloredContent = log.color && color[log.color]
174
+ ? color[log.color](content)
175
+ : color.bright(content);
176
+ this.stream.write(
177
+ color.yellow(rawPrefix) + " " + context + coloredContent +
178
+ (metaStr ? color.dim(metaStr) : "") + "\n",
179
+ );
180
+ } else if (isDebug) {
181
+ // Dim entire line for trace/debug
182
+ this.stream.write(color.dim(`${rawPrefix} ${context}${content}${metaStr}`) + "\n");
183
+ } else {
184
+ // Info — green prefix + bright content (matches [VIBE LOG] style)
185
+ const coloredContent = log.color && color[log.color]
186
+ ? color[log.color](content)
187
+ : color.bright(content);
188
+ this.stream.write(
189
+ color.green(rawPrefix) + " " + context + coloredContent +
190
+ (metaStr ? color.dim(metaStr) : "") + "\n",
191
+ );
192
+ }
178
193
  }
179
194
  }
180
195
 
@@ -103,7 +103,7 @@ function parseMultipart(req, res, media, options, resolve, reject) {
103
103
  },
104
104
  });
105
105
  } catch (err) {
106
- console.error("Busboy init failed:", err);
106
+ options.logger?.error(err, "[VIBE] Busboy init failed");
107
107
  return resolve();
108
108
  }
109
109
 
@@ -152,7 +152,10 @@ function parseMultipart(req, res, media, options, resolve, reject) {
152
152
  media.public &&
153
153
  !dest.startsWith(path.resolve(options.publicFolder || ""))
154
154
  ) {
155
- console.warn("Attempted upload outside public folder, skipping");
155
+ options.logger?.warn(
156
+ { dest, publicFolder: options.publicFolder },
157
+ "[VIBE] Attempted upload outside public folder, skipping",
158
+ );
156
159
  pendingWrites--;
157
160
  checkComplete();
158
161
  return file.resume();
@@ -161,7 +164,7 @@ function parseMultipart(req, res, media, options, resolve, reject) {
161
164
  try {
162
165
  if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
163
166
  } catch (err) {
164
- console.error("Failed to create upload folder:", err);
167
+ options.logger?.error(err, "[VIBE] Failed to create upload folder");
165
168
  pendingWrites--;
166
169
  checkComplete();
167
170
  return file.resume();
@@ -200,14 +203,14 @@ function parseMultipart(req, res, media, options, resolve, reject) {
200
203
  });
201
204
 
202
205
  file.on("error", (err) => {
203
- console.error("File stream error:", err);
206
+ options.logger?.error(err, "[VIBE] File stream error");
204
207
  writeStream.end();
205
208
  pendingWrites--;
206
209
  checkComplete();
207
210
  });
208
211
 
209
212
  writeStream.on("error", (err) => {
210
- console.error("Write stream error:", err);
213
+ options.logger?.error(err, "[VIBE] Write stream error");
211
214
  file.resume();
212
215
  pendingWrites--;
213
216
  checkComplete();
@@ -231,7 +234,7 @@ function parseMultipart(req, res, media, options, resolve, reject) {
231
234
  });
232
235
 
233
236
  bb.on("error", (err) => {
234
- console.error("Busboy error:", err);
237
+ options.logger?.error(err, "[VIBE] Busboy error");
235
238
  req.unpipe(bb);
236
239
  reject(err);
237
240
  });
@@ -266,7 +269,10 @@ function parseJson(req, res, media, options, resolve, reject) {
266
269
  req.on("data", (chunk) => {
267
270
  body += chunk;
268
271
  if (body.length > limit) {
269
- console.warn("JSON payload too large, destroying connection");
272
+ options.logger?.warn(
273
+ { limit, received: body.length },
274
+ "[VIBE] JSON payload too large, destroying connection",
275
+ );
270
276
  req.destroy();
271
277
  }
272
278
  });
@@ -243,11 +243,58 @@ const vibeResponseMethods = {
243
243
  * @param {Error} error
244
244
  */
245
245
  serverError(error) {
246
- console.error(error);
246
+ const logger = this._vibeOptions?.logger;
247
+ if (logger) {
248
+ logger.error(error, "[VIBE] Internal server error");
249
+ } else {
250
+ console.error(error);
251
+ }
247
252
  this.writeHead(500, JSON_CT);
248
253
  this.end(RESPONSES.serverError);
249
254
  },
250
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
+
251
298
  /**
252
299
  * Redirects the client to another URL.
253
300
  * @param {string} url
@@ -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
 
@@ -319,8 +338,14 @@ async function server(options, port, host, callback) {
319
338
  getNetworkIP(mainHost, port);
320
339
 
321
340
  const strategy = useTrieMatching ? "Trie (O(log n))" : "Linear (O(n))";
322
- console.log(
323
- `[VIBE] Route matching: ${strategy} (${options.routeCount} routes, ${staticRoutes.size} static, threshold: ${options.trieThreshold})`,
341
+ options.logger.info(
342
+ {
343
+ strategy,
344
+ routeCount: options.routeCount,
345
+ staticRoutes: staticRoutes.size,
346
+ trieThreshold: options.trieThreshold,
347
+ },
348
+ "[VIBE] Route matching strategy initialized",
324
349
  );
325
350
 
326
351
  if (callback) callback();
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
  */
@@ -237,10 +257,10 @@ export interface VibeRequest extends IncomingMessage {
237
257
  body: Record<string, any>;
238
258
  /** Uploaded files array (if multipart/form-data) */
239
259
  files?: UploadedFile[];
240
- /** Client IP address */
260
+ /** Real client IP — first entry from x-forwarded-for, x-real-ip, or socket address */
241
261
  ip?: string;
242
- /** Detailed client IP info */
243
- fullIp?: string;
262
+ /** Parsed cookies from the Cookie header (lazily evaluated on first access) */
263
+ cookies: Record<string, string>;
244
264
  /** Automatically generated UUID for the request lifecycle */
245
265
  id: string;
246
266
  /** Context-bound logger automatically stamped with the req.id constraint */
@@ -266,6 +286,21 @@ export interface VibeResponse extends ServerResponse {
266
286
  sendHtml: (filename: string) => void;
267
287
  redirect: (url: string, code?: number) => void;
268
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
+
269
304
  /** Sends a 200 OK response with a success message */
270
305
  success: (data?: any, message?: string) => void;
271
306
  /** Sends a 201 Created response */
@@ -302,9 +337,58 @@ export type Interceptor = (
302
337
  res: VibeResponse,
303
338
  ) => boolean | void | Promise<boolean | void>;
304
339
 
340
+ /**
341
+ * Scoped app interface passed to register() plugin callbacks.
342
+ * A subset of VibeApp — excludes server-level methods that make no sense
343
+ * inside an encapsulated plugin (listen, logRoutes, setPublicFolder, include).
344
+ */
345
+ export interface ScopedVibeApp {
346
+ get: RouteRegistrar;
347
+ post: RouteRegistrar;
348
+ put: RouteRegistrar;
349
+ del: RouteRegistrar;
350
+ patch: RouteRegistrar;
351
+ head: RouteRegistrar;
352
+
353
+ /** Register a global interceptor within this plugin scope */
354
+ plugin: (interceptor: Interceptor) => void;
355
+
356
+ /** Register a nested plugin */
357
+ register: (fn: PluginCallback, opts?: RegisterOptions) => Promise<void>;
358
+
359
+ /** Decorate the app with a custom property */
360
+ decorate: (name: string, value: any) => void;
361
+
362
+ /** Decorate request objects with a custom property */
363
+ decorateRequest: (name: string, value: any) => void;
364
+
365
+ /** Decorate response objects with a custom property */
366
+ decorateReply: (name: string, value: any) => void;
367
+
368
+ /** Override the error handler for this plugin scope */
369
+ setErrorHandler: (
370
+ fn: (error: Error, req: VibeRequest, res: VibeResponse) => void,
371
+ ) => void;
372
+
373
+ /** Structured logger — app.log.info(), .warn(), .error() etc. */
374
+ log: LoggerAPI;
375
+
376
+ /** Alias for log */
377
+ logger: LoggerAPI;
378
+
379
+ /** Legacy colorized string logger */
380
+ logLegacy: (
381
+ value: any,
382
+ typeOrColor?: ColorName | "info" | "error" | "warn" | "req",
383
+ ) => void;
384
+
385
+ /** Any decorators registered via decorate() are available as direct properties */
386
+ [key: string]: any;
387
+ }
388
+
305
389
  /** Plugin callback function (Fastify-style) */
306
390
  export type PluginCallback = (
307
- app: VibeApp,
391
+ app: ScopedVibeApp,
308
392
  opts: RegisterOptions,
309
393
  ) => void | Promise<void>;
310
394
 
@@ -391,11 +475,23 @@ export interface RouterAPI {
391
475
  head: RouteRegistrar;
392
476
 
393
477
  /**
394
- * Log helper supporting native colors and Vibe-stylized log levels
395
- * @param value The message or object to log
396
- * @param typeOrColor Optional color name (e.g. 'green') or level ('info', 'warn', 'error', 'req')
478
+ * Pino/Fastify-compatible structured logger.
479
+ * Available inside both `app.register()` plugins and `app.include()` sub-routers.
480
+ * @example
481
+ * api.log.info("Route registered");
482
+ * api.log.warn({ userId: 1 }, "Slow query");
483
+ */
484
+ log: LoggerAPI;
485
+
486
+ /** Alias for `log` — consistent with `app.logger` */
487
+ logger: LoggerAPI;
488
+
489
+ /**
490
+ * Legacy colorized string logger (plain terminal output).
491
+ * @param value The message to log
492
+ * @param typeOrColor Optional color name (e.g. 'green', 'red')
397
493
  */
398
- log: (
494
+ logLegacy: (
399
495
  value: any,
400
496
  typeOrColor?: ColorName | "info" | "error" | "warn" | "req",
401
497
  ) => void;
@@ -487,6 +583,17 @@ export interface VibeApp extends RouterAPI {
487
583
 
488
584
  /** Alias for `app.log` */
489
585
  logger: LoggerAPI;
586
+
587
+ /**
588
+ * Legacy colorized string logger (plain terminal output).
589
+ * Bypasses the Pino JSON interface — for simple dev-time messages.
590
+ * @param value The message to log
591
+ * @param typeOrColor Optional color name (e.g. 'green', 'red')
592
+ */
593
+ logLegacy: (
594
+ value: any,
595
+ typeOrColor?: ColorName | "info" | "error" | "warn" | "req",
596
+ ) => void;
490
597
  }
491
598
 
492
599
  /**
@@ -759,11 +866,3 @@ export function adapt(
759
866
  mw: (req: any, res: any, next: (err?: any) => void) => void,
760
867
  ): Interceptor;
761
868
 
762
- /**
763
- * Adapt multiple Express middlewares at once.
764
- * @param middlewares - Express middleware functions
765
- * @returns Array of Vibe-compatible interceptors
766
- */
767
- export function adaptAll(
768
- ...middlewares: Array<(req: any, res: any, next: (err?: any) => void) => void>
769
- ): Interceptor[];
package/vibe.js CHANGED
@@ -37,7 +37,6 @@ function pathToRegex(path) {
37
37
  * size: number
38
38
  * }>,
39
39
  * ip?: string,
40
- * fullIp?: string
41
40
  * }} VibeRequest
42
41
  */
43
42
 
@@ -430,7 +429,10 @@ const vibe = (config = {}) => {
430
429
  decorateRequest,
431
430
  decorateReply,
432
431
  register,
433
- log,
432
+ log: appLogger, // Structured logger (api.log.info / warn / error etc.)
433
+ logger: appLogger, // Alias — consistent with root app.logger
434
+ logLegacy: log, // Legacy colorized string logger (api.logLegacy(msg, color))
435
+ setErrorHandler: (fn) => { options.errorHandler = fn; },
434
436
  // Expose decorators
435
437
  ...options.decorators,
436
438
  };
@@ -486,7 +488,9 @@ const vibe = (config = {}) => {
486
488
  del: wrap("DELETE"),
487
489
  patch: wrap("PATCH"),
488
490
  head: wrap("HEAD"),
489
- log,
491
+ log: appLogger, // Structured logger — consistent with app.log
492
+ logger: appLogger, // Alias
493
+ logLegacy: log, // Legacy colorized string logger
490
494
  plugin,
491
495
  };
492
496
  }