{T}

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)
参数说明
appExpress 应用实例
METHODHTTP 方法(小写):getpostputdeletepatch
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多单词可读性好
避免动词/getUsersHTTP 方法已表示动作
嵌套不超过两级/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 = router

app.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.paramsreq.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 方法语义和资源命名规范

参考资源