This page gets you from a fresh clone to a verified change. Express has no build step, no transpiler, and no code generation: the JavaScript in lib/ is exactly what ships to npm, so the edit-test loop is seconds long.
Setup
You need Node.js 18 or newer ("engines": { "node": ">= 18" } in package.json). Then:
git clone https://github.com/expressjs/express.git
cd express
npm install
npm test
npm test runs the whole suite. The scripts defined in package.json:
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "mocha --require test/support/env --reporter spec --check-leaks test/ test/acceptance/",
"test-ci": "nyc --exclude examples --exclude test --exclude benchmarks --reporter=lcovonly --reporter=text npm test",
"test-cov": "nyc --exclude examples --exclude test --exclude benchmarks --reporter=html --reporter=text npm test",
"test-tap": "mocha --require test/support/env --reporter tap --check-leaks test/ test/acceptance/"
}
Note the --require test/support/env in the test command -- it forces NODE_ENV=test and silences deprecation noise before any test loads:
process.env.NODE_ENV = 'test';
process.env.NO_DEPRECATION = 'body-parser,express';
To run a single test file while iterating, use mocha directly with the same flag:
npx mocha --require test/support/env test/res.send.js
Repo tour
index.js-- one line, re-exportslib/express.js.lib/-- the entire runtime, six files. Read them in this order:express.js(factory, 81 lines),application.js(app object),request.jsandresponse.js(prototype extensions),view.js(templates),utils.js(setting compilers).test/-- one file per API member, named after what it tests:res.send.js,req.query.js,app.use.js,Router.js,Route.js. Fixtures live intest/fixtures/, shared helpers intest/support/.test/acceptance/-- one test file per example app;test/acceptance/cookies.jsdrivesexamples/cookiesend to end.examples/-- 26 runnable apps, each a directory with anindex.js. Run any of them withnode examples/content-negotiation.benchmarks/,Charter.md,Code-Of-Conduct.md, etc. -- project governance and tooling, not runtime code.
Your first change
A low-risk, well-contained first contribution is an example plus its acceptance test, because the pair is self-verifying and touches no runtime code. Pick an example -- say examples/params/index.js -- and its mirror test/acceptance/params.js. Change behavior in the example, watch the acceptance test fail, update the test, watch it pass:
node examples/params # manual check on http://localhost:3000
npx mocha --require test/support/env test/acceptance/params.js
For a runtime change, the same loop applies with the matching unit file. Every public member of req, res, and app has a dedicated test file, so the blast radius of an edit in lib/response.js is visible by running test/res.*.js.
Before opening a PR:
npm run lint
npm test
The test command runs with --check-leaks, so a test that leaks globals fails the suite even if its assertions pass.
Where the tests are the specification
Express's behavioral guarantees live in test/, not in prose. When you need to know what res.send promises about ETags, or when a 304 strips the body, the answer is an it(...) title in test/res.send.js. The testing page shows how those tests are built and how to write one.