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

@krknet/http-server

v3.0.0

Published

Express wrapper with API and pages routers, a loading page, request logging, and graceful shutdown

Readme

@krknet/http-server

Express wrapper with API and pages routers, a loading page, request logging, and graceful shutdown.

Installation

npm install @krknet/http-server @krknet/profiler

Requires Node.js 22.12.0 or later. @krknet/profiler is a peer dependency used for lifecycle logging.

Upgrading from 2.x? See the Upgrade Guide to v3.

Quick Start

const HTTPserver = require('@krknet/http-server')

class ApiRouter {
  constructor (logger) {
    this.router = new HTTPserver.Router()
    if (logger) this.router.use(logger)
    this.router.get('/status', (req, res) => res.json({ ok: true }))
  }

  getRouter () { return this.router }
}

const httpServer = new HTTPserver({ port: 8080, ApiRouter })

await httpServer.start() // GET http://127.0.0.1:8080/api/status
await httpServer.stop()

Public API

Options

new HTTPserver({
  ApiRouter, // ApiRouter class, mounted under /api/ - required
  PagesRouter, // PagesRouter class, mounted under /
  port: 8080, // 0 picks a random free port
  host: '127.0.0.1',
  loadingPage: null, // Absolute path of a page served under / until the PagesRouter has started
  doLog: true, // Lifecycle logging through @krknet/profiler
  doLogAPI: false, // Passes a morgan request logger to the ApiRouter
  doLogFrontend: false, // Passes a morgan request logger to the PagesRouter
  bodyParserOptions: {}, // Options for express.json()
  urlencodedOptions: {}, // Options for express.urlencoded(), merged over { extended: true }
  queryParser: 'extended', // Express 'query parser' setting; 'extended' parses ids[]=a and a[b]=1 as in Express 4, 'simple' is the Express 5 default
  helmetOptions: {}, // Options for helmet, merged over { contentSecurityPolicy: false, crossOriginResourcePolicy: false }
  trustProxy: false, // Express 'trust proxy' setting: true, hop count, subnet list or function
  doTrustProxy: false, // Alias for trustProxy: true
  stopTimeout: 10000, // Milliseconds open requests get to finish on stop()
  isDevelopment: global.__environment === 'development', // Passed to the PagesRouter as isHotReloading
  eventBus: global.__eventBus, // Receives system events, see below
  onStart: async app => {}, // Awaited before the routers are mounted
  onBeforeErrorHandlers: async app => {}, // Awaited after the routers, before the error handlers
  requestHandlers: [], // Handler or array, mounted first, e.g. a Sentry request handler
  errorHandlers: [], // Handler or array, mounted last, e.g. your error handlers
  initialMiddlewares: [], // { route: '/', handler } - before the body parsers and helmet
  middlewares: [], // { route: '/', handler } - after the body parsers and helmet
  statics: [] // { route: '/static/', path: path.join(global.__basePath, 'static'), options: {} } - options for express.static, e.g. { dotfiles: 'allow' }
})

Middleware order:

  1. Stable req.query (see below)
  2. requestHandlers
  3. initialMiddlewares
  4. express.json(), express.urlencoded(), req.body default (see below), helmet
  5. middlewares
  6. statics
  7. onStart(app)
  8. ApiRouter under /api/, then the PagesRouter (or loading page) under /
  9. onBeforeErrorHandlers(app)
  10. errorHandlers

Steps 1 to 6 run in the constructor, so they also apply when you mount on httpServer.app and call httpServer.rawServer.listen() without start(). Steps 7 to 10 run in start().

Deprecated options

sentryRequests and sentryErrors are aliases for requestHandlers and errorHandlers until 4.0. They accept the same values. If the new name is set (anything but undefined), it wins, so errorHandlers: [] mounts no error handlers even if sentryErrors is set.

Express 4 compatibility

  • req.query is parsed once per request and stored as a plain, writable property, as in Express 4. Mutations, deletions and assignment persist into later middlewares and handlers. (Plain Express 5 parses on every read, so mutations disappear and assignment throws.)
  • req.body is {} when no body was parsed, as in Express 4. Only undefined is replaced: bodies parsed as null, false or 0 (with bodyParserOptions: { strict: false }) are kept. initialMiddlewares run before the body parsers and still see undefined.

