Express 路由系统
路由是 Express 应用的核心机制,决定了应用程序如何响应客户端对特定端点的请求。一个完整的路由由三个要素组成:
| 要素 | 说明 | 示例 |
|---|---|---|
| URI 路径 | 请求的资源路径 | /users/:id |
| HTTP 方法 | 请求的操作类型 | GET、POST、PUT、DELETE |
| 处理函数 | 匹配时执行的回调 | (req, res) => {...} |
路由系统架构
请求匹配流程
code
HTTP 请求到达
│
▼
┌──────────────────────────────────────────────────────┐
│ 路由匹配引擎 │
├──────────────────────────────────────────────────────┤
│ 1. 按 HTTP 方法筛选 (GET/POST/PUT/DELETE...) │
│ 2. 按注册顺序匹配路径模式 │
│ 3. 提取路由参数到 req.params │
│ 4. 执行匹配的处理函数 │
└──────────────────────────────────────────────────────┘
│
▼
路由处理函数
│
▼
发送响应路由注册方式
| 方式 | 适用场景 | 特点 |
|---|---|---|
app.METHOD() | 应用级路由 | 直接绑定到应用实例 |
app.route() | 单路径多方法 | 链式调用,减少重复 |
express.Router() | 模块化路由 | 可插拔,便于组织 |
基本路由
路由定义语法
javascript
app.METHOD(PATH, HANDLER)| 参数 | 说明 |
|---|---|
app | Express 应用实例 |
METHOD | HTTP 方法(小写):get、post、put、delete、patch 等 |
PATH | 路由路径,支持字符串、字符串模式、正则表达式 |
HANDLER | 路由处理函数,接收 (req, res, next) 参数 |
HTTP 方法
Express 支持所有标准 HTTP 方法:
| 方法 | 用途 | RESTful 语义 |
|---|---|---|
app.get() | 获取资源 | 查询 |
app.post() | 创建资源 | 新增 |
app.put() | 完整更新资源 | 替换 |
app.patch() | 部分更新资源 | 修改 |
app.delete() | 删除资源 | 删除 |
app.head() | 获取响应头 | 元数据查询 |
app.options() | 获取支持的方法 | CORS 预检 |
app.all() | 匹配所有方法 | 通用处理 |
基本示例
javascript
const express = require("express")
const app = express()
// GET 请求
app.get("/", (req, res) => {
res.send("GET 请求:获取首页")
})
// POST 请求
app.post("/users", (req, res) => {
res.status(201).json({ message: "用户创建成功" })
})
// PUT 请求
app.put("/users/:id", (req, res) => {
res.json({ message: `用户 ${req.params.id} 已更新` })
})
// DELETE 请求
app.delete("/users/:id", (req, res) => {
res.status(204).send() // 无内容返回
})
// 匹配所有 HTTP 方法
app.all("/secret", (req, res) => {
res.send(`收到 ${req.method} 请求访问 /secret`)
})
app.listen(3000)路由路径匹配
Express 支持三种路径定义方式:字符串、字符串模式、正则表达式。
字符串路径
最常用的路径定义方式,精确匹配:
javascript
// 精确匹配 /about
app.get("/about", (req, res) => {
res.send("关于页面")
})
// 匹配 /profile
app.get("/profile", (req, res) => {
res.send("个人中心")
})字符串模式
使用特殊字符实现模糊匹配:
| 字符 | 含义 | 示例 |
|---|---|---|
? | 前一个字符出现 0 或 1 次 | /ab?cd 匹配 /acd、/abcd |
+ | 前一个字符出现 1 或多次 | /ab+cd 匹配 /abcd、/abbcd |
* | 任意字符 | /ab*cd 匹配 /abXYZcd |
() | 分组 | /ab(cd)?e 匹配 /abe、/abcde |
javascript
// ? - 可选字符
app.get("/ab?cd", (req, res) => {
res.send("匹配 /acd 或 /abcd")
})
// + - 重复字符
app.get("/ab+cd", (req, res) => {
res.send("匹配 /abcd, /abbcd, /abbbcd...")
})
// * - 通配符
app.get("/ab*cd", (req, res) => {
res.send("匹配 /ab 开头、cd 结尾的路径")
})
// () - 分组
app.get("/ab(cd)?e", (req, res) => {
res.send("匹配 /abe 或 /abcde")
})正则表达式
用于复杂的匹配规则:
javascript
// 匹配任何包含 'api' 的路径
app.get(/api/, (req, res) => {
res.send("路径包含 'api'")
})
// 匹配以 .jpg 结尾的路径
app.get(/.*\.jpg$/, (req, res) => {
res.send("请求的是 JPG 图片")
})
// 匹配数字 ID
app.get(/^\/users\/(\d+)$/, (req, res) => {
res.send(`数字 ID: ${req.params[0]}`)
})路由参数
路由参数用于捕获 URL 中指定位置的值,存储在 req.params 对象中。
必需参数
使用 :参数名 定义:
javascript
// 单个参数
app.get("/users/:id", (req, res) => {
res.json({ userId: req.params.id })
})
// GET /users/123 → { userId: "123" }
// 多个参数
app.get("/users/:userId/posts/:postId", (req, res) => {
const { userId, postId } = req.params
res.json({ userId, postId })
})
// GET /users/123/posts/456 → { userId: "123", postId: "456" }参数正则约束
限制参数的匹配格式:
javascript
// 只匹配数字
app.get("/users/:id(\\d+)", (req, res) => {
res.send(`数字 ID: ${req.params.id}`)
})
// GET /users/123 ✓
// GET /users/abc ✗
// 匹配 UUID 格式
app.get("/items/:uuid([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})", (req, res) => {
res.send(`UUID: ${req.params.uuid}`)
})
// GET /items/550e8400-e29b-41d4-a716-446655440000 ✓
// 匹配字母数字
app.get("/files/:name([a-zA-Z0-9]+)", (req, res) => {
res.send(`文件名: ${req.params.name}`)
})可选参数
Express 不直接支持可选参数,但可以通过定义多条路由实现:
javascript
// 方案一:多条路由
app.get("/books", listBooks) // 无参数
app.get("/books/:year", listBooks) // 年份参数
app.get("/books/:year/:month", listBooks) // 年月参数
function listBooks(req, res) {
const { year, month } = req.params
res.json({ year, month })
}
// 方案二:使用正则(不推荐,可读性差)
app.get(/^\/books(?:\/(\d{4})(?:\/(\d{2}))?)?$/, (req, res) => {
const year = req.params[0]
const month = req.params[1]
res.json({ year, month })
})参数验证中间件
javascript
// 验证 ID 是否为有效的 MongoDB ObjectId
const validateObjectId = (req, res, next) => {
const { id } = req.params
if (!/^[0-9a-fA-F]{24}$/.test(id)) {
return res.status(400).json({ error: "无效的 ID 格式" })
}
next()
}
app.get("/users/:id", validateObjectId, (req, res) => {
// id 已验证通过
res.json({ userId: req.params.id })
})路由处理函数
单个处理函数
javascript
app.get("/users", (req, res) => {
res.json({ users: [] })
})多个处理函数(中间件模式)
处理函数可以像中间件一样串联执行:
javascript
// 认证中间件
const authenticate = (req, res, next) => {
const token = req.headers.authorization
if (!token) {
return res.status(401).json({ error: "未授权" })
}
req.user = { id: 1, name: "Admin" } // 模拟解析用户
next()
}
// 日志中间件
const logger = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`)
next()
}
// 业务处理
const getProfile = (req, res) => {
res.json({ user: req.user })
}
// 组合多个处理函数
app.get("/profile", logger, authenticate, getProfile)数组形式组织处理函数
javascript
const validateInput = (req, res, next) => {
if (!req.body.name) {
return res.status(400).json({ error: "name 为必填项" })
}
next()
}
const sanitizeInput = (req, res, next) => {
req.body.name = req.body.name.trim()
next()
}
const createUser = (req, res) => {
res.status(201).json({ name: req.body.name })
}
// 使用数组
app.post("/users", [validateInput, sanitizeInput, createUser])复用处理函数
javascript
// 公共验证逻辑
const requireAuth = (req, res, next) => {
if (!req.session.userId) {
return res.redirect("/login")
}
next()
}
// 多个路由复用
app.get("/dashboard", requireAuth, (req, res) => {
res.render("dashboard")
})
app.get("/settings", requireAuth, (req, res) => {
res.render("settings")
})
app.get("/profile", requireAuth, (req, res) => {
res.render("profile")
})链式路由 app.route
app.route() 可以为同一个路径定义多个 HTTP 方法的处理函数,避免路径重复。
基本用法
javascript
app
.route("/book")
.get((req, res) => {
res.send("获取图书列表")
})
.post((req, res) => {
res.send("创建新图书")
})
.put((req, res) => {
res.send("更新图书")
})
.delete((req, res) => {
res.send("删除图书")
})结合中间件
javascript
const validateBook = (req, res, next) => {
if (!req.body.title) {
return res.status(400).json({ error: "书名不能为空" })
}
next()
}
app
.route("/books/:id")
.all(requireAuth) // 所有方法都先验证认证
.get((req, res) => {
res.send(`获取图书 ${req.params.id}`)
})
.put(validateBook, (req, res) => {
res.send(`更新图书 ${req.params.id}`)
})
.delete((req, res) => {
res.send(`删除图书 ${req.params.id}`)
})模块化路由 express.Router
express.Router 是一个迷你应用实例,用于创建可挂载的路由模块。
创建路由模块
routes/users.js:
javascript
const express = require("express")
const router = express.Router()
// 中间件:所有路由共享
router.use((req, res, next) => {
console.log("用户路由访问时间:", Date.now())
next()
})
// GET /users
router.get("/", (req, res) => {
res.json({ users: [] })
})
// GET /users/:id
router.get("/:id", (req, res) => {
res.json({ userId: req.params.id })
})
// POST /users
router.post("/", (req, res) => {
res.status(201).json({ message: "用户创建成功" })
})
// PUT /users/:id
router.put("/:id", (req, res) => {
res.json({ message: `用户 ${req.params.id} 已更新` })
})
// DELETE /users/:id
router.delete("/:id", (req, res) => {
res.status(204).send()
})
module.exports = router挂载路由模块
app.js:
javascript
const express = require("express")
const app = express()
// 引入路由模块
const usersRouter = require("./routes/users")
const productsRouter = require("./routes/products")
const ordersRouter = require("./routes/orders")
// 挂载到不同路径
app.use("/users", usersRouter)
app.use("/products", productsRouter)
app.use("/orders", ordersRouter)
// 实际路由映射:
// /users → usersRouter
// /users/:id → usersRouter
// /products → productsRouter
// /orders → ordersRouter
app.listen(3000)嵌套路由
javascript
// routes/admin/index.js
const express = require("express")
const router = express.Router()
const usersRouter = require("./users")
const settingsRouter = require("./settings")
// 嵌套挂载
router.use("/users", usersRouter)
router.use("/settings", settingsRouter)
router.get("/", (req, res) => {
res.send("管理后台首页")
})
module.exports = router
// app.js
app.use("/admin", require("./routes/admin"))
// 实际路径:
// /admin → admin 首页
// /admin/users → admin 用户管理
// /admin/settings → admin 设置路由级中间件
javascript
// routes/api.js
const express = require("express")
const router = express.Router()
// 路由级中间件:只对当前路由生效
router.use((req, res, next) => {
res.setHeader("X-API-Version", "1.0")
next()
})
// 认证中间件:保护所有路由
router.use(require("../middleware/auth"))
// 公开路由(需要调整顺序放在认证之前)
router.get("/health", (req, res) => {
res.json({ status: "ok" })
})
// 受保护的路由
router.get("/users", (req, res) => {
res.json({ users: [] })
})
module.exports = router获取路由参数(挂载路径)
javascript
// 路由模块可以通过 req.baseUrl 获取挂载前缀
router.get("/info", (req, res) => {
res.json({
baseUrl: req.baseUrl, // "/users"
path: req.path, // "/info"
originalUrl: req.originalUrl // "/users/info"
})
})路由匹配顺序
Express 按照路由注册的顺序进行匹配,第一个匹配的路由会被执行。
匹配规则
javascript
// ⚠️ 错误示例:顺序问题
app.get("/users/special", (req, res) => {
res.send("特殊用户")
})
app.get("/users/:id", (req, res) => {
res.send(`用户 ID: ${req.params.id}`)
})
// GET /users/special → "特殊用户" ✓
// GET /users/123 → "用户 ID: 123" ✓
// ⚠️ 反转顺序后
app.get("/users/:id", (req, res) => {
res.send(`用户 ID: ${req.params.id}`)
})
app.get("/users/special", (req, res) => {
res.send("特殊用户")
})
// GET /users/special → "用户 ID: special" ✗(被 :id 匹配)
// GET /users/123 → "用户 ID: 123" ✓最佳实践:静态路径优先
javascript
// ✅ 正确示例:静态路径放在前面
app.get("/users/me", (req, res) => {
res.send("当前登录用户")
})
app.get("/users/admin", (req, res) => {
res.send("管理员信息")
})
app.get("/users/:id", (req, res) => {
res.send(`用户 ID: ${req.params.id}`)
})使用 next('route') 跳过当前路由
javascript
app.get(
"/users/:id",
(req, res, next) => {
const { id } = req.params
if (id === "admin") {
return next("route") // 跳过后续处理函数,进入下一个匹配的路由
}
next() // 继续执行下一个处理函数
},
(req, res) => {
res.send(`普通用户: ${req.params.id}`)
}
)
app.get("/users/:id", (req, res) => {
res.send(`特殊处理: ${req.params.id}`)
})
// GET /users/admin → "特殊处理: admin"
// GET /users/123 → "普通用户: 123"RESTful API 设计规范
资源命名规范
| 规范 | 示例 | 说明 |
|---|---|---|
| 使用名词复数 | /users、/products | 资源集合 |
| 使用小写字母 | /user-profiles | 避免大写 |
| 使用连字符分隔 | /order-items | 多单词可读性好 |
| 避免动词 | ❌ /getUsers | HTTP 方法已表示动作 |
| 嵌套不超过两级 | /users/:id/posts | 避免过深嵌套 |
HTTP 方法与 CRUD 对应
javascript
// 用户资源 CRUD
const express = require("express")
const router = express.Router()
// 查询所有用户
router.get("/users", (req, res) => {
res.json({ users: [] })
})
// 查询单个用户
router.get("/users/:id", (req, res) => {
res.json({ id: req.params.id })
})
// 创建用户
router.post("/users", (req, res) => {
res.status(201).json({ message: "创建成功" })
})
// 更新用户(全量)
router.put("/users/:id", (req, res) => {
res.json({ message: "更新成功" })
})
// 更新用户(部分)
router.patch("/users/:id", (req, res) => {
res.json({ message: "部分更新成功" })
})
// 删除用户
router.delete("/users/:id", (req, res) => {
res.status(204).send()
})响应状态码规范
| 状态码 | 含义 | 使用场景 |
|---|---|---|
200 OK | 成功 | GET、PUT、PATCH 成功 |
201 Created | 已创建 | POST 创建资源成功 |
204 No Content | 无内容 | DELETE 成功 |
400 Bad Request | 请求错误 | 参数验证失败 |
401 Unauthorized | 未认证 | 缺少认证信息 |
403 Forbidden | 禁止访问 | 无权限 |
404 Not Found | 未找到 | 资源不存在 |
409 Conflict | 冲突 | 资源已存在 |
422 Unprocessable Entity | 无法处理 | 语义错误 |
500 Internal Server Error | 服务器错误 | 程序异常 |
API 版本管理
javascript
// 方案一:URL 路径版本
app.use("/api/v1", require("./routes/v1"))
app.use("/api/v2", require("./routes/v2"))
// 方案二:请求头版本
app.use("/api", (req, res, next) => {
const version = req.headers["accept-version"] || "v1"
req.apiVersion = version
next()
})项目结构组织
推荐目录结构
code
project/
├── app.js # 应用入口
├── routes/
│ ├── index.js # 路由入口,统一挂载
│ ├── users.js # 用户路由
│ ├── products.js # 产品路由
│ └── admin/
│ ├── index.js # 管理后台路由
│ ├── users.js # 管理用户
│ └── settings.js # 管理设置
├── controllers/ # 控制器(业务逻辑)
│ ├── userController.js
│ └── productController.js
└── middleware/ # 中间件
├── auth.js
└── validate.js路由入口文件
routes/index.js:
javascript
const express = require("express")
const router = express.Router()
// 引入各模块路由
const usersRouter = require("./users")
const productsRouter = require("./products")
const authRouter = require("./auth")
// 公共中间件
const authMiddleware = require("../middleware/auth")
// 公开路由(无需认证)
router.use("/auth", authRouter)
// 受保护路由
router.use("/users", authMiddleware, usersRouter)
router.use("/products", authMiddleware, productsRouter)
// API 文档路由
router.get("/docs", (req, res) => {
res.json({
endpoints: [
{ method: "POST", path: "/auth/login" },
{ method: "GET", path: "/users" },
{ method: "GET", path: "/products" }
]
})
})
module.exports = routerapp.js:
javascript
const express = require("express")
const app = express()
// 挂载统一路由入口
app.use("/api", require("./routes"))
app.listen(3000)控制器分离
controllers/userController.js:
javascript
const User = require("../models/User")
exports.list = async (req, res, next) => {
try {
const users = await User.find()
res.json({ users })
} catch (error) {
next(error)
}
}
exports.show = async (req, res, next) => {
try {
const user = await User.findById(req.params.id)
if (!user) {
return res.status(404).json({ error: "用户不存在" })
}
res.json(user)
} catch (error) {
next(error)
}
}
exports.create = async (req, res, next) => {
try {
const user = await User.create(req.body)
res.status(201).json(user)
} catch (error) {
next(error)
}
}
exports.update = async (req, res, next) => {
try {
const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true })
res.json(user)
} catch (error) {
next(error)
}
}
exports.destroy = async (req, res, next) => {
try {
await User.findByIdAndDelete(req.params.id)
res.status(204).send()
} catch (error) {
next(error)
}
}routes/users.js:
javascript
const express = require("express")
const router = express.Router()
const userController = require("../controllers/userController")
const { validateUser } = require("../middleware/validate")
router.get("/", userController.list)
router.get("/:id", userController.show)
router.post("/", validateUser, userController.create)
router.put("/:id", validateUser, userController.update)
router.delete("/:id", userController.destroy)
module.exports = router常见问题
Q1: 路由参数和查询字符串的区别?
javascript
// 路由参数 - URL 路径的一部分
app.get("/users/:id", (req, res) => {
console.log(req.params.id) // "123"
})
// GET /users/123
// 查询字符串 - ? 后面的部分
app.get("/search", (req, res) => {
console.log(req.query.q) // "node"
console.log(req.query.page) // "1"
})
// GET /search?q=node&page=1| 特点 | 路由参数 | 查询字符串 |
|---|---|---|
| 位置 | URL 路径 | ? 后面 |
| 获取方式 | req.params | req.query |
| 适用场景 | 标识资源 | 过滤、分页、排序 |
| 示例 | /users/123 | /users?page=1&limit=10 |
Q2: 如何处理 404 路由?
javascript
// 在所有路由之后添加
app.use((req, res) => {
res.status(404).json({
error: "资源未找到",
path: req.originalUrl
})
})Q3: 如何实现路由前缀?
javascript
// 方式一:app.use 挂载时指定前缀
app.use("/api/v1", router)
// 方式二:在路由模块内部统一前缀
const router = express.Router()
router.get("/users", ...) // 实际路径 /api/v1/users
// 注意:express.Router() 不支持 prefix 选项
// 以下写法是错误的:
// const router = express.Router({ prefix: '/api' }) ❌Q4: 如何获取客户端原始请求路径?
javascript
app.get("/users/:id", (req, res) => {
console.log(req.originalUrl) // "/users/123?fields=name"
console.log(req.path) // "/users/123"
console.log(req.baseUrl) // "" 或挂载前缀
console.log(req.params.id) // "123"
console.log(req.query) // { fields: "name" }
})Q5: 路由处理异步错误如何处理?
javascript
// Express 4.x 需要手动传递错误
app.get("/users/:id", async (req, res, next) => {
try {
const user = await User.findById(req.params.id)
if (!user) return res.status(404).json({ error: "用户不存在" })
res.json(user)
} catch (error) {
next(error) // 传递给错误处理中间件
}
})
// 使用包装函数简化
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next)
app.get("/users/:id", asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id)
if (!user) return res.status(404).json({ error: "用户不存在" })
res.json(user)
}))Q6: 如何限制路由只接受特定 Content-Type?
javascript
const requireJson = (req, res, next) => {
if (!req.is("application/json")) {
return res.status(415).json({ error: "需要 application/json 格式" })
}
next()
}
app.post("/users", requireJson, (req, res) => {
res.json({ message: "创建成功" })
})最佳实践总结
| 实践 | 说明 |
|---|---|
| 模块化组织 | 使用 express.Router() 拆分路由到独立文件 |
| 控制器分离 | 路由只负责 URL 映射,业务逻辑放在控制器 |
| 静态路径优先 | 具体路径放在参数化路由之前 |
| 统一响应格式 | 成功/错误响应使用统一的数据结构 |
| 参数验证 | 使用中间件验证路由参数有效性 |
| 错误传递 | 异步错误使用 next(error) 传递 |
| RESTful 规范 | 遵循 HTTP 方法语义和资源命名规范 |