⚡
URL里的三种数据 03|params、query、body到底该怎么分?
全栈转型计划 · 第1周 / Day 3
昨天我们已经能通过POST /api/tasks创建任务。今天继续给这个接口补上“修改任务状态”的能力。
这次不只是读取JSON,而是通过一个请求同时认识三种数据:
httpPATCH /api/tasks/任务ID?notify=true
Content-Type: application/json
{
"status": "done"
}它们分别表达:
任务ID:修改哪一个任务notify=true:修改后是否发送通知status:具体修改什么内容
今天的目标
实现:
httpPATCH /api/tasks/:taskId?notify=true
将指定任务的状态修改为:
texttodo → doing → done
成功响应:
json{
"data": {
"id": "任务ID",
"title": "整理8月项目复盘",
"status": "done"
},
"meta": {
"notified": true
}
}同时处理:
- 任务不存在:
404 - 状态不合法:
422 - JSON格式错误:
400
20分钟怎么分
- 4分钟:理解params、query和body
- 4分钟:让创建的任务暂存在数组中
- 8分钟:完成状态修改接口
- 4分钟:验证正常和异常请求
一、三种数据分别放什么?
| 类型 | 本次请求 | 用途 |
|---|---|---|
| Path Params | /tasks/:taskId |
表示操作哪个资源 |
| Query Params | ?notify=true |
控制筛选或可选行为 |
| Request Body | {"status":"done"} |
提交要创建或修改的数据 |
一个简单的判断方法:
text它是谁? → Path Params 这次怎么处理? → Query Params 要改成什么? → Request Body
所以修改任务状态,比较自然的接口是:
httpPATCH /api/tasks/123?notify=true { "status": "done" }
而不是:
httpPATCH /api/tasks { "taskId": "123", "notify": true, "status": "done" }
后者并非绝对错误,但资源身份和控制参数都混进了业务数据,接口意图不够清楚。
二、先保存昨天创建的任务
昨天创建了任务对象,但请求结束后没有保存它。
在server.ts顶部增加类型和数组:
tstype TaskStatus = 'todo' | 'doing' | 'done';
type Task = {
id: string;
title: string;
status: TaskStatus;
};
const tasks: Task[] = [];然后找到昨天创建任务的位置:
tsconst task: Task = {
id: randomUUID(),
title: body.title.trim(),
status: 'todo'
};在返回响应前增加:
tstasks.push(task);注意:这仍然只是内存数据。服务重启后任务会消失,第3周才会正式接入PostgreSQL。
三、今天的动手任务
在最终的404之前增加代码骨架:
tsconst taskPathMatch = url.pathname.match(
/^\/api\/tasks\/([^/]+)$/
);
if (taskPathMatch) {
// 1. 判断是否使用PATCH
// 2. 读取taskId路径参数
// 3. 读取notify查询参数
// 4. 根据taskId查找任务
// 5. 读取body中的status
// 6. 修改并返回任务
}这三个数据分别从这里获得:
tsconst taskId = taskPathMatch[1];
const notify = url.searchParams.get('notify');
const body = await readJsonBody(req);四、完成标准
1. 先创建一个任务
bashcurl \
-X POST \
-H 'Content-Type: application/json' \
-d '{"title":"整理8月项目复盘"}' \
http://localhost:3000/api/tasks复制返回的任务ID。
2. 修改任务状态
把下面的TASK_ID替换成刚才返回的ID:
bashcurl -i \
-X PATCH \
-H 'Content-Type: application/json' \
-d '{"status":"done"}' \
'http://localhost:3000/api/tasks/TASK_ID?notify=true'应该返回:
httpHTTP/1.1 200 OK并且:
json{
"data": {
"id": "TASK_ID",
"title": "整理8月项目复盘",
"status": "done"
},
"meta": {
"notified": true
}
}3. 测试不存在的任务
bashcurl -i \
-X PATCH \
-H 'Content-Type: application/json' \
-d '{"status":"done"}' \
'http://localhost:3000/api/tasks/not-exist?notify=true'应该返回404。
4. 测试错误状态
bashcurl -i \
-X PATCH \
-H 'Content-Type: application/json' \
-d '{"status":"finished"}' \
'http://localhost:3000/api/tasks/TASK_ID?notify=false'应该返回422。
五、解决思路
把下面代码放在最终的404之前:
tsconst taskPathMatch = url.pathname.match(
/^\/api\/tasks\/([^/]+)$/
);
if (taskPathMatch) {
if (method !== 'PATCH') {
res.statusCode = 405;
res.setHeader('Allow', 'PATCH');
return res.end(
JSON.stringify({
error: {
code: 'METHOD_NOT_ALLOWED',
message: '该接口只支持PATCH请求'
}
})
);
}
const taskId = decodeURIComponent(
taskPathMatch[1]
);
const notify =
url.searchParams.get('notify') === 'true';
const task = tasks.find(
item => item.id === taskId
);
if (!task) {
res.statusCode = 404;
return res.end(
JSON.stringify({
error: {
code: 'TASK_NOT_FOUND',
message: '任务不存在'
}
})
);
}
try {
const body = await readJsonBody(req) as {
status?: unknown;
};
const allowedStatuses: TaskStatus[] = [
'todo',
'doing',
'done'
];
if (
typeof body.status !== 'string' ||
!allowedStatuses.includes(
body.status as TaskStatus
)
) {
res.statusCode = 422;
return res.end(
JSON.stringify({
error: {
code: 'INVALID_STATUS',
message: '任务状态不正确'
}
})
);
}
task.status = body.status as TaskStatus;
res.statusCode = 200;
return res.end(
JSON.stringify({
data: task,
meta: {
notified: notify
}
})
);
} catch {
res.statusCode = 400;
return res.end(
JSON.stringify({
error: {
code: 'INVALID_JSON',
message: 'JSON格式不正确'
}
})
);
}
}六、把整个请求重新看一遍
httpPATCH /api/tasks/abc123?notify=true
Content-Type: application/json
{
"status": "done"
}后端的读取方式:
ts// 请求方法
req.method;
// PATCH
// 路径参数
taskPathMatch[1];
// abc123
// 查询参数
url.searchParams.get('notify');
// "true"
// 请求体
body.status;
// "done"这也是后续使用Express、FastAPI和Spring Boot时一直会遇到的基本模型。框架写法不同,但三种参数的职责不变。
生产环境最容易踩的坑
查询参数永远要先按字符串理解。
下面这段代码有问题:
tsconst notify = Boolean(
url.searchParams.get('notify')
);因为:
tsBoolean('true'); // true
Boolean('false'); // 仍然是true只要是非空字符串,转成布尔值就是true。
应该明确判断:
tsconst notify =
url.searchParams.get('notify') === 'true';否则前端明明传了notify=false,后端却仍然可能发送通知。
分享:掘金同步
如果这篇对你有帮助,欢迎关注公众号「前端达人」,每周更新实用前端干货。

