请求体解析与错误处理
🎯 引言
学完这篇文章,你的 Koa 接口就能接收前端 POST 过来的 JSON 数据了,还能用一套统一的错误处理机制兜底所有异常,让接口无论成功还是失败都返回规范的格式。这是把 demo 变成可用接口的关键一步。
🧱 用 koa-bodyparser 解析请求体
前端用 POST 提交数据时,数据放在请求体(body)里。Koa 核心不解析请求体,需要安装 koa-bodyparser。本文基于 Node.js v22 LTS + koa-bodyparser 4.4.1 编写和验证。
npm install koa-bodyparser
注册后,解析好的数据挂在 ctx.request.body 上:
import Koa from 'koa';
import Router from '@koa/router';
import bodyParser from 'koa-bodyparser';
const app = new Koa();
const router = new Router();
// 注意:要在路由之前注册,路由里才能拿到 body
app.use(bodyParser());
router.post('/users', async (ctx) => {
const data = ctx.request.body; // 前端发来的 JSON 数据
ctx.body = { message: '创建成功', data };
});
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);
前端用 fetch 发一个 POST 请求试试:
fetch('http://localhost:3000/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: '小明', age: 18 }),
});
服务端就能在 ctx.request.body 里拿到 { name: '小明', age: 18 } 这个对象。除了 JSON,koa-bodyparser 默认也能解析表单格式(x-www-form-urlencoded),不需要额外配置。
bodyParser() 必须注册在路由之前。中间件按注册顺序执行,如果路由先执行,里面读 ctx.request.body 只会拿到 undefined。🧅 统一错误处理:洋葱模型的妙用
接口代码随时可能抛错:参数不对、查询失败、代码 bug。如果每个路由都自己写 try/catch,既繁琐又容易漏。
回忆洋葱模型:最外层的中间件包裹着所有后续处理。在它里面写 try/catch,就能捕获所有中间件抛出的错误:
// 统一错误处理中间件,注册在靠前的位置
app.use(async (ctx, next) => {
try {
await next(); // 后面的任何中间件抛错,都会在这里被捕获
} catch (err) {
ctx.status = err.status || 500;
ctx.body = {
code: ctx.status,
message: err.message || '服务器开小差了',
};
console.error('接口错误:', err);
}
});
这就是 Koa 相比 Express 在错误处理上的明显优势:Express 需要单独的错误处理中间件(四个参数的那种),而 Koa 靠 await next() 加 try/catch 就自然实现了,还能捕获异步错误。
🛠 ctx.throw:主动抛出错误
业务里经常要主动拒绝请求,比如参数缺失。ctx.throw(状态码, 提示信息) 会抛出一个带状态码的错误,交给上面的统一中间件处理:
router.post('/users', async (ctx) => {
const { name, age } = ctx.request.body;
// 参数校验,不满足就抛出 400 错误
if (!name) {
ctx.throw(400, 'name 不能为空');
}
ctx.body = { message: '创建成功', data: { name, age } };
});
当前端发来的数据缺少 name 时,接口会返回:
{
"code": 400,
"message": "name 不能为空"
}
ctx.throw 抛出的错误自带 status 属性,统一中间件里的 err.status || 500 就是靠它拿到正确状态码的。
💡 404 处理
Koa 对未匹配路由的请求默认返回 404 和纯文本 Not Found。前后端分离项目里,我们希望连 404 也返回统一格式的 JSON。在所有路由之后加一个兜底中间件:
// 所有路由都没匹配上才会走到这里
app.use(async (ctx) => {
ctx.status = 404;
ctx.body = {
code: 404,
message: '接口不存在',
};
});
它的原理是洋葱模型的进入顺序:请求依次穿过前面的中间件,没有任何路由处理它,最后到达这个兜底中间件。注册顺序要注意,放在路由前面会把所有请求都当成 404。
🧰 完整示例:组装起来
把这几块拼在一起,一个规范的接口骨架就成型了:
import Koa from 'koa';
import Router from '@koa/router';
import bodyParser from 'koa-bodyparser';
const app = new Koa();
const router = new Router();
// 1. 统一错误处理(最外层)
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = { code: ctx.status, message: err.message || '服务器开小差了' };
}
});
// 2. 请求体解析(在路由之前)
app.use(bodyParser());
// 3. 业务路由
router.post('/users', async (ctx) => {
const { name } = ctx.request.body;
if (!name) {
ctx.throw(400, 'name 不能为空');
}
ctx.body = { code: 200, message: '创建成功', data: { name } };
});
app.use(router.routes());
app.use(router.allowedMethods());
// 4. 404 兜底(最后)
app.use(async (ctx) => {
ctx.status = 404;
ctx.body = { code: 404, message: '接口不存在' };
});
app.listen(3000);
记住这个注册顺序:错误处理 → body 解析 → 路由 → 404 兜底。每一层的位置都和洋葱模型息息相关。
🧾 小节总结
- 用
koa-bodyparser解析请求体,数据通过ctx.request.body读取,必须注册在路由之前。 - 在靠外层中间件里
try { await next() } catch,就能统一捕获所有后续中间件的错误。 ctx.throw(400, '提示')主动抛出带状态码的错误,交给统一中间件处理。- 未匹配路由 Koa 默认返回 404,可以在路由之后注册兜底中间件,返回统一格式的 JSON。
- 中间件注册顺序:错误处理 → body 解析 → 路由 → 404 兜底。
❓ 知识问答
Q1:ctx.request.body 和 ctx.body 有什么区别?
一个是“进”,一个是“出”:ctx.request.body 是客户端发来的请求数据,ctx.body 是我们返回给客户端的响应内容,方向完全相反。
Q2:ctx.throw 之后代码还会继续执行吗?
不会。ctx.throw 本质就是抛出一个 Error,函数会立即中断,后面的代码不再执行,错误被外层 catch 捕获。
Q3:async 函数里 throw 的错误也能被统一中间件捕获吗?
能。这正是 Koa 用 async/await 的好处,await next() 会把后续所有异步操作的错误都传递回来,一个 try/catch 全部兜住。
Q4:统一错误格式里的 code 字段有什么用?
方便前端统一判断。前端收到响应后先看 code 是不是 200,不是就按 message 提示用户,不用针对每种状态码单独处理。
🧪 小练习
- 写一个
POST /login接口,接收username和password,缺少任意一个就用ctx.throw(400, ...)提示,都提供了就返回登录成功的 JSON。
router.post('/login', async (ctx) => {
// 请在这里编写代码
});
- 用接口工具分别测试“参数齐全”和“缺少 password”两种情况,确认错误响应也是统一格式。
🎉 恭喜你已经能写出健壮的 Koa 接口了!下一篇是综合实战:用内存数据实现一套完整的 RESTful 用户接口,把前面学的东西全部串起来。
