⚡
把数据真正送进后端|POST请求里的JSON是怎么被读出来的?
全栈转型计划 · 第1周 / Day 2
昨天我们已经用Node.js原生HTTP模块接住了GET /api/health。但真正的业务不只是“打开一个地址”,前端还要把表单数据提交给后端。
今天给“项目与内容协作后台”增加第一个业务接口:
httpPOST /api/tasks
重点不是记住代码,而是看清:
text请求方法 + 请求路径 + 请求头 + 请求体
这四部分是怎么共同决定后端行为的。
今天的目标
接收前端提交的任务:
json{
"title": "整理8月项目复盘"
}创建成功后返回:
json{
"data": {
"id": "生成的任务ID",
"title": "整理8月项目复盘",
"status": "todo"
}
}同时处理三种错误:
- 不是JSON请求:返回
415 - JSON格式错误:返回
400 title为空:返回422
20分钟怎么分
- 3分钟:理解JSON请求的组成
- 7分钟:读取请求体
- 6分钟:创建任务并返回
- 4分钟:测试三个异常情况
一、前端到底发送了什么?
前端可能会这样写:
tsfetch('http://localhost:3000/api/tasks', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: '整理8月项目复盘'
})
});后端收到的不是一个现成的JavaScript对象,而是一段HTTP请求:
httpPOST /api/tasks HTTP/1.1
Content-Type: application/json
{"title":"整理8月项目复盘"}对应到Node.js:
| 请求内容 | Node.js读取位置 |
|---|---|
POST |
req.method |
/api/tasks |
req.url |
Content-Type |
req.headers |
| JSON正文 | 请求数据流 |
前三项可以直接读取,请求体却需要等待数据流接收完成。
二、今天的动手任务
在昨天的src/server.ts顶部增加:
tsimport { randomUUID } from 'node:crypto';
import type { IncomingMessage } from 'node:http';然后创建一个读取JSON的方法:
tsasync function readJsonBody(
req: IncomingMessage
): Promise<unknown> {
// 1. 保存收到的数据块
// 2. 限制请求体最大为1MB
// 3. 合并数据并使用JSON.parse解析
}接着在昨天的404处理之前,增加:
tsif (
url.pathname === '/api/tasks' &&
method === 'POST'
) {
// 检查Content-Type
// 读取JSON请求体
// 检查title
// 创建并返回任务
}注意:一定要放在最终的404之前,否则请求会提前被当作不存在的接口处理。
三、完成标准
正常创建
bashcurl -i \
-X POST \
-H 'Content-Type: application/json' \
-d '{"title":"整理8月项目复盘"}' \
http://localhost:3000/api/tasks应该返回:
httpHTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8正文类似:
json{
"data": {
"id": "bb74162f-5ddd-4bb5-8b33-624e5414c67f",
"title": "整理8月项目复盘",
"status": "todo"
}
}忘记声明JSON类型
bashcurl -i \
-X POST \
-d '{"title":"整理复盘"}' \
http://localhost:3000/api/tasks应该返回:
httpHTTP/1.1 415 Unsupported Media TypeJSON格式损坏
bashcurl -i \
-X POST \
-H 'Content-Type: application/json' \
-d '{"title":}' \
http://localhost:3000/api/tasks应该返回:
httpHTTP/1.1 400 Bad Request标题为空
bashcurl -i \
-X POST \
-H 'Content-Type: application/json' \
-d '{"title":" "}' \
http://localhost:3000/api/tasks应该返回:
httpHTTP/1.1 422 Unprocessable Entity四、解决思路
1. 读取JSON请求体
tsimport type { IncomingMessage } from 'node:http';
async function readJsonBody(
req: IncomingMessage
): Promise<unknown> {
const chunks: Buffer[] = [];
let totalSize = 0;
for await (const chunk of req) {
const buffer = Buffer.isBuffer(chunk)
? chunk
: Buffer.from(chunk);
totalSize += buffer.length;
if (totalSize > 1024 * 1024) {
throw new Error('BODY_TOO_LARGE');
}
chunks.push(buffer);
}
const rawBody = Buffer
.concat(chunks)
.toString('utf8');
return JSON.parse(rawBody);
}请求体可能分成多个数据块到达,所以要先收集,再合并成完整字符串。
2. 添加创建任务接口
把下面代码放在昨天代码的最终404之前:
tsif (url.pathname === '/api/tasks') {
if (method !== 'POST') {
res.statusCode = 405;
res.setHeader('Allow', 'POST');
return res.end(
JSON.stringify({
error: {
code: 'METHOD_NOT_ALLOWED',
message: '该接口只支持POST请求'
}
})
);
}
const contentType =
req.headers['content-type'] ?? '';
if (!contentType.includes('application/json')) {
res.statusCode = 415;
return res.end(
JSON.stringify({
error: {
code: 'UNSUPPORTED_MEDIA_TYPE',
message: '请求体必须使用JSON格式'
}
})
);
}
try {
const body = await readJsonBody(req) as {
title?: unknown;
};
if (
typeof body.title !== 'string' ||
body.title.trim() === ''
) {
res.statusCode = 422;
return res.end(
JSON.stringify({
error: {
code: 'INVALID_TITLE',
message: '任务标题不能为空'
}
})
);
}
const task = {
id: randomUUID(),
title: body.title.trim(),
status: 'todo'
};
res.statusCode = 201;
return res.end(
JSON.stringify({
data: task
})
);
} catch (error) {
if (
error instanceof Error &&
error.message === 'BODY_TOO_LARGE'
) {
res.statusCode = 413;
return res.end(
JSON.stringify({
error: {
code: 'PAYLOAD_TOO_LARGE',
message: '请求体不能超过1MB'
}
})
);
}
res.statusCode = 400;
return res.end(
JSON.stringify({
error: {
code: 'INVALID_JSON',
message: 'JSON格式不正确'
}
})
);
}
}因为现在请求处理过程中使用了await,昨天的服务器回调也要改成异步:
tsconst server = createServer(async (req, res) => {
// 原有代码
});五、今天真正要记住的链路
text前端将对象JSON.stringify() → 通过HTTP发送字符串 → Node.js分块接收请求体 → 合并为完整字符串 → JSON.parse()转回对象 → 校验字段 → 执行业务逻辑 → 返回JSON响应
前端中的对象,不会原封不动地“穿过网络”来到后端。网络传输的是字节,框架只是把读取和解析过程帮我们封装了。
生产环境最容易踩的坑
无限制地读取请求体:
tsfor await (const chunk of req) {
chunks.push(chunk);
}如果有人上传几百MB甚至更大的内容,Node.js会不断占用内存,严重时整个服务会被拖垮。
所以即使接口只接收一小段JSON,也必须限制请求体大小。后续使用Express时,这个限制会由JSON解析中间件负责,但仍然需要主动配置。
分享:掘金同步
如果这篇对你有帮助,欢迎关注公众号「前端达人」,每周更新实用前端干货。

