npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@avtechno/sfr

v2.1.1

Published

An opinionated way of writing services using ExpressJS.

Readme

Single File Router (SFR)

An opinionated, convention-over-configuration approach to building services in Node.js

npm version License: ISC


Overview

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.

Key Features

  • Single-File Declaration — Define validators, handlers, and controllers together in one cohesive module
  • Multi-Protocol Support — Build REST APIs, WebSocket servers, and Message Queue consumers using the same patterns
  • Automatic Validation — Integrate Joi schemas or Multer middleware for request validation
  • Dependency Injection — Inject shared dependencies into handlers and controllers via IoC pattern
  • Auto-Generated Documentation — Produces OpenAPI 3.0 specs (REST) and AsyncAPI 3.0 specs (MQ/WS) automatically
  • File-Based Routing — Directory structure determines endpoint paths, reducing manual configuration

Installation

npm install @avtechno/sfr
# or
yarn add @avtechno/sfr

Peer Dependencies

SFR works with the following libraries (install as needed):

npm install express joi amqplib socket.io

Quick Start

import sfr from "@avtechno/sfr";
import express from "express";

const app = express();
const PORT = 3000;

app.use(express.json());

await sfr(
  { root: "dist", path: "api", out: "docs" },
  {
    title: "My Service",
    description: "A sample SFR-powered service",
    version: "1.0.0",
    meta: {
      service: "my-service",
      domain: "example-org",
      type: "backend",
      language: "javascript",
      port: PORT
    }
  },
  { REST: app },
  "api"
);

app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

Project Structure

SFR expects your API files to be organized by protocol within the specified path:

project/
├── dist/                    # Compiled output (cfg.root)
│   └── api/                 # API directory (cfg.path)
│       ├── rest/            # REST endpoints
│       │   ├── users.mjs
│       │   └── auth/
│       │       └── login.mjs
│       ├── ws/              # WebSocket handlers
│       │   └── chat.mjs
│       └── mq/              # Message Queue consumers
│           └── notifications.mjs
├── docs/                    # Generated OpenAPI/AsyncAPI specs (cfg.out)
└── src/
    └── api/
        └── ...              # Source files

