Koa 简介

请求体解析与错误处理

学会用 koa-bodyparser 解析请求体,掌握统一错误处理、ctx.throw 与 404 自定义的写法。

🎯 引言

学完这篇文章,你的 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 上:

app.js
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.js
// 统一错误处理中间件,注册在靠前的位置
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(状态码, 提示信息) 会抛出一个带状态码的错误,交给上面的统一中间件处理:

app.js
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.js
// 所有路由都没匹配上才会走到这里
app.use(async (ctx) => {
    ctx.status = 404;
    ctx.body = {
        code: 404,
        message: '接口不存在',
    };
});

它的原理是洋葱模型的进入顺序:请求依次穿过前面的中间件,没有任何路由处理它,最后到达这个兜底中间件。注册顺序要注意,放在路由前面会把所有请求都当成 404。


🧰 完整示例:组装起来

把这几块拼在一起,一个规范的接口骨架就成型了:

app.js
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 提示用户,不用针对每种状态码单独处理。


🧪 小练习

  1. 写一个 POST /login 接口,接收 usernamepassword,缺少任意一个就用 ctx.throw(400, ...) 提示,都提供了就返回登录成功的 JSON。
app.js
router.post('/login', async (ctx) => {
    // 请在这里编写代码
});
  1. 用接口工具分别测试“参数齐全”和“缺少 password”两种情况,确认错误响应也是统一格式。

🎉 恭喜你已经能写出健壮的 Koa 接口了!下一篇是综合实战:用内存数据实现一套完整的 RESTful 用户接口,把前面学的东西全部串起来。