科技前沿 科普Web 基础教程建站系列

API 设计入门:RESTful 是什么、怎么用

📅 2026-07-14

后端写好之后,前端怎么跟它说话?答案就是 API。白物集目前有 19 个 API 路由,覆盖文章管理、知识卡片、收藏、数据统计等模块。这篇文章用白物集的实际代码,拆解 RESTful API 的核心概念和落地方式。

API 是一条「沟通通道」

前端页面(Astro SSR 渲染的 HTML)需要展示内容,但不能直接从数据库拿数据——数据库在前端不可达,也不应该暴露给客户端。API 层就是中间人:前端发一个 HTTP 请求,后端收到后去查数据库,把结果格式化成 JSON 返回。

白物集的 API 入口在 api/src/index.js。所有路由挂在一个版本前缀下:

const v1 = express.Router();
v1.use('/articles', apiLimiter, articlesRouter);
v1.use('/content/ingest', ingestLimiter, ingestRouter);
v1.use('/knowledge-cards', apiLimiter, cardsRouter);
// ... 其他路由
app.use('/api/v1', v1);

版本号 v1 不是装饰——API 一旦上线,改一个字段名都可能让前端崩溃。加版本号之后,升级时可以新旧并行,前端从容切换。

RESTful 的核心:资源 + 动词

REST 的全称是 Representational State Transfer。说人话:把每个业务实体当成一个「资源」(articles、cards、likes),用 HTTP 方法表示操作。

操作 HTTP 方法 URL 示例 作用
查列表 GET /api/v1/articles 获取文章列表
查单个 GET /api/v1/articles/:id 获取单篇文章
创建 POST /api/v1/content/ingest 写入新内容
更新 PATCH /api/v1/articles/:id 更新部分字段
删除 DELETE /api/v1/articles/:id 删除文章

白物集的文章列表路由 GET /articles 就是典型实现——支持筛选、搜索、分页、排序:

router.get('/', async (req, res) => {
  const parsed = articleQuerySchema.safeParse(req.query);
  if (!parsed.success) return zodError(res, parsed.error);

  const { category, status, search, tag, limit, offset, sort } = parsed.data;
  // 查数据库、走缓存、返回分页数据
  res.json({ articles: data, total: count, offset, limit });
});

参数全从 query string 里来:/api/v1/articles?category=AI&limit=10&offset=0。前端想看下一页就把 offset 加 10 再请求一次。分页用 offset/limit 而非 page/pageSize——前者在数据频繁插入时不会出现翻页重复,白物集踩过这个坑。

参数校验:不用 Zod 你就等着修 bug

RESTful API 最容易被忽略的一环是输入校验。没有校验的 API 等于开门迎客——谁来都能往数据库塞脏数据。

白物集用 Zod 做 schema 校验。定义一个文章查询参数的 schema:

export const articleQuerySchema = z.object({
  category: z.enum(ALL_CATEGORIES).optional(),
  status: z.enum(['published', 'draft', 'archived']).optional().default('published'),
  search: z.string().max(200).optional(),
  limit: z.coerce.number().int().min(1).max(200).optional().default(20),
  offset: z.coerce.number().int().min(0).optional().default(0),
  sort: z.enum(['published_at', 'created_at', 'title', 'id']).optional().default('published_at'),
});

z.coerce.number() 会把 query string 里的 "10" 自动转成数字 10z.enum() 直接拒绝无效的分类名。校验失败的响应也统一格式:

{ error: { code: 'VALIDATION_ERROR', message: 'Validation failed', details: [...] } }

这个统一格式来自 errors.js,所有错误响应共用一套模板,前端一个 switch(error.code) 就能处理全部异常。

统一响应格式:前端开发者的救命稻草

没有统一格式的 API 是最折磨人的——有的接口返 { data: [...] },有的返 { results: [...] },还有的只在成功时返对象、失败时返字符串。前端对接时每调一个接口就要花 10 分钟查文档。

白物集的响应格式只有两种:

成功: 直接返回数据本体,即 res.json({ articles: [...], total: N }) 失败: { error: { code: 'NOT_FOUND', message: 'Article #42 not found' } }

错误码用大写常量,前端直接判断 code:

export const ERROR_CODES = {
  NOT_FOUND: 'NOT_FOUND',
  UNAUTHORIZED: 'UNAUTHORIZED',
  RATE_LIMITED: 'RATE_LIMITED',
  VALIDATION_ERROR: 'VALIDATION_ERROR',
  INTERNAL_ERROR: 'INTERNAL_ERROR',
};

这套设计看起来简单,但实际开发中常被忽视。白物集在早期版本中做过每个路由自己写 res.status(500).json({ msg: 'error' }) 的写法,后来迁移到统一错误函数,数了数删掉了 47 处重复的 try-catch 错误处理。

权限控制:x-api-key 就够了

白物集的内容是公开的——任何人都可以 GET 文章列表。但只有写操作需要认证。做法很简单:客户端在 Header 里传一个 x-api-key,后端比对环境变量:

function requireIngestAuth(req, res, next) {
  const token = req.headers['x-api-key'];
  if (!token || token !== (process.env.INGEST_API_KEY || '')) {
    return unauthorized(res, 'Invalid or missing API key');
  }
  next();
}

// 只有写路由需要 auth
router.patch('/:id', requireIngestAuth, async (req, res) => { ... });
router.delete('/:id', requireIngestAuth, async (req, res) => { ... });

你没看错——就是比对两个字符串。对于个人项目的内容 API,不需要 OAuth 2.0 那一套。Key 只存在于服务器环境变量里,不出现在任何代码或前端代码中。日常管线脚本通过 SSH 登录 ECS 从 .env 读取 Key 后直接 POST 使用,Key 本身不会写入任何版本控制。

避坑:404 的两种写法

白物集犯过一个低级错误:文章的路由用了 /articles/:id,但如果前端把 URL 拼错了(比如传了字符串 "abc"),Zod 校验 z.coerce.number() 会返回 NaN,导致查询数据库时报错。

现在的做法是先校验参数再查数据库:

const parsed = articleIdParam.safeParse(req.params);
if (!parsed.success) return badRequest(res, 'Invalid article ID');

const { id } = parsed.data;
const { data, error } = await supabase.from('articles').select('*').eq('id', id).single();

if (error?.code === 'PGRST116') return notFound(res, `Article #${id} not found`);

PGRST116 是 Supabase 的「记录不存在」错误码,捕获它返回 404,不暴露数据库细节给客户端。

下一篇预告

API 设计入门之后,下一个问题是数据存哪。白物集选择了 Supabase(基于 PostgreSQL 的 BaaS)。下一篇讲数据库基础:从你熟悉的 Excel 表格出发,理解关系数据库的核心概念,以及如何在 Supabase 里建表、写查询。把 API 和数据库连起来,一个完整的数据链路就成型了。

← Zod:TypeScript 项目的最后一道防线 → Apple 频道 Vercel 开源 Native SDK:原生桌面开发新路径 →
🍎 Apple 深度分析
本文基于 Apple 公开资料及行业分析撰写。观点仅供参考与学习交流。