The file path determines the endpoint URL:

  • rest/users.mjs/api/users/*
  • rest/auth/login.mjs/api/auth/login/*

SFR File Structure

Each SFR module exports a default object created using REST(), WS(), or MQ() template functions:

| Component | Description | |-----------|-------------| | cfg | Configuration options (base directory, public/private access) | | validators | Request validation schemas (Joi objects or Multer middleware) | | handlers | Route handlers organized by HTTP method or MQ pattern | | controllers | Reusable data access functions (database calls, external APIs) |


Creating a REST Endpoint

import Joi from "joi";
import { REST } from "@avtechno/sfr";

export default REST({
  cfg: {
    base_dir: "users",    // Optional: Prepends to endpoint path
    public: false         // Requires authentication (default: false)
  },

  validators: {
    "get-profile": {
      user_id: Joi.string().uuid().required()
    },
    "update-profile": {
      name: Joi.string().min(2).max(100),
      email: Joi.string().email()
    }
  },

  handlers: {
    GET: {
      "get-profile": {
        summary: "Retrieve user profile",
        description: "Fetches the profile for a given user ID",
        tags: ["users", "profile"],
        async fn(req, res) {
          const { user_id } = req.query;
          const profile = await this.fetch_user(user_id);
          res.status(200).json({ data: profile });
        }
      }
    },

    PUT: {
      "update-profile": {
        summary: "Update user profile",
        description: "Updates profile information for authenticated user",
        tags: ["users", "profile"],
        deprecated: false,
        async fn(req, res) {
          const result = await this.update_user(req.body);
          res.status(200).json({ data: result });
        }
      }
    }
  },

  controllers: {
    async fetch_user(user_id) {
      // Access injected dependencies via `this`
      return this.db.users.findById(user_id);
    },

    async update_user(data) {
      return this.db.users.update(data);
    }
  }
});

Resulting Endpoints

| Method | Endpoint | Handler | |--------|----------|---------| | GET | /api/users/get-profile | get-profile | | PUT | /api/users/update-profile | update-profile |


Creating MQ Consumers

SFR supports multiple messaging patterns via RabbitMQ/AMQP:

| Pattern | Description | Class | |---------|-------------|-------| | Point-to-Point | Direct queue consumption | TargetedMQ | | Request-Reply | RPC-style messaging with responses | TargetedMQ | | Fanout | Broadcast to all subscribers | BroadcastMQ | | Direct | Route by exact routing key | BroadcastMQ | | Topic | Route by pattern-matched routing key | BroadcastMQ |

import Joi from "joi";
import { MQ } from "@avtechno/sfr";

export default MQ({
  cfg: {
    base_dir: "notifications"
  },

  validators: {
    "send-email": {
      to: Joi.string().email().required(),
      subject: Joi.string().required(),
      body: Joi.string().required()
    }
  },

  handlers: {
    "Point-to-Point": {
      "send-email": {
        summary: "Process email notification",
        options: { noAck: false },
        async fn(msg) {
          const { to, subject, body } = msg.content;
          await this.send_email(to, subject, body);
          this.mq.channel.ack(msg);
        }
      }
    },

    "Request-Reply": {
      "validate-email": {
        summary: "Validate email address",
        async fn(msg) {
          const result = await this.validate(msg.content.email);
          this.mq.reply(msg, { valid: result });
        }
      }
    }
  },

  controllers: {
    async send_email(to, subject, body) {
      // Email sending logic
    },

    async validate(email) {
      // Validation logic
      return true;
    }
  }
});

Creating WebSocket Handlers

SFR supports real-time WebSocket communication via Socket.IO. WebSocket SFRs follow the same pattern as REST and MQ:

import Joi from "joi";
import { WS } from "@avtechno/sfr";

export default WS({
  cfg: {
    namespace: "/chat",  // Socket.IO namespace (optional, defaults to "/")
    public: true         // Skip authentication checks
  },

  validators: {
    "send-message": {
      room: Joi.string().required(),
      message: Joi.string().min(1).max(1000).required(),
      sender: Joi.string().required()
    },
    "join-room": {
      room: Joi.string().required(),
      username: Joi.string().min(2).max(50).required()
    },
    "leave-room": {
      room: Joi.string().required()
    }
  },

  handlers: {
    "send-message": {
      summary: "Send a message to a room",
      description: "Broadcasts a message to all users in the specified room",
      tags: ["chat", "messaging"],
      async fn(ctx) {
        const { room, message, sender } = ctx.data;
        
        // Process via controller
        const processed = await this.process_message(room, sender, message);
        
        // Broadcast to room
        ctx.io.to(room).emit("new-message", processed);
        
        // Acknowledge if callback provided
        if (ctx.ack) ctx.ack({ success: true });
      }
    },

    "join-room": {
      summary: "Join a chat room",
      description: "Adds the user to a room and notifies other members",
      tags: ["chat", "rooms"],
      async fn(ctx) {
        const { room, username } = ctx.data;
        
        // Join Socket.IO room
        ctx.socket.join(room);
        
        // Notify other room members
        ctx.socket.to(room).emit("user-joined", { 
          username, 
          socket_id: ctx.socket.id 
        });
        
        if (ctx.ack) ctx.ack({ success: true, room });
      }
    },

    "leave-room": {
      summary: "Leave a chat room",
      tags: ["chat", "rooms"],
      async fn(ctx) {
        const { room } = ctx.data;
        
        ctx.socket.leave(room);
        ctx.socket.to(room).emit("user-left", { socket_id: ctx.socket.id });
        
        if (ctx.ack) ctx.ack({ success: true });
      }
    }
  },

  controllers: {
    async process_message(room, sender, message) {
      // Access injected dependencies via `this`
      return {
        id: `msg_${Date.now()}`,
        room,
        sender,
        message,
        timestamp: Date.now()
      };
    }
  }
});

WebSocket Handler Context

Each WebSocket handler receives a context object (ctx) with the following properties:

| Property | Type | Description | |----------|------|-------------| | io | Server | The Socket.IO server instance | | socket | Socket | The current client socket connection | | data | object | The validated event payload | | ack | function? | Optional acknowledgement callback |

Initializing with WebSocket

import sfr from "@avtechno/sfr";
import express from "express";
import { createServer } from "http";
import { Server } from "socket.io";

const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer);

await sfr(
  { root: "dist", path: "api", out: "docs" },
  { /* OAS config */ },
  {
    REST: app,
    WS: io      // Pass Socket.IO server
  },
  "api"
);

