Compatibility with Express 5 โ
In general, basically all features and options are supported. Use the Express 5.x documentation for API reference. Anything Express 5 removed is removed here too, so the list below covers only where this differs from Express 5 itself.
โ
- Full support (all features and options are supported)
๐ง - Partial support (some options are not supported)
โ - Not supported
express โ
- โ express()
- โ express.Router()
- โ express.json()
- โ express.urlencoded()
- โ express.static()
- โ options.index, options.redirect, options.fallthrough, options.extensions
- โ
options.dotfiles, plus
"ignore_files", which is Fulmine's own: it hides a dotfile that is the last segment while letting a dotted directory through
- โ
options.dotfiles, plus
- โ options.setHeaders, options.headers
- โ options.etag, options.lastModified, options.maxAge, options.immutable, options.cacheControl, options.acceptRanges
- โ
options.preCompressed, Fulmine's own: serve the
.bror.gztwin on disk, described under Performance tips
- โ
options.preCompressed, Fulmine's own: serve the
- โ express.text()
- โ express.raw()
- โ express.serverTiming(). Fulmine's own: Server-Timing carrying how the request was routed, described under Performance tips.
- โ
express.testing. Fulmine's own:
expectNative,expectDeclarative,routeReport,expectLazyandworkReport, described under Performance tips. - โ express.compression(). Fulmine's own, since Express has none: it is the compression module's options and behaviour built in, described under Performance tips.
- ๐ง express.request (this is not a constructor but a prototype for replacing methods)
- ๐ง express.response (this is not a constructor but a prototype for replacing methods)
- ๐ง express.application (likewise: a method added here is on every app)
- โ
express.isFulmine,
truehere and undefined on Express, for code that runs on either - โ
express.Route. Both
app.route("/path").get(...).post(...)and the class itself, for building a route by hand and dispatching to it.
Application โ
- โ app.listen(port[, host][, callback])
- โ app.listen(unix_socket[, callback])
- โ app.METHOD() (app.get, app.post, etc.)
- โ app.route()
- โ
app.router, and its
stackof layers - โ app.all()
- โ app.use()
- โ app.mountpath
- โ app.set()
- โ app.get()
- โ app.enable()
- โ app.disable()
- โ app.enabled()
- โ app.disabled()
- โ app.path()
- โ app.param(name, callback)
- โ app.engine()
- โ app.render()
- โ app.locals
- โ app.settings
- โ app.engines
- โ app.on("mount")
- โ HEAD method
- โ OPTIONS method
- โ QUERY method
What listen() hands back is the app, and it answers as an http.Server so the shutdown wrappers recognise it: app.close(), app.address(), app.listening, app.getConnections(), app.ref(), app.unref(), app.setTimeout() and the keepAliveTimeout family. See Differences from Express for what is behind them and what is not.
Application settings โ
- โ case sensitive routing
- โ env
- โ etag
- โ jsonp callback name
- โ json escape
- โ json replacer
- โ json spaces
- โ query parser
- โ strict routing
- โ subdomain offset
- โ trust proxy
- โ views
- โ view cache
- โ view engine
- โ x-powered-by
Three of these keep a compiled form alongside the value, which you can also set directly:
etag fn, the function that produces an ETag. Settingetagcompiles one; setting this replaces it.query parser fn, likewise forquery parser.trust proxy fn, likewise fortrust proxy.
Fulmine adds nine of its own:
body methods, unset by default. The body is read for POST, PUT, PATCH and QUERY, and this names the methods to read one for as well:app.set("body methods", ["DELETE"]). Reading a body no handler asks for costs about 15%, which is why the built-in list is short rather than every method.native routes, on by default. Off, every request walks the ordinary chain instead of letting ยตWS match what it can, which is slower and answers the same. It is a diagnostic rather than a tuning knob: it exists so one application can be served both ways and the two sets of answers compared, which is how the optimizer is tested. A compiled response needs a native registration to hang on, so turning this off turnsdeclarative responsesoff with it.etag methods, unset by default. Express computes the generated ETag for every method, and so does this until told otherwise.app.set("etag methods", ["GET", "HEAD"])skips the digest on every other method, where freshness is not defined and the validator can never match: worth 21% here on a 4KB POST answer. An ETag set by hand still goes out whatever the method.declarative responses, on by default. Lets a simple enough handler be compiled into a native uWS response, described under Performance tips.declarative request values, off by default. Lets a compiled response carry a piece of the request,res.send(req.params.id), written by uWS as it reads it and not as Express does; what changes is listed under the same tips.connection headers, on by default. Express sendsConnection: keep-aliveandKeep-Aliveon every response, and so does this. Turn it off and neither goes out, while a connection the client asked to close still answersConnection: close: it is the advertisement that goes, not the truth. Worth 2% to 3.5% here on a route that is not compiled, plus the bytes.file cache, on by default. Small files served byres.sendFilecome from a bounded in-process cache, checked against the file'sstaton every request, so an edited file is never served stale. Turn it off where every request has to reach the disk, which is what a public benchmark asks of a standard entry: it was worth about 4% on a 4KB file here, so the cost of turning it off is small.stat cache, off by default. Takes a duration,app.set("stat cache", "1s"). The size and mtime of a file served byres.sendFileorexpress.staticare remembered for that long, so a file that is asked for again inside the window costs no syscall at all. It was worth 15% on a 3KB file and 3% on a 200KB one, where the bytes are the work. What it costs is the one promise thefile cachekeeps: inside the window an edited file is served as it was, so keep the window shorter than you would notice.trust proxy protocol, off by default. Takesreq.ipfrom a PROXY protocol preamble, described under Behind a proxy. Read the warning there before turning it on.
Request โ
- โ implements Readable stream
- โ req.app
- โ req.baseUrl
- โ req.body
- โ req.cookies
- โ req.fresh
- โ req.hostname
- โ req.host
- โ req.header
- โ req.headers
- โ req.headersDistinct
- โ req.rawHeaders
- โ req.ip
- โ req.ips
- โ req.method
- โ req.url
- โ req.originalUrl
- โ req.params
- โ req.path
- โ req.protocol
- โ req.query
- โ req.res
- โ req.secure
- โ req.signedCookies
- โ req.stale
- โ req.subdomains
- โ req.xhr
- ๐ง req.route (route implementation is different from Express)
- ๐ง req.connection, req.socket (a stand-in, not a
net.Socket: the remote and local address and port,encrypted,address(),end(),write(),destroy(),pause()andresume()work;setTimeout(),setKeepAlive()andsetNoDelay()do nothing) - โ req.accepts()
- โ req.acceptsCharsets()
- โ req.acceptsEncodings()
- โ req.acceptsLanguages()
- โ req.get()
- โ req.is()
- โ req.range()
Response โ
- โ implements Writable stream
- โ res.app
- โ res.headersSent
- โ res.req
- โ res.locals
- โ res.append()
- โ res.attachment()
- โ res.cookie()
- โ res.clearCookie()
- โ res.download()
- โ res.end()
- โ res.format()
- โ res.getHeader(), res.get()
- โ res.json()
- โ res.jsonp()
- โ res.links()
- โ res.location()
- โ res.redirect()
- โ res.render()
- โ res.send()
- โ res.sendFile()
- โ options.maxAge
- โ options.root
- โ options.lastModified
- โ options.headers
- โ options.dotfiles
- โ options.acceptRanges
- โ options.cacheControl
- โ options.immutable
- โ Range header
- โ Setting ETag header
- โ If-Match header
- โ If-Modified-Since header
- โ If-Unmodified-Since header
- โ If-Range header
- โ res.sendStatus()
- โ res.header(), res.setHeader(), res.set()
- โ res.status()
- โ res.type()
- โ res.vary()
- โ res.removeHeader()
- โ res.write()
- โ res.writeHead()
- โ res.flushHeaders()
Router โ
- โ router.all()
- โ router.METHOD() (router.get, router.post, etc.)
- โ router.route()
- โ router.use()
- โ router.param(name, callback)
- โ options.caseSensitive
- โ options.strict
- โ options.mergeParams
- โ
the
express.router.requestTracingChannel ofnode:diagnostics_channel, as Express's router publishes it:start,end,asyncStart,asyncEndanderrorfor every middleware, route handler and error handler, with{ req, res, layer, error, errorHandler }, on ยตWS and throughhttp.createServer(app). Subscribe beforelisten(), as APMs ask anyway: a route ยตWS can answer on its own is compiled atlisten(), and while something listens at that moment none is, so every handler runs. One difference: an error a mounted router raises while matching, such as a parameter that does not decode, comes out after the router'sendinstead of inside it. - โ
node:http's
http.server.request.start,http.server.response.createdandhttp.server.response.finishchannels on ยตWS too, as node publishes them throughhttp.createServer(app): the server is the applisten()returns, and the request goes through itsemit("request"), where a tracer that wraps it (Sentry) opens its scope.
Tested middlewares โ
Almost all middlewares that are compatible with Express are compatible with Fulmine. Here's list of middlewares that we test for compatibility:
- โ express-fast-json-stringify
- โ
socket.io (via
io.attachApp(app.uwsApp), see WebSockets) - โ
body-parser (use
express.text()etc instead for better performance) - โ cookie-parser
- โ cookie-session
- โ
compression (use
express.compression()instead for better performance) - โ
serve-static (use
express.static()instead for better performance) - โ serve-index
- โ cors
- โ errorhandler
- โ method-override
- โ multer
- โ response-time
- โ express-fileupload
- โ express-session
- โ express-rate-limit
- โ express-subdomain
- โ vhost
- โ http-proxy-middleware
- โ express-http-proxy
- โ express-mongo-sanitize
- โ helmet
- โ passport
- โ morgan
- โ swagger-ui-express
- โ graphql-http
- โ better-sse
- โ supertest
- โ express-basic-auth
- โ express-validator
- โ serve-favicon
- โ on-finished and on-headers
Tested frameworks โ
The list above is middlewares. A framework built on Express is a much larger user of the Express surface than any application is, so those have a suite of their own, in integrations/: the same application served twice, once on Express and once here, with the two outputs compared byte for byte. The four that render pages, and the tsoa routes, are built first by that suite, so what is compared is what their own build produces.
- โ
NestJS through
fulmine.js/nest - โ
Next.js as a custom server,
next().getRequestHandler() - โ
Astro through
@astrojs/nodein middleware mode - โ
SvelteKit through
@sveltejs/adapter-node - โ
React Router v8 through
@react-router/express - โ
Apollo Server through
@as-integrations/express5 - โ
tRPC through
@trpc/server/adapters/express - โ
MCP through
@modelcontextprotocol/sdkon the Streamable HTTP transport, with the body read off the stream or handed over byexpress.json(). Runnable:examples/mcp.js - โ
tsoa: the routes
tsoa spec-and-routesgenerates from a decorated controller, registered withRegisterRoutes(app), validation errors included
Angular SSR works too but is not in that suite: it is an ordinary Express server.ts plus one line of build configuration.
Each of these mounts on an ordinary Express application, so there is nothing to install and nothing to configure beyond what that framework already asks for. Nest is the exception, and only because its adapter decides what to listen on: that one is fulmine.js/nest.
Tested view engines โ
Any Express view engine should work. Here's list of engines we include in our test suite:
- โ ejs
- โ pug
- โ express-dot-engine
- โ express-art-template
- โ express-handlebars
- โ swig
- โ mustache-express