Methods and Properties

  • await httpServer.start(): mounts the routers, listens and starts the PagesRouter. Rejects on listen errors such as EADDRINUSE, and when called a second time (one attempt per instance). If the PagesRouter fails to start, it is stopped and the HTTP server closed before start() rejects.
  • await httpServer.stop(): stops the PagesRouter and shuts down gracefully. After stopTimeout, open requests are terminated. Waits for an in-flight start(), and concurrent calls share one shutdown. The HTTP server is closed even if PagesRouter.stop() rejects; that error is rethrown afterwards.
  • httpServer.app: the Express app
  • httpServer.rawServer: the underlying http.Server, e.g. for socket servers
  • httpServer.address: the bound address ({ address, family, port }), or null while not listening
  • httpServer.options: the effective, frozen settings derived from the constructor options (some keys are renamed, e.g. bodyParser, helmet; handlers, middlewares and statics aren't included)
  • httpServer.isAPIRunning / httpServer.isFrontendRunning
  • httpServer.hasEventbus
  • httpServer.apiRouter / httpServer.pagesRouter: the router instances
  • HTTPserver.Router: express.Router

ApiRouter

class ApiRouter {
  constructor (logger) {} // morgan logger if doLogAPI is enabled, otherwise false
  getRouter () {} // returns an Express router, mounted under /api/
}

PagesRouter

class PagesRouter {
  constructor ({ logger, isHotReloading, httpServer }) {}
  async start (setRouter) {} // call setRouter(router) to mount a router under /, again on every reload
  async stop () {} // optional, awaited by httpServer.stop() and when start() rolls back
}

The loadingPage is served under / until start() has resolved.

Events

If an event bus is available (eventBus option or global.__eventBus), the server emits:

eventBus.emit('system', 'api', 'up' | 'down')
eventBus.emit('system', 'frontend', 'up' | 'down')

Development

Requires Node.js 22.12 or later, at runtime (engines) and for linting and tests (devEngines).

npm test
npm run lint

Notes

  • doLogAPI / doLogFrontend request logs skip OPTIONS requests and only use terminal control codes when stdout is a TTY.
  • For tests, use port: 0 and doLog: false and read the port from httpServer.address.port.
  • helmetOptions only needs your overrides, e.g. { xFrameOptions: false }; set contentSecurityPolicy or crossOriginResourcePolicy to re-enable those headers. helmet 8 throws (and so does the constructor) if both frameguard and its alias xFrameOptions are given.

Upgrade Guide to v3

3.0 moves to Express 5 and helmet 8. It keeps the Express 4 behavior for query parsing, req.query and req.body, so most changes are route strings and option names.

Node.js

Node.js >=22.12.0 is required.

Route strings

Express 5 uses path-to-regexp v8. *, ?, +, (...) and inline regex such as :id(\d+) are no longer valid in route strings and throw when the route is registered, i.e. at startup.

| Express 4 (http-server 2.x) | Express 5 (http-server 3.x) | Notes | |---|---|---| | '*' | '/{*splat}' | Matches / and every path. '/*splat' does not match /. | | '/kpis/*' | '/kpis/{*splat}' | Matches /kpis/ and deeper paths such as /kpis/a/b, like the old route. Neither matches /kpis. '/kpis/*splat' would stop matching /kpis/. | | ':type?/:printerID?/' | '{/:type}{/:printerID}/' | e.g. '/print/:type?/:printerID?/' → '/print{/:type}{/:printerID}/' matches /print/, /print/a/ and /print/a/b/. | | '/(:z)-(:x)-(:y).png' | '/:z-:x-:y.png' | The new form also works on Express 4. | | '/preview.(:extension)' | '/preview.:extension' | The old form already throws on Express 4.22 (http-server 2.4.0); the new form works on both. | | '/:zone([a-zA-Z0-9]+)/' | /^\/(?<zone>[a-zA-Z0-9]+)\/?$/ | Use a RegExp route with named groups (they become req.params.zone), or '/:zone/' with validation in the handler. If the constraint separated two routes (e.g. /:zone/ and /:location/), check the route precedence. |

  • The new syntax ({…}, *name) is Express 5 only: Express 4 treats it literally and returns 404. Ship the route fixes together with the 3.0 upgrade.
  • Wildcard params are named and are arrays: '/{*splat}' gives req.params.splat = ['a', 'b'] for /a/b, and no splat for /.
  • Route strings in page manifests and redirect JSON need the same changes. Rebuild generated route files (e.g. pages.json).

Requests and responses

  • req.query stays as in Express 4: queryParser: 'extended' is the default (ids[]=a gives an array, a[b]=1 an object), and req.query is one stable, writable object per request. Routers mounted on httpServer.app get this too. Code that creates its own Express 5 app outside http-server gets the Express 5 defaults: the simple parser and a req.query getter, so mutations disappear and assignment throws.
  • req.body defaults to {} only when no body was parsed. Bodies parsed as null, false or 0 (with bodyParserOptions: { strict: false }) are kept.
  • res.status() throws for anything but an integer from 100 to 999, including numeric strings such as '404', which Express 4 accepted. Error handlers that pass err.status through should guard it: Number.isInteger(s) && s >= 400 && s < 600 ? s : 500.
  • Async handlers: rejected promises and errors thrown in async handlers now reach the error handlers (on Express 4 the request hung and the rejection was unhandled). asyncMiddleware-style wrappers are redundant. Handlers that respond first and then throw now end up in the error handler too: the error is captured, but the response has already been sent.
  • Double slashes: a double slash right after a mount point (/api//x) returns 404.
  • body-parser 2: req._body is no longer set, and brotli (content-encoding: br) request bodies are decompressed instead of rejected.
  • Dot-files: express.static ignores dot-files and dot-directories. Pass options: { dotfiles: 'allow' } on a statics entry to serve e.g. .well-known/. res.sendFile returns 404 if the absolute path contains a dot-directory. The loadingPage keeps working, but your own res.sendFile calls (e.g. a 404 page) need { dotfiles: 'allow' } for trusted paths under a dot-directory.
  • Removed in Express 5: res.redirect('back') (use req.get('Referrer') || '/'), req.param(), res.send(status) (use res.sendStatus(status)), res.json(obj, status) (use res.status(status).json(obj)).

Options

  • sentryErrors → errorHandlers, sentryRequests → requestHandlers. Both take a handler or an array. The old names still work until 4.0. If the new name is set, it wins: an explicit errorHandlers: [] overrides sentryErrors.
  • errorHandlers, like the routers, are only mounted by start(). If you mount on httpServer.app and call rawServer.listen() yourself, mount your error handlers yourself.
  • helmetOptions are merged with the defaults { contentSecurityPolicy: false, crossOriginResourcePolicy: false }, so they only need your overrides (e.g. { frameguard: false }). helmet 8 throws if both frameguard and its alias xFrameOptions are given.
  • helmet 8 raises the HSTS max-age from 180 to 365 days and validates CSP directives more strictly.

Logging

The startup logs HTTP-Server started - :port once. The API running - :port/api/ line is gone; update anything that greps for it.

Sentry

  • Tested versions: @sentry/node 10.70.0 and 11.4.0. Other versions are untested. v5–v7 (Sentry.Handlers.*) must be upgraded before or together with 3.0.
  • Initialization order: call Sentry.init() before requiring @krknet/http-server. The Express instrumentation patches Express when it is loaded.
  • Tracing: comes from Sentry's Express integration, configured in Sentry.init() (the default integrations, or expressIntegration() explicitly).
  • Error capture depends on the SDK version:
    • v10: errors are only captured if you set it up. Call Sentry.setupExpressErrorHandler(app) in onBeforeErrorHandlers, or call Sentry.captureException in your own errorHandlers.
    • v11: the Express integration captures 5xx errors and errors without a status automatically. setupExpressErrorHandler isn't needed anymore and is deprecated, so remove it. To capture errors yourself instead, use expressIntegration({ shouldHandleError: false }) and call captureException in your errorHandlers. Without that option, the automatic event wins and your own call is dropped. Don't combine the option with setupExpressErrorHandler, which then captures nothing.