@krknet/http-server
v3.0.0
Published
Express wrapper with API and pages routers, a loading page, request logging, and graceful shutdown
Maintainers
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/profilerRequires 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:
- Stable
req.query(see below) requestHandlersinitialMiddlewaresexpress.json(),express.urlencoded(),req.bodydefault (see below), helmetmiddlewaresstaticsonStart(app)- ApiRouter under
/api/, then the PagesRouter (or loading page) under/ onBeforeErrorHandlers(app)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.queryis 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.bodyis{}when no body was parsed, as in Express 4. Onlyundefinedis replaced: bodies parsed asnull,falseor0(withbodyParserOptions: { strict: false }) are kept.initialMiddlewaresrun before the body parsers and still seeundefined.
Methods and Properties
await httpServer.start(): mounts the routers, listens and starts the PagesRouter. Rejects on listen errors such asEADDRINUSE, and when called a second time (one attempt per instance). If the PagesRouter fails to start, it is stopped and the HTTP server closed beforestart()rejects.await httpServer.stop(): stops the PagesRouter and shuts down gracefully. AfterstopTimeout, open requests are terminated. Waits for an in-flightstart(), and concurrent calls share one shutdown. The HTTP server is closed even ifPagesRouter.stop()rejects; that error is rethrown afterwards.httpServer.app: the Express apphttpServer.rawServer: the underlyinghttp.Server, e.g. for socket servershttpServer.address: the bound address ({ address, family, port }), ornullwhile not listeninghttpServer.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.isFrontendRunninghttpServer.hasEventbushttpServer.apiRouter/httpServer.pagesRouter: the router instancesHTTPserver.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 lintNotes
doLogAPI/doLogFrontendrequest logs skipOPTIONSrequests and only use terminal control codes when stdout is a TTY.- For tests, use
port: 0anddoLog: falseand read the port fromhttpServer.address.port. helmetOptionsonly needs your overrides, e.g.{ xFrameOptions: false }; setcontentSecurityPolicyorcrossOriginResourcePolicyto re-enable those headers. helmet 8 throws (and so does the constructor) if bothframeguardand its aliasxFrameOptionsare 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}'givesreq.params.splat=['a', 'b']for/a/b, and nosplatfor/. - Route strings in page manifests and redirect JSON need the same changes. Rebuild generated route files (e.g.
pages.json).
Requests and responses
req.querystays as in Express 4:queryParser: 'extended'is the default (ids[]=agives an array,a[b]=1an object), andreq.queryis one stable, writable object per request. Routers mounted onhttpServer.appget this too. Code that creates its own Express 5 app outside http-server gets the Express 5 defaults: thesimpleparser and areq.querygetter, so mutations disappear and assignment throws.req.bodydefaults to{}only when no body was parsed. Bodies parsed asnull,falseor0(withbodyParserOptions: { 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 passerr.statusthrough 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._bodyis no longer set, and brotli (content-encoding: br) request bodies are decompressed instead of rejected. - Dot-files:
express.staticignores dot-files and dot-directories. Passoptions: { dotfiles: 'allow' }on astaticsentry to serve e.g..well-known/.res.sendFilereturns 404 if the absolute path contains a dot-directory. TheloadingPagekeeps working, but your ownres.sendFilecalls (e.g. a 404 page) need{ dotfiles: 'allow' }for trusted paths under a dot-directory. - Removed in Express 5:
res.redirect('back')(usereq.get('Referrer') || '/'),req.param(),res.send(status)(useres.sendStatus(status)),res.json(obj, status)(useres.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 expliciterrorHandlers: []overridessentryErrors.errorHandlers, like the routers, are only mounted bystart(). If you mount onhttpServer.appand callrawServer.listen()yourself, mount your error handlers yourself.helmetOptionsare merged with the defaults{ contentSecurityPolicy: false, crossOriginResourcePolicy: false }, so they only need your overrides (e.g.{ frameguard: false }). helmet 8 throws if bothframeguardand its aliasxFrameOptionsare given.- helmet 8 raises the HSTS
max-agefrom 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/node10.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, orexpressIntegration()explicitly). - Error capture depends on the SDK version:
- v10: errors are only captured if you set it up. Call
Sentry.setupExpressErrorHandler(app)inonBeforeErrorHandlers, or callSentry.captureExceptionin your ownerrorHandlers. - v11: the Express integration captures 5xx errors and errors without a status automatically.
setupExpressErrorHandlerisn't needed anymore and is deprecated, so remove it. To capture errors yourself instead, useexpressIntegration({ shouldHandleError: false })and callcaptureExceptionin yourerrorHandlers. Without that option, the automatic event wins and your own call is dropped. Don't combine the option withsetupExpressErrorHandler, which then captures nothing.
- v10: errors are only captured if you set it up. Call
