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

@centralping/ergo-router

v0.6.0

Published

REST-compliant router for @centralping/ergo with strict Fast Fail semantics

Readme

CI codecov npm version OpenSSF Scorecard Node.js >=22 License: MIT

A REST-compliant router for ergo with strict Fast Fail semantics. Provides path matching via find-my-way, automatic REST compliance (405+Allow, HEAD, OPTIONS, PATCH enforcement), transport-level security, and seamless integration with ergo's composable middleware pipeline.

Why ergo-router?

  • Automatic REST compliance -- 405 Method Not Allowed with Allow header, HEAD falls back to GET, OPTIONS auto-responds with allowed methods, PATCH Content-Type enforcement. All per RFC 9110.
  • Transport-level security -- Security headers, CORS, rate limiting, and request ID generation run before routing, ensuring every response (including errors) is protected.
  • Declarative pipeline assembly -- Define routes with a config object; the router assembles the full Fast Fail pipeline (negotiation, auth, validation, execution) from ergo middleware automatically.
  • Graceful shutdown -- Built-in support for draining in-flight requests on SIGTERM/SIGINT.

Request Dispatch Flow

Incoming Request
  |
  +- 1. Transport Layer (every request)
  |    +- Request ID generation (response header)
  |    +- Security headers
  |    +- Rate limiting (429 short-circuit)
  |    +- CORS (preflight 204 short-circuit)
  |
  +- 2. REST Semantics
  |    +- OPTIONS -> 204 + Allow header
  |    +- PATCH Content-Type enforcement -> 415
  |    +- HEAD -> falls back to GET handler
  |    +- Route matching (find-my-way)
  |    +- 405 + Allow or 404
  |
  +- 3. Application Pipeline
       +- Route params seeded in accumulator
       +- Stage 1: Negotiation
       +- Stage 2: Authorization
       +- Stage 3: Validation
       +- Stage 4: Execution
       +- Implicit send()

Every early exit (429, 403, 404, 405, 415, preflight 204) includes security headers and request ID automatically.

Installation

npm install @centralping/ergo-router @centralping/ergo

Requires Node.js >= 22. @centralping/ergo is a peer dependency.

Quick Start

import createRouter from '@centralping/ergo-router';

const router = createRouter({
  transport: {
    requestId: {},
    security: {},
    cors: {origin: 'https://myapp.com'}
  },
  defaults: {
    accepts: {types: ['application/json']},
    timeout: {ms: 30000}
  }
});

router.get('/users/:id', {
  execute: (req, res, acc) => ({response: {body: {id: acc.route.params.id}}})
});

router.post('/users', {
  validate: {body: {type: 'object', properties: {name: {type: 'string'}}, required: ['name']}},
  execute: (req, res, acc) => ({response: {statusCode: 201, body: acc.body.parsed}})
});

router.listen(3000, () => console.log('Listening on :3000'));
import createRouter, {defineGet, definePost} from '@centralping/ergo-router';

const router = createRouter({
  transport: {
    requestId: {},
    security: {},
    cors: {origin: 'https://myapp.com'}
  },
  defaults: {
    accepts: {types: ['application/json']},
    timeout: {ms: 30000}
  }
});

router.get('/users/:id', defineGet(
  {url: true},
  (req, res, acc) => ({response: {body: {id: acc.route.params.id}}})
));

router.post('/users', definePost(
  {validate: {body: {type: 'object', properties: {name: {type: 'string'}}, required: ['name']}}},
  (req, res, acc) => ({response: {statusCode: 201, body: acc.body.parsed}})
));

router.listen(3000, () => console.log('Listening on :3000'));

Note: The defineGet and definePost helpers infer the accumulator type from enabled middleware keys — acc.route.params, acc.url, acc.body, etc. are fully typed without manual annotation. See Typed Route Helpers below.

Presets

Presets provide pre-built router configurations for common use cases. Spread them into createRouter() to get started quickly:

import createRouter, {presets} from '@centralping/ergo-router';

