Koa 简介

RESTful 接口实战

综合运用 @koa/router、koa-bodyparser、@koa/cors 与错误处理,实现一套完整的用户增删改查接口。

🎯 引言

学完这篇文章,你能独立写出一套规范的用户增删改查接口:路由拆分、请求体解析、跨域支持、统一错误处理全部到位,还能用前端 fetch 成功调用。这篇把前面五篇的知识串成一个完整项目,代码会稍长一些,但每一块你都已经学过。


🧱 项目结构与依赖

最终的目录结构:

koa-demo/
├── app.js            入口文件,组装所有中间件
├── routes/
│   └── users.js      用户模块路由
└── package.json

安装全部依赖。本文基于 Node.js v22 LTS 编写和验证,各包版本:Koa 3.2.1、@koa/router 15.7.0、koa-bodyparser 4.4.1、@koa/cors 5.0.0。

npm install koa @koa/router koa-bodyparser @koa/cors

@koa/cors 是跨域中间件。前端页面跑在 5173 端口、接口跑在 3000 端口时,浏览器会拦截跨域请求,注册它就能放行,一行搞定。


🛠 用户路由:增删改查

用一个内存数组模拟数据库,实现五个 RESTful 接口:

routes/users.js
import Router from '@koa/router';

const router = new Router({ prefix: '/users' });

// 内存数据,模拟数据库(重启后数据会还原)
let users = [
    { id: 1, name: '小明', age: 18 },
    { id: 2, name: '小红', age: 20 },
];
let nextId = 3;

// 获取用户列表:GET /users
router.get('/', async (ctx) => {
    ctx.body = { code: 200, data: users };
});

// 获取用户详情:GET /users/1
router.get('/:id', async (ctx) => {
    const user = users.find((u) => u.id === Number(ctx.params.id));

    if (!user) {
        ctx.throw(404, '用户不存在');
    }

    ctx.body = { code: 200, data: user };
});

// 新增用户:POST /users
router.post('/', async (ctx) => {
    const { name, age } = ctx.request.body;

    if (!name) {
        ctx.throw(400, 'name 不能为空');
    }

    const user = { id: nextId++, name, age };
    users.push(user);

    ctx.status = 201; // 创建成功
    ctx.body = { code: 201, data: user };
});

// 更新用户:PUT /users/1
router.put('/:id', async (ctx) => {
    const user = users.find((u) => u.id === Number(ctx.params.id));

    if (!user) {
        ctx.throw(404, '用户不存在');
    }

    const { name, age } = ctx.request.body;
    if (name !== undefined) user.name = name;
    if (age !== undefined) user.age = age;

    ctx.body = { code: 200, data: user };
});

// 删除用户:DELETE /users/1
router.delete('/:id', async (ctx) => {
    const index = users.findIndex((u) => u.id === Number(ctx.params.id));

    if (index === -1) {
        ctx.throw(404, '用户不存在');
    }

    users.splice(index, 1);
    ctx.body = { code: 200, message: '删除成功' };
});

export default router;

几个设计要点:

  • 状态码语义化:创建成功返回 201,找不到资源返回 404,参数错误返回 400,客户端一看状态码就知道发生了什么。
  • 就地更新:PUT 接口用 !== undefined 判断,只更新前端传了的字段,没传的保持原值。
  • 错误统一交给 ctx.throw:路由里不做响应格式处理,专注业务逻辑。

🧰 入口文件:组装中间件

app.js
import Koa from 'koa';
import bodyParser from 'koa-bodyparser';
import cors from '@koa/cors';
import usersRouter from './routes/users.js';

const app = new Koa();

// 1. 统一错误处理
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);
    }
});

// 2. 跨域与请求体解析
app.use(cors());
app.use(bodyParser());

// 3. 业务路由
app.use(usersRouter.routes());
app.use(usersRouter.allowedMethods());

// 4. 404 兜底
app.use(async (ctx) => {
    ctx.status = 404;
    ctx.body = { code: 404, message: '接口不存在' };
});

app.listen(3000, () => {
    console.log('服务器已启动:http://localhost:3000');
});

中间件的顺序沿用上篇总结的口诀:错误处理 → 通用中间件(跨域、body 解析) → 路由 → 404 兜底。这个骨架可以直接当作你以后 Koa 项目的起步模板。


💡 前端调用示例

接口写好了,前端用 fetch 调用一遍。假设页面跑在 Vite 项目的 5173 端口:

// 获取用户列表
const res = await fetch('http://localhost:3000/users');
const result = await res.json();
console.log(result.data); // 用户数组

// 新增用户
await fetch('http://localhost:3000/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: '小华', age: 22 }),
});

// 更新 id 为 1 的用户
await fetch('http://localhost:3000/users/1', {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ age: 19 }),
});

// 删除 id 为 2 的用户
await fetch('http://localhost:3000/users/2', {
    method: 'DELETE',
});

如果忘了注册 @koa/cors,浏览器控制台会报经典的 CORS 错误(Access-Control-Allow-Origin 相关字样),这是前后端分离开发的第一道坎,现在你知道怎么跨过去了。


🧾 小节总结

  • 用内存数组模拟数据库,一个路由文件就能实现完整的增删改查:GET 列表/详情、POST 新增、PUT 更新、DELETE 删除。
  • 状态码要语义化:200 成功、201 创建成功、400 参数错误、404 资源不存在。
  • 路由里用 ctx.throw 抛错误,响应格式由统一错误中间件处理,业务代码更干净。
  • @koa/cors 一行注册解决跨域,前后端分离项目必备。
  • 入口文件按“错误处理 → 通用中间件 → 路由 → 404 兜底”的顺序组装,可直接作为项目模板。

❓ 知识问答

Q1:内存数组的数据重启就丢了,正式项目怎么办?

正式项目会把数据存到数据库(如 MySQL、MongoDB)。这里用内存数组是为了聚焦 Koa 本身的写法,学会后只需把数组操作替换成数据库查询,接口结构完全不变。

Q2:PUT 和 PATCH 有什么区别?

PUT 是整体替换资源的语义,PATCH 是部分更新。实际项目里界限没那么严格,我们的示例用 PUT 实现了“只更新传入字段”的效果,这是常见的实用做法。

Q3:为什么没有处理鉴权(登录校验)?

鉴权通常用 JWT 等方案实现,会作为独立的中间件加在路由之前。它属于进阶内容,先把本文的基础骨架吃透,加鉴权只是“多写一个中间件”的事。

Q4:多个路由文件怎么注册?

和单个一样,每个模块导出 router,在 app.js 里逐个 app.use(xxxRouter.routes())app.use(xxxRouter.allowedMethods())。模块多了之后也可以写个循环自动加载。


🧪 小练习

  1. 模仿 users 模块,新建 routes/articles.js,实现文章的增删改查接口(字段:idtitlecontent),并在 app.js 中注册。
routes/articles.js
import Router from '@koa/router';

const router = new Router({ prefix: '/articles' });

// 请在这里编写代码

export default router;
  1. 在“获取列表”接口上支持查询参数 ?keyword=xxx,返回标题包含该关键词的文章。

🎉 恭喜你已经完成 Koa 课程的全部内容!从 hello world 到完整的 RESTful 接口,你已经具备了用 Koa 独立开发后端接口的能力。接下来可以尝试把内存数据换成真实数据库,让你的接口真正落地!