Appearance
前端开发者的 Koa CRUD 接口实战文档
更新: 6/28/2026 字数: 0 字 时长: 0 分钟
你已经会写 JS、懂异步、用过 fetch。所以这份文档不教 Promise、不讲 async/await 是什么。它只讲:作为前端,怎么用 Koa 这个框架,把"写接口"这件事从零做到能独立交付。
Koa 是 Express 原班人马打造的新一代 Node 框架,它小巧、现代、完全拥抱 async/await——对前端来说,它可能是最容易上手的后端框架,因为它的核心理念和你熟悉的 JS 异步写法天然契合。下面我们从思维差异讲起,一路带你做出带数据库、带校验、带错误处理的完整 CRUD。
一、先调频:Koa 和前端开发的核心思维差异

语法你都会,真正要扭过来的是这几个认知:
1. 从"处理界面"到"处理请求"。 前端代码围着用户和 DOM 转——点击、渲染、交互。Koa 代码围着 HTTP 请求转:一个请求进来,你拿到它的参数,干点活(查库、算数据),再把结果作为响应吐回去。你的"用户"不再是人,而是发请求的前端代码。
2. 代码是常驻运行的,不是用完即走。 前端页面刷新就重来,Koa 服务一旦启动就一直跑着,同时处理成千上万个请求。这意味着全局变量会被所有请求共享,你不能随便往全局塞用户数据(会串号)。
3. 一切围绕 ctx(上下文)这个对象。 这是 Koa 最核心的概念。Express 把请求和响应分成 req 和 res 两个参数,Koa 把它俩合并成一个 ctx:ctx.request 拿请求信息,ctx.body = xxx 设置返回内容。你所有操作都通过这一个 ctx 完成。
4. 错误用 try/catch 抓,而且能"统一兜底"。 好消息:Koa 全面拥抱 async/await,所以前端最熟悉的 try/catch 在这里照样好用,不像 Go 那样要手动判错。更爽的是 Koa 能在最外层放一个中间件统一捕获所有错误(下面会讲)。
一句话:前端是"事件驱动处理界面",Koa 是"请求驱动处理数据",而
ctx就是你和每个请求对话的唯一窗口。
二、Koa 的灵魂:洋葱模型中间件

学 Koa 绕不开"洋葱模型",但它一点都不玄。中间件就是一个个按顺序排队处理请求的函数,类比前端就是 axios 的拦截器,或者 redux 的 middleware。
每个中间件长这样,关键是那个 next:
javascript
app.use(async (ctx, next) => {
console.log('1 进入');
await next(); // 把控制权交给下一个中间件
console.log('1 出来'); // 等里面都跑完了,再回到这里
});请求像一根针,从外往里穿过每一层(next 之前的代码),到达最里面的业务逻辑,再原路从里往外穿出来(next 之后的代码)。所以打印顺序是:
1 进入 → 2 进入 → 业务处理 → 2 出来 → 1 出来这个设计的妙处在于:你可以在 next() 之前做"请求前"的事(记日志、鉴权、校验),在 next() 之后做"响应前"的事(统一格式化返回、算耗时)。
前端适配提示:
await next()里的await千万不能漏。漏了它,后面的中间件可能还没执行完,请求就提前返回了,这是新手头号 bug(下面踩坑章节还会强调)。
三、阶段一:搭环境,跑通第一个服务

1. 初始化项目
bash
mkdir koa-crud && cd koa-crud
npm init -y
npm install koa @koa/router koa-bodyparser
package.json里加一行"type": "module",这样就能用前端熟悉的import语法,而不是老旧的require。
2. 写第一个服务 app.js
javascript
import Koa from 'koa';
const app = new Koa();
// 最简单的中间件:所有请求都返回这句话
app.use(async (ctx) => {
ctx.body = { message: '你好,KOA' }; // 设置返回内容,自动转成 JSON
});
app.listen(3000, () => {
console.log('服务已启动:http://localhost:3000');
});3. 运行
bash
node app.js浏览器打开 http://localhost:3000,看到 {"message":"你好,KOA"} 就成功了。
前端适配提示:
ctx.body = xxx就是"设置响应内容"。给它一个对象,Koa 自动帮你转成 JSON 并设好Content-Type,比 Express 省心。- 改了代码要重启服务才生效。开发时装个
nodemon(npm i -D nodemon,用nodemon app.js启动),改完自动重启,体验接近前端热更新。
四、阶段二:CRUD 实战之一 —— 路由注册

