Express 24,430 ships almost nothing: a router, express.json, express.urlencoded, express.raw, express.text and express.static. Everything else is a package you choose, and each runs with full access to every request and response. Adding middleware widens your security perimeter.
| Package | Version | Direct deps | What it does |
|---|---|---|---|
| helmet 10,736 | 8.3.0 | 0 | Sets defensive response headers |
| cors 6,195 | 2.8.6 | 2 | Cross-origin request headers |
| morgan 8,204 | 1.12.1 | 5 | HTTP access logs |
| compression | 1.8.2 | 8 | gzip/Brotli response bodies |
| express-rate-limit 3,306 | 8.7.0 | 2 | Per-client request limits |
| express-session 6,354 | 1.19.0 | 8 | Server-side sessions |
Run five checks before npm i. Is it alive? Compare the latest publish date with the open issue count: cors is stable enough that its last release was January 2026, but an unreleased fix sitting in main for two years is a warning. Who maintains it? Packages under the expressjs organization on GitHub 29 are governed by the Express technical committee; a single-maintainer package with a million weekly downloads is a supply-chain risk whatever its code quality. How heavy is it? Every transitive dependency is another party with code in your process. Does it support Express 5? Packages that call req.param() or use Express 4 path syntax break. What does it touch? Read the source of anything handling bodies, cookies or credentials.
Run npm audit --omit=dev in continuous integration and npm outdated on a schedule. When a dependency is abandoned, the cheapest exit is the one you prepared for: mount it behind your own thin wrapper, app.use(accessLog()) rather than app.use(morgan('combined')), and replacing it touches one file.