首页 / Node.js 教程 / Express 路由

Node.js 教程

Express 路由

本教程共 76 篇 · 第 39 篇 · 更新于 2026-07-25 · 约 5 分钟阅读

Node.jsExpress路由Router参数

39. Express 路由

本节目标:路由方法、参数、查询字符串和 Router 拆分。

路由是 Web 框架的脊梁。没有它,所有请求都砸向同一个回调,代码很快变成一碗面条。Express 的路由系统既简单直白,又足够灵活——小到一行回调,大到按业务模块拆分,都能应付。

路由基础

路由的本质是「匹配路径 + HTTP 方法 → 执行处理函数」。Express 为每种 HTTP 方法都提供了对应的方法:

import express from 'express'
const app = express()

app.get('/', (req, res) => res.send('GET 首页'))
app.post('/users', (req, res) => res.send('创建用户'))
app.put('/users/:id', (req, res) => res.send('更新用户'))
app.delete('/users/:id', (req, res) => res.send('删除用户'))
app.patch('/users/:id', (req, res) => res.send('部分更新'))

如果你希望同一条路径响应所有方法,用 app.all()。注意 Express 5 的写法:

// 对 /health 路径响应任何 HTTP 方法
app.all('/health', (req, res) => {
  res.json({ status: 'ok', method: req.method })
})
Tip

app.all('/*splat') 可以放在所有路由最后面当「兜底」404,捕获前面没匹配到的所有请求。顺序很重要——Express 按代码出现的先后顺序匹配,谁在前谁先赢。

路由参数

URL 里经常带有动态 ID,比如 /users/42/posts/2024/summary。Express 用冒号 : 标记参数段:

app.get('/users/:userId/books/:bookId', (req, res) => {
  res.json({
    userId: req.params.userId,
    bookId: req.params.bookId
  })
})

访问 /users/42/books/7 会得到 {"userId":"42","bookId":"7"}。参数值永远是字符串,如果需要数字自己转换:

const id = Number(req.params.userId)
if (Number.isNaN(id)) {
  return res.status(400).json({ error: 'Invalid user id' })
}

参数名里不能用连字符 -,因为会被当成减号解析。如果 URL 里真有带横线的标识,改用下划线或者放在查询字符串里。

查询字符串

路由参数用来定位资源,查询字符串(query string)用来过滤、排序、分页。Express 自动帮你解析 ? 后面的内容:

app.get('/products', (req, res) => {
  const { category, page = '1', limit = '10' } = req.query
  res.json({
    category,
    page: Number(page),
    limit: Number(limit)
  })
})

访问 /products?category=phone&page=2&limit=20req.query 就是一个普通对象:

{ "category": "phone", "page": "2", "limit": "20" }
Warning

req.query 的值全是字符串。如果你拿它做数学运算或数据库查询,记得先转类型,不然 page + 1 会变成 "21" 而不是 3

路由拆分:Router

接口一多,app.js 就会膨胀。Express 提供了 express.Router(),让你按业务模块拆路由文件。

假设项目结构如下:

routes/
├── users.js
└── posts.js
app.js

routes/users.js

import express from 'express'
const router = express.Router()

router.get('/', (req, res) => {
  res.json([{ id: 1, name: 'tom' }])
})

router.get('/:id', (req, res) => {
  res.json({ id: req.params.id, name: 'tom' })
})

router.post('/', (req, res) => {
  res.status(201).json({ message: 'user created' })
})

export default router

app.js 里挂载:

import express from 'express'
import userRoutes from './routes/users.js'

const app = express()

app.use('/api/users', userRoutes)

这样 /api/users 指向用户列表,/api/users/42 指向单个用户。Router 本身也是一个中间件,你可以给它加专属的前置逻辑:

router.use((req, res, next) => {
  console.log('Users router hit:', req.url)
  next()
})

这段日志只会在访问 /api/users/* 时触发,不会影响别的路由。

路由匹配规则

Express 的路由匹配不只是「字符串相等」,它支持一些模式语法:

// ? 表示前面的字符可选
app.get('/ab?cd', handler)   // 匹配 /abcd 和 /acd

// + 表示前面的字符重复一次或多次
app.get('/ab+cd', handler)   // 匹配 /abcd, /abbcd, /abbbcd ...

// () 分组
app.get('/a(bc)?d', handler) // 匹配 /ad 和 /abcd

正则也能直接用:

// 匹配包含 "fly" 的路径
app.get(/.*fly$/, (req, res) => {
  res.send(req.path)
})

不过实际项目中,99% 的场景用普通字符串和 : 参数就够了。复杂的正则路由虽然强大,但可读性差,维护时容易踩坑。

多个处理函数与 next()

一条路由可以挂多个处理函数,它们按顺序执行。关键在于第三个参数 next()

app.get('/demo',
  (req, res, next) => {
    console.log('第一步')
    req.timestamp = Date.now()
    next()   // 把控制权交给下一个函数
  },
  (req, res) => {
    console.log('第二步,timestamp:', req.timestamp)
    res.send('Done')
  }
)

如果你忘了调用 next(),请求就会挂在那里直到超时。反之,如果你已经调用了 res.send() 又跑去调用 next(),Express 会抛「Cannot set headers after they are sent」错误。这个坑我踩过不止一次。

路由顺序的陷阱

Express 的路由匹配是「先匹配先返回」,所以宽泛的路径要放在后面。

// 错误示范
app.get('/:id', (req, res) => res.send('任意ID'))
app.get('/users', (req, res) => res.send('用户列表'))

这段代码里,访问 /users 永远不会走到第二条路由,因为 /users 已经被 /:id 截获了。正确顺序是:

app.get('/users', handler)
app.get('/:id', handler)
Tip

把精确路由放前面,参数路由放中间,兜底路由放最后。这既是 Express 的要求,也是所有路由框架的通用原则。