
1. 项目缘起与整体设计思路1.1 为什么选这个题目练手我一直觉得学一门后端技术最快的路径不是啃文档而是从零手搓一个能跑起来的 API 服务。原因很简单API 服务是几乎所有现代应用的骨架你手机上的外卖 App、公司内部的报表系统、甚至家里那台智能音箱背后都是一堆 HTTP 接口在互相喊话。把这块搞明白了后面学数据库、缓存、消息队列、容器化都是在这个骨架上挂肉。这次的小项目目标很明确用 Node.js Express 搭一个提供 JSON 数据的 RESTful API包含增删改查四个基本动作最后用 curl 验证。选 Node.js 而不是 Java、Go是因为它对新手最友好——不用编译改完代码刷新就能看到效果而且 JavaScript 语法对前端同学来说是零门槛。Express 则是 Node 生态里最成熟的 Web 框架社区资料多遇到问题一搜一大把。这个项目适合谁如果你刚学完 JavaScript 基础语法知道什么是函数、对象、数组但还没碰过后端或者你是前端开发者想搞清楚自己每天调用的接口是怎么写出来的再或者你是个运维/测试想自己搭个 Mock 服务方便联调——那这篇内容就是给你准备的。整个过程不需要你有服务器本地电脑就能跑通。1.2 技术选型的几个关键考量在动手之前我先把几个关键选择说清楚这样你后面看代码时不会一头雾水。为什么用 Express 而不是原生 http 模块Node.js 自带的http模块确实能起服务但你要自己处理路由匹配、请求体解析、错误响应格式写起来又臭又长。Express 把这些脏活累活封装好了一个app.get(/users, handler)就能定义一个接口中间件机制也让日志、鉴权、跨域这些横切关注点变得清爽。对于小项目来说Express 的够用且不啰嗦是最合适的。为什么数据先存在内存里很多教程一上来就让你装 MySQL、配连接池结果新手卡在环境配置上三天代码一行没写。我的建议是先用一个数组模拟数据库把 API 的逻辑跑通理解请求-响应的完整链路。等你对路由、状态码、JSON 序列化这些概念有感觉了再把存储层换成真正的数据库那时候你才知道为什么要用 ORM、为什么要做连接池。为什么用 curl 做验证浏览器只能方便地发 GET 请求要测 POST、PUT、DELETE 就得装 Postman 之类的工具。curl 是命令行工具几乎每台机器都有而且它把 HTTP 请求的原始形态暴露得很清楚——你能看到请求头、请求体、响应码、响应头这对理解 HTTP 协议帮助极大。热词里那一堆curl -v、curl: (56)、curl: (35)的报错其实都是大家在用 curl 调试时踩的坑后面我会专门讲怎么排查。1.3 整体架构长什么样整个服务的结构非常简单就三层入口层app.js负责创建 Express 应用、挂载中间件、启动监听端口。路由层定义 URL 路径和 HTTP 方法的映射关系比如GET /api/users对应获取用户列表。数据层一个内存数组充当数据库提供增删改查的原子操作。请求进来后的流程是先经过全局中间件日志、JSON 解析然后匹配到对应路由路由处理函数操作数据层最后把结果以 JSON 格式返回。这个流程和大型项目的本质是一样的只是大型项目会把每一层拆成独立的文件甚至独立的服务。提示不要小看这个玩具结构。我见过太多人一上来就搞微服务、搞分层架构结果连一个接口的完整生命周期都说不清楚。先把单体跑通再谈拆分。2. 环境准备与核心细节解析2.1 Node.js 安装版本选择与常见坑第一步是装 Node.js。这里有个关键点一定要装 LTS 版本。LTS 是长期支持版稳定、bug 少、社区兼容性好。热词里频繁出现的node.js lts下载、node.js 20、ubuntu安装node.js 20说明大家都在纠结版本问题。我的建议是直接用 20.x 的 LTS它在性能和 API 稳定性之间平衡得最好。安装方式分两种。Windows 和 macOS 用户直接去官网下载安装包一路下一步就行。Linux 用户比如 Ubuntu我推荐用 NodeSource 的源来装比系统自带的版本新curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt-get install -y nodejs装完后验证一下node -v npm -v如果node -v输出v20.x.x说明装好了。这里插一句热词里有个permission denied while trying to connect to the docker api虽然和 Node 没直接关系但道理相通——权限问题十有八九是没用对用户身份。在 Linux 上装全局包时如果报权限错误不要无脑sudo正确做法是配置 npm 的全局目录到用户目录下避免污染系统环境。2.2 初始化项目与依赖安装找个空文件夹执行mkdir my-api cd my-api npm init -y npm install expressnpm init -y会生成一个默认的package.json里面记录了项目名、版本、依赖等信息。npm install express会把 Express 装到node_modules目录并在package.json的dependencies里加上一条记录。这里有个新手常问的问题node_modules要不要提交到 Git绝对不要。这个目录动辄几百兆而且里面的内容完全可以通过package.json和package-lock.json还原。正确的做法是在.gitignore里加上node_modules/。另外我习惯在package.json的scripts里加一条启动命令scripts: { start: node app.js, dev: node --watch app.js }node --watch是 Node 20 内置的热重载功能改完代码自动重启省得你每次手动 CtrlC 再重跑。以前大家用 nodemon现在原生支持了少装一个依赖。2.3 理解 HTTP 方法与状态码在写代码前必须把 RESTful 的核心约定搞清楚否则你写出来的接口别人没法用。HTTP 方法语义对应操作幂等性GET获取资源查询是POST创建资源新增否PUT全量更新资源替换是PATCH部分更新资源修改否DELETE删除资源删除是幂等性这个词听起来唬人其实很简单同一个请求发一次和发十次对服务器状态的影响是一样的。GET 查十次数据不会变DELETE 删十次结果都是已删除所以它们幂等POST 发十次会创建十条记录所以不幂等。理解这个你设计接口时就不会乱用方法了。状态码方面记住几个常用的就够了200 OK请求成功正常返回数据。201 Created资源创建成功通常用于 POST。400 Bad Request客户端传的参数有问题。404 Not Found请求的资源不存在。500 Internal Server Error服务器自己出错了。热词里那个api error: 400 this models maximum context length is 1048576 tokens就是典型的 400 错误——客户端传的内容超限了服务端明确告诉你哪里不对。好的 API 就应该这样错误信息要具体别只返回一个出错了。3. 从零实现完整 API 服务3.1 搭建最小可运行服务先写一个最简单的版本确认环境没问题const express require(express); const app express(); const PORT 3000; app.get(/, (req, res) { res.json({ message: API 服务已启动 }); }); app.listen(PORT, () { console.log(服务运行在 http://localhost:${PORT}); });保存为app.js运行npm run dev然后在另一个终端执行curl http://localhost:3000/你应该看到{message:API 服务已启动}。这一步看着简单但它验证了整条链路Node 能跑、Express 能加载、端口能监听、curl 能通。任何复杂项目都应该从这样一个最小闭环开始别一上来就写几百行出了错你都不知道从哪查。3.2 设计数据模型与内存存储我们用用户管理作为业务场景。先定义一个内存数组let users [ { id: 1, name: 张三, email: zhangsanexample.com }, { id: 2, name: 李四, email: lisiexample.com } ]; let nextId 3;nextId用来生成新用户的 ID。为什么不直接用数组长度加一因为删除操作会让数组长度变化导致 ID 重复。用一个独立的计数器是更稳妥的做法。注意内存存储的数据在服务重启后会全部丢失这是它的固有特性。开发阶段无所谓但如果你要演示给别人看记得每次重启后重新造数据或者干脆写个初始化脚本。3.3 实现查询接口GET获取用户列表app.get(/api/users, (req, res) { res.json({ code: 0, data: users }); });获取单个用户app.get(/api/users/:id, (req, res) { const id parseInt(req.params.id, 10); const user users.find(u u.id id); if (!user) { return res.status(404).json({ code: 1, message: 用户不存在 }); } res.json({ code: 0, data: user }); });这里有几个细节值得说。第一req.params.id拿到的是字符串必须用parseInt转成数字再比较否则1 1永远是 false。热词里javascript判断数据类型是高频搜索说明很多人在这上面栽过跟头。第二我用了{ code, data, message }这种统一响应格式code: 0表示成功非零表示失败。这是国内很多团队的约定俗成好处是前端处理逻辑统一不用去猜每个接口的返回结构。3.4 实现创建接口POSTPOST 请求的数据在请求体里Express 默认不解析请求体需要挂一个中间件app.use(express.json());这行代码必须放在所有路由之前。然后写创建逻辑app.post(/api/users, (req, res) { const { name, email } req.body; if (!name || !email) { return res.status(400).json({ code: 1, message: name 和 email 不能为空 }); } const newUser { id: nextId, name, email }; users.push(newUser); res.status(201).json({ code: 0, data: newUser }); });用 curl 测试curl -X POST http://localhost:3000/api/users \ -H Content-Type: application/json \ -d {name:王五,email:wangwuexample.com}-H指定请求头-d指定请求体。Content-Type: application/json这个头千万不能漏否则express.json()中间件不会解析请求体req.body会是空对象。我见过太多人卡在这里反复检查代码逻辑结果问题出在请求头。3.5 实现更新与删除接口更新用 PUTapp.put(/api/users/:id, (req, res) { const id parseInt(req.params.id, 10); const index users.findIndex(u u.id id); if (index -1) { return res.status(404).json({ code: 1, message: 用户不存在 }); } const { name, email } req.body; users[index] { id, name: name || users[index].name, email: email || users[index].email }; res.json({ code: 0, data: users[index] }); });删除用 DELETEapp.delete(/api/users/:id, (req, res) { const id parseInt(req.params.id, 10); const index users.findIndex(u u.id id); if (index -1) { return res.status(404).json({ code: 1, message: 用户不存在 }); } const [removed] users.splice(index, 1); res.json({ code: 0, data: removed }); });findIndex返回匹配元素的下标找不到返回 -1这个判断模式在数组操作里非常常用。splice(index, 1)删除一个元素并返回被删除的元素数组用解构取出第一个就是被删的对象。3.6 加上日志中间件为了调试方便加一个简单的请求日志app.use((req, res, next) { console.log(${new Date().toISOString()} ${req.method} ${req.url}); next(); });next()是 Express 中间件的核心调用它才会把控制权交给下一个中间件或路由。忘记调用 next() 是新手最常见的 bug表现就是请求发出去后一直挂起浏览器转圈圈最后超时。热词里curl: (28) timeout这类超时错误排查时就要先看看是不是某个中间件卡住了。4. 常见问题与排查技巧实录4.1 curl 报错速查表调试 API 时 curl 的报错信息五花八门我整理了一份速查表报错信息含义排查方向curl: (7) Failed to connect连不上服务服务没启动或端口写错curl: (28) timeout连接超时中间件卡住或网络不通curl: (35) recv failure: connection reset连接被重置服务端崩溃或协议不匹配curl: (56) recv failure接收数据失败服务端提前关闭连接curl: (23) failure writing output写输出失败磁盘满或管道被关闭curl: (52) empty reply空响应服务端没返回任何内容就断了热词里curl 56 recv failure: 连接超时和curl: (35) recv failure: connection reset by peer出现频率很高。这两个错误的共同点是服务端在响应完成前就断开了连接。常见原因有三个一是代码里有未捕获的异常导致进程崩溃二是响应体太大客户端等不及断了三是中间件里调用了res.end()之后又试图写数据。排查这类问题的杀手锏是curl -v-v会把请求和响应的完整过程打印出来包括 TCP 连接、TLS 握手、请求头、响应头。你能清楚看到是在哪一步断的。如果连 TCP 都没建立起来那就是网络或端口问题如果请求发出去了但没响应那就是服务端逻辑问题。4.2 请求体解析失败的三种情况req.body是undefined或空对象是 POST/PUT 接口最常见的故障。我总结了三种情况情况一没挂express.json()中间件。这是最基础的检查app.use(express.json())是否在所有路由之前。情况二请求头没带Content-Type: application/json。Express 是根据这个头来决定用哪个解析器的头不对body 就是空的。情况三JSON 格式本身有语法错误。比如用了单引号、多了个逗号、中文没转义。express.json()解析失败会抛异常如果你没做错误处理客户端会收到一个 400 但信息不明确。可以加一个错误处理中间件app.use((err, req, res, next) { if (err instanceof SyntaxError err.status 400) { return res.status(400).json({ code: 1, message: JSON 格式错误 }); } next(err); });注意错误处理中间件有四个参数缺一不可Express 靠参数个数来识别它。4.3 端口占用与进程管理Error: listen EADDRINUSE: address already in use :::3000这个错误几乎每个人都遇到过。意思是 3000 端口被别的进程占了。解决办法有两个换端口或者杀掉占用进程。Linux/macOS 下查占用lsof -i :3000 kill -9 PIDWindows 下netstat -ano | findstr :3000 taskkill /PID PID /F我个人的习惯是开发时用 3000但准备一个环境变量来覆盖const PORT process.env.PORT || 3000;这样部署到云平台时平台注入的 PORT 环境变量会自动生效不用改代码。4.4 几个我踩过的坑坑一路由顺序影响匹配结果。app.get(/api/users/:id)如果写在app.get(/api/users/active)前面那么访问/api/users/active会被:id路由捕获id变成字符串active。具体路径要写在参数路径前面这是 Express 路由匹配的规则。坑二忘记处理异步错误。如果路由处理函数是 async 的里面抛出的异常 Express 默认捕获不到会导致进程崩溃。Node 20 之后可以用process.on(uncaughtException)兜底但更好的做法是用 try-catch 包住或者用express-async-errors这个库。坑三JSON 里的中文显示成乱码。这通常是终端编码问题不是服务的问题。res.json()会自动设置Content-Type: application/json; charsetutf-8数据本身是对的。用curl ... | python -m json.tool格式化一下就能正常显示。坑四ID 类型不一致。从 URL 拿到的 id 是字符串从请求体拿到的可能是数字比较时一定要统一类型。我习惯在入口处就parseInt后面全程用数字。5. 接口测试与验证实操5.1 完整的 curl 测试脚本把下面这段存成test.sh一键跑完所有接口的验证#!/bin/bash BASEhttp://localhost:3000/api/users echo 1. 获取用户列表 curl -s $BASE | python -m json.tool echo 2. 创建用户 curl -s -X POST $BASE \ -H Content-Type: application/json \ -d {name:赵六,email:zhaoliuexample.com} | python -m json.tool echo 3. 获取单个用户 curl -s $BASE/1 | python -m json.tool echo 4. 更新用户 curl -s -X PUT $BASE/1 \ -H Content-Type: application/json \ -d {name:张三丰} | python -m json.tool echo 5. 删除用户 curl -s -X DELETE $BASE/2 | python -m json.tool echo 6. 验证删除结果 curl -s $BASE | python -m json.tool-s参数让 curl 静默执行不显示进度条输出更干净。管道接python -m json.tool是为了格式化 JSON让结果可读。这个脚本跑一遍你就能确认所有接口是否正常工作。5.2 用 curl -v 深入观察 HTTP 交互想真正理解 HTTP一定要用curl -v看一次完整的交互curl -v -X POST http://localhost:3000/api/users \ -H Content-Type: application/json \ -d {name:测试,email:testexample.com}输出里以开头的是请求内容开头的是响应内容。你会看到请求行、请求头、请求体以及响应的状态行、响应头、响应体。这是理解 HTTP 协议最直观的方式比看任何文档都管用。特别注意响应头里的Content-Type、Content-Length、Date以及状态码。5.3 接口设计的几个经验之谈第一URL 用名词复数不要用动词。GET /api/users而不是GET /api/getUsers。HTTP 方法本身已经表达了动作URL 只负责定位资源。第二版本号放在 URL 里。/api/v1/users比/api/users更好因为将来接口不兼容升级时老客户端还能继续用 v1。第三分页参数用 query string。GET /api/users?page1size20不要塞到路径里。分页、排序、过滤这些都属于查询条件放 query 里语义最清晰。第四错误响应要包含足够信息。至少要有错误码、错误描述有条件的话加上出错的字段名。热词里api error: 400 the parameter messages.content.type specified in the request就是个好例子它明确告诉你是哪个参数的类型不对。6. 后续扩展方向这个项目虽然小但扩展空间很大。等你把基础版跑通后可以按这个顺序逐步加功能第一步接入真实数据库。把内存数组换成 SQLite零配置适合练手再换成 MySQL 或 PostgreSQL。你会接触到连接池、事务、SQL 注入防护这些概念。第二步加参数校验。手写if (!name)太原始用zod或joi这类库做 schema 校验代码更简洁错误信息更规范。第三步加鉴权。用 JWT 做登录态给接口加上Authorization头校验。这是从玩具走向能用的关键一步。第四步写自动化测试。用jestsupertest给每个接口写测试用例改代码时跑一遍就知道有没有破坏原有功能。第五步容器化部署。写个 Dockerfile把服务打包成镜像。这时候你会遇到热词里提到的permission denied while trying to connect to the docker api这类问题但那是另一个故事了。我个人在实际操作中的体会是小项目最大的价值不是功能本身而是让你把一条完整链路走通。从写代码、启动服务、发请求、看响应、排查错误这一圈走下来你对后端的理解会比看十篇文章都扎实。很多人学不会后端不是因为知识点太难而是因为一直在看别人写好的代码从没自己从零搭过一个能跑的东西。这个 API 服务就是你的起点把它跑起来然后大胆地改、大胆地加功能踩的坑越多学得越快。