Express 基础到中间件:系统掌握常用 API 与请求处理链

Express 基础到中间件:系统掌握常用 API 与请求处理链

  • 前言
  • [1. Express 基础:先建立运行模型](#1. Express 基础:先建立运行模型)
    • [1.1 Express 是什么,`express()` 又创建了什么](#1.1 Express 是什么,express() 又创建了什么)
    • [1.2 `express.json()` 与 `app.listen()` 各自负责什么](#1.2 express.json()app.listen() 各自负责什么)
  • [2. Express 路由:让 HTTP 方法找到业务处理逻辑](#2. Express 路由:让 HTTP 方法找到业务处理逻辑)
    • [2.1 路由由请求方法、路径和处理函数共同组成](#2.1 路由由请求方法、路径和处理函数共同组成)
    • [2.2 用一组用户路由理解 GET、POST、PUT、PATCH、DELETE](#2.2 用一组用户路由理解 GET、POST、PUT、PATCH、DELETE)
  • [3. Request 请求对象:数据究竟从哪里来](#3. Request 请求对象:数据究竟从哪里来)
    • [3.1 `req.body`、`req.query`、`req.params` 与 `req.headers`](#3.1 req.bodyreq.queryreq.paramsreq.headers)
    • [3.2 三个容易踩中的请求数据误区](#3.2 三个容易踩中的请求数据误区)
  • [4. Response 响应对象:状态码与数据如何返回前端](#4. Response 响应对象:状态码与数据如何返回前端)
    • [4.1 `res` 对象与常用响应方法](#4.1 res 对象与常用响应方法)
    • [4.2 状态码不是装饰,而是响应语义的一部分](#4.2 状态码不是装饰,而是响应语义的一部分)
  • [5. Express 中间件:一条按顺序推进的请求处理链](#5. Express 中间件:一条按顺序推进的请求处理链)
    • [5.1 什么是 Middleware,`app.use()` 做了什么](#5.1 什么是 Middleware,app.use() 做了什么)
    • [5.2 `next()` 为什么能进入下一层,执行顺序从哪里来](#5.2 next() 为什么能进入下一层,执行顺序从哪里来)
    • [5.3 `app.use(express.json())` 为什么仍然是在注册中间件](#5.3 app.use(express.json()) 为什么仍然是在注册中间件)
    • [5.4 自定义中间件与路由到底是什么关系](#5.4 自定义中间件与路由到底是什么关系)
  • [6. 错误处理中间件:让异常沿另一条链统一收口](#6. 错误处理中间件:让异常沿另一条链统一收口)
    • [6.1 `next()` 与 `next(error)` 的本质区别](#6.1 next()next(error) 的本质区别)
    • [6.2 从路由异常到统一错误响应的完整过程](#6.2 从路由异常到统一错误响应的完整过程)
    • [6.3 为什么错误处理中间件通常放在路由后面](#6.3 为什么错误处理中间件通常放在路由后面)
  • [7. 串起 Express 的完整请求生命周期](#7. 串起 Express 的完整请求生命周期)
    • [7.1 一次请求如何穿过普通链与错误链](#7.1 一次请求如何穿过普通链与错误链)
    • [7.2 用一套简化示例完成最终复盘](#7.2 用一套简化示例完成最终复盘)
  • 总结

前言

刚接触 Express 时,很多人会把它理解成一组 API:用 app.get() 写查询接口,用 app.post() 接收数据,再用 res.json() 返回结果。这样确实能把服务跑起来,但一旦加入登录校验、请求日志、参数解析和统一错误处理,代码很容易变得混乱。

真正理解 Express 的关键,不是背诵方法名,而是建立一幅清晰的运行图景:一次 HTTP 请求进入应用后,会沿着注册顺序依次经过若干处理函数;每一层都可以读取请求、修改上下文、结束响应,或者把控制权交给下一层。 这条按顺序执行的处理链,就是 Express 最核心的设计。

本文先梳理创建应用、路由、请求对象和响应对象,再重点拆解普通中间件与错误处理中间件。读完后,不仅能写出常见的增删改查接口,也能解释 app.use(express.json()) 为什么有效、next() 究竟做了什么,以及异常如何从路由一路流向统一错误出口。

1. Express 基础:先建立运行模型

1.1 Express 是什么,express() 又创建了什么

Express 是运行在 Node.js 之上的 Web 框架。Node.js 原生 http 模块已经能接收请求、发送响应,但开发者还要自行判断 URL、区分请求方法、解析请求体,并组织大量重复逻辑。Express 在这些底层能力之上提供了更简洁的路由系统和中间件机制,让开发者把精力集中在业务处理上。

Express 应用可以看成一个由路由和中间件组成的请求处理器。 它接收 Node.js 的请求与响应对象,对其进行增强,然后按照注册顺序调度处理函数。

安装 Express 后,可以用最少的代码启动一个 HTTP 服务:

bash 复制代码
npm install express
javascript 复制代码
const express = require('express');

const app = express();

app.get('/hello', (req, res) => {
  res.json({ message: 'Hello Express' });
});

app.listen(3000, '127.0.0.1', () => {
  console.log('Server is running at http://127.0.0.1:3000');
});

express() 是一个工厂函数,调用后返回应用对象 app。后续的 app.use()app.get()app.post()app.listen(),本质上都在配置或启动这个应用。上面的请求到达后,Express 会寻找与 GET /hello 匹配的处理函数,并通过 res.json() 结束本次请求---响应周期。

1.2 express.json()app.listen() 各自负责什么

客户端发送 JSON 时,网络上传输的是一段字节流,Express 不会凭空知道它对应哪个 JavaScript 对象。express.json() 提供 JSON 请求体解析能力,常见注册方式如下:

javascript 复制代码
app.use(express.json());

app.post('/users', (req, res) => {
  console.log(req.body);
  res.status(201).json({ data: req.body });
});

当请求头包含合适的 Content-Type: application/json 时,解析器会读取请求体、尝试解析 JSON,并把结果放到 req.body。因此它通常要注册在需要读取 req.body 的路由之前。解析失败时,错误会进入 Express 的错误处理流程。

app.listen() 则让应用开始监听网络连接。常见参数如下表所示:

参数 作用 常见取值
port 指定监听端口 30008080
host 指定监听地址 127.0.0.10.0.0.0
callback 监听成功后执行 输出启动信息、执行初始化提示

127.0.0.1 只允许本机访问,适合本地开发;0.0.0.0 表示监听本机所有 IPv4 网络接口,常用于容器或局域网访问。只写 app.listen(3000) 时,底层会使用系统默认的未指定地址,具体表现可能受操作系统网络栈影响。开发阶段显式写出 host,更容易判断服务究竟对谁开放。

2. Express 路由:让 HTTP 方法找到业务处理逻辑

2.1 路由由请求方法、路径和处理函数共同组成

HTTP 请求不只有 URL,还包含请求方法。Express 用 app.get()app.post()app.put()app.patch()app.delete() 对应常见 HTTP 方法。它们的基本形式都是:

javascript 复制代码
app.METHOD(path, handler);

只有请求方法与路径都匹配 ,对应的处理函数才会进入执行流程。例如 app.get('/users') 不会处理 POST /usersapp.post('/users') 也不会处理 POST /articles

Express API HTTP 方法 常见语义 典型结果
app.get() GET 查询资源 返回列表或详情
app.post() POST 创建资源 创建成功后返回新资源
app.put() PUT 整体替换资源 用完整数据替换旧状态
app.patch() PATCH 局部更新资源 只修改提交的字段
app.delete() DELETE 删除资源 返回删除结果或空响应

这些语义是 API 设计约定,不是 Express 自动实现的业务规则。比如调用 app.delete() 不会自动删除数据库记录,它只是匹配 DELETE 请求;真正的删除动作仍由处理函数完成。

2.2 用一组用户路由理解 GET、POST、PUT、PATCH、DELETE

下面用内存数组演示五种常见路由。示例重点是观察 HTTP 方法与业务意图的对应关系:

javascript 复制代码
const users = [{ id: 1, name: 'Ada', role: 'admin' }];

app.get('/users', (req, res) => {
  res.json({ data: users });
});

app.post('/users', (req, res) => {
  const user = { id: Date.now(), ...req.body };
  users.push(user);
  res.status(201).json({ data: user });
});

app.put('/users/:id', (req, res) => {
  const id = Number(req.params.id);
  const index = users.findIndex((user) => user.id === id);
  users[index] = { id, name: req.body.name, role: req.body.role };
  res.json({ data: users[index] });
});

app.patch('/users/:id', (req, res) => {
  const id = Number(req.params.id);
  const user = users.find((item) => item.id === id);
  Object.assign(user, req.body);
  res.json({ data: user });
});

app.delete('/users/:id', (req, res) => {
  const id = Number(req.params.id);
  const index = users.findIndex((user) => user.id === id);
  const [removedUser] = users.splice(index, 1);
  res.json({ data: removedUser });
});

PUTPATCH 最容易混淆。可以把 PUT 理解为"提交一份完整的新状态",未提交字段可能不再保留;PATCH 则表示"只提交需要修改的部分"。实际项目还需要处理参数校验、资源不存在、ID 非法等情况,这些问题会在后面的错误处理链中统一解决。

3. Request 请求对象:数据究竟从哪里来

3.1 req.bodyreq.queryreq.paramsreq.headers

路由处理函数中的 req 表示当前请求。请求数据可能位于路径、查询字符串、请求体或请求头中,位置不同,表达的含义也不同。假设客户端发起如下请求:

text 复制代码
PATCH /users/42?notify=true
Content-Type: application/json
Authorization: Bearer example-token

{"name":"Grace"}

在 Express 中可以这样读取:

javascript 复制代码
app.patch('/users/:id', (req, res) => {
  const userId = req.params.id;
  const shouldNotify = req.query.notify;
  const changes = req.body;
  const authorization = req.headers.authorization;

  res.json({ userId, shouldNotify, changes, authorization });
});

四类数据的区别如下:

属性 数据位置 示例结果 适合表达
req.params 路径占位符 { id: '42' } 明确定位某个资源
req.query ? 后的查询字符串 { notify: 'true' } 筛选、分页、排序、可选开关
req.body HTTP 请求体 { name: 'Grace' } 创建或修改时提交的主体数据
req.headers HTTP 请求头 { authorization: 'Bearer ...' } 认证、内容类型、客户端能力等元信息

其中 bodyqueryparams 的核心区别,不只是读取方式不同,更在于它们承担的语义不同:路径参数标识"操作谁",查询参数描述"怎样查询或附加什么选项",请求体承载"要提交什么数据"。

3.2 三个容易踩中的请求数据误区

第一,路径参数和查询参数通常都是字符串。req.params.id 读取到的 42 默认是字符串,req.query.notify 读取到的 true 也未必是布尔值,因此参与计算或判断前要主动转换并校验。

第二,req.body 不是天然存在的可靠对象。应用需要先注册对应的解析中间件;客户端也要发送匹配的 Content-Type。即使解析成功,其中的数据仍然来自外部输入,不能直接用于数据库操作或权限判断。

第三,Node.js 会把常见请求头名称规范为小写,因此通常通过 req.headers.authorizationreq.headers['content-type'] 读取。请求头适合携带元信息,不适合塞入大段业务数据。

所有来自 req 的值都应视为不可信输入。 读取只是第一步,类型转换、格式校验、权限判断和长度限制同样属于接口边界的一部分。

4. Response 响应对象:状态码与数据如何返回前端

4.1 res 对象与常用响应方法

路由处理函数中的 res 是 Express 提供的 Response 响应对象,代表服务器准备返回给当前客户端的 HTTP 响应。Express 在 Node.js 原生响应对象之上增加了许多便捷方法,因此后端可以通过它设置状态码和响应头、写入响应体,并最终结束请求---响应周期。

res.json()res.send() 等并不是普通的业务函数,而是响应对象上的方法。它们解决的问题是:把 JavaScript 中的处理结果转换成符合 HTTP 规则的响应并发送给客户端。 常用方法的职责如下:

API 是否直接结束响应 主要作用 典型场景
res.json(data) 将数据序列化为 JSON,并设置 JSON 内容类型 REST API 返回对象、数组或错误信息
res.send(data) 根据数据类型发送字符串、HTML、Buffer 等内容 返回文本、HTML 或二进制内容
res.status(code) 设置 HTTP 状态码,并返回 res 自身 json()send()end() 链式组合
res.redirect(url) 返回重定向响应,默认使用 302 状态码 登录后跳转、旧地址迁移
res.sendStatus(code) 设置状态码,并发送对应的简短状态文本 快速返回 404、403 等结果
res.end() 直接结束响应,不再写入正文 返回 204 无内容响应
javascript 复制代码
app.get('/health', (req, res) => {
  res.send('OK');
});

app.get('/profile', (req, res) => {
  res.json({ id: 1, name: 'Ada' });
});

app.post('/users', (req, res) => {
  const user = { id: 2, ...req.body };
  res.status(201).json({ data: user });
});

app.get('/old-home', (req, res) => {
  res.redirect('/home');
});

app.delete('/sessions/current', (req, res) => {
  res.status(204).end();
});

res.json() 会把对象或数组序列化为 JSON,并设置相应的 Content-Type,因此它是接口返回结构化数据时最清晰的选择。res.send() 更通用:传入字符串时可以返回文本或 HTML,传入 Buffer 时可以返回二进制内容。虽然 res.send() 也能处理部分 JavaScript 数据类型,但 API 返回对象或数组时优先使用 res.json(),能让代码意图更加明确。

res.status(201) 只负责把状态码设置为 201,并没有立即发送响应。因为该方法返回的仍是当前 res,所以可以继续调用 .json()。这就是 res.status().json() 能链式书写的原因,等价写法如下:

javascript 复制代码
res.status(201);
res.json({ message: 'Created' });

res.redirect('/home') 会要求客户端访问新的地址,默认状态码为 302;需要表达永久迁移时,也可以使用 res.redirect(301, '/home')res.sendStatus(404) 相当于快速设置 404 并发送简短的状态文本,而 res.status(404).json({ message: '资源不存在' }) 更适合需要统一 JSON 错误结构的接口。

4.2 状态码不是装饰,而是响应语义的一部分

前端通常会同时读取状态码和响应体。状态码用于快速表达请求结果,JSON 则携带更具体的数据或错误说明。

状态码 含义 常见使用场景
200 请求成功 查询、更新成功
201 资源创建成功 POST 创建成功
204 成功但无响应体 删除成功且无需返回数据
400 请求不合法 参数格式错误、缺少必填项
401 未通过身份认证 未登录、令牌无效
403 已识别身份但无权限 普通用户访问管理功能
404 资源不存在 根据 ID 未查到目标
500 服务内部错误 未预期异常

一旦执行 res.json()res.send()res.redirect()res.sendStatus()res.end(),通常就应当 return 或结束当前处理逻辑。响应已经发送后再次发送,会触发"响应头已发送"一类错误。一条请求链最终只能完成一次有效响应。

javascript 复制代码
app.get('/users/:id', (req, res) => {
  const user = users.find((item) => item.id === Number(req.params.id));

  if (!user) {
    return res.status(404).json({ message: '用户不存在' });
  }

  return res.json({ data: user });
});

这里的 return 不是把响应数据返回给 Express,而是立即结束当前 JavaScript 函数,避免后面的 res.json() 再次执行。响应完成后也不应随意调用 next(),否则后续处理层可能继续修改一个已经结束的响应。

5. Express 中间件:一条按顺序推进的请求处理链

5.1 什么是 Middleware,app.use() 做了什么

中间件本质上是一个参与请求---响应周期的函数。普通中间件接收三个参数:reqresnext

javascript 复制代码
function requestLogger(req, res, next) {
  console.log(`${req.method} ${req.originalUrl}`);
  next();
}

app.use(requestLogger);

中间件不是夹在前端和后端之间的神秘服务,而是 Express 请求处理链上的一个函数节点。 它可以执行代码、读取或修改 reqres、直接结束响应,也可以调用 next() 继续向后传递控制权。

三个参数各自承担明确职责:

参数 含义 中间件中的常见用途
req 当前请求对象 读取参数、挂载用户信息、记录请求上下文
res 当前响应对象 设置响应头、直接拒绝请求、返回结果
next Express 提供的推进函数 继续普通流程或把异常交给错误流程

app.use() 用于把中间件注册到应用的处理栈中。不指定路径时,它会参与后续匹配到的各类请求;指定路径时,它只参与该挂载路径及其子路径下的请求。

javascript 复制代码
app.use('/admin', (req, res, next) => {
  console.log('进入管理模块');
  next();
});

这个中间件会参与 /admin/admin/users 等路径的请求,而且不限定 GET 或 POST。它的价值在于把日志、鉴权、跨域处理、请求体解析等横切逻辑从具体业务路由中抽离出来。

5.2 next() 为什么能进入下一层,执行顺序从哪里来

Express 在注册 app.use()app.METHOD() 时,会按照代码执行顺序把对应处理层加入内部栈。请求到达后,调度器从前往后检查每一层:路径和请求方法满足条件时执行;不满足时继续寻找下一层。

next 不是开发者提前定义好的下一个业务函数,而是 Express 在调度当前层时传入的推进函数。它关联着当前请求的调度状态。调用 next() 后,Express 会从当前位置之后继续扫描处理栈,找到下一层符合条件的普通中间件或路由处理函数。

javascript 复制代码
app.use((req, res, next) => {
  console.log('1. 请求日志');
  next();
});

app.use(express.json());

app.post('/users', (req, res) => {
  console.log('2. 进入创建用户路由');
  res.status(201).json({ data: req.body });
});

收到 POST /users 后,请求先经过日志中间件;日志中间件调用 next(),JSON 解析器随后处理请求体;解析器完成后继续推进,最终进入路由并发送响应。这里的顺序不是优先级猜测,而是注册顺序带来的确定结果

如果把 app.use(express.json()) 放到路由后面,路由执行时解析器还没有运行,req.body 就无法按预期获得 JSON 对象。类似地,鉴权中间件放在受保护路由之后,也无法保护前面已经完成响应的路由。

一层处理函数通常有三种选择:

  • 调用 next(),让普通处理链继续向后推进。
  • 调用 res.json()res.send() 等方法结束响应,不再调用 next()
  • 调用 next(error),离开普通流程并进入错误处理流程。

如果既不结束响应也不调用 next(),请求就会停在当前层,客户端持续等待。这正是很多接口"卡住不返回"的常见原因。

5.3 app.use(express.json()) 为什么仍然是在注册中间件

这行代码初看容易产生疑问:普通中间件明明写成 (req, res, next) => {},为什么 express.json() 没有显式传入三个参数?关键在于它包含两个不同阶段:

javascript 复制代码
const jsonParser = express.json();

console.log(typeof jsonParser); // function

app.use(jsonParser);

第一阶段调用 express.json(),它根据配置创建并返回一个中间件函数 ;第二阶段由 app.use() 注册这个返回值。此时只是登记函数,并没有处理具体请求。等请求真正到达时,Express 才会调用 jsonParser(req, res, next)

也可以给解析器传配置:

javascript 复制代码
const jsonParser = express.json({ limit: '100kb' });

app.use(jsonParser);

这种"调用工厂函数,得到已配置中间件"的形式非常常见。express.json() 表面没有写 reqresnext,是因为这三个参数属于未来的请求处理阶段,而当前代码处于中间件创建与注册阶段。

5.4 自定义中间件与路由到底是什么关系

路由处理函数并不是中间件之外的另一套机制。app.get()app.post() 注册的处理函数同样位于请求处理栈中,只是它们增加了更严格的匹配条件:既要匹配路径,也要匹配 HTTP 方法。

javascript 复制代码
function requireAuth(req, res, next) {
  const token = req.headers.authorization;

  if (!token) {
    return res.status(401).json({ message: '请先登录' });
  }

  req.user = { id: 1, role: 'admin' };
  next();
}

app.get('/profile', requireAuth, (req, res) => {
  res.json({ data: req.user });
});

在这个路由中,requireAuth 是路由级中间件。认证失败时,它直接返回 401,后面的业务处理函数不会执行;认证成功时,它把用户信息挂到 req.user,调用 next() 后,下一层便能复用这份上下文。

注册方式 匹配规则 典型职责
app.use(handler) 不限定 HTTP 方法,参与后续请求 全局日志、通用解析
app.use('/api', handler) 匹配路径前缀,不限定 HTTP 方法 模块级鉴权、模块级日志
app.get(path, handler) 匹配 GET 与指定路径 查询业务
app.post(path, handler) 匹配 POST 与指定路径 创建业务
app.METHOD(path, m1, m2) 同一路由内按顺序执行多个处理函数 路由校验、权限检查、业务处理

因此,可以把 Express 应用理解为很多"条件不同、职责不同的中间件层"串起来的结果。全局中间件处理共性问题,路由级中间件处理局部前置条件,最后的业务处理函数生成响应。

6. 错误处理中间件:让异常沿另一条链统一收口

6.1 next()next(error) 的本质区别

普通流程中,next() 表示当前层处理完毕,请继续寻找下一层普通中间件或路由处理函数;next(error) 则告诉 Express 当前请求发生了异常,需要切换到错误处理流程。

调用方式 调度含义 后续寻找目标
next() 正常推进 下一个匹配的普通中间件或路由处理函数
next(error) 进入错误流程 后续匹配的错误处理中间件

错误处理中间件必须保留四个参数,其函数签名是 (err, req, res, next)

javascript 复制代码
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;

  res.status(statusCode).json({
    message: err.message || '服务器内部错误'
  });
});

Express 通过四参数形式识别错误处理中间件。即使当前逻辑没有直接使用最后一个 next,也应保留它,以免该函数被当成普通中间件。

next(error) 不是立即向客户端发送错误,而是把错误和控制权交还给 Express 调度器。 最终由后面的错误处理中间件决定状态码、响应结构和日志策略。

6.2 从路由异常到统一错误响应的完整过程

下面的例子把"前后串联"的过程完整展现出来。路由只负责识别业务异常并向后传递,错误处理中间件负责统一生成响应。

javascript 复制代码
async function findUser(id) {
  if (id === '500') {
    throw new Error('数据服务暂时不可用');
  }

  return id === '1' ? { id: 1, name: 'Ada' } : null;
}

app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await findUser(req.params.id);

    if (!user) {
      const error = new Error('用户不存在');
      error.statusCode = 404;
      throw error;
    }

    res.json({ data: user });
  } catch (error) {
    next(error);
  }
});

app.use((err, req, res, next) => {
  if (res.headersSent) {
    return next(err);
  }

  const statusCode = err.statusCode || 500;

  res.status(statusCode).json({
    message: err.message || '服务器内部错误'
  });
});

这段逻辑的执行顺序可以拆成七步:

  1. 路由处理函数开始执行,并在 try 中等待查询结果。
  2. 查询失败,或者主动抛出"用户不存在"异常。
  3. catch 捕获异常并调用 next(error)
  4. Express 切换到错误处理流程,跳过后续普通处理层。
  5. 调度器从当前位置继续向后寻找四参数错误处理中间件。
  6. 错误处理中间件根据异常设置 404 或 500 状态码。
  7. res.status(statusCode).json(...) 统一向客户端返回 JSON 错误信息。

res.headersSent 判断用于处理响应已经开始发送的特殊情况。此时再设置状态码或响应头可能引发二次错误,因此把异常继续交给 Express 的默认错误处理器更稳妥。

在 Express 5 中,如果返回 Promise 的路由处理函数发生拒绝或抛出异常,Express 能自动把异常交给错误流程;显式的 try/catch + next(error) 仍然有助于初学者看清传递过程,也便于兼容大量 Express 4 项目。无论采用哪种写法,核心都不变:让异常进入统一错误链,而不是在各处随意拼装响应。

6.3 为什么错误处理中间件通常放在路由后面

原因仍然是执行顺序。Express 的调度器从当前层向后寻找下一层,不会在发生异常后回到栈顶重新扫描。如果错误处理中间件注册在路由之前,而异常由后面的路由产生,它就无法接住这次异常。

推荐顺序如下:

javascript 复制代码
app.use(express.json());
app.use(requestLogger);

app.get('/users/:id', getUser);
app.post('/users', createUser);

app.use(notFoundHandler);
app.use(errorHandler);

其中 notFoundHandler 可以在所有业务路由都未响应时创建 404 异常,最后的 errorHandler 则作为统一出口。

javascript 复制代码
function notFoundHandler(req, res, next) {
  const error = new Error('接口不存在');
  error.statusCode = 404;
  next(error);
}

function errorHandler(err, req, res, next) {
  const statusCode = err.statusCode || 500;

  res.status(statusCode).json({
    message: err.message || '服务器内部错误'
  });
}

统一错误处理比每个接口重复响应更合理,主要体现在以下方面:

  • 响应结构一致:前端不必适配多种错误格式。
  • 状态码策略集中:404、400、500 等映射更容易维护。
  • 业务代码更聚焦:路由负责业务成功路径,异常交给统一出口。
  • 日志与安全策略统一:生产环境可集中记录内部错误,同时避免把堆栈和敏感细节直接暴露给客户端。

统一处理并不意味着所有异常都变成 500。合理做法是给可预期的业务错误附加状态码或错误类型,再由错误处理中间件完成最终映射;无法识别的异常才回退到 500。

7. 串起 Express 的完整请求生命周期

7.1 一次请求如何穿过普通链与错误链

到这里,可以把各类 API 放回同一幅运行图景中:

text 复制代码
客户端发送 HTTP 请求
        ↓
Express 接收请求并创建增强后的 req、res
        ↓
按注册顺序进入 app.use() 中间件
        ↓
express.json() 解析 JSON,请求日志与鉴权继续处理
        ↓
app.get() / app.post() 等路由匹配方法与路径
        ↓
正常:res.status().json() 发送结果并结束周期
        ↓
异常:catch 捕获后调用 next(error)
        ↓
Express 跳过普通层,寻找后续错误处理中间件
        ↓
统一设置状态码并通过 res.json() 返回错误信息
        ↓
客户端收到 HTTP 响应

需要注意,正常分支和异常分支不会在同一次处理中全部执行。正常情况下,路由发送响应后周期结束;异常情况下,next(error) 把请求切换到错误链,由错误处理中间件完成响应。

7.2 用一套简化示例完成最终复盘

下面是一套可以直接理解各层位置的简化结构。重点不是业务复杂度,而是观察注册顺序和每一层的职责:

javascript 复制代码
const express = require('express');

const app = express();
const PORT = 3000;
const HOST = '127.0.0.1';

// 1. 内置解析中间件:先把 JSON 转成 req.body
app.use(express.json());

// 2. 自定义普通中间件:记录请求后继续向下
app.use((req, res, next) => {
  console.log(`${req.method} ${req.originalUrl}`);
  next();
});

// 3. 路由:匹配方法和路径,处理正常业务
app.post('/users', (req, res, next) => {
  try {
    const { name } = req.body;

    if (!name) {
      const error = new Error('name 不能为空');
      error.statusCode = 400;
      throw error;
    }

    const user = { id: Date.now(), name };
    res.status(201).json({ data: user });
  } catch (error) {
    next(error);
  }
});

// 4. 未匹配任何业务路由时,创建 404 异常
app.use((req, res, next) => {
  const error = new Error('接口不存在');
  error.statusCode = 404;
  next(error);
});

// 5. 错误处理中间件:必须放在业务路由之后
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;

  res.status(statusCode).json({
    message: err.message || '服务器内部错误'
  });
});

// 6. 启动服务器
app.listen(PORT, HOST, () => {
  console.log(`Server is running at http://${HOST}:${PORT}`);
});

这段代码中,express.json()、日志函数、POST 路由、404 处理和统一错误处理都属于处理链的一部分,只是匹配条件与职责不同。请求体合法时,POST 路由通过 201 和 JSON 结束响应;缺少 name 时,异常经 next(error) 跳转到错误链,以 400 返回;路径不匹配时,404 层主动创建异常,同样交给统一出口。

阶段 关键 API 是否继续向后 主要产物
接收并解析 app.use(express.json()) 解析成功后继续 req.body
通用处理 app.use((req, res, next) => {}) 通常调用 next() 日志、身份、上下文
路由匹配 app.get()app.post() 成功时通常结束响应 业务结果
异常传递 next(error) 切换到错误链 错误对象
统一响应 app.use((err, req, res, next) => {}) 通常结束响应 状态码与 JSON 错误信息

总结

Express 的常用 API 并不是彼此孤立的工具。express() 创建应用,app.listen() 让应用开始接收网络请求;express.json() 等中间件先加工请求,app.get()app.post() 等路由再依据 HTTP 方法和路径选择业务逻辑;处理函数从 req.paramsreq.queryreq.bodyreq.headers 读取不同语义的数据,最后通过 res.status()res.json()res.send() 完成响应。

更重要的是,Express 应被理解为一条有顺序的请求处理链。普通中间件用 next() 继续正常流程,路由可以直接结束响应,异常则通过 next(error) 进入四参数错误处理中间件。注册顺序决定请求先经过谁、后经过谁,也决定解析、鉴权和错误处理能否在正确位置生效。掌握这条主线后,再面对日志、认证、参数校验、路由拆分等工程需求,就能清楚判断每段逻辑应该放在哪一层。

相关推荐
En^_^Joy8 小时前
Django中间件:请求拦截和响应(类似装饰器)
中间件·django·sqlite
lazy H1 天前
不同的消息队列有什么区别?Kafka、RabbitMQ、RocketMQ、Pulsar、ActiveMQ 选型对比
后端·中间件·kafka·rabbitmq·rocketmq
Lumistory3 天前
城市建筑照明的落地与长效运营观察
大数据·数据库·中间件·光照贴图
笨#小孩5 天前
中间件解析漏洞与编辑器漏洞:Web安全绕过的“捷径”
web安全·中间件·编辑器
程序员良辰7 天前
【TongWeb8】使用 Crontab 定时重启和检测 TongWeb 服务
java·中间件·tomcat
IT白小白7 天前
每日问答中间件篇
中间件
番茄炒鸡蛋加糖8 天前
分布式 CAP、BASE 理论 & 中间件 CAP 取舍
分布式·中间件
Embedded-Xin9 天前
中间件学习-Iceoryx2零基础入门
学习·中间件
Embedded-Xin9 天前
中间件—zenoh零基础入门
linux·中间件·rust·机器人·自动驾驶·嵌入式