vibe-gx 4.1.0 → 4.1.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
@@ -46,7 +46,7 @@ npm install vibe-gx
46
46
  | :---------------------------- | :--------------------------------------------------------- |
47
47
  | 🚀 **Code-Gen Serialization** | Schema-compiled JSON serializers via `new Function()` |
48
48
  | 🎯 **Hybrid Router** | O(1) static + O(log n) Trie routing |
49
- | 🔌 **Plugin System** | Fastify-style `register()` with encapsulation |
49
+ | 🔌 **Plugin System** | Encapsulated `register()` with optional route prefixes |
50
50
  | 🎨 **Decorators** | Extend app, request, and response |
51
51
  | ⚡ **Cluster Mode** | Built-in multi-process scaling |
52
52
  | 💾 **LRU Cache** | Built-in response caching with ETag |
@@ -133,7 +133,52 @@ app.post("/users", (req) => {
133
133
 
134
134
  ---
135
135
 
136
- ## 🔌 Plugins (Fastify-style)
136
+ ## 📝 Logging & Error Handling
137
+
138
+ 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.
139
+
140
+ ### JSON Structured Logging
141
+
142
+ Initialize the app with `logger: { lifecycle: true }` or add `prettyPrint: true` for development to get beautiful, human-readable terminal output.
143
+
144
+ ```javascript
145
+ const app = vibe({
146
+ logger: {
147
+ lifecycle: true,
148
+ prettyPrint: process.env.NODE_ENV !== "production",
149
+ },
150
+ });
151
+
152
+ // JSON native bindings
153
+ app.log.info({ database: "online" }, "System booting...");
154
+ ```
155
+
156
+ ### Contextual Sub-Loggers
157
+
158
+ Every incoming request dynamically extracts a fast UUID exposed securely on `req.id` natively piping through to `req.log`.
159
+
160
+ ```javascript
161
+ app.get("/users/:id", (req) => {
162
+ req.log.warn("Database lookup constraint fired");
163
+ // Production Output -> {"level":40,"time":123,"reqId":"abcd-123", "msg":"..."}
164
+
165
+ return { success: true };
166
+ });
167
+ ```
168
+
169
+ ### Central Error Abstraction
170
+
171
+ 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:
172
+
173
+ ```javascript
174
+ app.get("/test", (req, res) => {
175
+ return new Error("Something went wrong");
176
+ });
177
+ ```
178
+
179
+ ---
180
+
181
+ ## 🔌 Plugin System
137
182
 
138
183
  Plugins provide encapsulated route groups with optional prefixes:
139
184
 
@@ -215,13 +260,13 @@ Extend app, request, or response with custom properties:
215
260
  // App decorator - shared config
216
261
  app.decorate("config", { env: "production", version: "1.0.0" });
217
262
 
218
- // Access via app.decorators in main app
219
- app.get("/version", () => ({ version: app.decorators.config.version }));
263
+ // Access directly on the app
264
+ app.get("/version", () => ({ version: app.config.version }));
220
265
 
221
- // In plugins, decorators are spread directly (no .decorators)
266
+ // Same in plugins decorators are spread directly
222
267
  app.register(
223
268
  async (api) => {
224
- api.get("/env", () => ({ env: api.config.env })); // Direct access
269
+ api.get("/env", () => ({ env: api.config.env }));
225
270
  },
226
271
  { prefix: "/api" },
227
272
  );
@@ -541,22 +586,26 @@ app.post(
541
586
 
542
587
  ### Application
543
588
 
544
- | Method | Description |
545
- | :----------------------------------------------- | :------------------- |
546
- | `app.get/post/put/del/patch/head(path, handler)` | Register route |
547
- | `app.listen(port, host?, callback?)` | Start server |
548
- | `app.register(fn, { prefix })` | Register plugin |
549
- | `app.plugin(fn)` | Global interceptor |
550
- | `app.decorate(name, value)` | Add app property |
551
- | `app.decorateRequest(name, value)` | Add to all requests |
552
- | `app.decorateReply(name, value)` | Add to all responses |
553
- | `app.setPublicFolder(path)` | Set static folder |
554
- | `app.logRoutes()` | Log all routes |
589
+ | Method | Description |
590
+ | :----------------------------------------------- | :--------------------- |
591
+ | `vibe({ logger?: LoggerConfig })` | Initialize app |
592
+ | `app.setErrorHandler(fn)` | Override error handler |
593
+ | `app.get/post/put/del/patch/head(path, handler)` | Register route |
594
+ | `app.listen(port, host?, callback?)` | Start server |
595
+ | `app.register(fn, { prefix })` | Register plugin |
596
+ | `app.plugin(fn)` | Global interceptor |
597
+ | `app.decorate(name, value)` | Add app property |
598
+ | `app.decorateRequest(name, value)` | Add to all requests |
599
+ | `app.decorateReply(name, value)` | Add to all responses |
600
+ | `app.setPublicFolder(path)` | Set static folder |
601
+ | `app.logRoutes()` | Log all routes |
555
602
 
556
603
  ### Request (`req`)
557
604
 
558
605
  | Property | Description |
559
606
  | :------------ | :------------------------- |
607
+ | `req.id` | Auto-generated UUID logic |
608
+ | `req.log` | Context-bound logger API |
560
609
  | `req.params` | Route parameters (`:id`) |
561
610
  | `req.query` | Query string (`?page=1`) |
562
611
  | `req.body` | Parsed JSON/form body |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-gx",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
4
4
  "description": "A lightweight, high-performance Node.js web framework.",
5
5
  "type": "module",
6
6
  "main": "vibe.js",
@@ -142,11 +142,16 @@ export function handleError(error, req, res) {
142
142
  const isDev = process.env.NODE_ENV !== "production";
143
143
  const message = error.message || "Unknown error";
144
144
 
145
- // Log error (full stack in dev, message only in production)
146
- if (isDev) {
147
- console.error("[VIBE ERROR]:", error);
145
+ // Log error using context-aware structured logger if available
146
+ if (req && req.log) {
147
+ req.log.error(error);
148
148
  } else {
149
- console.error("[VIBE ERROR]:", message);
149
+ // Fallback: full stack in dev, message only in production
150
+ if (isDev) {
151
+ console.error("[VIBE ERROR]:", error);
152
+ } else {
153
+ console.error("[VIBE ERROR]:", message);
154
+ }
150
155
  }
151
156
 
152
157
  if (!res.headersSent) {
@@ -0,0 +1,166 @@
1
+ import os from "os";
2
+ import { color } from "../helpers/colors.js";
3
+
4
+ const LOG_LEVELS = {
5
+ trace: 10,
6
+ debug: 20,
7
+ info: 30,
8
+ warn: 40,
9
+ error: 50,
10
+ fatal: 60,
11
+ };
12
+
13
+ const LEVEL_NAMES = {
14
+ 10: "TRACE",
15
+ 20: "DEBUG",
16
+ 30: "INFO",
17
+ 40: "WARN",
18
+ 50: "ERROR",
19
+ 60: "FATAL",
20
+ };
21
+
22
+ /**
23
+ * High-performance structured JSON logger (Fastify/Pino style).
24
+ */
25
+ export class Logger {
26
+ constructor(options = {}) {
27
+ this.level = LOG_LEVELS[options.level || "info"] || 30;
28
+ this.prettyPrint = options.prettyPrint || false;
29
+ this.lifecycle = options.lifecycle || false;
30
+ this.stream = options.stream || process.stdout;
31
+ this.bindings = options.bindings || {};
32
+
33
+ if (!this.bindings.pid) this.bindings.pid = process.pid;
34
+ if (!this.bindings.hostname) this.bindings.hostname = os.hostname();
35
+ }
36
+
37
+ /**
38
+ * Creates a sub-logger with scoped bindings (e.g. reqId).
39
+ */
40
+ child(bindings) {
41
+ return new Logger({
42
+ level: Object.keys(LOG_LEVELS).find(
43
+ (key) => LOG_LEVELS[key] === this.level,
44
+ ),
45
+ prettyPrint: this.prettyPrint,
46
+ lifecycle: this.lifecycle,
47
+ stream: this.stream,
48
+ bindings: { ...this.bindings, ...bindings },
49
+ });
50
+ }
51
+
52
+ trace(obj, msg, c) {
53
+ this._log(10, obj, msg, c);
54
+ }
55
+ debug(obj, msg, c) {
56
+ this._log(20, obj, msg, c);
57
+ }
58
+ info(obj, msg, c) {
59
+ this._log(30, obj, msg, c);
60
+ }
61
+ warn(obj, msg, c) {
62
+ this._log(40, obj, msg, c);
63
+ }
64
+ error(obj, msg, c) {
65
+ this._log(50, obj, msg, c);
66
+ }
67
+ fatal(obj, msg, c) {
68
+ this._log(60, obj, msg, c);
69
+ }
70
+
71
+ _log(level, obj, msg, c) {
72
+ if (level < this.level) return;
73
+
74
+ const base = {
75
+ level,
76
+ time: Date.now(),
77
+ ...this.bindings,
78
+ };
79
+
80
+ let logData = {};
81
+ let customColor = undefined;
82
+
83
+ if (obj instanceof Error) {
84
+ logData.err = {
85
+ type: obj.name || "Error",
86
+ message: obj.message,
87
+ stack: obj.stack,
88
+ };
89
+ if (typeof msg === "string") logData.msg = msg;
90
+ else logData.msg = obj.message;
91
+ if (typeof c === "string") customColor = c;
92
+ } else if (typeof obj === "string") {
93
+ logData.msg = obj;
94
+ if (typeof msg === "string") customColor = msg;
95
+ } else if (typeof obj === "object" && obj !== null) {
96
+ logData = { ...obj };
97
+ if (typeof msg === "string") logData.msg = msg;
98
+ if (typeof c === "string") customColor = c;
99
+ } else {
100
+ logData.msg = String(obj);
101
+ if (typeof msg === "string") customColor = msg;
102
+ }
103
+
104
+ if (customColor) {
105
+ logData.color = customColor;
106
+ }
107
+
108
+ const finalLog = { ...base, ...logData };
109
+
110
+ if (this.prettyPrint) {
111
+ this._printPretty(finalLog);
112
+ } else {
113
+ this.stream.write(JSON.stringify(finalLog) + "\n");
114
+ }
115
+ }
116
+
117
+ _printPretty(log) {
118
+ const time = new Date(log.time).toLocaleTimeString();
119
+ const lvlName = LEVEL_NAMES[log.level] || "INFO";
120
+ let prefixC = color.cyan;
121
+ if (log.level >= 50) prefixC = color.red;
122
+ else if (log.level === 40) prefixC = color.yellow;
123
+ else if (log.level <= 20) prefixC = color.dim;
124
+
125
+ const prefix = prefixC(`[VIBE ${lvlName} ${time}]`);
126
+ let context = "";
127
+ if (log.reqId) {
128
+ context = `\x1b[90m[${log.reqId}]\x1b[0m `;
129
+ }
130
+
131
+ let content = log.msg || "";
132
+ if (log.color && color[log.color]) {
133
+ content = color[log.color](content);
134
+ }
135
+
136
+ if (log.err && log.err.stack) {
137
+ content += "\n" + prefixC(log.err.stack);
138
+ }
139
+
140
+ // Attempt to print remaining metadata if it's not standard
141
+ const skipKeys = [
142
+ "level",
143
+ "time",
144
+ "pid",
145
+ "hostname",
146
+ "reqId",
147
+ "msg",
148
+ "err",
149
+ "color",
150
+ ];
151
+ let metaStr = "";
152
+ for (const key of Object.keys(log)) {
153
+ if (!skipKeys.includes(key)) {
154
+ metaStr += ` \x1b[90m${key}=${JSON.stringify(log[key])}\x1b[0m`;
155
+ }
156
+ }
157
+
158
+ this.stream.write(`${prefix} ${context}${content}${metaStr}\n`);
159
+ }
160
+ }
161
+
162
+ export function createLogger(options = {}) {
163
+ return new Logger(options);
164
+ }
165
+
166
+ export default createLogger;
@@ -35,6 +35,10 @@ const vibeResponseMethods = {
35
35
  throw new Error("Response data is not a sendable data type");
36
36
  }
37
37
 
38
+ if (data instanceof Error) {
39
+ return this._vibeOptions.errorHandler(data, this.req, this);
40
+ }
41
+
38
42
  if (typeof data === "object" && data !== null) {
39
43
  if (!this.headersSent) this.writeHead(this.statusCode || 200, JSON_CT);
40
44
  this.end(JSON.stringify(data));
@@ -50,6 +54,9 @@ const vibeResponseMethods = {
50
54
  * @param {Object} data
51
55
  */
52
56
  json(data) {
57
+ if (data instanceof Error) {
58
+ return this._vibeOptions.errorHandler(data, this.req, this);
59
+ }
53
60
  if (!this.headersSent) this.writeHead(this.statusCode || 200, JSON_CT);
54
61
  this.end(JSON.stringify(data));
55
62
  },
@@ -1,8 +1,8 @@
1
1
  import http from "http";
2
+ import crypto from "crypto";
2
3
  import { error, getNetworkIP, handleError, isSendAble } from "./handler.js";
3
4
  import bodyParser from "./parser.js";
4
5
  import { installResponseMethods, initResponse } from "./response.js";
5
- import dns from "node:dns/promises";
6
6
  import { parseQuery } from "../native.js";
7
7
 
8
8
  // Pre-allocated headers (frozen for V8 optimization)
@@ -90,6 +90,25 @@ async function server(options, port, host, callback) {
90
90
 
91
91
  // Main request handler - ULTRA OPTIMIZED
92
92
  function reqListener(req, res) {
93
+ req.id = crypto.randomUUID();
94
+ req.log = options.logger.child({ reqId: req.id });
95
+
96
+ if (options.loggerConfig && options.loggerConfig.lifecycle) {
97
+ req.startTime = Date.now();
98
+ req.log.info({ type: "req" }, "Incoming request");
99
+
100
+ res.on("finish", () => {
101
+ req.log.info(
102
+ {
103
+ type: "res",
104
+ statusCode: res.statusCode,
105
+ responseTimeMs: Date.now() - req.startTime,
106
+ },
107
+ "Request completed",
108
+ );
109
+ });
110
+ }
111
+
93
112
  // Fast pathname extraction
94
113
  const url = req.url;
95
114
  const qIdx = url.indexOf("?");
@@ -151,13 +170,19 @@ async function server(options, port, host, callback) {
151
170
  if (result && typeof result.then === "function") {
152
171
  result
153
172
  .then((val) => {
173
+ if (val instanceof Error) {
174
+ return options.errorHandler(val, req, res);
175
+ }
154
176
  if (val !== undefined && !res.writableEnded) {
155
177
  res.writeHead(200, JSON_HEADERS);
156
178
  res.end(serialize ? serialize(val) : JSON.stringify(val));
157
179
  }
158
180
  })
159
- .catch((err) => handleError(err, req, res));
181
+ .catch((err) => options.errorHandler(err, req, res));
160
182
  } else if (typeof result === "object" && result !== null) {
183
+ if (result instanceof Error) {
184
+ return options.errorHandler(result, req, res);
185
+ }
161
186
  res.writeHead(200, JSON_HEADERS);
162
187
  res.end(serialize ? serialize(result) : JSON.stringify(result));
163
188
  } else {
@@ -166,7 +191,7 @@ async function server(options, port, host, callback) {
166
191
  }
167
192
  }
168
193
  } catch (err) {
169
- handleError(err, req, res);
194
+ options.errorHandler(err, req, res);
170
195
  }
171
196
  return;
172
197
  }
@@ -226,6 +251,9 @@ async function server(options, port, host, callback) {
226
251
  // Execute handler
227
252
  if (typeof handler === "function") {
228
253
  const result = await handler(req, res);
254
+ if (result instanceof Error) {
255
+ return options.errorHandler(result, req, res);
256
+ }
229
257
  if (result !== undefined && !res.writableEnded) {
230
258
  if (serialize) {
231
259
  // Pre-compiled schema serializer — fastest path
@@ -247,7 +275,7 @@ async function server(options, port, host, callback) {
247
275
  throw new Error("Invalid handler type");
248
276
  }
249
277
  } catch (err) {
250
- handleError(err, req, res);
278
+ options.errorHandler(err, req, res);
251
279
  }
252
280
  }
253
281
 
@@ -256,10 +284,7 @@ async function server(options, port, host, callback) {
256
284
 
257
285
  const vibe_server = http.createServer(reqListener);
258
286
 
259
- vibe_server.listen(port, mainHost, async () => {
260
- try {
261
- await dns.lookup("::", { all: true });
262
- } catch {}
287
+ vibe_server.listen(port, mainHost, () => {
263
288
  getNetworkIP(mainHost, port);
264
289
 
265
290
  const strategy = useTrieMatching ? "Trie (O(log n))" : "Linear (O(n))";
@@ -271,7 +296,29 @@ async function server(options, port, host, callback) {
271
296
  });
272
297
 
273
298
  vibe_server.on("error", (err) => {
274
- error(`Port ${port} is already in use! \n${err.message}`);
299
+ if (err.code === "EADDRINUSE") {
300
+ error(`Port ${port} is already in use! \n${err.message}`);
301
+ process.exit(1);
302
+ } else {
303
+ error(`Server error: \n${err.message}`);
304
+ }
305
+ });
306
+
307
+ // Graceful shutdown support for node --watch, nodemon, and cluster mode
308
+ const shutdown = () => {
309
+ // vibe_server.close stops accepting new connections
310
+ // Existing keep-alive connections will still prevent instant exit,
311
+ // so we force an exit if it takes longer than 3 seconds.
312
+ vibe_server.close(() => {
313
+ process.exit(0);
314
+ });
315
+ setTimeout(() => process.exit(0), 3000).unref();
316
+ };
317
+
318
+ process.on("SIGTERM", shutdown);
319
+ process.on("SIGINT", shutdown);
320
+ process.on("message", (msg) => {
321
+ if (msg === "shutdown") shutdown();
275
322
  });
276
323
  }
277
324
 
@@ -145,7 +145,17 @@ export class LRUCache {
145
145
  */
146
146
  export function cacheMiddleware(cache) {
147
147
  return (req, res) => {
148
- const key = LRUCache.key(req.method, req.url);
148
+ // Use the full original URL (includes query string) for the cache key.
149
+ // req.url is overwritten with just the pathname by the server internals,
150
+ // so we fall back to req._rawUrl which preserves the full URL.
151
+ // We also append serialized route params so that parameterised routes
152
+ // (e.g. /users/:id) with different param values get distinct cache entries.
153
+ const rawUrl = req._rawUrl || req.url;
154
+ const paramsStr =
155
+ req.params && Object.keys(req.params).length > 0
156
+ ? JSON.stringify(req.params)
157
+ : "";
158
+ const key = LRUCache.key(req.method, rawUrl + paramsStr);
149
159
  const entry = cache.get(key);
150
160
 
151
161
  if (entry) {
@@ -164,16 +174,45 @@ export function cacheMiddleware(cache) {
164
174
  return false; // Stop execution
165
175
  }
166
176
 
167
- // Store original json method to intercept response
177
+ // Store original json and end methods to intercept response
168
178
  const originalJson = res.json.bind(res);
179
+ const originalEnd = res.end.bind(res);
180
+
181
+ // Intercept res.json (explicit json calls by handler)
169
182
  res.json = (data) => {
170
- // Cache the response
171
183
  const newEntry = cache.set(key, data);
172
184
  res.setHeader("ETag", newEntry.etag);
173
185
  res.setHeader("X-Cache", "MISS");
174
186
  originalJson(data);
175
187
  };
176
188
 
189
+ // Intercept res.end (implicit return-value path in server.js uses
190
+ // res.writeHead + res.end directly, bypassing res.json).
191
+ // Note: res.getHeader() does NOT see headers set via res.writeHead(),
192
+ // so we can't check Content-Type that way. Instead, try JSON.parse directly.
193
+ res.end = (body) => {
194
+ if (body && !res._vibeCached) {
195
+ try {
196
+ const parsed = JSON.parse(body);
197
+ // Only cache plain objects/arrays — not error objects, not primitives
198
+ if (
199
+ typeof parsed === "object" &&
200
+ parsed !== null &&
201
+ !parsed.error // skip error responses
202
+ ) {
203
+ res._vibeCached = true;
204
+ const newEntry = cache.set(key, parsed);
205
+ // setHeader is safe here — headers not yet flushed
206
+ res.setHeader("ETag", newEntry.etag);
207
+ res.setHeader("X-Cache", "MISS");
208
+ }
209
+ } catch {
210
+ // Not JSON — skip caching
211
+ }
212
+ }
213
+ originalEnd(body);
214
+ };
215
+
177
216
  return true; // Continue to handler
178
217
  };
179
218
  }
package/vibe.d.ts CHANGED
@@ -172,6 +172,49 @@ export interface RegisterOptions {
172
172
  [key: string]: any;
173
173
  }
174
174
 
175
+ // ==========================================
176
+ // Logging System
177
+ // ==========================================
178
+
179
+ export interface LoggerConfig {
180
+ /** If true, automatically logs request lifecycle hooks (Incoming Request, Request Completed) */
181
+ lifecycle?: boolean;
182
+ /** If true, formats JSON output into human-readable Vibe-styled terminal lines (like pino-pretty) */
183
+ prettyPrint?: boolean;
184
+ /** Custom writable stream to output logs to (defaults to process.stdout) */
185
+ stream?: NodeJS.WritableStream;
186
+ }
187
+
188
+ /**
189
+ * Fastify/Pino-compatible structured logger API.
190
+ *
191
+ * All methods accept a message string OR a structured object (Pino-style).
192
+ * An optional color string can be passed as the last argument — in prettyPrint
193
+ * mode it will colorize the terminal output, in production mode it writes as
194
+ * a plain JSON `{ color: "..." }` key for log pipelines.
195
+ *
196
+ * @example
197
+ * req.log.info("Processing payment");
198
+ * req.log.info({ userId: 42, amount: 100 }, "Payment initiated");
199
+ * req.log.error(new Error("DB timeout"));
200
+ * req.log.warn("Slow query detected", "yellow"); // color override
201
+ */
202
+ export interface LoggerAPI {
203
+ trace(obj: object | string | Error, msg?: string, color?: ColorName): void;
204
+ debug(obj: object | string | Error, msg?: string, color?: ColorName): void;
205
+ info(obj: object | string | Error, msg?: string, color?: ColorName): void;
206
+ warn(obj: object | string | Error, msg?: string, color?: ColorName): void;
207
+ error(obj: object | string | Error, msg?: string, color?: ColorName): void;
208
+ fatal(obj: object | string | Error, msg?: string, color?: ColorName): void;
209
+ /** Returns a child logger with merged bindings (e.g., { reqId }) */
210
+ child(bindings: Record<string, any>): LoggerAPI;
211
+ }
212
+
213
+ export interface VibeConfig {
214
+ /** Configuration for the native Vibe terminal logger */
215
+ logger?: LoggerConfig | boolean;
216
+ }
217
+
175
218
  // ==========================================
176
219
  // Request & Response Extensions
177
220
  // ==========================================
@@ -192,6 +235,10 @@ export interface VibeRequest extends IncomingMessage {
192
235
  ip?: string;
193
236
  /** Detailed client IP info */
194
237
  fullIp?: string;
238
+ /** Automatically generated UUID for the request lifecycle */
239
+ id: string;
240
+ /** Context-bound logger automatically stamped with the req.id constraint */
241
+ log: LoggerAPI;
195
242
  /** Custom properties added via decorateRequest */
196
243
  [key: string]: any;
197
244
  }
@@ -338,11 +385,14 @@ export interface RouterAPI {
338
385
  head: RouteRegistrar;
339
386
 
340
387
  /**
341
- * Log helper
342
- * @param value The message to log
343
- * @param color Optional color name (e.g. 'green', 'red')
388
+ * Log helper supporting native colors and Vibe-stylized log levels
389
+ * @param value The message or object to log
390
+ * @param typeOrColor Optional color name (e.g. 'green') or level ('info', 'warn', 'error', 'req')
344
391
  */
345
- log: (value: any, color?: ColorName) => void;
392
+ log: (
393
+ value: any,
394
+ typeOrColor?: ColorName | "info" | "error" | "warn" | "req",
395
+ ) => void;
346
396
 
347
397
  /** Register a global interceptor */
348
398
  plugin: (interceptor: Interceptor) => void;
@@ -401,15 +451,44 @@ export interface VibeApp extends RouterAPI {
401
451
  maybeFunc?: (router: RouterAPI) => void,
402
452
  ) => void;
403
453
 
404
- /** Access app decorators */
454
+ /**
455
+ * Access app decorators
456
+ * @type {Record<string, any>}
457
+ */
405
458
  readonly decorators: Record<string, any>;
459
+
460
+ /**
461
+ * Override the default error handler (Fastify-style).
462
+ * Called for any unhandled `throw`, `return new Error()`, or `res.send(error)`.
463
+ * @example
464
+ * app.setErrorHandler((error, req, res) => {
465
+ * req.log.error(error);
466
+ * res.status(503).json({ success: false, message: error.message });
467
+ * });
468
+ */
469
+ setErrorHandler(
470
+ fn: (error: Error, req: VibeRequest, res: VibeResponse) => void,
471
+ ): void;
472
+
473
+ /**
474
+ * Pino/Fastify-compatible structured logger instance.
475
+ * Use for application-level logging outside of routes.
476
+ * @example
477
+ * app.log.info({ db: "connected" }, "Server ready");
478
+ * app.log.info("Server ready", "green"); // with color in prettyPrint mode
479
+ */
480
+ log: LoggerAPI;
481
+
482
+ /** Alias for `app.log` */
483
+ logger: LoggerAPI;
406
484
  }
407
485
 
408
486
  /**
409
487
  * Initialize a new Vibe application.
488
+ * @param config Optional application configuration
410
489
  * @returns Vibe application instance
411
490
  */
412
- export default function vibe(): VibeApp;
491
+ export default function vibe(config?: VibeConfig): VibeApp;
413
492
 
414
493
  // ==========================================
415
494
  // LRU Cache
package/vibe.js CHANGED
@@ -4,6 +4,8 @@ import { color } from "./utils/helpers/colors.js";
4
4
  import { RouteTrie } from "./utils/core/trie.js";
5
5
  import { PathToRegex } from "./utils/core/handler.js";
6
6
  import { compileSerializer } from "./utils/core/compile-serializer.js";
7
+ import { createLogger, Logger } from "./utils/core/logger.js";
8
+ import { handleError } from "./utils/core/handler.js";
7
9
 
8
10
  /**
9
11
  * Helper to generate regex for a path
@@ -129,9 +131,11 @@ function pathToRegex(path) {
129
131
 
130
132
  /**
131
133
  * Initializes a Vibe application instance.
134
+ * @param {Object} [config={}]
135
+ * @param {Object|boolean} [config.logger] - Logger configuration
132
136
  * @returns {VibeApp}
133
137
  */
134
- const vibe = () => {
138
+ const vibe = (config = {}) => {
135
139
  // Route trie for O(log n) matching (used when routes > threshold)
136
140
  const trie = new RouteTrie();
137
141
 
@@ -144,6 +148,14 @@ const vibe = () => {
144
148
  // Static routes Map for O(1) lookup (routes without params)
145
149
  const staticRoutes = new Map();
146
150
 
151
+ // Logger initialization
152
+ const loggerConfig =
153
+ config.logger !== false ? config.logger || {} : { level: "silent" };
154
+ const appLogger =
155
+ config.logger instanceof Logger
156
+ ? config.logger
157
+ : createLogger(loggerConfig);
158
+
147
159
  // Internal configuration
148
160
  const options = {
149
161
  trie,
@@ -156,6 +168,9 @@ const vibe = () => {
156
168
  decorators: {},
157
169
  requestDecorators: {},
158
170
  replyDecorators: {},
171
+ logger: appLogger,
172
+ loggerConfig,
173
+ errorHandler: handleError,
159
174
  };
160
175
 
161
176
  // Register default landing route
@@ -460,6 +475,8 @@ const vibe = () => {
460
475
  throw new Error(`Decorator '${name}' already exists`);
461
476
  }
462
477
  options.decorators[name] = value;
478
+ // Also set directly on the app object for easy access (app.name)
479
+ if (app) app[name] = value;
463
480
  }
464
481
 
465
482
  /**
@@ -532,12 +549,13 @@ const vibe = () => {
532
549
  }
533
550
 
534
551
  /**
535
- * Logs a message with optional color
536
- * @param {string} message
537
- * @param {string} [colorValue="reset"]
552
+ * Log messages out using the Vibe stylized legacy logger.
553
+ * Native string logging bypasses the Pino JSON interface.
538
554
  */
539
- const log = (message, colorValue = "reset") =>
540
- process.stdout.write(`${color[colorValue](message)}\n`);
555
+ const log = (message, typeOrColor = "reset") => {
556
+ const c = color[typeOrColor] || color.reset;
557
+ process.stdout.write(c(message) + "\n");
558
+ };
541
559
 
542
560
  // Build the app object with decorators
543
561
  const app = {
@@ -549,8 +567,13 @@ const vibe = () => {
549
567
  head,
550
568
  listen,
551
569
  logRoutes,
552
- log,
570
+ log: appLogger, // Standard Fastify-like exposure (app.log.info())
571
+ logger: appLogger,
572
+ logLegacy: log,
553
573
  setPublicFolder,
574
+ setErrorHandler: (fn) => {
575
+ options.errorHandler = fn;
576
+ },
554
577
  include,
555
578
  plugin,
556
579
  register,