首页 / Node.js 教程 / koa REST API

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 轻快多了。