Koa 本身不带路由,要装 @koa/router。路由就是"哪个 URL + 哪个方法,交给哪个函数处理",和前端路由的概念一致。
javascript
import Koa from 'koa';
import Router from '@koa/router';
import bodyParser from 'koa-bodyparser';
const app = new Koa();
const router = new Router({ prefix: '/api' }); // 所有路由统一加 /api 前缀
app.use(bodyParser()); // 关键中间件:解析 POST/PUT 请求体,否则拿不到 body
// RESTful 风格的 CRUD 路由
router.get('/users', listUsers); // 查列表
router.get('/users/:id', getUser); // 查单个,:id 是路径参数
router.post('/users', createUser); // 新增
router.put('/users/:id', updateUser); // 修改
router.delete('/users/:id', deleteUser); // 删除
app.use(router.routes()); // 挂载路由
app.use(router.allowedMethods()); // 自动处理不支持的方法
app.listen(3000);一个处理函数怎么取参数:
javascript
async function getUser(ctx) {
const { id } = ctx.params; // 路径参数 /users/:id
const { page } = ctx.query; // 查询参数 ?page=1
// POST/PUT 的请求体在 ctx.request.body(注意不是 ctx.body!)
ctx.body = { id, page };
}前端适配提示:
ctx.request.body是"读取前端传来的数据",ctx.body是"设置要返回的数据"。这俩名字像但含义相反,是最容易搞混的点,务必分清。- 拿请求体必须先
app.use(bodyParser()),否则ctx.request.body永远是 undefined——新手必踩。
五、阶段三:CRUD 实战之二 —— 数据库操作

直接写 SQL 对新手不友好,用 ORM 把表映射成 JS 对象操作更顺手。这里用 Sequelize + SQLite(零配置,不用单独装数据库),它在前端圈认知度高。
bash
npm install sequelize sqlite3连接数据库 + 定义模型
javascript
// db.js
import { Sequelize, DataTypes } from 'sequelize';
export const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './app.db', // 数据存到本地文件
logging: false,
});
// 定义 User 模型,对应数据库的一张表
export const User = sequelize.define('User', {
name: { type: DataTypes.STRING, allowNull: false },
email: { type: DataTypes.STRING, allowNull: false },
});
// 同步:按模型自动建表(开发期方便)
await sequelize.sync();在处理函数里增删改查
javascript
import { User } from './db.js';
// 查列表
async function listUsers(ctx) {
const users = await User.findAll();
ctx.body = { data: users };
}
// 查单个
async function getUser(ctx) {
const user = await User.findByPk(ctx.params.id);
if (!user) {
ctx.status = 404;
ctx.body = { error: '用户不存在' };
return;
}
ctx.body = { data: user };
}
// 新增
async function createUser(ctx) {
const { name, email } = ctx.request.body;
const user = await User.create({ name, email });
ctx.body = { data: user };
}
// 修改
async function updateUser(ctx) {
const user = await User.findByPk(ctx.params.id);
if (!user) { ctx.status = 404; ctx.body = { error: '用户不存在' }; return; }
await user.update(ctx.request.body);
ctx.body = { data: user };
}
// 删除
async function deleteUser(ctx) {
await User.destroy({ where: { id: ctx.params.id } });
ctx.body = { message: '删除成功' };
}前端适配提示:
- 所有数据库操作都是异步的,前面一定要加
await。漏了await,你拿到的是个 Promise 而不是真实数据,返回给前端就是个空壳。- ORM 方法名很直观:
findAll(查全部)、findByPk(按主键查,Pk = Primary Key)、create、update、destroy。
六、阶段四:CRUD 实战之三 —— 参数校验与错误处理

