尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零搭建Node.js Express API服务:环境配置、路由设计与curl调试实战

从零搭建Node.js Express API服务:环境配置、路由设计与curl调试实战 1. 为什么我建议每个开发者都动手搭一次 API 服务1.1 从调别人的接口到被别人调接口的跨越大部分前端或者脚本开发者日常干得最多的事就是调别人的 APIfetch 一个天气接口、请求一次大模型、拉一遍商品列表。但真正让你技术认知上一个台阶的是自己从零把一个 API 服务跑起来让别人来调你。这个转变看着简单实际上涉及的东西比想象中多——路由设计、请求解析、错误处理、跨域、日志、部署、进程守护每一个环节都是真实项目里绕不开的坑。我见过太多人卡在我会写 JavaScript但不知道怎么把它变成一个服务这一步。他们能写函数、能操作 DOM、能跑通一段算法但一旦要对外暴露一个 HTTP 接口就不知道从哪下手了。这篇内容就是写给这批人的你不需要会 Docker、不需要懂 Nginx 反向代理、不需要买服务器只要一台能装 Node.js 的机器跟着走一遍就能拥有一个真正能被 curl 调通的 API 服务。1.2 这个实战项目到底能做出什么最终成品是一个跑在本地 3000 端口的 HTTP 服务提供几个基础接口健康检查、用户列表查询、单个用户查询、以及一个接收 POST 请求创建用户并返回结果的接口。听起来朴素但麻雀虽小五脏俱全——它包含了 RESTful 风格的路由设计、JSON 请求体解析、路径参数提取、状态码返回、统一错误处理、请求日志中间件。你把这套骨架吃透换成任何业务逻辑比如接大模型、接数据库、接第三方支付回调都是同一套模式。技术栈选的是Node.js Express JavaScript不引入 TypeScript、不引入数据库、不引入任何构建工具。原因很直接初学阶段每多一个依赖就多一层认知负担先把 HTTP 服务本身的运转逻辑搞清楚比一上来就堆全家桶有价值得多。等你把这套跑通了再往上加 TypeScript、加 Prisma、加 Redis都是顺水推舟的事。1.3 适合谁来跟着做会写基础 JavaScript但对后端服务没有概念的开发者想给自己的小工具、小脚本套一个 HTTP 接口的人需要快速验证某个想法不想折腾复杂框架的人想理解 curl 到底在干什么、HTTP 请求长什么样的人如果你已经能熟练用 Express 写 CRUD这篇可能对你偏简单但如果你连中间件执行顺序都说不清楚那正好往下看。2. 环境准备Node.js 装不对后面全是坑2.1 Node.js 版本选择与安装方式对比这一步是新手翻车率最高的地方。网上搜node.js安装出来的教程五花八门有让你下 exe 双击的有让你用 nvm 的有让你 apt install 的。我直接给结论优先用 nvm 装 LTS 版本原因后面说。先看几种方式的对比安装方式适用场景优点坑点官网 exe/msi 安装包Windows 单版本使用图形化无脑下一步升级要卸载重装多版本切换麻烦apt/yum 包管理器Linux 服务器一条命令搞定版本往往偏旧权限问题多nvm需要多版本切换一条命令切版本干净需要额外配置 shelln 工具已有 Node 想升级轻量依赖已有 Node我推荐 nvm 的核心理由是项目之间 Node 版本冲突是常态。你手上这个项目用 Node 20 跑得好好的下一个项目依赖的某个包只支持 Node 18没有版本管理器你就得反复卸载重装纯浪费时间。Linux 或 macOS 下装 nvmcurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash装完记得重开终端或者手动 source 一下配置文件。然后nvm install 20 nvm use 20 nvm alias default 20最后一句是把 20 设为默认版本不然每次开新终端都要手动 use 一次很烦。Windows 用户用 nvm-windows去 GitHub releases 下安装包命令基本一致。2.2 验证安装是否真的成功装完别急着往下走先验证node -v npm -v正常应该输出类似v20.11.0和10.2.4。如果node -v报 command not found八成是 PATH 没配好或者终端没重启。这时候别硬着头皮往下做先把环境搞定否则后面每一步都会报错你会以为是代码问题其实是环境问题。提示如果你在 Ubuntu 上遇到ubuntu安装node.js 20相关的权限报错比如permission denied大概率是用了 sudo 装全局包导致的目录归属混乱。用 nvm 装可以完全避开这个问题因为所有东西都在你的用户目录下。2.3 初始化项目与依赖安装新建一个目录进去初始化mkdir my-api-server cd my-api-server npm init -y npm install expressnpm init -y会生成一个默认的 package.json-y表示所有问题都选默认值。装 express 的时候注意看终端输出如果卡住不动多半是网络问题可以换国内镜像源npm config set registry https://registry.npmmirror.com装完之后你的目录里会多出一个 node_modules 文件夹和一个 package-lock.json。node_modules 不用管它也绝对不要提交到 git后面会讲 .gitignore。3. 核心代码拆解一个 API 服务的最小骨架3.1 从 20 行代码理解 Express 的运转逻辑先上最小可运行版本别急着堆功能const express require(express); const app express(); const PORT 3000; app.use(express.json()); app.get(/health, (req, res) { res.json({ status: ok, timestamp: Date.now() }); }); app.listen(PORT, () { console.log(Server running at http://localhost:${PORT}); });就这 12 行已经是一个能跑的 API 服务了。逐行拆require(express)引入框架CommonJS 写法Node 原生支持express()创建一个应用实例所有路由和中间件都挂在这个实例上app.use(express.json())是中间件作用是自动把请求体里的 JSON 字符串解析成 JavaScript 对象挂到req.body上。没有这一行你 POST 过来的 JSON 在req.body里就是 undefinedapp.get(/health, handler)注册一个 GET 路由res.json()会自动设置Content-Type: application/json并把对象序列化成字符串发回去app.listen启动 HTTP 服务器监听指定端口这里有个新手常问的问题为什么express.json()要放在路由前面因为 Express 的中间件是按注册顺序依次执行的。请求进来先过中间件再到路由处理函数。如果你把app.use(express.json())写在路由后面请求到达路由时 body 还没被解析req.body就是空的。这个顺序问题坑过无数人。3.2 路由设计RESTful 风格到底怎么落地接下来把用户相关的接口加上。我用一个内存数组模拟数据不接数据库目的是让你专注在 HTTP 层let users [ { id: 1, name: 张三, email: zhangsanexample.com }, { id: 2, name: 李四, email: lisiexample.com } ]; // 获取所有用户 app.get(/api/users, (req, res) { res.json({ code: 0, data: users, total: users.length }); }); // 获取单个用户 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: 404, message: 用户不存在 }); } res.json({ code: 0, data: user }); }); // 创建用户 app.post(/api/users, (req, res) { const { name, email } req.body; if (!name || !email) { return res.status(400).json({ code: 400, message: name 和 email 必填 }); } const newUser { id: users.length ? Math.max(...users.map(u u.id)) 1 : 1, name, email }; users.push(newUser); res.status(201).json({ code: 0, data: newUser }); });几个关键点值得展开说。路径参数:id的提取。req.params.id拿到的永远是字符串哪怕你请求的是/api/users/1拿到的也是1而不是1。所以必须parseInt转换否则users.find(u u.id id)永远匹配不上因为1 1是 false。这个坑我踩过当时排查了半小时才反应过来。状态码的语义。创建成功返回 201 而不是 200资源不存在返回 404参数错误返回 400。这些不是形式主义前端和网关会根据状态码做不同处理。你全返回 200前端就得靠解析 body 里的 code 字段判断多一层麻烦。id 生成逻辑。Math.max(...users.map(u u.id)) 1是取当前最大 id 加一。注意空数组时Math.max()返回-Infinity所以要加个三元判断。真实项目里这个活应该交给数据库的自增主键这里只是模拟。3.3 统一响应格式为什么我坚持用 code/data/message你可能注意到我所有响应都套了一层{ code, data, message }。这是我在实际项目里坚持的一个约定理由是前端处理响应时逻辑统一。不管成功失败先看 codecode 为 0 取 data非 0 弹 message。不用每个接口都写一套判断。对比一下两种风格// 风格 A直接返回数据 res.json(users); // 风格 B包装一层 res.json({ code: 0, data: users, message: success });风格 A 简单但一旦要加错误码、加分页信息、加调试信息就得改所有接口的返回结构前端也得跟着改。风格 B 一开始多写几个字后面扩展从容得多。当然这不是绝对真理如果是对外开放的公共 API遵循 HTTP 状态码 标准 body 更合适内部项目用包装风格效率更高。4. 中间件与错误处理让服务真正能用4.1 请求日志中间件排查问题的第一手资料服务跑起来之后你迟早会遇到接口返回不对但不知道请求到底长什么样的情况。这时候一个日志中间件能救命app.use((req, res, next) { const start Date.now(); const { method, url } req; res.on(finish, () { const duration Date.now() - start; console.log([${new Date().toISOString()}] ${method} ${url} ${res.statusCode} ${duration}ms); }); next(); });这里的关键是res.on(finish, ...)。为什么不在中间件里直接打印因为中间件执行的时候响应还没发出去res.statusCode还是默认的 200拿不到真实状态码。监听finish事件等响应真正发送完毕再打印才能拿到准确的状态码和耗时。next()必须调用否则请求会卡在这个中间件里永远到不了路由。这是 Express 中间件最容易犯的错——忘了 next()然后对着请求一直 pending抓耳挠腮。4.2 404 与全局错误处理兜底的最后一道防线Express 默认的 404 页面是 HTML对 API 服务来说很不友好。加一个兜底// 放在所有路由之后 app.use((req, res) { res.status(404).json({ code: 404, message: 接口 ${req.method} ${req.url} 不存在 }); }); // 全局错误处理四个参数缺一不可 app.use((err, req, res, next) { console.error(未捕获错误:, err.stack); res.status(500).json({ code: 500, message: 服务器内部错误 }); });错误处理中间件必须是四个参数(err, req, res, next)少一个 Express 就不认会当成普通中间件。这个设计很反直觉但没办法是框架约定。还有一个细节404 处理必须放在所有路由注册之后。因为 Express 是按顺序匹配的你放前面所有请求都会被它拦截后面的路由永远走不到。4.3 用 try/catch 包裹异步逻辑如果你的路由处理函数里有异步操作比如调数据库、调外部 API一定要用 try/catch 包住app.get(/api/slow, async (req, res, next) { try { await new Promise(r setTimeout(r, 1000)); res.json({ code: 0, data: done }); } catch (err) { next(err); // 交给全局错误处理 } });注意next(err)这个用法——把错误传给 nextExpress 会自动跳到错误处理中间件。如果你直接res.status(500).json(...)也行但统一走全局处理更规范日志也集中。提示Express 4.x 不会自动捕获 async 函数里抛出的错误必须手动 try/catch 或 next(err)。Express 5.x 改进了这一点但生产环境里 4.x 还是主流别指望框架帮你兜底。5. 用 curl 验证接口命令行里的调试利器5.1 curl 基础用法与常见参数服务跑起来后用 curl 测一遍。curl 是验证 API 最直接的工具比 Postman 轻量比浏览器地址栏灵活能发 POST、能带 header。# 健康检查 curl http://localhost:3000/health # 获取用户列表 curl http://localhost:3000/api/users # 获取单个用户 curl http://localhost:3000/api/users/1 # 创建用户-X 指定方法-H 指定 header-d 指定 body curl -X POST http://localhost:3000/api/users \ -H Content-Type: application/json \ -d {name:王五,email:wangwuexample.com}-H Content-Type: application/json这一行不能省。因为express.json()中间件只解析 Content-Type 为 application/json 的请求体你不带这个 headerreq.body就是空的接口会返回 400。5.2 curl 报错速查那些年我们踩过的网络坑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 from server空响应服务端没返回任何内容就断了curl -v是排查利器会打印完整的请求和响应头包括 DNS 解析、TCP 连接、TLS 握手、发送的 header、收到的 header。接口返回不对的时候先curl -v看一眼很多时候问题一目了然——比如发现请求根本没带 Content-Type或者服务端返回的是 500 而不是你以为的 200。5.3 用 curl 做一轮完整的功能验证按顺序跑一遍确认每个接口都正常# 1. 健康检查应该返回 status: ok curl -s http://localhost:3000/health | node -e process.stdin.on(data,dconsole.log(JSON.parse(d))) # 2. 列表应该返回 2 个用户 curl -s http://localhost:3000/api/users # 3. 查单个存在的用户 curl -s http://localhost:3000/api/users/1 # 4. 查不存在的用户应该返回 404 curl -s -o /dev/null -w %{http_code}\n http://localhost:3000/api/users/999 # 5. 创建用户应该返回 201 curl -s -X POST http://localhost:3000/api/users \ -H Content-Type: application/json \ -d {name:赵六,email:zhaoliuexample.com} # 6. 再查列表应该变成 3 个用户 curl -s http://localhost:3000/api/users第 4 条里的-o /dev/null -w %{http_code}\n是个实用技巧丢弃响应体只打印状态码。验证状态码的时候特别方便不用在一堆 JSON 里找。6. 常见问题与排查实录6.1 端口被占用怎么办启动时报EADDRINUSE: address already in use :::3000说明 3000 端口被别的进程占了。两个办法# 查谁占了端口macOS/Linux lsof -i :3000 # Windows netstat -ano | findstr :3000找到 PID 后 kill 掉或者干脆换个端口。我一般习惯在代码里把端口写成可配置的const PORT process.env.PORT || 3000;这样启动时PORT4000 node server.js就能换端口不用改代码。6.2 req.body 为 undefined 的三种原因这是新手遇到最多的问题按概率排序忘了app.use(express.json())或者写在了路由后面curl 没带Content-Type: application/json中间件不解析body 不是合法 JSON比如单引号包了 JSON、多了逗号、少了引号排查方法在路由里加一行console.log(req.headers[content-type], req.body)一眼就能看出是哪种。6.3 中文乱码问题如果返回的中文在终端里显示成乱码通常是终端编码问题不是服务问题。res.json()会自动设置charsetutf-8浏览器里看是正常的。终端里可以用curl ... | iconv -f utf-8 -t utf-8或者直接看浏览器。6.4 修改代码后不生效Node 不会自动重启改完代码必须手动 CtrlC 停掉再node server.js。嫌麻烦可以装 nodemonnpm install -D nodemon然后在 package.json 的 scripts 里加scripts: { dev: nodemon server.js, start: node server.js }之后npm run dev改代码自动重启。-D表示装成开发依赖生产环境不需要它。6.5 常见问题速查表现象最可能的原因快速验证启动报 EADDRINUSE端口被占lsof -i :3000请求一直 pending中间件忘了 next()检查所有 app.usereq.body 为空没解析 JSON看有没有 express.json()404 但路由明明写了路由顺序或路径拼写curl -v看实际请求路径返回 500 无信息未捕获异常看服务端控制台堆栈中文乱码终端编码换浏览器验证7. 从能跑到能交付几个提升质感的细节7.1 项目结构拆分别把所有代码塞一个文件单文件跑通之后建议拆一下目录为后续扩展做准备my-api-server/ ├── src/ │ ├── routes/ │ │ └── users.js │ ├── middlewares/ │ │ └── logger.js │ └── app.js ├── server.js ├── package.json └── .gitignoreapp.js只负责创建 express 实例、挂中间件、挂路由server.js只负责app.listen。这样拆分的好处是测试的时候可以直接 import app 而不启动服务器部署的时候也清晰。路由用express.Router()拆出去// src/routes/users.js const express require(express); const router express.Router(); router.get(/, (req, res) { /* ... */ }); router.get(/:id, (req, res) { /* ... */ }); router.post(/, (req, res) { /* ... */ }); module.exports router;主文件里app.use(/api/users, usersRouter)挂上去。这样/api/users这个前缀只在主文件里出现一次改起来方便。7.2 .gitignore 与 package.json scripts.gitignore至少包含node_modules/ .env *.log .DS_Storenode_modules 绝对不能提交几百兆的东西而且依赖靠 package.json 就能还原。.env里放敏感配置数据库密码、API key更不能提交。package.json 的 scripts 建议配全scripts: { start: node server.js, dev: nodemon server.js }7.3 生产环境进程守护别让服务一崩就没了开发时node server.js够用但生产环境这么跑一旦进程崩溃或者服务器重启服务就没了。用 pm2 守护npm install -g pm2 pm2 start server.js --name my-api pm2 save pm2 startuppm2 save保存当前进程列表pm2 startup生成开机自启配置。之后pm2 logs看日志pm2 restart my-api重启pm2 status看状态。这套组合拳下来服务基本能做到崩了自动拉起、重启自动恢复。提示pm2 的日志默认存在~/.pm2/logs/下时间久了会占满磁盘。建议配一下pm2 install pm2-logrotate自动切割日志。8. 后续可以怎么扩展这套骨架跑通之后往上加东西的路子很宽。想接数据库把内存数组换成 SQLite 或 PostgreSQL 查询即可路由层几乎不用动。想加鉴权写一个校验 token 的中间件挂在需要保护的路由前面。想接大模型在路由处理函数里发一个 HTTP 请求出去把返回结果包装成统一格式返回。想加限流express-rate-limit一行中间件搞定。我个人在实际操作中的体会是先把最小闭环跑通再逐步加复杂度。很多人一上来就想搞微服务、搞容器化、搞 CI/CD结果卡在环境配置上三天没写出第一行业务代码。先用最朴素的方式让接口能通哪怕数据是写死的、日志是 console.log 打的只要 curl 能调通你就已经跨过了从 0 到 1 那道坎。剩下的从 1 到 100都是在这个能跑的骨架上做加法心态和难度完全不一样。最后分享一个小技巧每次加新接口先用 curl 测通再写前端调用。命令行里调通了说明服务端逻辑没问题前端再出问题就只可能是前端的事排查范围直接砍一半。这个习惯帮我省了无数来回扯皮的时间。
返回列表