@avtechno/sfr 1.0.17 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +890 -45
  2. package/dist/index.mjs +20 -2
  3. package/dist/logger.mjs +9 -37
  4. package/dist/mq.mjs +46 -0
  5. package/dist/observability/index.mjs +143 -0
  6. package/dist/observability/logger.mjs +128 -0
  7. package/dist/observability/metrics.mjs +177 -0
  8. package/dist/observability/middleware/mq.mjs +156 -0
  9. package/dist/observability/middleware/rest.mjs +120 -0
  10. package/dist/observability/middleware/ws.mjs +135 -0
  11. package/dist/observability/tracer.mjs +163 -0
  12. package/dist/sfr-pipeline.mjs +412 -12
  13. package/dist/templates.mjs +8 -1
  14. package/dist/types/index.d.mts +8 -1
  15. package/dist/types/logger.d.mts +9 -3
  16. package/dist/types/mq.d.mts +19 -0
  17. package/dist/types/observability/index.d.mts +45 -0
  18. package/dist/types/observability/logger.d.mts +54 -0
  19. package/dist/types/observability/metrics.d.mts +74 -0
  20. package/dist/types/observability/middleware/mq.d.mts +46 -0
  21. package/dist/types/observability/middleware/rest.d.mts +33 -0
  22. package/dist/types/observability/middleware/ws.d.mts +35 -0
  23. package/dist/types/observability/tracer.d.mts +90 -0
  24. package/dist/types/sfr-pipeline.d.mts +42 -1
  25. package/dist/types/templates.d.mts +1 -6
  26. package/package.json +29 -4
  27. package/src/index.mts +66 -3
  28. package/src/logger.mts +16 -51
  29. package/src/mq.mts +49 -0
  30. package/src/observability/index.mts +184 -0
  31. package/src/observability/logger.mts +169 -0
  32. package/src/observability/metrics.mts +266 -0
  33. package/src/observability/middleware/mq.mts +187 -0
  34. package/src/observability/middleware/rest.mts +143 -0
  35. package/src/observability/middleware/ws.mts +162 -0
  36. package/src/observability/tracer.mts +205 -0
  37. package/src/sfr-pipeline.mts +468 -18
  38. package/src/templates.mts +14 -5
  39. package/src/types/index.d.ts +241 -16
  40. package/dist/example.mjs +0 -33
  41. package/dist/types/example.d.mts +0 -11
  42. package/src/example.mts +0 -35
package/README.md CHANGED
@@ -1,72 +1,917 @@
1
1
  # Single File Router (SFR)
2
2
 
3
- ### Overview
4
- SFR allows the declaration of services including its controllers, handlers and validators in one single file.
3
+ > An opinionated, convention-over-configuration approach to building services in Node.js
5
4
 
