Skip to content

前端开发者的 Koa CRUD 接口实战文档

更新: 6/28/2026 字数: 0 字 时长: 0 分钟

你已经会写 JS、懂异步、用过 fetch。所以这份文档不教 Promise、不讲 async/await 是什么。它只讲:作为前端,怎么用 Koa 这个框架,把"写接口"这件事从零做到能独立交付。

Koa 是 Express 原班人马打造的新一代 Node 框架,它小巧、现代、完全拥抱 async/await——对前端来说,它可能是最容易上手的后端框架,因为它的核心理念和你熟悉的 JS 异步写法天然契合。下面我们从思维差异讲起,一路带你做出带数据库、带校验、带错误处理的完整 CRUD。

一、先调频:Koa 和前端开发的核心思维差异

前端思维 vs Koa 后端思维

语法你都会,真正要扭过来的是这几个认知:

1. 从"处理界面"到"处理请求"。 前端代码围着用户和 DOM 转——点击、渲染、交互。Koa 代码围着 HTTP 请求转:一个请求进来,你拿到它的参数,干点活(查库、算数据),再把结果作为响应吐回去。你的"用户"不再是人,而是发请求的前端代码。

2. 代码是常驻运行的,不是用完即走。 前端页面刷新就重来,Koa 服务一旦启动就一直跑着,同时处理成千上万个请求。这意味着全局变量会被所有请求共享,你不能随便往全局塞用户数据(会串号)。

3. 一切围绕 ctx(上下文)这个对象。 这是 Koa 最核心的概念。Express 把请求和响应分成 reqres 两个参数,Koa 把它俩合并成一个 ctxctx.request 拿请求信息,ctx.body = xxx 设置返回内容。你所有操作都通过这一个 ctx 完成。

4. 错误用 try/catch 抓,而且能"统一兜底"。 好消息:Koa 全面拥抱 async/await,所以前端最熟悉的 try/catch 在这里照样好用,不像 Go 那样要手动判错。更爽的是 Koa 能在最外层放一个中间件统一捕获所有错误(下面会讲)。

一句话:前端是"事件驱动处理界面",Koa 是"请求驱动处理数据",而 ctx 就是你和每个请求对话的唯一窗口。

二、Koa 的灵魂:洋葱模型中间件

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(下面踩坑章节还会强调)。

三、阶段一:搭环境,跑通第一个服务

搭建 Koa 环境跑通第一个服务

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 省心。
  • 改了代码要重启服务才生效。开发时装个 nodemonnpm i -D nodemon,用 nodemon app.js 启动),改完自动重启,体验接近前端热更新。

四、阶段二:CRUD 实战之一 —— 路由注册

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)、createupdatedestroy

六、阶段四: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 joi
javascript
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 的高频踩坑点 + 适配技巧

前端学 Koa 的高频踩坑点

把散落的坑汇总成清单,对照检查:

踩坑点说明适配技巧
ctx.bodyctx.request.body 搞混前者是返回内容,后者是接收的请求体记口诀:"request.body 收,ctx.body 发"
忘记 await next() 的 await中间件提前返回、洋葱模型断裂凡是 next() 前面都加 await
没用 bodyParser 就读 bodyctx.request.body 是 undefinedPOST/PUT 前必须 app.use(bodyParser())
忘记设 ctx.body前端收到 404 Not Found(Koa 默认)每个接口都要给 ctx.body 赋值
数据库/异步操作漏 await返回的是 Promise 而非真实数据所有 ORM、IO 操作前加 await
错误中间件位置放错抓不到后面中间件的错误错误处理中间件必须放在最前面
跨域 CORS 没配前端联调请求被浏览器拦@koa/corsapp.use(cors())
try/catch 没包异步异步错误没被同步 catch 抓到用 async 函数 + await,让错误能被外层捕获
全局变量存请求数据多请求并发时数据串号请求相关数据只放 ctx 里,别放全局

给前端的提效建议:

  1. 开发用 nodemon,改完自动重启,体验接近前端热更新。
  2. 善用 ctx.throw(状态码, 消息),配合统一错误中间件,业务代码极简。
  3. 接口测试用 Apifox/Postman,比每次写 curl 方便,能存请求集合反复测。
  4. 项目变大后分目录:把路由、控制器(处理函数)、模型(数据库)、中间件拆开放,别全堆在 app.js