Express.js 中间件速查表 - Express 常用命令与 API 开发大全
这套速查给用 Node 写 REST API 或 Web 应用的开发者,从小型内部服务到带 EJS 页面的完整站点都覆盖。内容围绕 app/router 搭建、带 :id 和查询串的路由、读懂 req 与拼好 res,再到 json/static/cors/helmet/morgan/compression 这一串中间件、EJS 渲染、JWT 鉴权,以及 async 错误处理那套写法。与随手抄的 Express 片段不同,这里按请求从进来到返回的生命周期分层排布。读完应能搭起应用、按正确顺序挂中间件和路由,并把异步错误收敛到统一 handler,而不是到处 try/catch。
典型使用场景
面向用 Node.js 写 HTTP 服务的开发者,尤其是需要从零搭起一个带鉴权与 JSON 接口的后端时。本表的价值在于把 Express 最容易踩坑的顺序问题一次讲清:为什么 express.json() 必须写在路由注册之前、为什么错误处理中间件必须放最后且恰好四个参数、异步 handler 里抛错为什么默认不会进错误中间件。此外还覆盖了 Router 拆模块、JWT 签发校验、模板引擎挂载这一条从空项目到可上线接口的完整链路,配套的 wrap 包装器与 res.status().json() 链式写法可以直接抄进工程。
应用创建 5
const app = express()app.use(express.json())app.listen(3000, () => {})const router = express.Router()app.set("port", process.env.PORT || 3000)路由定义 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 6
req.params.idreq.query.pagereq.body.namereq.headers["content-type"]req.cookies.tokenreq.get("User-Agent")响应对象 Response 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")常用中间件 8
app.use((req, res, next) => { next() })express.urlencoded({ extended: true })app.use("/static", express.static("public"))cors()helmet()morgan("dev")cookie-parser()compression()模板引擎与 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.authorization错误处理与实用模式 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 = router参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
express() | 创建应用实例,后续所有设置都挂在它上面 | const app = express() |
express.json() | 解析 JSON 请求体到 req.body,必须先于路由注册 | app.use(express.json()) |
express.urlencoded({ extended: true }) | 解析 HTML 表单请求体到 req.body | app.use(express.urlencoded({ extended: true })) |
app.use(path, mw) | 把中间件或子路由挂到指定前缀,所有匹配请求都会经过 | app.use("/api", router) |
app.use(express.static("public")) | 对外提供静态目录,不用逐个写路由 | app.use("/static", express.static("public")) |
app.set(key, value) | 设置应用级配置,如端口、模板引擎、模板目录 | app.set("views", path.join(__dirname, "views")) |
app.listen(port, cb) | 监听端口启动服务 | app.listen(3000, () => {}) |
express.Router() | 创建可独立挂载的路由器,便于按模块拆分 | const router = express.Router() |
router.route(path).get().post() | 在一条链上聚合定义同一路径的多个方法 | router.route("/users").get(list).post(create) |
app.all("/api/*", mw) | 通配所有 HTTP 方法,常用于鉴权等通用前置逻辑 | app.all("/api/*", auth) |
req.params.id | 读取路径参数 :id 的值 | app.put("/users/:id", handler) |
req.query.page | 读取查询字符串参数(值是字符串) | const page = req.query.page |
req.get("User-Agent") | 按名读取请求头 | const ua = req.get("User-Agent") |
req.cookies.token | 读取 Cookie,需先挂 cookie-parser | app.use(cookieParser()) |
res.send(str) | 发送文本响应并结束本次请求 | res.send("hello") |
res.json(obj) | 序列化为 application/json 返回,写 API 最常用 | res.json({ data }) |
res.status(201).json(obj) | 先设状态码再返回 JSON,链式一步到位 | res.status(201).json({ ok: true }) |
res.redirect(path) | 发送 302 跳转响应 | res.redirect("/login") |
res.sendFile(absPath) | 把磁盘文件流式返回给客户端 | res.sendFile(path.join(__dirname, "index.html")) |
res.set(key, value) | 设置单个响应头 | res.set("X-Custom", "value") |
res.render(view, data) | 渲染模板并输出 HTML 响应 | res.render("index", { title: "Home" }) |
helmet() | 批量设置 X-Content-Type-Options 等 HTTP 安全头 | app.use(helmet()) |
morgan("dev") | 精简单行请求日志,开发排查直观 | app.use(morgan("dev")) |
cors() | 加跨域响应头,前后端分离时按需配来源白名单 | app.use(cors()) |
compression() | 压缩响应体,大 JSON/文本显著减体积 | app.use(compression()) |
jwt.sign(payload, secret, { expiresIn }) | 签发带过期时间的 JWT | jwt.sign(payload, secret, { expiresIn: "1h" }) |
jwt.verify(token, secret) | 校验并解析 token,签名不符或过期会抛错 | const data = jwt.verify(token, secret) |
req.headers.authorization | 取 Bearer 形式的 token 原文 | const token = req.headers.authorization |
next(err) | 把错误交给后续错误处理中间件 | next(err) |
app.use((err, req, res, next) => ...) | 全局错误处理中间件,必须四个参数且放最后 | app.use((err, req, res, next) => res.status(500).json({ error: err.message })) |
module.exports = router | 导出路由模块供 app.use 挂载 | module.exports = router |
易错点与避坑指南
现象POST 请求里 req.body 是 undefined,访问 req.body.name 直接抛 TypeError。
原因express.json() 挂载在路由注册之后,或根本忘了挂载,body 解析器没有生效。
处置把所有 app.use(express.json()) 提到所有 app.get/app.post 之前;表单提交则补 app.use(express.urlencoded({ extended: true }))。
现象异步 handler 里 throw new Error 之后请求一直挂着,客户端超时而不是返回 500。
原因Express 4 不会自动捕获 async 函数返回的 rejected Promise,错误没被 next() 转交。
处置用 wrap 包装器把 async handler 包一层:const wrap = (fn) => (req, res, next) => fn(req, res, next).catch(next),或升级到会自动捕获的版本并保持 handler 统一走包装。
现象自定义的错误处理中间件写了四个参数却完全没被调用,错误直接冒泡到默认处理器。
原因中间件被注册在了路由之前,或者用 app.use 挂载时顺序放在了错误发生点之前,Express 按注册顺序匹配。
处置把 (err, req, res, next) 四参数中间件放到所有路由和 404 兜底之后,作为最后一段 app.use。
现象不传任何参数的中间件不被 Express 识别,函数体完全不执行。
原因Express 靠函数参数个数(length === 4)识别错误处理中间件,箭头函数写成一个参数就被当普通中间件。
处置显式写出四个形参:(err, req, res, next) => { ... },即使 next 用不到也要留着。
现象重启后进程仍然占着 3000 端口,报 EADDRINUSE。
原因脚本里抛出的异常没有被处理,Node 进程因未捕获异常退不干净,端口未释放。
处置补上 process.on("uncaughtException", handler) 与 process.on("unhandledRejection", handler) 做收尾退出,再确认没有遗留的 node 进程占用端口。
现象读 req.cookies.token 得到 undefined,读请求头里的 token 却是好的。
原因没有挂 cookie-parser 中间件,Express 本身不解析 Cookie。
处置加 app.use(cookieParser()),且必须放在任何会读 req.cookies 的路由之前。
现象静态资源请求全部 404,目录明明存在。
原因express.static 的参数是相对进程工作目录的路径,用相对路径时进程 cwd 与预期不一致就找不到目录。
处置改用绝对路径:express.static(path.join(__dirname, "public")),避免依赖启动目录。
排障路径
1确认中间件注册顺序(body 解析器必须在路由前)
grep -n "app.use\|router\." server.js输出中 express.json() 的行号应小于任何 app.get/app.post 的行号,否则 req.body 会是 undefined。
2确认错误处理中间件参数个数并位于末尾
grep -n "err, req, res, next" server.js应只匹配到一处,且该 app.use 出现在文件所有路由定义之后。
3打开精简请求日志观察实际命中了哪些路由
app.use(morgan("dev")); app.listen(3000, () => {})终端每请求输出一行摘要,方法、URL、状态码一目了然,可直接判断是否落到 404 兜底。
4定位进程级未捕获异常与 Promise 拒绝
node --trace-uncaught server.js配合 process.on("uncaughtException", handler) 与 process.on("unhandledRejection", handler) 打印完整堆栈,定位是哪个请求链路漏了 next(err)。
5打印请求体与请求头确认解析链路
app.use((req, res, next) => { console.log(req.body, req.headers["content-type"], req.get("User-Agent")); next() })中间件里必须调用 next(),否则请求会停在这一层。
6校验 JWT 是否有效
const data = jwt.verify(token, secret)签名不符或已过期会抛错,鉴权中间件里用 try/catch 捕获后调用 next(err) 或 res.status(401).json({ error: "unauthorized" })。
命令示例
最小可用 API:JSON 解析 + 路由 + 201 响应
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() 必须挂在路由之前,否则 POST 的 req.body 是空对象;GET 里用 req.params.id、POST 里用 req.body.name,分别对应表内的路径参数与请求体取值。
拆分子路由挂到 /api 前缀,同一路径链式定义多方法
// 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 把同一路径的多个方法收在一处,比散落的 app.get/app.post 更难漏改;子模块只导出 router,由主应用决定前缀,换前缀时不必进子文件。
async 错误包装器 + 全局错误中间件
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 不会自动捕获 async 函数返回的 rejected Promise,必须用 wrap 把 catch 接到 next(err) 上,否则请求既不成功也不返回,客户端一直挂到超时。
JWT 登录 + 前缀路由统一鉴权
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(..., { expiresIn }) 定生命周期,鉴权用 jwt.verify 校验签名与过期。把中间件挂在 /api 前缀上,所有子路由自动受保护,不必逐个路由包一层。
常见坑与注意事项
- 错误处理中间件必须是 (err, req, res, next) 四个参数:Express 按参数个数识别它,写成 (err, req, res) 会被当普通中间件,线上错误全部静默变成请求挂起。
- app.use 的执行顺序即匹配顺序:404 兜底、express.static 或错误中间件一旦写在业务路由之前,所有请求都会被它先截走,表现为「接口全部 404」且日志无记录。
- express.json() 默认只接受 application/json 且请求体上限约 100kb;表单和文件上传用错解析器会拿到空 body,上传接口则直接被拒,需要 urlencoded 或专门的 multipart 中间件。
- req.params.id、req.query.page 全是外部输入,直接拼进 SQL 或文件路径就是注入与目录穿越;至少要做类型/白名单校验,否则一个请求就能拖库或读任意文件。
- res.send() / res.json() 之后再写内容或再次响应会抛 Cannot set headers after they are sent,日志里只看到这个报错,真正的首个异常常在更早的 await 上。
- jwt.verify 抛错默认会把完整 token 与签名细节带进错误信息;直接 res.json({ error: e.message }) 回给客户端会泄露密钥材料,必须只返回 401 这类固定文案。
提示
- 中间件顺序很重要,body-parser 必须在路由之前,错误处理中间件必须放最后。
- 错误处理中间件必须 4 个参数 (err, req, res, next),否则 Express 不识别。
- 生产环境用 helmet() 加安全头,cors() 按需配置白名单。
- express.static 可设置多个目录,按顺序查找文件。
- Router 可模块化路由,app.use("/api", router) 挂载子路由。
常见问题
中间件顺序到底怎么排?为什么改完就报 req.body 是 undefined?
req.body 为空几乎都是顺序问题。app.use(express.json()) 必须挂在所有读取请求体的路由之前,否则路由先命中并执行,body 还没被解析。同理 req.cookies.token 需要先 cookie-parser();表单提交要 app.use(express.urlencoded({ extended: true })) 才能解析 application/x-www-form-urlencoded。另一头,错误处理中间件必须放在所有路由之后,且必须写满四个参数 (err, req, res, next),否则 Express 不识别它,async 错误会变成进程挂起。正确的整体顺序是:解析类 → 日志/安全类(morgan、helmet、cors、compression)→ 路由 → 404 兜底 → 错误处理。
async 路由里的异常为什么没进错误中间件?
Express 4 不会自动捕获 async 函数返回的 rejected Promise,必须靠两种方式之一。第一种是包装器:const wrap = (fn) => (req, res, next) => fn(req, res, next).catch(next),把 rejection 交给 next(err),再由全局错误处理中间件 (err, req, res, next) => res.status(500).json({ error: err.message }) 收口,路由写成 app.post("/users", wrap(async (req, res) => { ... }))。第二种是显式 try/catch 后调 next(err)。校验失败之类不该算 500 的错误,可以在错误中间件里按错误类型映射状态码。进程级兜底再用 process.on("uncaughtException", handler) 和 process.on("unhandledRejection", handler)。
req.params、query、body、headers、cookies 分别从哪来?
五个取值口对应请求的五个部位。req.params.id 来自 URL 路径里的 :id 占位符,注册 app.put("/users/:id", handler) 后在 /users/7 上得到 "7"。req.query.page 来自查询字符串 ?page=2,值是字符串,分页筛选常用。req.body.name 来自请求体,必须先 express.json() 或 urlencoded() 解析。req.headers["content-type"] 读请求头判断内容类型或携带的 token,也可以用更顺手的 req.get("User-Agent")。req.cookies.token 读 Cookie,需要 cookie-parser()。取认证 token 的常见做法是从 req.headers.authorization 剥出 "Bearer <token>" 再 jwt.verify(token, secret)。
res.send、res.json、sendFile、redirect 该怎么选?
按「返回什么」选。res.send("hello") 发的是默认 Content-Type: text/html 的文本,结束本次请求;res.json({ data }) 把对象序列化为 JSON 并以 application/json 返回,写 API 最常用。创建资源时链式写 res.status(201).json({ ok: true }),先设状态码再返回,一步到位。res.redirect("/login") 发 302 跳转,未登录等鉴权流程用。res.sendFile(path.join(__dirname, "index.html")) 直接把磁盘文件流式返回,静态页或一键下载用。自定义响应头用 res.set("X-Custom", "value"),比如设置缓存策略。服务端渲染页面则用 res.render("index", { title: "Home" }),它按 app.set("views", ...) 指定的目录找模板。
路由多了怎么组织,中间件各自解决什么问题?
用 Router 拆模块:const router = express.Router() 定义子路由,app.use("/api", router) 挂到前缀,router.route("/users").get(list).post(create) 链式定义同一路径的多个方法,最后 module.exports = router 导出供入口挂载;app.all("/api/*", handler) 适合做鉴权这类对所有方法生效的前缀逻辑。中间件各司其职:morgan("dev") 输出一行精简请求摘要便于开发排查,helmet() 批量设置 X-Content-Type-Options 等安全头,cors() 处理跨域并可按白名单配来源,compression() 压缩大 JSON/文本响应省带宽,express.static("public") 把目录以 /static 前缀对外提供静态资源,不用逐个写路由。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。