CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Node.js 后端入门/课时/Express 入门
RawBlame
课时目录12 节

Node.js 后端入门

  1. 01Node.js 是什么
  2. 02运行时与 process
  3. 03模块与 npm
  4. 04文件系统与 path
  5. 05http 模块
  6. 06Express 入门
  7. 07事件循环与异步
  8. 08环境变量与配置
  9. 09Streams 基础
  10. 10错误处理
  11. 11测试 node:test
  12. 12综合练习与选型

Express 入门

路由、中间件、静态文件、与 Spring MVC 对照。

40 分钟

Express 入门

这一课你将能做到

  1. 用 Express 搭出带路由参数、查询参数的 REST API,并对接 Postman/curl 验证
  2. 编写并正确排序中间件(含 express.json()、自定义日志、静态资源)
  3. 用错误处理中间件统一兜底 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:

// 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;

app.js 挂载:

const booksRouter = require('./routes/books');
app.use(express.json());
app.use('/api/books', booksRouter);   // 前缀 + 子路由表

routes/books.js 里只写 / 和 /:id,挂载时统一加 /api/books 前缀——这就是中大型 Express 项目的目录套路:routes/(路由)、controllers/(处理函数)、middlewares/(中间件)分层。

讲清楚要点

  1. 中间件是 (req, res, next) => {} 的函数,app.use / app.get 注册的都会按注册顺序依次执行;next() 放行,不调 next 也不响应就是「悬挂请求」。
  2. res.send / res.json 是「一站式」响应:自动设 Content-Type、自动 end。用原生式 res.writeHead + res.end 反而是反模式。
  3. 四参数错误中间件必须注册在最后:普通中间件三个参数,错误中间件四个(err 打头)。Express 按参数个数区分身份,写错就永远收不到错误。
  4. req.params / req.query / req.body 分工:params 来自路径(:id),query 来自 ?a=b,body 来自请求体(需 express.json)。三者都可能是字符串,数字比较前要转类型。
  5. 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.jsonERR_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) 再比

卡点急救

  1. Cannot find module 'express':没装依赖就运行了。在项目目录 npm install express,确认 node_modules 生成。
  2. req.body 是 undefined:app.use(express.json()) 没注册,或注册位置在路由之后;curl 测试也要带 -H "Content-Type: application/json"。
  3. 请求无响应一直转圈:某个中间件既没响应也没 next。检查流水线每个分支的出口。
  4. 改代码不生效:没重启。npm run dev(node --watch)替代手动重启。
  5. 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,配置走环境变量。

自测三问

  1. 一个请求先后经过「json 中间件 → 日志中间件 → 路由」,三者注册顺序反了会发生什么?
  2. 错误处理中间件的函数签名是什么?它必须注册在什么位置?
  3. req.params.id、req.query.keyword、req.body.title 分别从哪来?各自的类型要注意什么?

小结一句话

Express = http 模块 + 中间件流水线 + 路由表:按序注册中间件、Router 拆模块、404 与错误中间件收尾,就是它的全部骨架。

延伸

  • Express 官方文档(中文)
  • Express 路由指南
  • 下一课:事件循环与异步——搞懂 Node 单线程高并发的底层机制,告别「回调地狱」与「为什么执行顺序和写的顺序不一样」。
加载本节练习…
← 上一课http 模块下一课 →事件循环与异步