这是"玩具"和"能交付"的分水岭。前端传来的数据不可信,必须校验;任何环节都可能出错,必须优雅处理。
统一错误处理中间件(洋葱模型的最佳实践)
利用洋葱模型,在最外层放一个 try/catch,把里面所有中间件的错误一网打尽:
javascript
// 放在所有中间件最前面
app.use(async (ctx, next) => {
try {
await next(); // 后面任何地方 throw,都会被这里抓住
} catch (err) {
ctx.status = err.status || 500;
ctx.body = {
error: err.message || '服务器内部错误',
};
// 真实项目这里还会记录错误日志
console.error('[错误]', err);
}
});有了它,业务代码里只管 throw,不用每个函数都写 try/catch,非常清爽。
参数校验
用 joi 这个流行的校验库声明规则:
bash
npm install joijavascript
import Joi from 'joi';
const createUserSchema = Joi.object({
name: Joi.string().required().messages({ 'any.required': '姓名必填' }),
email: Joi.string().email().required().messages({ 'string.email': '邮箱格式不对' }),
});
// 改造新增函数,加上校验
async function createUser(ctx) {
// 1. 校验参数,不合法直接抛错(被上面的中间件统一接住)
const { error, value } = createUserSchema.validate(ctx.request.body);
if (error) {
ctx.throw(400, error.details[0].message); // ctx.throw 是 Koa 内置的抛错方法
}
// 2. 校验通过,写库
const user = await User.create(value);
ctx.body = { data: user };
}前端适配提示:
ctx.throw(400, '消息')是 Koa 内置的抛错快捷方式,会被最外层错误中间件捕获并返回 400。比手写ctx.status+ctx.body简洁。- 错误处理中间件必须放在最前面,因为洋葱模型里它要先进入、最后出来,才能包住后面所有逻辑。
- 状态码约定:参数错
400、资源不存在404、服务器错500,和前端对齐方便联调。
七、把它们拼起来:完整可运行骨架
到这一步,整合成一个能直接 node app.js 跑起来的服务:
javascript
// app.js
import Koa from 'koa';
import Router from '@koa/router';
import bodyParser from 'koa-bodyparser';
import Joi from 'joi';
import { Sequelize, DataTypes } from 'sequelize';
// ---- 数据库 ----
const sequelize = new Sequelize({ dialect: 'sqlite', storage: './app.db', logging: false });
const User = sequelize.define('User', {
name: { type: DataTypes.STRING, allowNull: false },
email: { type: DataTypes.STRING, allowNull: false },
});
await sequelize.sync();
// ---- 应用 ----
const app = new Koa();
const router = new Router({ prefix: '/api' });
// 统一错误处理(最外层)
app.use(async (ctx, next) => {
try { await next(); }
catch (err) {
ctx.status = err.status || 500;
ctx.body = { error: err.message };
}
});
app.use(bodyParser());
// 校验规则
const userSchema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required(),
});
// CRUD 路由
router.get('/users', async (ctx) => {
ctx.body = { data: await User.findAll() };
});
router.get('/users/:id', async (ctx) => {
const user = await User.findByPk(ctx.params.id);
if (!user) ctx.throw(404, '用户不存在');
ctx.body = { data: user };
});
router.post('/users', async (ctx) => {
const { error, value } = userSchema.validate(ctx.request.body);
if (error) ctx.throw(400, error.details[0].message);
ctx.body = { data: await User.create(value) };
});
router.put('/users/:id', async (ctx) => {
const user = await User.findByPk(ctx.params.id);
if (!user) ctx.throw(404, '用户不存在');
ctx.body = { data: await user.update(ctx.request.body) };
});
router.delete('/users/:id', async (ctx) => {
await User.destroy({ where: { id: ctx.params.id } });
ctx.body = { message: '删除成功' };
});
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000, () => console.log('http://localhost:3000'));用 curl 测一下:
bash
# 新增
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name":"张三","email":"z@test.com"}'
# 查询
curl http://localhost:3000/api/users能返回 JSON,恭喜——一个带路由、数据库、校验、错误处理的完整 CRUD 接口交付完成。
八、前端学 Koa 的高频踩坑点 + 适配技巧

把散落的坑汇总成清单,对照检查:
| 踩坑点 | 说明 | 适配技巧 |
|---|---|---|
ctx.body 和 ctx.request.body 搞混 | 前者是返回内容,后者是接收的请求体 | 记口诀:"request.body 收,ctx.body 发" |
忘记 await next() 的 await | 中间件提前返回、洋葱模型断裂 | 凡是 next() 前面都加 await |
| 没用 bodyParser 就读 body | ctx.request.body 是 undefined | POST/PUT 前必须 app.use(bodyParser()) |
忘记设 ctx.body | 前端收到 404 Not Found(Koa 默认) | 每个接口都要给 ctx.body 赋值 |
| 数据库/异步操作漏 await | 返回的是 Promise 而非真实数据 | 所有 ORM、IO 操作前加 await |
| 错误中间件位置放错 | 抓不到后面中间件的错误 | 错误处理中间件必须放在最前面 |
| 跨域 CORS 没配 | 前端联调请求被浏览器拦 | 装 @koa/cors,app.use(cors()) |
| try/catch 没包异步 | 异步错误没被同步 catch 抓到 | 用 async 函数 + await,让错误能被外层捕获 |
| 全局变量存请求数据 | 多请求并发时数据串号 | 请求相关数据只放 ctx 里,别放全局 |
给前端的提效建议:
- 开发用
nodemon,改完自动重启,体验接近前端热更新。 - 善用
ctx.throw(状态码, 消息),配合统一错误中间件,业务代码极简。 - 接口测试用 Apifox/Postman,比每次写 curl 方便,能存请求集合反复测。
- 项目变大后分目录:把路由、控制器(处理函数)、模型(数据库)、中间件拆开放,别全堆在
app.js。