Express.js Cheatsheet - Express Middleware & API Reference
This reference is for Node.js developers building REST APIs and web apps on Express, from a small internal service to a full EJS-rendered site. It covers the app/router setup, defining routes with params/query, reading the request and shaping the response, wiring the middleware ecosystem (express.json, static, cors, helmet, morgan, compression), serving EJS views, adding JWT auth, and the async error-handling pattern. Unlike a generic Express snippet list, entries are grouped by the layer of the request lifecycle you are working on. After reading you should be able to stand up an app, mount routers and middleware in the right order, and centralize async error handling instead of scattering try/catch.
Typical Use Case
For developers building HTTP services in Node.js, especially anyone taking a JSON API with authentication from an empty directory to something deployable. The main value is ordering, because that is where Express bites: express.json() must be registered before routes, error-handling middleware must come last with exactly four parameters, and a throw inside an async handler does not reach the error middleware by default. Router for module splitting, JWT issuing and verification, and template engine mounting complete the chain.
App Setup 5
const app = express()app.use(express.json())app.listen(3000, () => {})const router = express.Router()app.set("port", process.env.PORT || 3000)Route Definition 7
app.get("/users", handler)app.post("/users", handler)app.put("/users/:id", handler)app.delete("/users/:id", handler)app.all("/api/*", handler)app.use("/api", router)router.route("/users").get(list).post(create)Request Object 6
req.params.idreq.query.pagereq.body.namereq.headers["content-type"]req.cookies.tokenreq.get("User-Agent")Response Object 6
res.send("hello")res.json({ data })res.status(201).json({ ok: true })res.redirect("/login")res.sendFile(path.join(__dirname, "index.html"))res.set("X-Custom", "value")Common Middleware 8
app.use((req, res, next) => { next() })express.urlencoded({ extended: true })app.use("/static", express.static("public"))cors()helmet()morgan("dev")cookie-parser()compression()Template Engines & JWT 6
app.set("view engine", "ejs")app.set("views", path.join(__dirname, "views"))res.render("index", { title: "Home" })jwt.sign(payload, secret, { expiresIn: "1h" })jwt.verify(token, secret)req.headers.authorizationError Handling & Patterns 7
const wrap = (fn) => (req, res, next) => fn(req, res, next).catch(next)app.use((req, res) => res.status(404).send("Not Found"))app.use((err, req, res, next) => res.status(500).json({ error: err.message }))next(err)process.on("uncaughtException", handler)process.on("unhandledRejection", handler)module.exports = routerParameter matrix
| Parameter | Effect | Example |
|---|---|---|
express() | Creates the application instance that every later setting hangs off | const app = express() |
express.json() | Parses JSON request bodies into req.body, and must be registered before the routes | app.use(express.json()) |
express.urlencoded({ extended: true }) | Parses HTML form bodies into req.body | app.use(express.urlencoded({ extended: true })) |
app.use(path, mw) | Mounts middleware or sub-routers under a prefix; every matching request passes through | app.use("/api", router) |
app.use(express.static("public")) | Serves a static directory without writing a route per file | app.use("/static", express.static("public")) |
app.set(key, value) | Sets application-level settings such as the port, template engine and view directory | app.set("views", path.join(__dirname, "views")) |
app.listen(port, cb) | Binds the port and starts the server | app.listen(3000, () => {}) |
express.Router() | Creates a router you can mount independently, which keeps modules separable | const router = express.Router() |
router.route(path).get().post() | Groups several methods on the same path into a single chain | router.route("/users").get(list).post(create) |
app.all("/api/*", mw) | Matches every HTTP method, the usual shape for auth and other cross-cutting pre-work | app.all("/api/*", auth) |
req.params.id | Reads a path parameter such as :id | app.put("/users/:id", handler) |
req.query.page | Reads a query string parameter, which always arrives as a string | const page = req.query.page |
req.get("User-Agent") | Reads a request header by name | const ua = req.get("User-Agent") |
req.cookies.token | Reads cookies, and requires cookie-parser to be mounted first | app.use(cookieParser()) |
res.send(str) | Sends a text response and ends the request | res.send("hello") |
res.json(obj) | Serialises to application/json, the workhorse for APIs | res.json({ data }) |
res.status(201).json(obj) | Sets the status code and returns JSON in one chained call | res.status(201).json({ ok: true }) |
res.redirect(path) | Sends a 302 redirect | res.redirect("/login") |
res.sendFile(absPath) | Streams a file from disk back to the client | res.sendFile(path.join(__dirname, "index.html")) |
res.set(key, value) | Sets a single response header | res.set("X-Custom", "value") |
res.render(view, data) | Renders a template and emits an HTML response | res.render("index", { title: "Home" }) |
helmet() | Sets HTTP security headers such as X-Content-Type-Options in bulk | app.use(helmet()) |
morgan("dev") | Trims request logging to a single line, which keeps debugging readable | app.use(morgan("dev")) |
cors() | Adds CORS headers; with a split frontend, allow-list the origins you actually need | app.use(cors()) |
compression() | Compresses the response body, which pays off on large JSON and text | app.use(compression()) |
jwt.sign(payload, secret, { expiresIn }) | Signs a JWT that carries an expiry | jwt.sign(payload, secret, { expiresIn: "1h" }) |
jwt.verify(token, secret) | Verifies and parses the token, throwing on a bad signature or an expired one | const data = jwt.verify(token, secret) |
req.headers.authorization | Pulls the raw Bearer token out of the header | const token = req.headers.authorization |
next(err) | Hands the error to the downstream error-handling middleware | next(err) |
app.use((err, req, res, next) => ...) | Global error handler; it needs all four parameters and must come last | app.use((err, req, res, next) => res.status(500).json({ error: err.message })) |
module.exports = router | Exports a router module for mounting with app.use | module.exports = router |
Common pitfalls
Symptomreq.body is undefined in a POST handler and touching req.body.name throws a TypeError.
Causeexpress.json() was mounted after the routes, or never mounted at all, so the body parser never ran.
FixHoist every app.use(express.json()) above all app.get and app.post calls; for form posts also add app.use(express.urlencoded({ extended: true })).
SymptomAfter throw new Error inside an async handler the request hangs until the client times out instead of returning 500.
CauseExpress 4 does not catch a rejected Promise returned by an async function, so the error never reaches next().
FixWrap the handler with const wrap = (fn) => (req, res, next) => fn(req, res, next).catch(next), or move to a version that catches automatically and keep every handler behind the wrapper.
SymptomA four-argument error handler is never called and the error bubbles up to the default one.
CauseThe middleware was registered before the routes, or its app.use sits earlier in the chain than the throw site; Express matches in registration order.
FixRegister the (err, req, res, next) middleware after every route and after the 404 fallback, as the final app.use.
SymptomA zero-argument middleware is not recognised and its body never runs.
CauseExpress identifies error handlers by arity (length === 4), and an arrow function written with one parameter is just ordinary middleware.
FixSpell out all four parameters, (err, req, res, next) => { ... }, even when next goes unused.
SymptomAfter a restart the process still holds port 3000 and reports EADDRINUSE.
CauseAn exception thrown in a script goes unhandled, so the Node process cannot exit cleanly and never releases the port.
FixAdd process.on("uncaughtException", handler) and process.on("unhandledRejection", handler) so shutdown is deliberate, then confirm no stray node process is holding the port.
Symptomreq.cookies.token is undefined while the token in the request header reads fine.
Causecookie-parser is not mounted, since Express does not parse cookies on its own.
FixAdd app.use(cookieParser()) ahead of any route that touches req.cookies.
SymptomEvery static asset 404s even though the directory plainly exists.
Causeexpress.static resolves relative to the process working directory, and a cwd that differs from the expectation hides the directory.
FixUse an absolute path, express.static(path.join(__dirname, "public")), so the launch directory no longer matters.
Troubleshooting
1Check middleware registration order (body parser before routes)
grep -n "app.use\|router\." server.jsThe line number of express.json() should be smaller than any app.get or app.post line, otherwise req.body is undefined.
2Confirm the error handler arity and that it sits last
grep -n "err, req, res, next" server.jsExactly one match is expected, and its app.use must come after every route in the file.
3Turn on the terse request log to see which routes actually matched
app.use(morgan("dev")); app.listen(3000, () => {})One line per request with method, URL and status, which immediately shows whether you fell through to the 404 handler.
4Locate uncaught exceptions and rejected promises at the process level
node --trace-uncaught server.jsCombine process.on("uncaughtException", handler) with process.on("unhandledRejection", handler) to print the full stack and find the request path that forgot next(err).
5Print the body and headers to confirm the parsing chain
app.use((req, res, next) => { console.log(req.body, req.headers["content-type"], req.get("User-Agent")); next() })The middleware must call next(), otherwise the request stalls at that layer.
6Validate a JWT
const data = jwt.verify(token, secret)A bad signature or an expired token throws; catch it in the auth middleware and call next(err) or res.status(401).json({ error: "unauthorized" }).
Command Examples
Minimal JSON API: body parsing, routes, and a 201 response
const express = require("express")
const app = express()
app.use(express.json())
app.get("/users/:id", (req, res) => {
res.json({ id: req.params.id })
})
app.post("/users", (req, res) => {
res.status(201).json({ ok: true, name: req.body.name })
})
app.listen(3000, () => console.log("up"))express.json() must be mounted before the routes, otherwise req.body on a POST is an empty object. GET reads req.params.id and POST reads req.body.name, matching the path-parameter and body entries in this sheet.
Split routes onto an /api prefix and chain methods per path
// routes/users.js
const router = express.Router()
router.route("/users")
.get((req, res) => res.json({ page: req.query.page }))
.post((req, res) => res.status(201).json(req.body))
module.exports = router
// app.js
app.use(express.json())
app.use("/api", router)router.route keeps every method for one path in a single place, so a method is less likely to be missed during edits. The child module only exports the router and the host app owns the prefix, so changing /api never means touching the child file.
Wrap async handlers and funnel errors into one error middleware
const wrap = (fn) => (req, res, next) => fn(req, res, next).catch(next)
app.get("/boom", wrap(async (req, res) => {
throw new Error("upstream failed")
}))
app.use((err, req, res, next) => {
if (res.headersSent) return next(err)
res.status(500).json({ error: err.message })
})Express does not catch a rejected promise returned by an async handler on its own. wrap routes the rejection into next(err); without it the request neither succeeds nor fails and the client hangs until timeout.
JWT login plus prefix-wide auth on /api
app.post("/login", (req, res) => {
const token = jwt.sign({ sub: req.body.name }, secret, { expiresIn: "1h" })
res.json({ token })
})
app.use("/api", (req, res, next) => {
try {
req.user = jwt.verify(req.headers.authorization.replace("Bearer ", ""), secret)
next()
} catch (e) {
res.status(401).json({ error: "unauthorized" })
}
})jwt.sign with expiresIn bounds the lifetime of the token and jwt.verify checks both signature and expiry. Mounting the middleware on the /api prefix protects every subroute at once instead of wrapping each handler.
Common Pitfalls
- The error handler must declare exactly four parameters (err, req, res, next): Express identifies it by arity, so (err, req, res) is treated as ordinary middleware and every server error turns into a silently hanging request.
- app.use order is match order. A 404 fallback, express.static, or the error middleware placed before your business routes swallows every request first, which looks like "all endpoints 404" with nothing in the log.
- express.json() only accepts application/json and caps the body at roughly 100kb. Using it for forms or uploads yields an empty body, and large uploads are rejected outright; use urlencoded or a dedicated multipart middleware.
- req.params.id and req.query.page are untrusted input. Interpolating them into SQL or filesystem paths is injection and directory traversal, so a single request can dump your database or read arbitrary files.
- Writing anything after res.send() / res.json(), or responding twice, throws Cannot set headers after they are sent. That error is a symptom; the real first exception usually sits on an earlier await.
- By default jwt.verify errors carry the token and signature details. Returning e.message straight to the client leaks key material; respond with a fixed 401 message instead.
Tips
- Middleware order matters: body-parsers must come before routes, and error middleware must be last.
- Error-handling middleware needs 4 args (err, req, res, next), or Express won't recognize it.
- In production, use helmet() for security headers and configure cors() whitelist as needed.
- express.static can serve multiple directories, searched in order.
- Routers modularize routes; mount sub-routes with app.use('/api', router).
FAQ
How should middleware be ordered, and why did req.body suddenly come back undefined?
An empty req.body is nearly always an ordering problem. app.use(express.json()) must be mounted before any route that reads the body, otherwise the route matches and runs while the body is still unparsed. Likewise req.cookies.token requires cookie-parser() first, and HTML form posts need app.use(express.urlencoded({ extended: true })) to decode application/x-www-form-urlencoded. At the other end, the error handler must come after every route and must declare all four parameters (err, req, res, next), or Express will not recognise it and async errors leave the request hanging. The working order is: parsers, then logging/security (morgan, helmet, cors, compression), then routes, then the 404 fallback, then the error handler.
Why do exceptions from async route handlers never reach my error middleware?
Express 4 does not automatically catch a rejected Promise returned by an async function; you need one of two bridges. Either use a wrapper — const wrap = (fn) => (req, res, next) => fn(req, res, next).catch(next) — which hands the rejection to next(err) for the global handler (err, req, res, next) => res.status(500).json({ error: err.message }) to render, and write routes as app.post("/users", wrap(async (req, res) => { ... })). Or catch explicitly with try/catch and call next(err). Map validation failures to a non-500 status inside the error handler rather than letting everything be a server error. Keep process.on("uncaughtException", handler) and process.on("unhandledRejection", handler) as the last process-level net.
Where do req.params, query, body, headers and cookies each come from?
They map onto five distinct parts of the request. req.params.id comes from the :id placeholder in the path: register app.put("/users/:id", handler) and /users/7 yields "7". req.query.page comes from the query string ?page=2, always a string, and is the usual source for paging and filters. req.body.name comes from the body and requires express.json() or urlencoded() to have parsed it first. req.headers["content-type"] reads a header to branch on content type or spot a token, with req.get("User-Agent") as the ergonomic form. req.cookies.token needs cookie-parser(). The common auth pattern is stripping "Bearer <token>" out of req.headers.authorization and calling jwt.verify(token, secret).
How do I choose between res.send, res.json, sendFile and redirect?
Choose by what the response carries. res.send("hello") emits text with the default Content-Type: text/html and ends the request. res.json({ data }) serialises the object and returns it as application/json — the workhorse for APIs. For creation, chain the status first: res.status(201).json({ ok: true }). res.redirect("/login") issues a 302 and fits login and auth flows. res.sendFile(path.join(__dirname, "index.html")) streams a file straight off disk for static pages or one-click downloads. Set custom headers with res.set("X-Custom", "value"), for instance for cache policy. Server-rendered pages use res.render("index", { title: "Home" }), which resolves templates relative to the directory set by app.set("views", ...).
How do I organise a growing route table, and what is each middleware for?
Split with a Router: create one with const router = express.Router(), mount it with app.use("/api", router), define several methods on one path compactly via router.route("/users").get(list).post(create), and finish with module.exports = router so the entry file can mount it. app.all("/api/*", handler) fits prefix logic such as authentication that must apply to every method. Each middleware has a distinct job: morgan("dev") prints a terse one-line request summary for debugging, helmet() sets a batch of HTTP security headers including X-Content-Type-Options, cors() handles cross-origin with a configurable allowlist, compression() shrinks large JSON and text responses, and express.static("public") serves a directory under a /static prefix so you need no per-file route.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us