Express 入门
这一课你将能做到
- 用 Express 搭出带路由参数、查询参数的 REST API,并对接 Postman/curl 验证
- 编写并正确排序中间件(含
express.json()、自定义日志、静态资源)
- 用错误处理中间件统一兜底 404 与业务异常
为什么要学
上一课用原生 http 模块手工干了三件苦活:if-else 判断路由、监听 data 事件收 body、手动设状态码和头。请求一多代码就乱成面条。Express 人话:给 http 模块套上一层「中间件流水线 + 路由表」,把苦活变成一行行的声明。
它是 Node 生态事实上的 Web 框架标准(Koa、Fastify 的设计都绕不开它的中间件概念),学会 Express,等于同时看懂了大半 Node Web 框架的设计语言。
先建立图像
Express 的核心是「中间件流水线」:请求像传送带上的包裹,依次经过你注册的函数,每个函数可以选择处理它、修改它,或者传递给下一个:
请求 GET /api/users/42
│
▼
[express.json()] ──► [我的日志中间件] ──► [路由匹配 /api/users/:id]
│ │ │
收集body 打印耗时 取出 id=42 处理
│ │ │
└────── 每个环节都能调用 next() 交给下一个 ──┘
│
出错时 → next(err) 直达错误处理中间件 → 返回 500
三条铁律先记住:中间件有顺序;不调 next 就要自己收尾响应;next(err) 会跳过中间所有环节直达错误处理。
跟着做
步骤 1:安装并起最小应用
新建 node-express 目录,npm init -y,安装:
npm install express
新建 app.js:
// app.js
const express = require('express');
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
res.send('hello express');
});
app.listen(PORT, () => {
console.log(`http://localhost:${PORT}`);
});
node app.js
浏览器访问 http://localhost:3000 看到 hello express。对比原生 http 版:没有 createServer、没有手动设头,res.send 自动判断类型并设置 Content-Type、自动 end。
开发期建议顺手把 package.json 的 scripts 改成热重载:
"scripts": {
"dev": "node --watch app.js"
}
npm run dev # 改代码自动重启,Node 20 内置 --watch,无需 nodemon
步骤 2:路由参数与查询参数
把 CRUD 骨架补上:
// app.js(在 app.get('/') 之后追加)
const books = [
{ id: 1, title: 'Node 入门' },
{ id: 2, title: 'Express 实战' },
];
// 列表:GET /api/books?keyword=Express
app.get('/api/books', (req, res) => {
const keyword = req.query.keyword; // 查询参数
const result = keyword
? books.filter(b => b.title.includes(keyword))
: books;
res.json({ data: result, total: result.length });
});
// 详情:GET /api/books/1
app.get('/api/books/:id', (req, res) => {
const book = books.find(b => b.id === Number(req.params.id));
if (!book) {
return res.status(404).json({ error: '图书不存在' });
}
res.json(book);
});
// 创建:POST /api/books
app.post('/api/books', (req, res) => {
const { title } = req.body ?? {};
if (!title) {
return res.status(400).json({ error: 'title 必填' });
}
const book = { id: books.length + 1, title };
books.push(book);
res.status(201).json(book);
});
注意别忘了注册 body 解析(见步骤 3),否则 req.body 是 undefined。测试:
curl "http://localhost:3000/api/books?keyword=Express"
# {"data":[{"id":2,"title":"Express 实战"}],"total":1}
curl -X POST http://localhost:3000/api/books ^
-H "Content-Type: application/json" -d "{\"title\":\"新书\"}"
# {"id":3,"title":"新书"}
对照上一课::id 路由参数取代了 if 判断和手工解析;req.body 取代了监听 data/end;res.json 取代了 stringify + 设头。
步骤 3:中间件——顺序就是生命线
// app.js —— 中间件必须注册在"要用它的路由之前"
app.use(express.json()); // ① 解析 JSON body → req.body
app.use(express.urlencoded({ extended: true })); // ② 顺带支持表单提交
// ③ 自定义日志中间件
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => { // 响应完成时触发
console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${Date.now() - start}ms`);
});
next(); // 必须调用,流水线才能继续
});
// ④ 静态资源:public 目录下的文件按路径直接可访问
app.use(express.static('public'));
curl http://localhost:3000/api/books
# GET /api/books 200 3ms ← 终端里出现
把 app.use(express.json()) 挪到 POST 路由之后会怎样?req.body 变回 undefined——顺序错了,包裹到达驿站时快递员已经下班。这就是「中间件顺序就是生命线」的直观版本。
步骤 4:404 与统一错误处理
在 app.js 末尾(所有路由之后)追加两个收尾中间件:
// 404:能走到这里,说明没有路由匹配上
app.use((req, res) => {
res.status(404).json({ error: `路径不存在: ${req.originalUrl}` });
});
// 错误处理中间件:4 个参数一个都不能少(Express 靠参数个数识别)
app.use((err, req, res, next) => {
console.error(err);
res.status(err.status ?? 500).json({ error: err.message ?? '服务器内部错误' });
});
// 放在某条路由里,比如详情路由改造:
app.get('/api/books/:id', (req, res, next) => {
const book = books.find(b => b.id === Number(req.params.id));
if (!book) {
const err = new Error('图书不存在');
err.status = 404;
return next(err); // 跳过中间所有环节,直达错误中间件
}
res.json(book);
});
curl http://localhost:3000/api/books/99
# {"error":"图书不存在"} ← 状态码 404,由 err.status 提供
整个错误链路:业务代码 next(err) → 错误中间件统一记录、统一格式返回。异常处理从「每个路由自己写 try-catch」变成「全站一个出口」。
步骤 5:用 Router 拆分模块
路由一多,全塞 app.js 会失控。Express 的 Router 就是「可挂载的子路由表」。
// routes/books.js
const express = require('express');
const router = express.Router();
const books = [
{ id: 1, title: 'Node 入门' },
];
router.get('/', (req, res) => {
res.json({ data: books });
});
router.get('/:id', (req, res, next) => {
const book = books.find(b => b.id === Number(req.params.id));
if (!book) {
const err = new Error('图书不存在');
err.status = 404;
return next(err);
}
res.json(book);
});
router.post('/', (req, res) => {
const book = { id: books.length + 1, title: req.body.title };
books.push(book);
res.status(201).json(book);
});
module.exports = router;
const booksRouter = require('./routes/books');
app.use(express.json());
app.use('/api/books', booksRouter); // 前缀 + 子路由表
routes/books.js 里只写 / 和 /:id,挂载时统一加 /api/books 前缀——这就是中大型 Express 项目的目录套路:routes/(路由)、controllers/(处理函数)、middlewares/(中间件)分层。
讲清楚要点
- 中间件是
(req, res, next) => {} 的函数,app.use / app.get 注册的都会按注册顺序依次执行;next() 放行,不调 next 也不响应就是「悬挂请求」。
res.send / res.json 是「一站式」响应:自动设 Content-Type、自动 end。用原生式 res.writeHead + res.end 反而是反模式。
- 四参数错误中间件必须注册在最后:普通中间件三个参数,错误中间件四个(err 打头)。Express 按参数个数区分身份,写错就永远收不到错误。
req.params / req.query / req.body 分工:params 来自路径(:id),query 来自 ?a=b,body 来自请求体(需 express.json)。三者都可能是字符串,数字比较前要转类型。
- Express 5 会自动转发 async 函数抛出的错误到错误中间件;Express 4 不会(rejected promise 会被吞),要么手写 try-catch + next(err),要么用 express-async-errors 这类补丁包。看到本课代码里显式
next(err) 的写法,兼容两个大版本。
你可能会懵的地方
-
app.use 和 app.get 有什么区别?
app.get('/path', fn) 只匹配该路径的 GET 请求;app.use('/path', fn) 匹配该路径前缀下的所有方法,且常用来挂中间件/子路由。404 和错误兜底必须用 app.use,因为没有具体路径。
-
express.json() 到底干了什么?
对应上一课的手工活:检查 Content-Type 是 application/json → 监听 req 的 data/end → 拼接 → JSON.parse → 挂到 req.body。没注册它,req.body 就是 undefined。
-
中间件里 next() 之后还能写代码吗?
能,但不该有「副作用」。规范写法:next() 后立刻 return,避免「响应已经发出还继续处理」的双重响应错误。日志中间件里用 res.on('finish') 就是为了在响应后拿状态码。
-
静态资源和 API 路由会打架吗?
express.static('public') 只在 public 里有同名文件时响应,否则 next 放行。习惯把 static 放在 API 路由之后、404 之前,让 API 优先匹配。
-
为什么我的 404 中间件拦截了正常请求?
它注册在了正常路由前面。404 依赖「前面的路由都没匹配上」这一事实,永远放最后(错误中间件之前)。
错法对照
| 错法 | 问题 | 正解 |
|---|
express.json() 注册在 POST 路由之后 | req.body 永远 undefined | 中间件注册在需要它的路由之前 |
| 错误中间件写成 3 个参数 | Express 不识别,错误直接 500 默认页 | (err, req, res, next) 四参数,一个不少 |
res.send 之后又 res.json | ERR_HTTP_SEND_BUFFERED/二次响应报错 | 每个分支只响应一次,响应后 return |
404 用 app.get('*', ...) | Express 5 中 * 语法已变(需 /*splat) | 用无路径的 app.use((req,res)=>...) 兜底 |
忘记调用 next() | 请求悬挂,客户端一直等 | 处理完响应,否则 next() 放行 |
req.params.id === 1 比较数字 | params 是字符串,永不相等 | Number(req.params.id) 再比 |
卡点急救
Cannot find module 'express':没装依赖就运行了。在项目目录 npm install express,确认 node_modules 生成。
req.body 是 undefined:app.use(express.json()) 没注册,或注册位置在路由之后;curl 测试也要带 -H "Content-Type: application/json"。
- 请求无响应一直转圈:某个中间件既没响应也没 next。检查流水线每个分支的出口。
- 改代码不生效:没重启。
npm run dev(node --watch)替代手动重启。
EADDRINUSE:上一版进程还占着 3000 端口,关掉旧终端或 taskkill;换端口也行。
面试常问
-
Express 的中间件机制是什么?
一组按注册顺序执行的 (req, res, next) 函数,请求依次穿过;next() 调用下一个,next(err) 跳过后续直达四参数错误中间件。路由本质上也是中间件(多了路径与方法匹配)。这构成了 Express 的洋葱式执行流。
-
错误处理中间件为什么必须 4 个参数?
Express 内部按 fn.length 判断函数身份:4 参数(err, req, res, next)视为错误处理器,只接收 next(err) 传来的错误;3 参数视为普通中间件。少一个参数就永远收不到错误。
-
Express 和原生 http 模块的关系?
Express 基于 http.createServer 封装:注册的所有中间件最终被组织成一个大函数,传给 createServer。它补齐了路由、body 解析、静态资源、错误集中处理等 Web 框架必备能力,底层 req/res 仍是 Node 的流。
-
怎么组织一个中型 Express 项目?
app.js 只做装配:注册全局中间件、挂载 Router(按资源拆分 routes/)、404 与错误中间件收尾;业务逻辑下沉到 controllers/service,配置走环境变量。
自测三问
- 一个请求先后经过「json 中间件 → 日志中间件 → 路由」,三者注册顺序反了会发生什么?
- 错误处理中间件的函数签名是什么?它必须注册在什么位置?
req.params.id、req.query.keyword、req.body.title 分别从哪来?各自的类型要注意什么?
小结一句话
Express = http 模块 + 中间件流水线 + 路由表:按序注册中间件、Router 拆模块、404 与错误中间件收尾,就是它的全部骨架。
延伸