Node.js RESTful API
用 Express 写一套规范的 CRUD 接口:状态码、错误格式、分页、版本。
Key Points资源用名词复数做路径,动作用 HTTP 方法表达,状态码表达结果。
资源与方法
| Method + Path | Meaning | Success |
|---|---|---|
GET /users | 列表 | 200 |
GET /users/1 | 详情 | 200 |
POST /users | 创建 | 201 + Location |
PUT /users/1 | 整体替换 | 200 |
PATCH /users/1 | 局部更新 | 200 |
DELETE /users/1 | 删除 | 204 |
完整实现
JavaScript
const express = require('express');
const app = express();
app.use(express.json());
let seq = 2;
const users = [
{ id: 1, name: '张三', email: 'zhang@example.com' },
{ id: 2, name: '李四', email: 'li@example.com' },
];
// 列表(带分页)
app.get('/api/v1/users', (req, res) => {
const page = Math.max(1, Number(req.query.page) || 1);
const size = Math.min(100, Number(req.query.size) || 10);
const start = (page - 1) * size;
res.json({
data: users.slice(start, start + size),
page,
size,
total: users.length,
});
});
// 详情
app.get('/api/v1/users/:id', (req, res) => {
const user = users.find((u) => u.id === Number(req.params.id));
if (!user) return res.status(404).json({ error: '用户不存在' });
res.json(user);
});
// 创建
app.post('/api/v1/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(422).json({ error: 'name 和 email 必填' });
}
const user = { id: ++seq, name, email };
users.push(user);
res.status(201).location(`/api/v1/users/${user.id}`).json(user);
});
// 局部更新
app.patch('/api/v1/users/:id', (req, res) => {
const user = users.find((u) => u.id === Number(req.params.id));
if (!user) return res.status(404).json({ error: '用户不存在' });
Object.assign(user, req.body, { id: user.id });
res.json(user);
});
// 删除
app.delete('/api/v1/users/:id', (req, res) => {
const i = users.findIndex((u) => u.id === Number(req.params.id));
if (i === -1) return res.status(404).json({ error: '用户不存在' });
users.splice(i, 1);
res.status(204).end();
});
app.listen(3000);试一试
Bash
curl http://127.0.0.1:3000/api/v1/users?page=1&size=1
curl -X POST http://127.0.0.1:3000/api/v1/users \
-H "Content-Type: application/json" \
-d '{"name":"王五","email":"wang@example.com"}'
curl -X DELETE -i http://127.0.0.1:3000/api/v1/users/3Output
{"data":[{"id":1,"name":"张三","email":"zhang@example.com"}],"page":1,"size":1,"total":2}
HTTP/1.1 201 Created
Location: /api/v1/users/3
Content-Type: application/json; charset=utf-8
{"id":3,"name":"王五","email":"wang@example.com"}
HTTP/1.1 204 No Content状态码怎么选
| Code | When |
|---|---|
| 200 | 查询、更新成功且返回内容 |
| 201 | 创建成功,配 Location |
| 204 | 成功但没有响应体(删除) |
| 400 | 请求格式错(JSON 解析失败) |
| 401 | 没登录 / token 无效 |
| 403 | 登录了但没权限 |
| 404 | 资源不存在 |
| 409 | 冲突(邮箱已注册) |
| 422 | 格式对但语义错(校验失败) |
| 429 | 限流 |
Never return 200 with
{code: 500}. That blinds every HTTP middleware, monitor, cache and gateway — they only read the status line. Put business codes in the body as a supplement, not a replacement.分页与筛选
JavaScript
// 偏移分页:简单,但深翻页慢
GET /users?page=100&size=20
// 游标分页:稳定,适合信息流
GET /users?cursor=eyJpZCI6MTk5fQ&size=20
// 筛选与排序
GET /users?status=active&sort=-createdAt&fields=id,name资源与方法
| 方法与路径 | 含义 | 成功码 |
|---|---|---|
GET /users | 列表 | 200 |
GET /users/1 | 详情 | 200 |
POST /users | 创建 | 201 + Location |
PUT /users/1 | 整体替换 | 200 |
PATCH /users/1 | 局部更新 | 200 |
DELETE /users/1 | 删除 | 204 |
完整实现
JavaScript
const express = require('express');
const app = express();
app.use(express.json());
let seq = 2;
const users = [
{ id: 1, name: '张三', email: 'zhang@example.com' },
{ id: 2, name: '李四', email: 'li@example.com' },
];
// 列表(带分页)
app.get('/api/v1/users', (req, res) => {
const page = Math.max(1, Number(req.query.page) || 1);
const size = Math.min(100, Number(req.query.size) || 10);
const start = (page - 1) * size;
res.json({
data: users.slice(start, start + size),
page,
size,
total: users.length,
});
});
// 详情
app.get('/api/v1/users/:id', (req, res) => {
const user = users.find((u) => u.id === Number(req.params.id));
if (!user) return res.status(404).json({ error: '用户不存在' });
res.json(user);
});
// 创建
app.post('/api/v1/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(422).json({ error: 'name 和 email 必填' });
}
const user = { id: ++seq, name, email };
users.push(user);
res.status(201).location(`/api/v1/users/${user.id}`).json(user);
});
// 局部更新
app.patch('/api/v1/users/:id', (req, res) => {
const user = users.find((u) => u.id === Number(req.params.id));
if (!user) return res.status(404).json({ error: '用户不存在' });
Object.assign(user, req.body, { id: user.id });
res.json(user);
});
// 删除
app.delete('/api/v1/users/:id', (req, res) => {
const i = users.findIndex((u) => u.id === Number(req.params.id));
if (i === -1) return res.status(404).json({ error: '用户不存在' });
users.splice(i, 1);
res.status(204).end();
});
app.listen(3000);试一试
Bash
curl http://127.0.0.1:3000/api/v1/users?page=1&size=1
curl -X POST http://127.0.0.1:3000/api/v1/users \
-H "Content-Type: application/json" \
-d '{"name":"王五","email":"wang@example.com"}'
curl -X DELETE -i http://127.0.0.1:3000/api/v1/users/3Output
{"data":[{"id":1,"name":"张三","email":"zhang@example.com"}],"page":1,"size":1,"total":2}
HTTP/1.1 201 Created
Location: /api/v1/users/3
Content-Type: application/json; charset=utf-8
{"id":3,"name":"王五","email":"wang@example.com"}
HTTP/1.1 204 No Content状态码怎么选
| 码 | 何时 |
|---|---|
| 200 | 查询、更新成功且返回内容 |
| 201 | 创建成功,配 Location |
| 204 | 成功但没有响应体(删除) |
| 400 | 请求格式错(JSON 解析失败) |
| 401 | 没登录 / token 无效 |
| 403 | 登录了但没权限 |
| 404 | 资源不存在 |
| 409 | 冲突(邮箱已注册) |
| 422 | 格式对但语义错(校验失败) |
| 429 | 限流 |
不要用 200 包一个
{code: 500} 表示失败。那会让所有 HTTP 中间件、监控、缓存、网关都失明——它们只看状态码。业务错误码放在响应体里做补充,不要替代状态码。分页与筛选
JavaScript
// 偏移分页:简单,但深翻页慢
GET /users?page=100&size=20
// 游标分页:稳定,适合信息流
GET /users?cursor=eyJpZCI6MTk5fQ&size=20
// 筛选与排序
GET /users?status=active&sort=-createdAt&fields=id,name