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.

Languages·45 commands·Last updated 2026-07-21
expressnodeweb

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()
Create the app instance
app.use(express.json())
Mount global middleware (parse JSON body)
app.listen(3000, () => {})
Listen on a port to start the server
const router = express.Router()
Create a mountable router
app.set("port", process.env.PORT || 3000)
Set app config

Route Definition 7

app.get("/users", handler)
GET route
app.post("/users", handler)
POST route
app.put("/users/:id", handler)
PUT route, :id is a param
app.delete("/users/:id", handler)
DELETE route
app.all("/api/*", handler)
Catch-all route matching any method
app.use("/api", router)
Mount a sub-router module
router.route("/users").get(list).post(create)
Chainable route definition

Request Object 6

req.params.id
URL route params
req.query.page
Query string params
req.body.name
Request body (needs express.json())
req.headers["content-type"]
Request headers
req.cookies.token
Cookie (needs cookie-parser)
req.get("User-Agent")
Get a specific request header

Response Object 6

res.send("hello")
Send a text response
res.json({ data })
Return JSON
res.status(201).json({ ok: true })
Set status code and return JSON
res.redirect("/login")
Redirect
res.sendFile(path.join(__dirname, "index.html"))
Send a file
res.set("X-Custom", "value")
Set a response header

Common Middleware 8

app.use((req, res, next) => { next() })
Custom middleware
express.urlencoded({ extended: true })
Parse URL-encoded form bodies
app.use("/static", express.static("public"))
Serve static files
cors()
CORS middleware
helmet()
Security HTTP headers middleware
morgan("dev")
Request logging middleware
cookie-parser()
Parse cookies
compression()
Response compression middleware

Template Engines & JWT 6

app.set("view engine", "ejs")
Set the template engine
app.set("views", path.join(__dirname, "views"))
Set the views directory
res.render("index", { title: "Home" })
Render a template and respond
jwt.sign(payload, secret, { expiresIn: "1h" })
Sign a JWT token
jwt.verify(token, secret)
Verify a JWT token
req.headers.authorization
Read the Bearer token from the Authorization header

Error Handling & Patterns 7

const wrap = (fn) => (req, res, next) => fn(req, res, next).catch(next)
Async error wrapper that auto-catches async errors
app.use((req, res) => res.status(404).send("Not Found"))
404 fallback handler
app.use((err, req, res, next) => res.status(500).json({ error: err.message }))
Global error-handling middleware (must have 4 args)
next(err)
Pass an error to the error middleware
process.on("uncaughtException", handler)
Catch uncaught exceptions
process.on("unhandledRejection", handler)
Catch unhandled Promise rejections
module.exports = router
Export the router for app.use to mount

Parameter matrix

ParameterEffectExample
express()Creates the application instance that every later setting hangs offconst app = express()
express.json()Parses JSON request bodies into req.body, and must be registered before the routesapp.use(express.json())
express.urlencoded({ extended: true })Parses HTML form bodies into req.bodyapp.use(express.urlencoded({ extended: true }))
app.use(path, mw)Mounts middleware or sub-routers under a prefix; every matching request passes throughapp.use("/api", router)
app.use(express.static("public"))Serves a static directory without writing a route per fileapp.use("/static", express.static("public"))
app.set(key, value)Sets application-level settings such as the port, template engine and view directoryapp.set("views", path.join(__dirname, "views"))
app.listen(port, cb)Binds the port and starts the serverapp.listen(3000, () => {})
express.Router()Creates a router you can mount independently, which keeps modules separableconst router = express.Router()
router.route(path).get().post()Groups several methods on the same path into a single chainrouter.route("/users").get(list).post(create)
app.all("/api/*", mw)Matches every HTTP method, the usual shape for auth and other cross-cutting pre-workapp.all("/api/*", auth)
req.params.idReads a path parameter such as :idapp.put("/users/:id", handler)
req.query.pageReads a query string parameter, which always arrives as a stringconst page = req.query.page
req.get("User-Agent")Reads a request header by nameconst ua = req.get("User-Agent")
req.cookies.tokenReads cookies, and requires cookie-parser to be mounted firstapp.use(cookieParser())
res.send(str)Sends a text response and ends the requestres.send("hello")
res.json(obj)Serialises to application/json, the workhorse for APIsres.json({ data })
res.status(201).json(obj)Sets the status code and returns JSON in one chained callres.status(201).json({ ok: true })
res.redirect(path)Sends a 302 redirectres.redirect("/login")
res.sendFile(absPath)Streams a file from disk back to the clientres.sendFile(path.join(__dirname, "index.html"))
res.set(key, value)Sets a single response headerres.set("X-Custom", "value")
res.render(view, data)Renders a template and emits an HTML responseres.render("index", { title: "Home" })
helmet()Sets HTTP security headers such as X-Content-Type-Options in bulkapp.use(helmet())
morgan("dev")Trims request logging to a single line, which keeps debugging readableapp.use(morgan("dev"))
cors()Adds CORS headers; with a split frontend, allow-list the origins you actually needapp.use(cors())
compression()Compresses the response body, which pays off on large JSON and textapp.use(compression())
jwt.sign(payload, secret, { expiresIn })Signs a JWT that carries an expiryjwt.sign(payload, secret, { expiresIn: "1h" })
jwt.verify(token, secret)Verifies and parses the token, throwing on a bad signature or an expired oneconst data = jwt.verify(token, secret)
req.headers.authorizationPulls the raw Bearer token out of the headerconst token = req.headers.authorization
next(err)Hands the error to the downstream error-handling middlewarenext(err)
app.use((err, req, res, next) => ...)Global error handler; it needs all four parameters and must come lastapp.use((err, req, res, next) => res.status(500).json({ error: err.message }))
module.exports = routerExports a router module for mounting with app.usemodule.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

  1. 1Check middleware registration order (body parser before routes)

    grep -n "app.use\|router\." server.js

    The line number of express.json() should be smaller than any app.get or app.post line, otherwise req.body is undefined.

  2. 2Confirm the error handler arity and that it sits last

    grep -n "err, req, res, next" server.js

    Exactly one match is expected, and its app.use must come after every route in the file.

  3. 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.

  4. 4Locate uncaught exceptions and rejected promises at the process level

    node --trace-uncaught server.js

    Combine process.on("uncaughtException", handler) with process.on("unhandledRejection", handler) to print the full stack and find the request path that forgot next(err).

  5. 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.

  6. 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