const router = createRouter({
  ...presets.jsonApi,
  transport: {cors: {origin: 'https://myapp.com'}},
  defaults: {...presets.jsonApi.defaults, timeout: {ms: 30000}},
});
import createRouter, {presets} from '@centralping/ergo-router';

const router = createRouter({
  ...presets.jsonApi,
  transport: {cors: {origin: 'https://myapp.com'}},
  defaults: {...presets.jsonApi.defaults, timeout: {ms: 30000}},
});

presets.jsonApi

Enables transport-level request ID and security headers, and restricts content negotiation to application/json.

| Key | Value | Purpose | | --- | --- | --- | | transport.requestId | {} | Generate unique request IDs | | transport.security | {} | Set security response headers | | defaults.accepts | {types: ['application/json']} | Restrict to JSON content type |

Excludes (deployment-specific): auth, CORS origin, rate limiting.

Override semantics: Standard shallow spread. Overriding transport replaces the entire transport object. To extend defaults while preserving preset values, use nested spread:

defaults: {...presets.jsonApi.defaults, timeout: {ms: 30000}}

presets.sse

Configures the router for Server-Sent Events. Enables transport-level request ID and security headers, disables compression (prevents buffering of streamed chunks), disables timeout (SSE connections are long-lived), and restricts content negotiation to text/event-stream.

| Key | Value | Purpose | | --- | --- | --- | | transport.requestId | {} | Generate unique request IDs | | transport.security | {} | Set security response headers | | defaults.compress | false | Prevent SSE chunk buffering | | defaults.timeout | false | Allow long-lived connections | | defaults.accepts | {types: ['text/event-stream']} | Restrict to event stream content type |

Excludes (deployment-specific): auth, CORS origin, rate limiting.

Per-route: SSE routes should set noSend: true so the handler can write the event stream directly. noSend is a route option and cannot be set in defaults.

import createRouter, {presets} from '@centralping/ergo-router';

const router = createRouter({...presets.sse});

router.get('/events', {
  noSend: true,
  execute: (_req, res) => {
    res.writeHead(200, {'content-type': 'text/event-stream'});
    res.write('data: hello\n\n');
    // keep connection open for streaming...
  }
});
import createRouter, {presets} from '@centralping/ergo-router';

const router = createRouter({...presets.sse});

router.get('/events', {
  noSend: true,
  execute: (_req, res) => {
    res.writeHead(200, {'content-type': 'text/event-stream'});
    res.write('data: hello\n\n');
  }
});

presets.webhooks

Configures the router for webhook receivers. Enables transport-level request ID and security headers, restricts content negotiation to application/json, and requires the Idempotency-Key header for safe at-least-once delivery.

| Key | Value | Purpose | | --- | --- | --- | | transport.requestId | {} | Generate unique request IDs | | transport.security | {} | Set security response headers | | defaults.accepts | {types: ['application/json']} | Restrict to JSON content type | | defaults.idempotency | {required: true} | Require Idempotency-Key header |

Excludes (deployment-specific): auth, CORS origin, rate limiting.

import createRouter, {presets} from '@centralping/ergo-router';

const router = createRouter({...presets.webhooks});

router.post('/hooks', {
  execute: (req, res, acc) => ({
    response: {body: {received: true, key: acc.idempotency.key}}
  })
});
import createRouter, {presets, definePost} from '@centralping/ergo-router';

const router = createRouter({...presets.webhooks});

router.post('/hooks', definePost(
  {idempotency: {required: true}},
  (_req, _res, acc) => ({
    response: {body: {received: true, key: acc.idempotency.key}}
  })
));

presets.public

Configures the router for public read-only APIs. Enables transport-level request ID, security headers, and rate limiting (built-in defaults: 100 req/60s), restricts content negotiation to application/json, and sets Cache-Control: public, max-age=300.

| Key | Value | Purpose | | --- | --- | --- | | transport.requestId | {} | Generate unique request IDs | | transport.security | {} | Set security response headers | | transport.rateLimit | {} | Rate limiting (100 req/60s defaults) | | defaults.accepts | {types: ['application/json']} | Restrict to JSON content type | | defaults.cacheControl | {public: true, maxAge: 300} | 5-minute public cache |

