大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
RESTful API
这一课你将能做到
- 用 RESTful 规范设计 API 接口
- 正确使用 HTTP 方法(GET/POST/PUT/DELETE)和状态码
- 设计统一响应格式,处理错误和分页
为什么要学
团队开发时,前端要调后端的接口。如果没有规范,每个人写的接口风格都不一样:/getUser、/user/get、/api/query_user,前端同学会疯掉。RESTful 是一套业界公认的 API 设计规范,让接口风格统一、易理解、易维护。
人话:RESTful 是「交通法规」,让前后端在同一套规则下协作,减少沟通成本。
先建立图像
不好的设计(动词 + 名词混搭):
POST /api/getUser
POST /api/deleteCourse
GET /api/query_course_list
RESTful 设计(名词 + HTTP 方法):
GET /api/users/1 # 获取用户
POST /api/users # 创建用户
PUT /api/users/1 # 更新用户
DELETE /api/users/1 # 删除用户
GET /api/courses # 课程列表
GET /api/courses/1 # 单个课程
跟着做
步骤 1:URL 设计——用名词,不用动词
// 不好
app.post('/api/createCourse', ...);
app.post('/api/deleteCourse', ...);
app.get('/api/getCourseList', ...);
// 好
app.post('/api/courses', ...); // 创建
app.delete('/api/courses/:id', ...); // 删除
app.get('/api/courses', ...); // 列表
app.get('/api/courses/:id', ...); // 详情
app.put('/api/courses/:id', ...); // 更新
步骤 2:HTTP 方法的语义
| 方法 | 语义 | 幂等性 | 示例 |
|---|---|---|---|
| GET | 查询 | 是 | GET /users/1 |
| POST | 创建 | 否 | POST /users |
| PUT | 全量更新 | 是 | PUT /users/1 |
| PATCH | 部分更新 | 否 | PATCH /users/1 |
| DELETE | 删除 | 是 | DELETE /users/1 |
幂等性:多次调用效果相同。GET、PUT、DELETE 幂等;POST 不幂等(重复调用会创建多条)。
步骤 3:HTTP 状态码
// 2xx 成功
200 OK // 查询、更新成功
201 Created // 创建成功(POST 返回)
204 No Content // 删除成功,无返回内容
// 4xx 客户端错误
400 Bad Request // 参数错误
401 Unauthorized // 未登录
403 Forbidden // 无权限
404 Not Found // 资源不存在
409 Conflict // 冲突(如用户名已存在)
// 5xx 服务器错误
500 Internal Server Error // 服务器异常
// 示例
app.post('/api/users', (req, res) => {
const { username } = req.body;
if (!username) {
return res.status(400).json({ code: 400, msg: '用户名不能为空' });
}
if (exists(username)) {
return res.status(409).json({ code: 409, msg: '用户名已存在' });
}
const user = create({ username });
res.status(201).json({ code: 0, data: user });
});
步骤 4:统一响应格式
// 成功
{
"code": 0,
"msg": "success",
"data": { /* 业务数据 */ }
}
// 失败
{
"code": 400,
"msg": "用户名不能为空",
"data": null
}
// 分页
{
"code": 0,
"msg": "success",
"data": {
"list": [ ... ],
"total": 100,
"page": 1,
"pageSize": 20
}
}
步骤 5:实战——完整的课程 CRUD
const express = require('express');
const router = express.Router();
// 列表(支持分页、筛选)
router.get('/courses', async (req, res) => {
const { page = 1, pageSize = 20, category, keyword } = req.query;
const offset = (page - 1) * pageSize;
let sql = 'SELECT * FROM course WHERE 1=1';
const params = [];
if (category) {
sql += ' AND category = $1';
params.push(category);
}
if (keyword) {
sql += ` AND title ILIKE $${params.length + 1}`;
params.push(`%${keyword}%`);
}
sql += ' ORDER BY created_at DESC LIMIT $' + (params.length + 1) +
' OFFSET $' + (params.length + 2);
params.push(pageSize, offset);
const result = await pool.query(sql, params);
const total = await pool.query('SELECT COUNT(*) FROM course');
res.json({
code: 0,
data: {
list: result.rows,
total: parseInt(total.rows[0].count),
page: parseInt(page),
pageSize: parseInt(pageSize)
}
});
});
// 详情
router.get('/courses/:id', async (req, res) => {
const { id } = req.params;
const result = await pool.query('SELECT * FROM course WHERE id = $1', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ code: 404, msg: '课程不存在' });
}
res.json({ code: 0, data: result.rows[0] });
});
// 创建
router.post('/courses', async (req, res) => {
const { title, description } = req.body;
if (!title) {
return res.status(400).json({ code: 400, msg: '标题不能为空' });
}
const result = await pool.query(
'INSERT INTO course (title, description) VALUES ($1, $2) RETURNING *',
[title, description]
);
res.status(201).json({ code: 0, data: result.rows[0] });
});
// 更新
router.put('/courses/:id', async (req, res) => {
const { id } = req.params;
const { title, description } = req.body;
const result = await pool.query(
'UPDATE course SET title = $1, description = $2 WHERE id = $3 RETURNING *',
[title, description, id]
);
if (result.rows.length === 0) {
return res.status(404).json({ code: 404, msg: '课程不存在' });
}
res.json({ code: 0, data: result.rows[0] });
});
// 删除
router.delete('/courses/:id', async (req, res) => {
const { id } = req.params;
const result = await pool.query('DELETE FROM course WHERE id = $1 RETURNING id', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ code: 404, msg: '课程不存在' });
}
res.status(204).send();
});
module.exports = router;
步骤 6:版本管理
// URL 版本(推荐)
app.use('/api/v1/courses', coursesV1);
app.use('/api/v2/courses', coursesV2);
// 或 Header 版本
app.use('/api/courses', (req, res, next) => {
const version = req.headers['api-version'] || '1';
if (version === '2') {
return coursesV2(req, res, next);
}
coursesV1(req, res, next);
});
步骤 7:错误处理中间件
// 统一错误处理
app.use((err, req, res, next) => {
console.error(err.stack);
if (err.name === 'ValidationError') {
return res.status(400).json({ code: 400, msg: err.message });
}
if (err.name === 'UnauthorizedError') {
return res.status(401).json({ code: 401, msg: '未登录' });
}
res.status(500).json({ code: 500, msg: '服务器内部错误' });
});
讲清楚要点
- URL 用名词复数:
/users而非/user,/courses而非/getCourseList。 - HTTP 方法表达动作:GET 查、POST 增、PUT 改、DELETE 删。
- 状态码表达结果:2xx 成功、4xx 客户端错、5xx 服务器错。
- 响应格式统一:
{ code, msg, data }三段式。 - 版本管理:URL 加
/v1/、/v2/,便于后续升级。
你可能会懵的地方
-
PUT 和 PATCH 区别? PUT 是全量更新(传所有字段),PATCH 是部分更新(只传要改的字段)。实际项目用 PUT 更多。
-
为什么 POST 不幂等? POST 用于创建,重复调用会创建多条。GET、PUT、DELETE 重复调用效果相同(幂等)。
-
400 和 422 区别? 400 是通用参数错误;422 是语义错误(参数格式对但业务不合法,如「库存不足」)。一般用 400 就够。
-
删除返回 204 还是 200? 204 无内容,更语义化;200 可返回删除的资源信息。看团队约定。
-
GraphQL 和 RESTful 选哪个? RESTful 简单、缓存好;GraphQL 灵活(前端指定字段)、减少请求。中小项目 RESTful 足够。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
POST /api/getUser | 动词 + 名词,不符合规范 | GET /api/users/:id |
| 所有请求都用 POST | 丢失 HTTP 方法语义 | 按操作类型选方法 |
| 所有响应都返回 200 | 丢失状态码语义 | 错误用 4xx/5xx |
| 响应格式不统一 | 前端处理麻烦 | 统一 { code, msg, data } |
| URL 大小写混用 | 不统一、可能 404 | 全小写,单词用 - 分隔 |
卡点急救
- 跨域报错:后端加 CORS 中间件,或前端配置代理。
- 404 但接口存在:检查路由顺序(精确路由在前)、URL 拼写、HTTP 方法。
- 参数拿不到:GET 用
req.query,POST JSON 用req.body(需express.json()中间件)。
面试常问
-
RESTful 核心原则? 资源(名词)+ HTTP 方法(动作)+ 状态码(结果)。无状态、可缓存、统一接口。
-
GET 和 POST 区别? GET 幂等、参数在 URL(有长度限制、可见)、可缓存;POST 不幂等、参数在 body(安全、无长度限制)。
-
如何设计分页?
?page=1&pageSize=20或?offset=0&limit=20。返回list、total、page、pageSize。 -
如何设计文件上传接口?
POST /api/files,Content-Type: multipart/form-data,返回文件 URL 或 ID。
自测三问
PUT和PATCH的区别?- 为什么删除成功返回 204 而不是 200?
- 设计一个「用户收藏课程」的接口?
小结一句话
RESTful 用名词 + HTTP 方法 + 状态码设计 API,统一响应格式,让前后端协作顺畅。
延伸
- RESTful API 设计最佳实践
- 下一课:JWT 认证保护 API。