httpServer.listen(3000);

WebSocket Features

  • Namespaces — Use cfg.namespace to organize events into logical groups
  • Rooms — Use ctx.socket.join() and ctx.socket.to() for room-based messaging
  • Validation — Joi schemas validate incoming event data before handler execution
  • Acknowledgements — Support for Socket.IO acknowledgement callbacks via ctx.ack
  • Error Handling — Validation errors and handler exceptions emit {event}:error events

Dependency Injection

Inject shared dependencies (database connections, external services, etc.) into handlers and controllers:

import sfr, { inject } from "@avtechno/sfr";
import { PrismaClient } from "@prisma/client";
import Redis from "ioredis";

const db = new PrismaClient();
const redis = new Redis();

// Inject dependencies before initializing SFR
inject({
  handlers: {
    redis,
    logger: console
  },
  controllers: {
    db,
    redis
  }
});

// Now all handlers can access `this.redis` and `this.logger`
// All controllers can access `this.db` and `this.redis`

Accessing Injected Dependencies

// In handlers
async fn(req, res) {
  this.logger.info("Request received");
  const cached = await this.redis.get(key);
  // ...
}

// In controllers
async fetch_data(id) {
  return this.db.table.findUnique({ where: { id } });
}

Validation

Joi Validation

Define validation schemas as plain objects with Joi validators:

validators: {
  "create-user": {
    name: Joi.string().min(2).max(50).required(),
    email: Joi.string().email().required(),
    age: Joi.number().integer().min(18).optional()
  }
}
  • GET requests: Validates req.query
  • POST/PUT/PATCH requests: Validates req.body

Multer Validation (File Uploads)

Pass Multer middleware directly as validators:

import multer from "multer";

const upload = multer({ dest: "uploads/" });

validators: {
  "upload-avatar": upload.single("avatar")
}

Configuration Reference

Parser Configuration (ParserCFG)

| Property | Type | Description | |----------|------|-------------| | root | string | Root directory of compiled source files | | path | string | Directory containing SFR modules | | out | string | Output directory for generated API specs |

SFR Configuration (SFRConfig)

| Property | Type | Default | Description | |----------|------|---------|-------------| | base_dir | string | "" | Prefix path for all endpoints in the SFR | | public | boolean | false | Skip authentication/authorization checks | | rate_limit | RateLimitConfig \| false | Uses global default | Per-route rate limit configuration (see Rate Limiting section) |

OAS Configuration (OASConfig)

| Property | Type | Description | |----------|------|-------------| | title | string | Service name | | description | string | Service description | | version | string | Semantic version | | meta.service | string | Service identifier | | meta.domain | string | Organization/domain name | | meta.type | "backend" | "frontend" | Service type | | meta.language | string | Programming language | | meta.port | number | Service port |


Rate Limiting

SFR includes built-in rate limiting support via express-rate-limit to protect your APIs from abuse and ensure fair resource usage.

Global Configuration

Set default rate limiting for all routes:

import { set_rate_limit_config } from "@avtechno/sfr";

set_rate_limit_config({
  default: {
    max: 100,              // Maximum requests
    windowMs: 60000,       // Time window (1 minute)
    message: "Too many requests, please try again later.",
    statusCode: 429,
    standardHeaders: true,  // Include RateLimit-* headers
    legacyHeaders: false    // Include X-RateLimit-* headers
  }
});

Per-Route Configuration

Override the global default in individual SFR files:

import { REST } from "@avtechno/sfr";

export default REST({
  cfg: {
    base_dir: "api",
    // Custom rate limit for this route
    rate_limit: {
      max: 10,           // 10 requests
      windowMs: 60000     // per minute
    }
  },
  // ... rest of config
});

Disable Rate Limiting