Excludes (deployment-specific): auth, CORS origin.

import createRouter, {presets} from '@centralping/ergo-router';

const router = createRouter({
  ...presets.public,
  transport: {...presets.public.transport, rateLimit: {max: 30}}
});

router.get('/data', {
  execute: () => ({response: {body: {items: []}}})
});
import createRouter, {presets} from '@centralping/ergo-router';

const router = createRouter({
  ...presets.public,
  transport: {...presets.public.transport, rateLimit: {max: 30}}
});

router.get('/data', {
  execute: () => ({response: {body: {items: []}}})
});

API Overview

createRouter(options?)

Creates a new router instance with optional transport and default middleware configuration.

| Option | Description | | --------------------- | ------------------------------------------------ | | transport.requestId | Request ID generation config | | transport.security | Security headers (HSTS, CSP, etc.) | | transport.cors | CORS configuration | | transport.rateLimit | Rate limiting (sliding window) | | defaults.* | Default middleware options applied to all routes | | timing | boolean \| {header?, precision?} — inject X-Response-Time header measuring pipeline execution time (default false) | | onResponse | function — post-send observation hook (req, res, responseInfo, domainAcc) fired after every route response |

Route Methods

router.get(path, config);
router.post(path, config);
router.put(path, config);
router.patch(path, config);
router.delete(path, config);

router.use(...fns);

router.use(...fns) registers application-level middleware that is prepended to every declarative/array pipeline before all four stages (Negotiation, Authorization, Validation, Execution). Returns the router for chaining. Array pipelines only — raw function handlers (router.get('/path', fn)) bypass router.use() middleware.

Route Config

| Key | Description | Standard | | ---------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | | execute | Route handler (req, res, domainAcc, responseAcc) => {response?, value?} (required). Most handlers use 3 args: (req, res, acc) — see below. | -- | | tracing | OpenTelemetry tracing options or false | W3C Trace Context | | validate | JSON Schema for body/query validation | -- | | accepts | Content negotiation options or false | RFC 9110 §12.5 | | authorization | Authorization strategy options or false | RFC 6750, RFC 7617 | | csrf | CSRF options or false | OWASP CSRF | | body | Body parsing options or false (auto for POST/PUT/PATCH) | RFC 7578 | | cookie | Cookie parsing options or false | RFC 6265 | | url | URL parsing options or false (auto for GET/DELETE) | -- | | logger | Request logging options or false | -- | | timeout | Request timeout options or false | -- | | compress | Response compression options or false | RFC 9110 §12.5.3 | | securityHeaders | Security header options or false | RFC 6797 | | cacheControl | Cache-Control options or false | RFC 9111 | | jsonApiQuery | JSON:API query parsing options or false | JSON:API | | preconditionRequired | 428 enforcement for PUT/PATCH or false | RFC 6585 §3 | | idempotency | Idempotency-Key header enforcement options or false | Idempotency-Key (Internet-Draft) | | paginate | Pagination options or false. Auto-includes URL parsing. | RFC 8288 | | prefer | Prefer header parsing options or false | RFC 7240 | | rateLimit | Per-route rate limit options or false | RFC 6585 §4 | | use | Custom middleware array {fn, setPath} config objects or bare functions, or false | -- | | openapi | OpenAPI annotation object (summary, description, tags, responses, etc.) | OpenAPI 3.1 | | noSend | Skip automatic send() — handler manages the response directly (boolean, default false) | -- | | send | Per-route send() options (prettify, etag, vary, envelope, etc.) | -- | | catchHandler | Per-route error handler (req, res, err, domainAcc). Receives normalized errors with statusCode/status and the domain accumulator (may be partially populated) | -- | | onResponse | Per-route post-send observation hook (req, res, responseInfo, domainAcc). Fires before the router-level hook. Does not fire when catchHandler takes over | -- |

Auto-included Middleware

Some middleware is automatically included based on the HTTP method, even when not explicitly configured. Setting the key to false disables auto-inclusion.

