Skip to content

Latest commit

 

History

1,750 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Fulmine.js: the drop-in Express 5 replacement, up to 22x faster

Fulmine (lightning in Italian ⚡) is an Express 5 compatible web framework for Node.js, built on µWebSockets.js instead of node:http. Same API, same middleware, same tests. Change one line and your Express application runs faster.

Docs: fulmine.sndesign.it

const express = require("fulmine.js"); // instead of require("express")

npm version npm downloads Node.js 22 | 24 | 26 HTTP Arena Coverage Status CodeQL OpenSSF Scorecard OpenSSF Best Practices License

Why Fulmine

  • Faster than Express, measured. 1.2x to 4.5x on plain routing, 1.7x to 5x on a request with a body, 7x to 22x on a large route table. Routes are matched in C++ by µWS's own router, and a simple enough handler is answered without running any JavaScript at all.
  • Zero rewrite. helmet, cors, passport, morgan, multer, express-session and the rest of the Express ecosystem keep working. Not "mostly": every test runs against real Express first and the output must match byte for byte, and Express 5's own test suite passes whole.
  • Your framework works too. NestJS, Next.js, Astro, SvelteKit, React Router, Angular SSR, Apollo Server, tRPC, tsoa, MCP servers: each one is served twice in CI, on Express and on Fulmine, and compared.
  • Ranked in public. See HttpArena and web-frameworks, run on their hardware with their rules. No figure is copied here, the boards are the current ones.
  • More than Express, when you want it. Multi-core cluster on one port, native WebSockets, built-in compression and pre-compressed static files, Server-Timing, PROXY protocol, TLS. All optional.
  • Typed, TypeScript first. ESM, CommonJS, named imports and the Express types you already use.

Quick start

npx fulmine.js create my-app   # a server, a package.json and a Dockerfile that works, --ts for TypeScript
cd my-app && npm install && npm run dev

Or in a project you already have:

npm install fulmine.js
const express = require("fulmine.js");
const app = express();

app.use(express.json());
app.get("/users/:id", (req, res) => res.json({ id: req.params.id }));

app.listen(3000);

ESM and TypeScript work the same way, named imports included:

import express, { Router, json } from "fulmine.js";
import type { Request, Response } from "fulmine.js";

Requirements: Node 22, 24 or 26, on Linux, macOS or Windows, x64 or arm64. Not Alpine (glibc 2.38+ is needed) and not Bun; pnpm refuses the install (ERR_PNPM_EXOTIC_SUBDEP on uWebSockets.js) until one command is run. npx fulmine.js verify tells you in thirty seconds whether this machine, your package manager and your Docker image can run it, and Deploying has the Dockerfile that works.

Migrate an existing Express app

One command rewrites the imports across a whole project, and then tells you the handful of things that behave differently:

npx fulmine.js verify              # can this machine and this image even run it
npx fulmine.js migrate --dry-run   # say what it would change, change nothing
npx fulmine.js migrate             # do it
npx fulmine.js override            # when a framework requires express in its own code, not in yours
npx fulmine.js angular             # angular.json's server build, one line of config
npx fulmine.js pnpm                # the two lines a pnpm project needs before it installs this
npx fulmine.js differences         # just the list of what to check by hand

NestJS is one import, FulmineExpressAdapter from fulmine.js/nest. Angular SSR's server.ts is an ordinary Express application and takes the one-line change. A framework that requires Express in its own code, not in yours, is answered with a package manager override, and override writes it. The whole guide: Migrating.

Where the speed comes from

Express finds a route by walking its stack and testing each layer against the path, on every request. Fulmine hands every route it can to µWS's router, which matches in C++, and works out at listen() which middlewares stand in front of each one. Arriving at a handler costs no matching at all, and the gap grows with the route table instead of shrinking.

On top of that, most of what makes a request expensive is work that simply does not happen: the body is not read unless a handler asks, the headers are not copied out of µWS unless something reads them, the request is not turned into a stream unless something streams it. A handler simple enough to be read at registration time is compiled into a static response and answered by µWS itself.

npx fulmine.js profile prints what listen() decided about each of your routes, and npx fulmine.js explain /api/items tells the story of one request. Ten measured tips, from express.compression() to cluster: "auto", are in Performance.

Beyond Express

Everything Express does, plus these. Each one is a runnable file in examples/.

Feature One line
One process per core, one port, no primary in the path express({ cluster: "auto" })
Native WebSockets, with an upgrade(req, res) hook for auth app.ws("/room/:id", { open, message, close })
socket.io io.attachApp(app.uwsApp)
Compression built in, 50% more requests per second than compression app.use(express.compression())
Serve the .br and .gz twins your build already wrote express.static(dir, { preCompressed: true })
Server-Timing with how the request was routed app.use(express.serverTiming())
Assert in a test that a route stays on the fast path express.testing.expectNative(app, ["/api/*"])
HTTPS without https.createServer express({ uwsOptions: { key_file_name, cert_file_name } })
PROXY protocol from HAProxy, AWS NLB, nginx, Envoy app.set("trust proxy protocol", true)

Details in WebSockets, Performance and Deploying.

Compatibility

Use the Express 5 documentation as the reference: the application, request, response and router APIs are all there, settings included. The full checklist, the tested middlewares, frameworks and view engines, and the eight settings Fulmine adds, are in Compatibility.

A few things answer differently because there is no node:http underneath: app.listen() returns the app, which also answers as an http.Server; TLS is configured through express(); the body is read for POST, PUT, PATCH and QUERY unless told otherwise; x-powered-by is off. The complete list, with the reason for each: Differences from Express.

Compared with similar projects

  • ultimate-express is what Fulmine is derived from, and is the closest relative by far. It targets Express 4, keeps the v4 API surface and its deprecations. Fulmine targets Express 5 only, which removes the compatibility layer for everything v5 dropped, and is typed. If you are on Express 4, use ultimate-express.
  • hyper-express has a similar API but is not a drop-in replacement. It implements much of the functionality differently, which produces quirks that make switching an existing application difficult, and most Express middleware is unsupported.
  • uwebsockets-express is closer to a drop-in replacement, but misses a lot of the API, depends on Express by calling its methods under the hood, and does not use the native µWS router.
  • express on Bun benefits from Bun using µWS for its HTTP module, but performs no µWS-specific optimizations.

Documentation

Versioning

The major number tracks Express, not semver. Fulmine 5.x follows Express 5. If Express 6 arrives, Fulmine goes to 6, and that is the only reason the major ever moves. Minor is for new behaviour, patch for fixes, so a breaking change can land in a minor: it is in the changelog under its own heading, but the version number alone will not warn you. If you pin, pin the minor.

License

Apache-2.0, with the credits and the other licences in Attribution. Found something exploitable? Report it privately, see SECURITY.md.

Releases

Sponsor this project

Contributors

Languages