To disable rate limiting for a specific route:

cfg: {
  rate_limit: false  // Disables rate limiting for this route
}

Advanced Configuration

All express-rate-limit options are supported:

import RedisStore from "rate-limit-redis";
import Redis from "ioredis";

const redis = new Redis();

set_rate_limit_config({
  default: {
    max: 100,
    windowMs: 60000,
    // Custom key generator (e.g., by user ID)
    keyGenerator: (req) => req.user?.id || req.ip,
    // Skip rate limiting for certain requests
    skip: (req) => req.user?.isAdmin === true,
    // Custom store (Redis, MongoDB, etc.)
    store: new RedisStore({ client: redis })
  }
});

Note: Rate limiting middleware is applied before validation and authentication middleware.


Auto-Generated Documentation

SFR automatically generates API documentation in standard formats:

  • REST endpoints → OpenAPI 3.0 specification
  • MQ consumers → AsyncAPI 3.0 specification

Documentation is written to the cfg.out directory as YAML files, enabling integration with:

  • Swagger UI / ReDoc
  • Postman / Insomnia import
  • API gateways
  • SDK generators
  • Automated testing tools

Handler Metadata

Enhance generated documentation with metadata:

handlers: {
  GET: {
    "fetch-items": {
      summary: "Short description",           // Brief endpoint summary
      description: "Detailed explanation",    // Full documentation
      tags: ["inventory", "read"],            // Categorization tags
      deprecated: false,                      // Mark as deprecated
      async fn(req, res) { /* ... */ }
    }
  }
}

Error Handling

SFR provides automatic error handling for both handlers and controllers:

| Context | Supported Error Styles | |---------|------------------------| | Handlers | throw new Error(), next(error) | | Controllers | Promise.reject(), return Promise.resolve/reject |

Example

handlers: {
  GET: {
    "risky-operation": {
      async fn(req, res, next) {
        try {
          const result = await this.dangerous_call();
          res.json({ data: result });
        } catch (error) {
          // Option 1: Throw (SFR catches this)
          throw error;

          // Option 2: Pass to Express error handler
          // next(error);
        }
      }
    }
  }
},

controllers: {
  async dangerous_call() {
    // Rejections are automatically caught
    if (bad_condition) {
      return Promise.reject(new Error("Operation failed"));
    }
    return result;
  }
}

MQ Integration

Initializing with Message Queue

import sfr, { MQLib } from "@avtechno/sfr";

const mq = new MQLib();
await mq.init(process.env.RABBITMQ_URL);

await sfr(
  { root: "dist", path: "api", out: "docs" },
  { /* OAS config */ },
  {
    REST: app,
    MQ: mq.get_channel()
  },
  "api"
);

Using MQ in REST Handlers

When MQ is configured, handlers receive access to messaging patterns:

handlers: {
  POST: {
    "trigger-job": {
      async fn(req, res) {
        // Access injected MQ patterns
        await this.mq["Point-to-Point"].produce("job-queue", req.body);
        res.json({ status: "queued" });
      }
    }
  }
}

Debugging

Enable debug output by setting the environment variable:

DEBUG_SFR=true node server.js

This displays:

  • Number of mounted endpoints per protocol
  • Resolved paths for each SFR
  • Protocol status indicators

API Reference

Exports

| Export | Description | |--------|-------------| | default | Main SFR initializer function | | REST | Template function for REST endpoints | | WS | Template function for WebSocket handlers | | MQ | Template function for MQ consumers | | MQLib | Helper class for MQ connection management | | BroadcastMQ | MQ class for Fanout/Direct/Topic patterns | | TargetedMQ | MQ class for Point-to-Point/Request-Reply patterns | | inject | Dependency injection function | | set_observability_options | Configure observability settings | | sfr_logger | Logger with automatic trace context | | with_span | Create custom tracing spans | | get_trace_context | Get current trace/span IDs |


Observability (OpenTelemetry)

SFR includes built-in OpenTelemetry-compatible observability with automatic instrumentation for all protocols.