| Middleware | Auto-included for | Behavior | | ---------- | ----------------- | ------------------------------------------------------------------------------------ | | url | GET, DELETE | URL/query parsing is included by default for methods that do not have a request body | | url | when paginate active | URL parsing is auto-included for any method when pagination is enabled | | body | POST, PUT, PATCH | Body parsing is included by default for methods that carry a request body |

The execute function receives four arguments: (req, res, domainAcc, responseAcc). Most handlers only need the domain accumulator (acc in the examples above) which carries route params, parsed body, auth identity, and other middleware outputs. The response accumulator is available as the 4th argument for advanced use cases — see the Architecture page for the full two-accumulator model.

Typed Route Helpers

defineGet, definePost, and defineRoute enable TypeScript to infer the domain accumulator type from enabled middleware config keys — providing fully typed acc in execute callbacks without manual generic annotation.

| Helper | Auto-includes | Use for | | --- | --- | --- | | defineGet(config, execute) | {url: UrlResult} | GET, DELETE routes | | definePost(config, execute) | {body: BodyResult} | POST, PUT, PATCH routes | | defineRoute(config, execute) | — | Method-agnostic (add url/body explicitly) |

import createRouter, {defineGet, definePost} from '@centralping/ergo-router';

const router = createRouter({
  transport: {requestId: {}, security: {}},
  defaults: {accepts: {types: ['application/json']}}
});

router.get('/users/:id', defineGet(
  {authorization: true, url: true},
  (req, res, acc) => {
    acc.auth;         // AuthorizationResult — typed
    acc.url.query;    // Record<string, string | string[]> — typed
    acc.route.params; // Record<string, string> — always present
    return {response: {body: {id: acc.route.params.id}}};
  }
));

router.post('/users', definePost(
  {authorization: true, body: {limit: 2048}},
  (req, res, acc) => {
    acc.auth;         // AuthorizationResult — typed
    acc.body.parsed;  // unknown — typed
    return {response: {statusCode: 201, body: acc.body.parsed}};
  }
));
import createRouter, {defineGet, definePost} from '@centralping/ergo-router';

const router = createRouter({
  transport: {requestId: {}, security: {}},
  defaults: {accepts: {types: ['application/json']}}
});

router.get('/users/:id', defineGet(
  {authorization: true, url: true},
  (req, res, acc) => {
    acc.auth;         // AuthorizationResult
    acc.url.query;    // Record<string, string | string[]>
    acc.route.params; // Record<string, string>
    return {response: {body: {id: acc.route.params.id}}};
  }
));

router.post('/users', definePost(
  {authorization: true, body: {limit: 2048}},
  (req, res, acc) => {
    acc.auth;         // AuthorizationResult
    acc.body.parsed;  // unknown
    return {response: {statusCode: 201, body: acc.body.parsed}};
  }
));

Keys set to false correctly suppress their accumulator type. paginate transitively includes URL types.

Known limitation: Middleware enabled via createRouter({defaults: {...}}) is not visible to type inference. Add the key explicitly to the route config for typed access.

Advanced types: RouteConfigBase, InferAccumulator<C>, AutoGetAccumulator<C>, and AutoPostAccumulator<C> are exported for custom inference helpers.

graceful(handler, options?)

Creates an HTTP server with graceful lifecycle management. Starts listening after optional startup hooks and handles SIGTERM/SIGINT with connection draining.

import createRouter, {graceful} from '@centralping/ergo-router';

const router = createRouter({...});
const {server, shutdown} = await graceful(router.handle(), {
  port: 3000,
  onStartup: async ({log}) => { /* connect DB, etc. */ },
  onShutdown: async ({log, signal}) => { /* cleanup */ }
});

Note: The promise returned by graceful() resolves after the server is already listening. Do not attach a 'listening' event listener to server after await graceful(...) — it will never fire because the event has already been emitted. The function logs Server listening on {hostname}:{port} upon successful bind.

OpenTelemetry SDK Lifecycle