6
- Featuring a configurable Inversion of Control pattern for dependency libraries, It allows developers to easily inject dependencies on either handlers or controllers for convenient and easy access.
5
+ [![npm version](https://img.shields.io/npm/v/@avtechno/sfr.svg)](https://www.npmjs.com/package/@avtechno/sfr)
6
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
7
7
 
8
- Due to it's self-contained and structured format, The SFR has allowed for the development of several extensions such as:
8
+ ---
9
9
 
10
- * OpenAPI Service Translator
10
+ ## Overview
11
11
 
12
- `A plugin which also uses API-Bundler's ability to extract metadata from individual SFRs
13
- It's main purpose is to translate and create service-level documentation which follows the OpenAPI Standard, this essentially opens up doors to extensive tooling support, instrumentation, automated endpoint testing, automated documentation generation, just to name a few.`
12
+ **SFR** is a declarative routing framework that allows you to define your entire API service—handlers, validators, and controllers—within a single, structured file. By embracing a **convention over configuration** philosophy, SFR eliminates boilerplate and enforces consistency across your codebase.
14
13
 
15
- * UAC - ACM Self-Registration Scheme
14
+ ### Key Features
16
15
 
17
- `The in-house API-Bundler was designed to extract useful information from individual SFRs, termed service-artifacts, they are reported to a centralized authority through the well-documented UAC - ACM Self-Registration Scheme, this is prerogative to the grand scheme of Resource Administration.`
16
+ - **Single-File Declaration** Define validators, handlers, and controllers together in one cohesive module
17
+ - **Multi-Protocol Support** — Build REST APIs, WebSocket servers, and Message Queue consumers using the same patterns
18
+ - **Automatic Validation** — Integrate [Joi](https://joi.dev/) schemas or [Multer](https://github.com/expressjs/multer) middleware for request validation
19
+ - **Dependency Injection** — Inject shared dependencies into handlers and controllers via IoC pattern
20
+ - **Auto-Generated Documentation** — Produces OpenAPI 3.0 specs (REST) and AsyncAPI 3.0 specs (MQ/WS) automatically
21
+ - **File-Based Routing** — Directory structure determines endpoint paths, reducing manual configuration
18
22
 
19
- ### Structure
23
+ ---
20
24
 
21
- A regular SFR is composed of the following objects
25
+ ## Installation
22
26
 
23
- |Object Name|Description|
24
- |-|-|
25
- |CFG| configuration information relayed to the API-bundler (service-parser).|
26
- |Validators|POJO representation of what values are allowed for each endpoint.|
27
- |Handlers| Express middlewares which acts as the main logic block for each endpoint.|
28
- |Controllers|Functions that execute calls to the database.|
27
+ ```bash
28
+ npm install @avtechno/sfr
29
+ # or
30
+ yarn add @avtechno/sfr
31
+ ```
32
+
33
+ ### Peer Dependencies
29
34
 
30
- ### Usage
35
+ SFR works with the following libraries (install as needed):
36
+
37
+ ```bash
38
+ npm install express joi amqplib socket.io
39
+ ```
31
40
 
32
- ``` javascript
41
+ ---
33
42
 
34
- import sfr from "@hawkstow/sfr";
43
+ ## Quick Start
44
+
45
+ ```javascript
46
+ import sfr from "@avtechno/sfr";
35
47
  import express from "express";
36
48
 
37
- const api_path = "api";
38
- const doc_path = "docs";
49
+ const app = express();
50
+ const PORT = 3000;
51
+
52
+ app.use(express.json());
53
+
54
+ await sfr(
55
+ { root: "dist", path: "api", out: "docs" },
56
+ {
57
+ title: "My Service",
58
+ description: "A sample SFR-powered service",
59
+ version: "1.0.0",
60
+ meta: {
61
+ service: "my-service",
62
+ domain: "example-org",
63
+ type: "backend",
64
+ language: "javascript",
65
+ port: PORT
66
+ }
67
+ },
68
+ { REST: app },
69
+ "api"
70
+ );
71
+
72
+ app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Project Structure
78
+
79
+ SFR expects your API files to be organized by protocol within the specified path:
80
+
81
+ ```
82
+ project/
83
+ ├── dist/ # Compiled output (cfg.root)
84
+ │ └── api/ # API directory (cfg.path)
85
+ │ ├── rest/ # REST endpoints
86
+ │ │ ├── users.mjs
87
+ │ │ └── auth/
88
+ │ │ └── login.mjs
89
+ │ ├── ws/ # WebSocket handlers
90
+ │ │ └── chat.mjs
91
+ │ └── mq/ # Message Queue consumers
92
+ │ └── notifications.mjs
93
+ ├── docs/ # Generated OpenAPI/AsyncAPI specs (cfg.out)
94
+ └── src/
95
+ └── api/
96
+ └── ... # Source files
97
+ ```
98
+
99
+ The file path determines the endpoint URL:
100
+ - `rest/users.mjs` → `/api/users/*`
101
+ - `rest/auth/login.mjs` → `/api/auth/login/*`
102
+
103
+ ---
104
+
105
+ ## SFR File Structure
106
+
107
+ Each SFR module exports a default object created using `REST()`, `WS()`, or `MQ()` template functions:
108
+
109
+ | Component | Description |
110
+ |-----------|-------------|
111
+ | **cfg** | Configuration options (base directory, public/private access) |
112
+ | **validators** | Request validation schemas (Joi objects or Multer middleware) |
113
+ | **handlers** | Route handlers organized by HTTP method or MQ pattern |
114
+ | **controllers** | Reusable data access functions (database calls, external APIs) |
115
+
116
+ ---
117
+
118
+ ## Creating a REST Endpoint
119
+
120
+ ```javascript
121
+ import Joi from "joi";
122
+ import { REST } from "@avtechno/sfr";
123
+
124
+ export default REST({
125
+ cfg: {
126
+ base_dir: "users", // Optional: Prepends to endpoint path
127
+ public: false // Requires authentication (default: false)
128
+ },
129
+
130
+ validators: {
131
+ "get-profile": {
132
+ user_id: Joi.string().uuid().required()
133
+ },
134
+ "update-profile": {
135
+ name: Joi.string().min(2).max(100),
136
+ email: Joi.string().email()
137
+ }
138
+ },
139
+
140
+ handlers: {
141
+ GET: {
142
+ "get-profile": {
143
+ summary: "Retrieve user profile",
144
+ description: "Fetches the profile for a given user ID",
145
+ tags: ["users", "profile"],
146
+ async fn(req, res) {
147
+ const { user_id } = req.query;
148
+ const profile = await this.fetch_user(user_id);
149
+ res.status(200).json({ data: profile });
150
+ }
151
+ }
152
+ },
153
+
154
+ PUT: {
155
+ "update-profile": {
156
+ summary: "Update user profile",
157
+ description: "Updates profile information for authenticated user",
158
+ tags: ["users", "profile"],
159
+ deprecated: false,
160
+ async fn(req, res) {
161
+ const result = await this.update_user(req.body);
162
+ res.status(200).json({ data: result });
163
+ }
164
+ }
165
+ }
166
+ },
167
+
168
+ controllers: {
169
+ async fetch_user(user_id) {
170
+ // Access injected dependencies via `this`
171
+ return this.db.users.findById(user_id);
172
+ },
173
+
174
+ async update_user(data) {
175
+ return this.db.users.update(data);
176
+ }
177
+ }
178
+ });
179
+ ```
180
+
181
+ ### Resulting Endpoints
182
+
183
+ | Method | Endpoint | Handler |
184
+ |--------|----------|---------|
185
+ | GET | `/api/users/get-profile` | `get-profile` |
186
+ | PUT | `/api/users/update-profile` | `update-profile` |
187
+
188
+ ---
189
+
190
+ ## Creating MQ Consumers
191
+
192
+ SFR supports multiple messaging patterns via RabbitMQ/AMQP:
193
+
194
+ | Pattern | Description | Class |
195
+ |---------|-------------|-------|
196
+ | **Point-to-Point** | Direct queue consumption | `TargetedMQ` |
197
+ | **Request-Reply** | RPC-style messaging with responses | `TargetedMQ` |
198
+ | **Fanout** | Broadcast to all subscribers | `BroadcastMQ` |
199
+ | **Direct** | Route by exact routing key | `BroadcastMQ` |
200
+ | **Topic** | Route by pattern-matched routing key | `BroadcastMQ` |
201
+
202
+ ```javascript
203
+ import Joi from "joi";
204
+ import { MQ } from "@avtechno/sfr";
205
+
206
+ export default MQ({
207
+ cfg: {
208
+ base_dir: "notifications"
209
+ },
210
+
211
+ validators: {
212
+ "send-email": {
213
+ to: Joi.string().email().required(),
214
+ subject: Joi.string().required(),
215
+ body: Joi.string().required()
216
+ }
217
+ },
218
+
219
+ handlers: {
220
+ "Point-to-Point": {
221
+ "send-email": {
222
+ summary: "Process email notification",
223
+ options: { noAck: false },
224
+ async fn(msg) {
225
+ const { to, subject, body } = msg.content;
226
+ await this.send_email(to, subject, body);
227
+ this.mq.channel.ack(msg);
228
+ }
229
+ }
230
+ },
231
+
232
+ "Request-Reply": {
233
+ "validate-email": {
234
+ summary: "Validate email address",
235
+ async fn(msg) {
236
+ const result = await this.validate(msg.content.email);
237
+ this.mq.reply(msg, { valid: result });
238
+ }
239
+ }
240
+ }
241
+ },
242
+
243
+ controllers: {
244
+ async send_email(to, subject, body) {
245
+ // Email sending logic
246
+ },
247
+
248
+ async validate(email) {
249
+ // Validation logic
250
+ return true;
251
+ }
252
+ }
253
+ });
254
+ ```
255
+
256
+ ---
257
+
258
+ ## Creating WebSocket Handlers
259
+
260
+ SFR supports real-time WebSocket communication via [Socket.IO](https://socket.io/). WebSocket SFRs follow the same pattern as REST and MQ:
261
+
262
+ ```javascript
263
+ import Joi from "joi";
264
+ import { WS } from "@avtechno/sfr";
265
+
266
+ export default WS({
267
+ cfg: {
268
+ namespace: "/chat", // Socket.IO namespace (optional, defaults to "/")
269
+ public: true // Skip authentication checks
270
+ },
271
+
272
+ validators: {
273
+ "send-message": {
274
+ room: Joi.string().required(),
275
+ message: Joi.string().min(1).max(1000).required(),
276
+ sender: Joi.string().required()
277
+ },
278
+ "join-room": {
279
+ room: Joi.string().required(),
280
+ username: Joi.string().min(2).max(50).required()
281
+ },
282
+ "leave-room": {
283
+ room: Joi.string().required()
284
+ }
285
+ },
286
+
287
+ handlers: {
288
+ "send-message": {
289
+ summary: "Send a message to a room",
290
+ description: "Broadcasts a message to all users in the specified room",
291
+ tags: ["chat", "messaging"],
292
+ async fn(ctx) {
293
+ const { room, message, sender } = ctx.data;
294
+
295
+ // Process via controller
296
+ const processed = await this.process_message(room, sender, message);
297
+
298
+ // Broadcast to room
299
+ ctx.io.to(room).emit("new-message", processed);
300
+
301
+ // Acknowledge if callback provided
302
+ if (ctx.ack) ctx.ack({ success: true });
303
+ }
304
+ },
305
+
306
+ "join-room": {
307
+ summary: "Join a chat room",
308
+ description: "Adds the user to a room and notifies other members",
309
+ tags: ["chat", "rooms"],
310
+ async fn(ctx) {
311
+ const { room, username } = ctx.data;
312
+
313
+ // Join Socket.IO room
314
+ ctx.socket.join(room);
315
+
316
+ // Notify other room members
317
+ ctx.socket.to(room).emit("user-joined", {
318
+ username,
319
+ socket_id: ctx.socket.id
320
+ });
321
+
322
+ if (ctx.ack) ctx.ack({ success: true, room });
323
+ }
324
+ },
325
+
326
+ "leave-room": {
327
+ summary: "Leave a chat room",
328
+ tags: ["chat", "rooms"],
329
+ async fn(ctx) {
330
+ const { room } = ctx.data;
331
+
332
+ ctx.socket.leave(room);
333
+ ctx.socket.to(room).emit("user-left", { socket_id: ctx.socket.id });
334
+
335
+ if (ctx.ack) ctx.ack({ success: true });
336
+ }
337
+ }
338
+ },
339
+
340
+ controllers: {
341
+ async process_message(room, sender, message) {
342
+ // Access injected dependencies via `this`
343
+ return {
344
+ id: `msg_${Date.now()}`,
345
+ room,
346
+ sender,
347
+ message,
348
+ timestamp: Date.now()
349
+ };
350
+ }
351
+ }
352
+ });
353
+ ```
354
+
355
+ ### WebSocket Handler Context
356
+
357
+ Each WebSocket handler receives a context object (`ctx`) with the following properties:
358
+
359
+ | Property | Type | Description |
360
+ |----------|------|-------------|
361
+ | `io` | `Server` | The Socket.IO server instance |
362
+ | `socket` | `Socket` | The current client socket connection |
363
+ | `data` | `object` | The validated event payload |
364
+ | `ack` | `function?` | Optional acknowledgement callback |
365
+
366
+ ### Initializing with WebSocket
367
+
368
+ ```javascript
369
+ import sfr from "@avtechno/sfr";
370
+ import express from "express";
371
+ import { createServer } from "http";
372
+ import { Server } from "socket.io";
39
373
 
40
374
  const app = express();
375
+ const httpServer = createServer(app);
376
+ const io = new Server(httpServer);
377
+
378
+ await sfr(
379
+ { root: "dist", path: "api", out: "docs" },
380
+ { /* OAS config */ },
381
+ {
382
+ REST: app,
383
+ WS: io // Pass Socket.IO server
384
+ },
385
+ "api"
386
+ );
387
+
388
+ httpServer.listen(3000);
389
+ ```
390
+
391
+ ### WebSocket Features
392
+
393
+ - **Namespaces** — Use `cfg.namespace` to organize events into logical groups
394
+ - **Rooms** — Use `ctx.socket.join()` and `ctx.socket.to()` for room-based messaging
395
+ - **Validation** — Joi schemas validate incoming event data before handler execution
396
+ - **Acknowledgements** — Support for Socket.IO acknowledgement callbacks via `ctx.ack`
397
+ - **Error Handling** — Validation errors and handler exceptions emit `{event}:error` events
398
+
399
+ ---
400
+
401
+ ## Dependency Injection
402
+
403
+ Inject shared dependencies (database connections, external services, etc.) into handlers and controllers:
404
+
405
+ ```javascript
406
+ import sfr, { inject } from "@avtechno/sfr";
407
+ import { PrismaClient } from "@prisma/client";
408
+ import Redis from "ioredis";
41
409
 
42
- /*
43
- Note:
44
- SFR Bundler will look for two folders, named "rest" and "ws" inside the "path".
45
- The placement of SFRs define the protocol that they'll use.
46
- `
47
- i.e:rest SFRs reside within "rest", websocket SFRs reside in "ws".
48
- */
410
+ const db = new PrismaClient();
411
+ const redis = new Redis();
49
412
 
50
- sfr({
51
- root : "dist", //Specifies the working directory
52
- path : api_path, //Specifies the API directory i.e: where SFR files reside
53
- out : doc_path //Specifies the directory for the resulting OAS documents
54
- }, app);
413
+ // Inject dependencies before initializing SFR
414
+ inject({
415
+ handlers: {
416
+ redis,
417
+ logger: console
418
+ },
419
+ controllers: {
420
+ db,
421
+ redis
422
+ }
423
+ });
55
424
 
425
+ // Now all handlers can access `this.redis` and `this.logger`
426
+ // All controllers can access `this.db` and `this.redis`
56
427
  ```
57
428
 
58
- ### Error-Handling
59
- The library automatically handles errors on both handlers and controllers, however, care must be taken on how error-handling is done by the developer, the following table illustrates what error-handling styles are allowed for both cases.
429
+ ### Accessing Injected Dependencies
430
+
431
+ ```javascript
432
+ // In handlers
433
+ async fn(req, res) {
434
+ this.logger.info("Request received");
435
+ const cached = await this.redis.get(key);
436
+ // ...
437
+ }
438
+
439
+ // In controllers
440
+ async fetch_data(id) {
441
+ return this.db.table.findUnique({ where: { id } });
442
+ }
443
+ ```
444
+
445
+ ---
446
+
447
+ ## Validation
448
+
449
+ ### Joi Validation
450
+
451
+ Define validation schemas as plain objects with Joi validators:
452
+
453
+ ```javascript
454
+ validators: {
455
+ "create-user": {
456
+ name: Joi.string().min(2).max(50).required(),
457
+ email: Joi.string().email().required(),
458
+ age: Joi.number().integer().min(18).optional()
459
+ }
460
+ }
461
+ ```
462
+
463
+ - **GET requests**: Validates `req.query`
464
+ - **POST/PUT/PATCH requests**: Validates `req.body`
465
+
466
+ ### Multer Validation (File Uploads)
467
+
468
+ Pass Multer middleware directly as validators:
469
+
470
+ ```javascript
471
+ import multer from "multer";
472
+
473
+ const upload = multer({ dest: "uploads/" });
474
+
475
+ validators: {
476
+ "upload-avatar": upload.single("avatar")
477
+ }
478
+ ```
479
+
480
+ ---
481
+
482
+ ## Configuration Reference
483
+
484
+ ### Parser Configuration (`ParserCFG`)
485
+
486
+ | Property | Type | Description |
487
+ |----------|------|-------------|
488
+ | `root` | `string` | Root directory of compiled source files |
489
+ | `path` | `string` | Directory containing SFR modules |
490
+ | `out` | `string` | Output directory for generated API specs |
491
+
492
+ ### SFR Configuration (`SFRConfig`)
493
+
494
+ | Property | Type | Default | Description |
495
+ |----------|------|---------|-------------|
496
+ | `base_dir` | `string` | `""` | Prefix path for all endpoints in the SFR |
497
+ | `public` | `boolean` | `false` | Skip authentication/authorization checks |
498
+ | `rate_limit` | `RateLimitConfig \| false` | Uses global default | Per-route rate limit configuration (see Rate Limiting section) |
499
+
500
+ ### OAS Configuration (`OASConfig`)
501
+
502
+ | Property | Type | Description |
503
+ |----------|------|-------------|
504
+ | `title` | `string` | Service name |
505
+ | `description` | `string` | Service description |
506
+ | `version` | `string` | Semantic version |
507
+ | `meta.service` | `string` | Service identifier |
508
+ | `meta.domain` | `string` | Organization/domain name |
509
+ | `meta.type` | `"backend"` \| `"frontend"` | Service type |
510
+ | `meta.language` | `string` | Programming language |
511
+ | `meta.port` | `number` | Service port |
512
+
513
+ ---
514
+
515
+ ## Rate Limiting
516
+
517
+ SFR includes built-in rate limiting support via `express-rate-limit` to protect your APIs from abuse and ensure fair resource usage.
518
+
519
+ ### Global Configuration
520
+
521
+ Set default rate limiting for all routes:
522
+
523
+ ```javascript
524
+ import { set_rate_limit_config } from "@avtechno/sfr";
525
+
526
+ set_rate_limit_config({
527
+ default: {
528
+ max: 100, // Maximum requests
529
+ windowMs: 60000, // Time window (1 minute)
530
+ message: "Too many requests, please try again later.",
531
+ statusCode: 429,
532
+ standardHeaders: true, // Include RateLimit-* headers
533
+ legacyHeaders: false // Include X-RateLimit-* headers
534
+ }
535
+ });
536
+ ```
537
+
538
+ ### Per-Route Configuration
539
+
540
+ Override the global default in individual SFR files:
541
+
542
+ ```javascript
543
+ import { REST } from "@avtechno/sfr";
544
+
545
+ export default REST({
546
+ cfg: {
547
+ base_dir: "api",
548
+ // Custom rate limit for this route
549
+ rate_limit: {
550
+ max: 10, // 10 requests
551
+ windowMs: 60000 // per minute
552
+ }
553
+ },
554
+ // ... rest of config
555
+ });
556
+ ```
557
+
558
+ ### Disable Rate Limiting
559
+
560
+ To disable rate limiting for a specific route:
561
+
562
+ ```javascript
563
+ cfg: {
564
+ rate_limit: false // Disables rate limiting for this route
565
+ }
566
+ ```
567
+
568
+ ### Advanced Configuration
569
+
570
+ All `express-rate-limit` options are supported:
571
+
572
+ ```javascript
573
+ import RedisStore from "rate-limit-redis";
574
+ import Redis from "ioredis";
575
+
576
+ const redis = new Redis();
577
+
578
+ set_rate_limit_config({
579
+ default: {
580
+ max: 100,
581
+ windowMs: 60000,
582
+ // Custom key generator (e.g., by user ID)
583
+ keyGenerator: (req) => req.user?.id || req.ip,
584
+ // Skip rate limiting for certain requests
585
+ skip: (req) => req.user?.isAdmin === true,
586
+ // Custom store (Redis, MongoDB, etc.)
587
+ store: new RedisStore({ client: redis })
588
+ }
589
+ });
590
+ ```
591
+
592
+ **Note:** Rate limiting middleware is applied before validation and authentication middleware.
593
+
594
+ ---
595
+
596
+ ## Auto-Generated Documentation
597
+
598
+ SFR automatically generates API documentation in standard formats:
599
+
600
+ - **REST endpoints** → OpenAPI 3.0 specification
601
+ - **MQ consumers** → AsyncAPI 3.0 specification
602
+
603
+ Documentation is written to the `cfg.out` directory as YAML files, enabling integration with:
604
+
605
+ - Swagger UI / ReDoc
606
+ - Postman / Insomnia import
607
+ - API gateways
608
+ - SDK generators
609
+ - Automated testing tools
610
+
611
+ ### Handler Metadata
612
+
613
+ Enhance generated documentation with metadata:
614
+
615
+ ```javascript
616
+ handlers: {
617
+ GET: {
618
+ "fetch-items": {
619
+ summary: "Short description", // Brief endpoint summary
620
+ description: "Detailed explanation", // Full documentation
621
+ tags: ["inventory", "read"], // Categorization tags
622
+ deprecated: false, // Mark as deprecated
623
+ async fn(req, res) { /* ... */ }
624
+ }
625
+ }
626
+ }
627
+ ```
628
+
629
+ ---
630
+
631
+ ## Error Handling
632
+
633
+ SFR provides automatic error handling for both handlers and controllers:
634
+
635
+ | Context | Supported Error Styles |
636
+ |---------|------------------------|
637
+ | **Handlers** | `throw new Error()`, `next(error)` |
638
+ | **Controllers** | `Promise.reject()`, `return Promise.resolve/reject` |
639
+
640
+ ### Example
641
+
642
+ ```javascript
643
+ handlers: {
644
+ GET: {
645
+ "risky-operation": {
646
+ async fn(req, res, next) {
647
+ try {
648
+ const result = await this.dangerous_call();
649
+ res.json({ data: result });
650
+ } catch (error) {
651
+ // Option 1: Throw (SFR catches this)
652
+ throw error;
653
+
654
+ // Option 2: Pass to Express error handler
655
+ // next(error);
656
+ }
657
+ }
658
+ }
659
+ }
660
+ },
661
+
662
+ controllers: {
663
+ async dangerous_call() {
664
+ // Rejections are automatically caught
665
+ if (bad_condition) {
666
+ return Promise.reject(new Error("Operation failed"));
667
+ }
668
+ return result;
669
+ }
670
+ }
671
+ ```
672
+
673
+ ---
674
+
675
+ ## MQ Integration
676
+
677
+ ### Initializing with Message Queue
678
+
679
+ ```javascript
680
+ import sfr, { MQLib } from "@avtechno/sfr";
681
+
682
+ const mq = new MQLib();
683
+ await mq.init(process.env.RABBITMQ_URL);
684
+
685
+ await sfr(
686
+ { root: "dist", path: "api", out: "docs" },
687
+ { /* OAS config */ },
688
+ {
689
+ REST: app,
690
+ MQ: mq.get_channel()
691
+ },
692
+ "api"
693
+ );
694
+ ```
695
+
696
+ ### Using MQ in REST Handlers
697
+
698
+ When MQ is configured, handlers receive access to messaging patterns:
699
+
700
+ ```javascript
701
+ handlers: {
702
+ POST: {
703
+ "trigger-job": {
704
+ async fn(req, res) {
705
+ // Access injected MQ patterns
706
+ await this.mq["Point-to-Point"].produce("job-queue", req.body);
707
+ res.json({ status: "queued" });
708
+ }
709
+ }
710
+ }
711
+ }
712
+ ```
713
+
714
+ ---
715
+
716
+ ## Debugging
717
+
718
+ Enable debug output by setting the environment variable:
719
+
720
+ ```bash
721
+ DEBUG_SFR=true node server.js
722
+ ```
723
+
724
+ This displays:
725
+ - Number of mounted endpoints per protocol
726
+ - Resolved paths for each SFR
727
+ - Protocol status indicators
728
+
729
+ ---
730
+
731
+ ## API Reference
732
+
733
+ ### Exports
734
+
735
+ | Export | Description |
736
+ |--------|-------------|
737
+ | `default` | Main SFR initializer function |
738
+ | `REST` | Template function for REST endpoints |
739
+ | `WS` | Template function for WebSocket handlers |
740
+ | `MQ` | Template function for MQ consumers |
741
+ | `MQLib` | Helper class for MQ connection management |
742
+ | `BroadcastMQ` | MQ class for Fanout/Direct/Topic patterns |
743
+ | `TargetedMQ` | MQ class for Point-to-Point/Request-Reply patterns |
744
+ | `inject` | Dependency injection function |
745
+ | `set_observability_options` | Configure observability settings |
746
+ | `sfr_logger` | Logger with automatic trace context |
747
+ | `with_span` | Create custom tracing spans |
748
+ | `get_trace_context` | Get current trace/span IDs |
749
+
750
+ ---
751
+
752
+ ## Observability (OpenTelemetry)
753
+
754
+ SFR includes built-in OpenTelemetry-compatible observability with automatic instrumentation for all protocols.
755
+
756
+ ### Features
757
+
758
+ - **Tracing**: Distributed traces across REST, WebSocket, and MQ handlers
759
+ - **Metrics**: Request counts, latencies, error rates, connection gauges
760
+ - **Logging**: Structured logs with automatic trace context correlation
761
+ - **Auto-instrumentation**: Express, Socket.IO, and AMQP are automatically traced
762
+
763
+ ### Quick Start
764
+
765
+ Observability is enabled by default and uses your `OASConfig` for service identification:
766
+
767
+ ```javascript
768
+ import sfr from "@avtechno/sfr";
769
+
770
+ // Service name and version are extracted from oas_cfg automatically
771
+ await sfr(
772
+ { root: "dist", path: "api", out: "docs" },
773
+ {
774
+ title: "My Service",
775
+ version: "1.0.0",
776
+ meta: {
777
+ service: "my-service", // Used as service.name in traces
778
+ domain: "example-org",
779
+ type: "backend",
780
+ language: "javascript",
781
+ port: 3000
782
+ }
783
+ },
784
+ { REST: app }
785
+ );
786
+ ```
787
+
788
+ ### Configuration
789
+
790
+ Configure observability before initializing SFR:
791
+
792
+ ```javascript
793
+ import sfr, { set_observability_options } from "@avtechno/sfr";
794
+
795
+ set_observability_options({
796
+ enabled: true, // Enable/disable (default: true)
797
+ otlp_endpoint: "http://localhost:4318", // OTLP collector endpoint
798
+ auto_instrumentation: true, // Auto-instrument common libraries
799
+ log_format: "json", // 'json' or 'pretty'
800
+ debug: false // Enable OTel SDK debug logs
801
+ });
802
+
803
+ await sfr(cfg, oas_cfg, connectors);
804
+ ```
805
+
806
+ ### Environment Variables
807
+
808
+ | Variable | Description | Default |
809
+ |----------|-------------|---------|
810
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint | `http://localhost:4318` |
811
+ | `SFR_TELEMETRY_ENABLED` | Enable/disable telemetry | `true` |
812
+ | `SFR_LOG_FORMAT` | Log format (`json`/`pretty`) | `pretty` (dev), `json` (prod) |
813
+ | `DEBUG_SFR` | Enable debug logging | `false` |
814
+
815
+ ### Custom Spans in Handlers
816
+
817
+ Add custom tracing spans for fine-grained observability:
818
+
819
+ ```javascript
820
+ import { REST, with_span } from "@avtechno/sfr";
821
+
822
+ export default REST({
823
+ handlers: {
824
+ GET: {
825
+ "get-user": {
826
+ async fn(req, res) {
827
+ // Create a child span for database operation
828
+ const user = await with_span({
829
+ name: "db:fetch_user",
830
+ attributes: { "user.id": req.params.id }
831
+ }, async (span) => {
832
+ return this.db.users.findById(req.params.id);
833
+ });
834
+
835
+ res.json({ data: user });
836
+ }
837
+ }
838
+ }
839
+ }
840
+ });
841
+ ```
842
+
843
+ ### Logging with Trace Context
844
+
845
+ Logs automatically include trace context when a span is active:
846
+
847
+ ```javascript
848
+ import { REST, sfr_logger, create_child_logger } from "@avtechno/sfr";
849
+
850
+ export default REST({
851
+ handlers: {
852
+ POST: {
853
+ "create-order": {
854
+ async fn(req, res) {
855
+ // Logs include trace_id and span_id automatically
856
+ sfr_logger.info("Creating order", { user_id: req.user.id });
857
+
858
+ // Create a child logger with additional context
859
+ const order_logger = create_child_logger({ order_id: "ord_123" });
860
+ order_logger.debug("Processing payment");
861
+
862
+ res.json({ data: { success: true } });
863
+ }
864
+ }
865
+ }
866
+ }
867
+ });
868
+ ```
869
+
870
+ ### Graceful Shutdown
871
+
872
+ Ensure telemetry is flushed before shutdown:
873
+
874
+ ```javascript
875
+ import { shutdown_observability } from "@avtechno/sfr";
876
+
877
+ process.on("SIGTERM", async () => {
878
+ await shutdown_observability();
879
+ process.exit(0);
880
+ });
881
+ ```
882
+
883
+ ### Metrics Available
884
+
885
+ | Metric | Type | Description |
886
+ |--------|------|-------------|
887
+ | `sfr.rest.requests.total` | Counter | Total REST requests |
888
+ | `sfr.rest.request.duration` | Histogram | Request latency (ms) |
889
+ | `sfr.rest.errors.total` | Counter | Total REST errors |
890
+ | `sfr.ws.connections.active` | Gauge | Active WebSocket connections |
891
+ | `sfr.ws.events.total` | Counter | Total WebSocket events |
892
+ | `sfr.mq.messages.received` | Counter | Total MQ messages received |
893
+ | `sfr.mq.processing.duration` | Histogram | Message processing time (ms) |
894
+
895
+ > 📖 **For comprehensive OpenTelemetry documentation**, including integration guides for Prometheus, Grafana, Loki, Jaeger, and other backends, see [OpenTelemetry Documentation](./docs/09-opentelemetry.md).
896
+
897
+ ---
898
+
899
+ ## Roadmap
900
+
901
+ - [ ] Multer upload validation in OpenAPI specs
902
+ - [x] Built-in structured logging
903
+ - [x] WebSocket protocol implementation
904
+ - [x] Rate limiting middleware integration
905
+ - [ ] GraphQL protocol support
906
+
907
+ ---
908
+
909
+ ## Contributing
60
910
 
61
- |Handlers|Controllers|
62
- |-|-|
63
- |Exception Throws|Promise.resolve/reject returns|
64
- |Passing Exceptions to Next fn||
911
+ Found a bug or have a feature request? Please open an issue on our [GitHub Issues](https://github.com/avtechno/sfr/issues) page.
65
912
 
913
+ ---
66
914
 
67
- ### Bug Reporting
68
- If you've found a bug, please file it in our Github Issues Page so we can have a look at it, perhaps fix it XD
915
+ ## License
69
916
 
70
- TODO:
71
- * Multer Upload Validation
72
- * Built-in Logging
917
+ ISC © Emmanuel Abellana