RESTful API 设计与实战
本教程共 76 篇 · 第 42 篇 · 更新于 2026-07-25 · 约 6 分钟阅读
42. RESTful API 设计与实战
本节目标:REST 原则、状态码、统一响应结构和完整 CRUD 实现。
REST(Representational State Transfer)不是什么协议,而是一种设计 Web API 的风格。它的核心思想很简单:URL 表示资源,HTTP 方法表示对资源的操作,响应状态码表示结果。听起来像废话?但做到位和随便写,差距非常大。
这节我们先讲设计原则,再手搓一套完整的 CRUD 接口,代码可以直接跑。
REST 的核心原则
1. 资源即名词
URL 里不要用动词。/getUsers 和 /createUser 是 RPC 风格,REST 风格是:
GET /users // 获取用户列表
GET /users/42 // 获取单个用户
POST /users // 创建用户
PUT /users/42 // 完整更新
PATCH /users/42 // 部分更新
DELETE /users/42 // 删除用户
2. 无状态
服务器不保存客户端的上下文。每次请求必须自带全部信息(比如通过 JWT Token 或 Session ID)。这样横向扩容时随便加机器,不需要考虑会话粘滞。
3. 正确使用状态码
| 状态码 | 场景 |
|---|---|
| 200 OK | GET、PUT、PATCH、DELETE 成功 |
| 201 Created | POST 创建成功 |
| 204 No Content | 删除成功,不返回 body |
| 400 Bad Request | 参数校验失败 |
| 401 Unauthorized | 未登录 |
| 403 Forbidden | 无权限 |
| 404 Not Found | 资源不存在 |
| 500 Internal Server Error | 服务端异常 |
4. 返回一致的响应结构
前后端对接最烦的就是「这个接口返回数组,那个接口返回对象,出错时又变成字符串」。建议统一结构:
{
"code": 0,
"message": "ok",
"data": { ... }
}
项目结构
rest-demo/
├── app.js
├── routes/
│ └── users.js
├── controllers/
│ └── userController.js
└── package.json
实际代码量不大,拆分是为了演示真实项目的组织方式。你可以把逻辑都塞一个文件,但千万别养成这个习惯。
完整 CRUD 代码
app.js
import express from 'express'
import userRoutes from './routes/users.js'
const app = express()
const port = 3000
app.use(express.json())
// 统一响应封装
app.use((req, res, next) => {
res.sendSuccess = (data, message = 'ok') => {
res.json({ code: 0, message, data })
}
res.sendError = (message, statusCode = 500, code = statusCode) => {
res.status(statusCode).json({ code, message, data: null })
}
next()
})
// 路由
app.use('/users', userRoutes)
// 404
app.use((req, res) => {
res.sendError('Not Found', 404)
})
// 错误处理(Express 5 自动捕获异步错误)
app.use((err, req, res, next) => {
console.error(err)
res.sendError(err.message || 'Internal Server Error', err.status || 500)
})
app.listen(port, () => {
console.log(`API server at http://localhost:${port}`)
})
controllers/userController.js
// 内存存储,实际项目换成数据库
let users = [
{ id: 1, name: 'Tom', email: 'tom@example.com' },
{ id: 2, name: 'Jerry', email: 'jerry@example.com' }
]
let nextId = 3
export const getUsers = (req, res) => {
res.sendSuccess(users)
}
export const getUser = (req, res) => {
const id = Number(req.params.id)
const user = users.find(u => u.id === id)
if (!user) {
return res.sendError('User not found', 404)
}
res.sendSuccess(user)
}
export const createUser = (req, res) => {
const { name, email } = req.body
if (!name || !email) {
return res.sendError('Name and email are required', 400)
}
const user = { id: nextId++, name, email }
users.push(user)
res.status(201).sendSuccess(user, 'Created')
}
export const updateUser = (req, res) => {
const id = Number(req.params.id)
const user = users.find(u => u.id === id)
if (!user) {
return res.sendError('User not found', 404)
}
const { name, email } = req.body
if (name) user.name = name
if (email) user.email = email
res.sendSuccess(user)
}
export const deleteUser = (req, res) => {
const id = Number(req.params.id)
const index = users.findIndex(u => u.id === id)
if (index === -1) {
return res.sendError('User not found', 404)
}
users.splice(index, 1)
res.status(204).send()
}
routes/users.js
import express from 'express'
import {
getUsers, getUser, createUser, updateUser, deleteUser
} from '../controllers/userController.js'
const router = express.Router()
router.get('/', getUsers)
router.get('/:id', getUser)
router.post('/', createUser)
router.put('/:id', updateUser)
router.delete('/:id', deleteUser)
export default router
运行:
node app.js
测试接口
用 curl 或者 Postman 都可以,这里用 curl 演示:
# 获取所有用户
curl http://localhost:3000/users
# 获取单个用户
curl http://localhost:3000/users/1
# 创建用户
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"Spike","email":"spike@example.com"}'
# 更新用户
curl -X PUT http://localhost:3000/users/1 \
-H "Content-Type: application/json" \
-d '{"name":"Tommy"}'
# 删除用户
curl -X DELETE http://localhost:3000/users/2
也可以用 Node.js 内置的 fetch(自 v18 起全局可用)写测试脚本:
const base = 'http://localhost:3000'
async function test() {
const create = await fetch(`${base}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Tyke', email: 'tyke@example.com' })
})
console.log('POST', await create.json())
const list = await fetch(`${base}/users`)
console.log('GET', await list.json())
}
test()
分页与过滤
列表接口默认返回全部数据,量大了会拖垮服务。分页是最基本的优化:
export const getUsers = (req, res) => {
const page = Number(req.query.page) || 1
const limit = Number(req.query.limit) || 10
const keyword = req.query.keyword || ''
let result = users
if (keyword) {
result = result.filter(u => u.name.includes(keyword))
}
const start = (page - 1) * limit
const end = start + limit
const data = result.slice(start, end)
res.sendSuccess({
list: data,
total: result.length,
page,
limit
})
}
请求 /users?page=2&limit=5&keyword=Tom 就能拿到第二页、每页 5 条、名字包含 Tom 的数据。
Tip实际项目里分页和过滤应该在数据库层做(SQL 的
LIMIT/OFFSET或 MongoDB 的skip/limit),而不是把数据全捞到内存里再过滤。这里用内存数组只是为了演示接口形态。
版本控制
API 一旦上线,就不能随便改字段名或删接口,否则下游会炸。给 URL 加版本号是常见做法:
import v1UserRoutes from './routes/v1/users.js'
import v2UserRoutes from './routes/v2/users.js'
app.use('/api/v1/users', v1UserRoutes)
app.use('/api/v2/users', v2UserRoutes)
v2 可以复用 v1 的控制器,只改需要变化的部分。大版本变更才开新路径,小改动尽量保持兼容。