When using tracing, manage the OTEL SDK via graceful() hooks:

import {NodeSDK} from '@opentelemetry/sdk-node';
import createRouter, {graceful} from '@centralping/ergo-router';

const sdk = new NodeSDK({
  /* exporters, instrumentations */
});

const router = createRouter({defaults: {tracing: true}});
// ... register routes ...

await graceful(router.handle(), {
  onStartup: async () => {
    sdk.start();
  },
  onShutdown: async () => {
    await sdk.shutdown();
  }
});

See the full API reference for detailed options and examples.

generateOpenAPI(router, options?)

Generates an OpenAPI 3.1 specification document from a router's registered routes. Available via the @centralping/ergo-router/openapi sub-path export.

import createRouter from '@centralping/ergo-router';
import generateOpenAPI from '@centralping/ergo-router/openapi';

const router = createRouter({
  defaults: {
    accepts: {types: ['application/json']},
    authorization: {strategies: [{type: 'Bearer', authorizer: verifyToken}]}
  }
});

router.get('/users/:id', {
  openapi: {
    summary: 'Get user by ID',
    tags: ['Users'],
    responses: {
      200: {description: 'User found'},
      404: {description: 'User not found'}
    }
  },
  validate: {params: {type: 'object', properties: {id: {type: 'string', format: 'uuid'}}}},
  execute: (req, res, acc) => ({response: {body: {id: acc.route.params.id}}})
});

const spec = generateOpenAPI(router, {
  title: 'My API',
  version: '1.0.0',
  description: 'REST API with OpenAPI spec',
  servers: [{url: 'https://api.example.com'}]
});
// spec is a valid OpenAPI 3.1 document

The generator automatically extracts:

  • Path parameters from :param patterns (with schema from validate.params)
  • Query parameters from validate.query
  • Request body from validate.body (POST/PUT/PATCH only)
  • Security schemes from authorization.strategies
  • Content types from accepts.types

Config keys are resolved against router.defaults using the same precedence as the pipeline builder (route > defaults > omitted).

Route Table

router.routeTable() returns a formatted string summarizing all registered routes, enabled middleware, and transport configuration. Designed for startup logging:

import createRouter, {graceful} from '@centralping/ergo-router';

const router = createRouter({
  transport: {requestId: {}, security: {}, cors: {origin: 'https://myapp.com'}},
  defaults: {accepts: {types: ['application/json']}, timeout: {ms: 30000}}
});

router.get('/users/:id', {authorization: true, execute: (req, res, acc) => ({response: {body: {id: acc.route.params.id}}})});
router.post('/users', {validate: {body: {type: 'object'}}, execute: (req, res, acc) => ({response: {body: acc.body.parsed}})});

const {server} = await graceful(router.handle(), {
  onStartup: ({log}) => {
    log.info(router.routeTable());
  }
});

Output:

Routes:
  GET  /users/:id  [accepts, url, authorization, timeout]
  POST /users      [accepts, body, validate, timeout]

Transport:
  requestId: enabled
  security: enabled
  rateLimit: disabled
  cors: enabled (origin: https://myapp.com)

Non-declarative routes (raw function or array handlers) are shown as (custom). Methods are right-padded for visual alignment.

Standards Compliance

| RFC / Standard | Description | ergo-router Feature | | ------------------------------------------------------------------- | ------------------------------ | ----------------------------------------- | | RFC 9110 | HTTP Semantics | 405+Allow, HEAD/OPTIONS/PATCH enforcement | | RFC 9457 | Problem Details for HTTP APIs | Structured error responses | | RFC 6797 | HTTP Strict Transport Security | Transport security headers | | RFC 6585 | Additional HTTP Status Codes | Rate limiting (429) | | Fetch Standard | CORS Protocol | Transport CORS handling | | OpenAPI 3.1 | API Description Format | Route-based spec generation |

Documentation

Development

npm install
npm test            # lint + format check + tests with coverage
npm run test:watch  # watch mode
npm run lint        # ESLint
npm run format      # Prettier

License

MIT © 2019-present Jason Cust