Features

  • Tracing: Distributed traces across REST, WebSocket, and MQ handlers
  • Metrics: Request counts, latencies, error rates, connection gauges
  • Logging: Structured logs with automatic trace context correlation
  • Auto-instrumentation: Express, Socket.IO, and AMQP are automatically traced

Quick Start

Observability is enabled by default and uses your OASConfig for service identification:

import sfr from "@avtechno/sfr";

// Service name and version are extracted from oas_cfg automatically
await sfr(
  { root: "dist", path: "api", out: "docs" },
  {
    title: "My Service",
    version: "1.0.0",
    meta: {
      service: "my-service",    // Used as service.name in traces
      domain: "example-org",
      type: "backend",
      language: "javascript",
      port: 3000
    }
  },
  { REST: app }
);

Configuration

Configure observability before initializing SFR:

import sfr, { set_observability_options } from "@avtechno/sfr";

set_observability_options({
  enabled: true,                           // Enable/disable (default: true)
  otlp_endpoint: "http://localhost:4318",  // OTLP collector endpoint
  auto_instrumentation: true,              // Auto-instrument common libraries
  log_format: "json",                      // 'json' or 'pretty'
  debug: false                             // Enable OTel SDK debug logs
});

await sfr(cfg, oas_cfg, connectors);

Environment Variables

| Variable | Description | Default | |----------|-------------|---------| | OTEL_EXPORTER_OTLP_ENDPOINT | OTLP collector endpoint | http://localhost:4318 | | SFR_TELEMETRY_ENABLED | Enable/disable telemetry | true | | SFR_LOG_FORMAT | Log format (json/pretty) | pretty (dev), json (prod) | | DEBUG_SFR | Enable debug logging | false |

Custom Spans in Handlers

Add custom tracing spans for fine-grained observability:

import { REST, with_span } from "@avtechno/sfr";

export default REST({
  handlers: {
    GET: {
      "get-user": {
        async fn(req, res) {
          // Create a child span for database operation
          const user = await with_span({
            name: "db:fetch_user",
            attributes: { "user.id": req.params.id }
          }, async (span) => {
            return this.db.users.findById(req.params.id);
          });

          res.json({ data: user });
        }
      }
    }
  }
});

Logging with Trace Context

Logs automatically include trace context when a span is active:

import { REST, sfr_logger, create_child_logger } from "@avtechno/sfr";

export default REST({
  handlers: {
    POST: {
      "create-order": {
        async fn(req, res) {
          // Logs include trace_id and span_id automatically
          sfr_logger.info("Creating order", { user_id: req.user.id });

          // Create a child logger with additional context
          const order_logger = create_child_logger({ order_id: "ord_123" });
          order_logger.debug("Processing payment");

          res.json({ data: { success: true } });
        }
      }
    }
  }
});

Graceful Shutdown

Ensure telemetry is flushed before shutdown:

import { shutdown_observability } from "@avtechno/sfr";

process.on("SIGTERM", async () => {
  await shutdown_observability();
  process.exit(0);
});

Metrics Available

| Metric | Type | Description | |--------|------|-------------| | sfr.rest.requests.total | Counter | Total REST requests | | sfr.rest.request.duration | Histogram | Request latency (ms) | | sfr.rest.errors.total | Counter | Total REST errors | | sfr.ws.connections.active | Gauge | Active WebSocket connections | | sfr.ws.events.total | Counter | Total WebSocket events | | sfr.mq.messages.received | Counter | Total MQ messages received | | sfr.mq.processing.duration | Histogram | Message processing time (ms) |

📖 For comprehensive OpenTelemetry documentation, including integration guides for Prometheus, Grafana, Loki, Jaeger, and other backends, see OpenTelemetry Documentation.


Roadmap

  • [ ] Multer upload validation in OpenAPI specs
  • [x] Built-in structured logging
  • [x] WebSocket protocol implementation
  • [x] Rate limiting middleware integration
  • [ ] GraphQL protocol support
  • [ ] gRPC protocol support

Now that I'm using a diverse set of communication protocols, I may rewrite SFR into a more pluggable and modular format with each protocol and major feature being treated as an installable extension.


Contributing

Found a bug or have a feature request? Please open an issue on our GitHub Issues page.


License

ISC © Emmanuel Abellana