Node.js 教程
koa REST API
本教程共 76 篇 · 第 47 篇 · 更新于 2026-07-25 · 约 4 分钟阅读
Node.jskoaRESTAPI校验
47. koa REST API
本节目标:用 koa 构建 RESTful API,参数校验和错误处理。
前后端分离已经是主流。后端只提供 JSON 接口,前端用 React/Vue 消费。这种模式里,后端就是一套 REST API。本章我们用 koa 搭建一个规范的 REST API,包括路由设计、错误处理和响应格式统一。
REST 设计规范
REST(Representational State Transfer)不是技术标准,而是一种设计风格。核心思想是把 URL 当成资源,用 HTTP 方法表示操作:
| 方法 | 含义 | 示例 |
|---|---|---|
| GET | 读取资源 | GET /api/users 获取用户列表 |
| GET | 读取单个资源 | GET /api/users/123 获取 ID 为 123 的用户 |
| POST | 创建资源 | POST /api/users 创建新用户 |
| PUT | 全量更新 | PUT /api/users/123 替换用户 123 |
| PATCH | 局部更新 | PATCH /api/users/123 修改用户部分字段 |
| DELETE | 删除资源 | DELETE /api/users/123 删除用户 123 |
URL 里只放名词,别放动词。/api/getUser 是错的,应该是 /api/users/:id。
基础 CRUD 实现
import Koa from 'koa';
import Router from '@koa/router';
import { bodyParser } from '@koa/bodyparser';
const app = new Koa();
const router = new Router({ prefix: '/api' });
// 模拟数据库
const users = new Map();
let nextId = 1;
// 列表 + 分页
router.get('/users', async (ctx) => {
const page = parseInt(ctx.query.page || '1');
const size = parseInt(ctx.query.size || '10');
const all = Array.from(users.values());
const list = all.slice((page - 1) * size, page * size);
ctx.body = {
data: list,
pagination: { page, size, total: all.length }
};
});
// 详情
router.get('/users/:id', async (ctx) => {
const user = users.get(ctx.params.id);
if (!user) {
ctx.status = 404;
ctx.body = { error: 'USER_NOT_FOUND', message: '用户不存在' };
return;
}
ctx.body = { data: user };
});
// 创建
router.post('/users', async (ctx) => {
const { name, email } = ctx.request.body;
const id = String(nextId++);
const user = { id, name, email, createdAt: new Date().toISOString() };
users.set(id, user);
ctx.status = 201;
ctx.body = { data: user };
});
// 更新
router.put('/users/:id', async (ctx) => {
const user = users.get(ctx.params.id);
if (!user) {
ctx.status = 404;
ctx.body = { error: 'USER_NOT_FOUND' };
return;
}
const { name, email } = ctx.request.body;
user.name = name ?? user.name;
user.email = email ?? user.email;
ctx.body = { data: user };
});
// 删除
router.delete('/users/:id', async (ctx) => {
const existed = users.delete(ctx.params.id);
if (!existed) {
ctx.status = 404;
ctx.body = { error: 'USER_NOT_FOUND' };
return;
}
ctx.status = 204;
});
app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);
统一响应格式
客户端需要可预期的响应结构。推荐统一包装:
// 成功
{ "data": { ... } }
// 列表
{ "data": [ ... ], "pagination": { "page": 1, "size": 10, "total": 100 } }
// 失败
{ "error": "ERROR_CODE", "message": "人类可读的错误说明" }
写个中间件自动包装:
app.use(async (ctx, next) => {
try {
await next();
// 如果 body 已经是对象但没包装,自动包一层
if (ctx.body && typeof ctx.body === 'object' && !ctx.body.data && !ctx.body.error) {
ctx.body = { data: ctx.body };
}
} catch (err) {
ctx.status = err.status || 500;
ctx.body = {
error: err.code || 'INTERNAL_ERROR',
message: err.message || '服务器内部错误'
};
}
});
集中式错误处理
API 里到处写 if (!user) { ctx.status = 404; return; } 很累。可以自定义错误类:
class ApiError extends Error {
constructor(status, code, message) {
super(message);
this.status = status;
this.code = code;
}
}
// 控制器里直接抛
router.get('/users/:id', async (ctx) => {
const user = users.get(ctx.params.id);
if (!user) {
throw new ApiError(404, 'USER_NOT_FOUND', '用户不存在');
}
ctx.body = { data: user };
});
配合上面的错误捕获中间件,控制器代码干净很多。
用 curl 测试
# 创建用户
curl -X POST http://localhost:3000/api/users \
-H 'Content-Type: application/json' \
-d '{"name":"Alice","email":"alice@example.com"}'
# 获取列表
curl 'http://localhost:3000/api/users?page=1&size=5'
# 获取单个
curl http://localhost:3000/api/users/1
# 更新
curl -X PUT http://localhost:3000/api/users/1 \
-H 'Content-Type: application/json' \
-d '{"name":"Alice Wang"}'
# 删除
curl -X DELETE http://localhost:3000/api/users/1
Tip测试 REST API 时,我习惯把 curl 命令存成
.http文件,配合 VS Code 的 REST Client 插件一键发送,比开 Postman 轻快多了。