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

资讯详情

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

Node.js + Express 实战:从零搭建天气查询 API 服务

Node.js + Express 实战:从零搭建天气查询 API 服务 1. 项目缘起与整体设计思路1.1 为什么选这个题目练手我一直觉得学一门技术最快的路径不是看文档而是动手做一个能跑起来的东西。API 服务就是这样一个绝佳的练手项目——它足够小小到一个人一两天就能搞定又足够完整完整到能覆盖后端开发的核心链路接收请求、处理逻辑、返回响应、错误处理、日志记录。这次我选的技术栈是Node.js Express。原因很直接JavaScript 一门语言从前端写到后端不用切换思维Express 的生态成熟到几乎任何需求都能找到现成的中间件再加上现在有 AI 辅助编码很多样板代码可以直接生成省下来的时间可以花在真正需要思考的架构设计上。这个项目适合谁如果你已经会一点 JavaScript 基础语法知道什么是函数、什么是对象但从来没自己从零搭过一个后端服务那这篇内容就是写给你的。如果你已经写过 Express 但一直是复制粘贴别人的代码不清楚每一行在干什么那这篇也能帮你把知识串起来。1.2 这个 API 服务到底要做什么我给自己定的目标很明确做一个天气查询 API 服务。用户传一个城市名服务返回这个城市的天气信息。听起来简单但麻雀虽小五脏俱全需要一个 HTTP 服务器接收请求需要路由来区分不同的接口需要参数校验防止用户传乱七八糟的东西需要调用外部数据源获取天气需要统一的响应格式需要错误处理不能一报错就崩需要日志方便排查问题这七个需求基本上就是一个生产级 API 服务的骨架。把这个项目吃透以后换任何业务场景套路都是一样的。1.3 技术选型的几个关键决策为什么用 Express 而不是 Fastify这个问题我被问过很多次。Fastify 性能确实更好基准测试数据摆在那里。但对于小项目来说Express 的优势在于中间件生态最丰富、文档最全、遇到问题搜一下就有答案。Fastify 的插件体系虽然设计得更现代但学习曲线更陡。我的建议是先把 Express 用熟理解 HTTP 服务的本质再去尝试 Fastify 不迟。为什么不用 TypeScript小项目实战的目的是快速验证想法TypeScript 的类型定义在项目初期反而是一种负担。等你把业务逻辑跑通了再迁移到 TypeScript 也不迟。当然如果你已经熟悉 TypeScript直接用也没问题。AI 在这个项目里扮演什么角色我的用法是让 AI 生成样板代码和重复性逻辑比如路由注册、错误处理中间件、参数校验规则。但核心的业务逻辑和架构决策必须自己来。AI 生成的代码你要能看懂、能改、能调试否则出了问题你连从哪下手都不知道。2. 环境搭建与项目初始化2.1 Node.js 安装的坑与正确姿势Node.js 的安装看起来简单但版本选择有讲究。我推荐用LTS 版本长期支持版不要追最新的 Current 版本。LTS 版本经过充分测试生态兼容性最好。截至我写这篇内容的时候Node.js 20.x 和 22.x 都是 LTS选哪个都行。安装方式我强烈建议用nvmNode Version Manager而不是直接去官网下载安装包。原因很简单不同项目可能依赖不同的 Node.js 版本nvm 让你可以在版本之间一键切换。Windows 用户可以用 nvm-windowsMac 和 Linux 用户直接用官方的 nvm 脚本。安装完成后打开终端验证一下node -v npm -v两个命令都能输出版本号说明安装成功。如果提示“command not found”大概率是环境变量没配好检查一下 nvm 的安装路径是否加到了 PATH 里。注意不要用 sudo 安装全局 npm 包这会导致权限问题。如果遇到权限报错正确做法是配置 npm 的全局目录到用户目录下而不是加 sudo。2.2 项目目录结构设计很多人写小项目习惯把所有代码塞进一个index.js一开始确实爽但改到第三天就痛苦了。我建议从一开始就按职责分目录weather-api/ ├── src/ │ ├── routes/ # 路由定义 │ │ └── weather.js │ ├── controllers/ # 业务逻辑 │ │ └── weatherController.js │ ├── services/ # 外部服务调用 │ │ └── weatherService.js │ ├── middlewares/ # 中间件 │ │ ├── errorHandler.js │ │ └── requestLogger.js │ ├── utils/ # 工具函数 │ │ └── response.js │ └── app.js # Express 应用配置 ├── .env # 环境变量 ├── .gitignore ├── package.json └── server.js # 入口文件这个结构的好处是路由只管 URL 和 HTTP 方法的映射控制器管业务逻辑服务层管数据获取。三层各司其职以后要换数据源只改服务层要加新接口只加路由和控制器。2.3 初始化项目与依赖安装mkdir weather-api cd weather-api npm init -y npm install express dotenv axios npm install -D nodemon这里解释一下每个依赖的作用expressWeb 框架处理 HTTP 请求的核心dotenv读取.env文件里的环境变量比如端口号、API 密钥axios发 HTTP 请求用来调用外部天气数据接口nodemon开发依赖监听文件变化自动重启服务开发时必备在package.json里加两个脚本{ scripts: { start: node server.js, dev: nodemon server.js } }开发时用npm run dev部署时用npm start。3. 核心代码实现与关键细节3.1 入口文件与 Express 应用分离很多教程把app.listen()直接写在app.js里我不推荐这种做法。把应用配置和启动逻辑分开好处是测试的时候可以直接导入 app 而不启动服务器。server.js只做一件事——启动服务const app require(./src/app); const PORT process.env.PORT || 3000; app.listen(PORT, () { console.log(服务已启动监听端口 ${PORT}); });src/app.js负责组装中间件和路由const express require(express); const requestLogger require(./middlewares/requestLogger); const errorHandler require(./middlewares/errorHandler); const weatherRoutes require(./routes/weather); const app express(); app.use(express.json()); app.use(requestLogger); app.use(/api/weather, weatherRoutes); app.use(errorHandler); module.exports app;注意中间件的顺序express.json()必须在路由之前否则req.body拿不到数据错误处理中间件必须在所有路由之后否则捕获不到路由里抛出的错误。3.2 路由层只做映射不写逻辑路由层的职责非常单一把 URL 和 HTTP 方法映射到对应的控制器函数。不要在路由里写业务逻辑这是新手最容易犯的错误。const express require(express); const router express.Router(); const weatherController require(../controllers/weatherController); router.get(/:city, weatherController.getWeatherByCity); router.get(/:city/forecast, weatherController.getForecast); module.exports router;这里定义了两个接口GET /api/weather/:city查当前天气GET /api/weather/:city/forecast查未来几天预报。:city是路径参数Express 会自动把它解析到req.params.city。3.3 控制器层参数校验与响应组装控制器是业务逻辑的入口它要做三件事校验参数、调用服务层、组装响应。const weatherService require(../services/weatherService); const { success, error } require(../utils/response); async function getWeatherByCity(req, res, next) { try { const { city } req.params; if (!city || city.trim().length 0) { return res.status(400).json(error(城市名不能为空)); } if (city.length 50) { return res.status(400).json(error(城市名过长)); } const weatherData await weatherService.fetchWeather(city); res.json(success(weatherData)); } catch (err) { next(err); } } module.exports { getWeatherByCity, getForecast };几个关键点参数校验要前置。不要等到调用外部服务了才发现参数不对那样浪费一次网络请求。校验规则要具体比如城市名长度限制、特殊字符过滤。用next(err)传递错误。在 async 函数里throw的错误不会自动被 Express 捕获必须手动传给next()。这是 Express 的一个经典坑很多人在这里栽过跟头。响应格式要统一。我定义了一个response.js工具function success(data, message ok) { return { code: 0, message, data }; } function error(message, code 1) { return { code, message, data: null }; }这样前端拿到响应后只需要判断code是否为 0不用去猜每个接口的返回结构。3.4 服务层外部数据获取与容错服务层负责真正去拿数据。我用的是一个公开的天气数据接口通过 axios 调用const axios require(axios); const API_BASE process.env.WEATHER_API_BASE; const API_KEY process.env.WEATHER_API_KEY; async function fetchWeather(city) { const url ${API_BASE}/current.json; const params { key: API_KEY, q: city, lang: zh }; const response await axios.get(url, { params, timeout: 5000 }); return { city: response.data.location.name, temperature: response.data.current.temp_c, condition: response.data.current.condition.text, humidity: response.data.current.humidity, windSpeed: response.data.current.wind_kph, updatedAt: response.data.current.last_updated }; } module.exports { fetchWeather };这里有几个实战经验一定要设 timeout。不设超时的话外部接口挂了你的服务也会跟着挂请求会一直挂在那里直到客户端超时。5 秒是个合理的值。返回数据要裁剪。外部接口返回的字段可能几十个但你只需要其中几个。在服务层就把数据裁剪成你需要的结构控制器和前端都不用关心原始数据结构。API 密钥放环境变量。绝对不要把密钥硬编码在代码里然后提交到代码仓库。.env文件要加到.gitignore里。3.5 中间件日志与错误处理请求日志中间件记录每个请求的方法、路径、耗时function requestLogger(req, res, next) { const start Date.now(); res.on(finish, () { const duration Date.now() - start; console.log(${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms); }); next(); }用res.on(finish)而不是直接在next()前打印是因为要等响应完成才能拿到状态码和耗时。全局错误处理中间件是最后一道防线function errorHandler(err, req, res, next) { console.error(未捕获错误:, err.message); if (err.code ECONNABORTED) { return res.status(504).json({ code: 1, message: 外部服务超时 }); } if (err.response err.response.status 404) { return res.status(404).json({ code: 1, message: 城市不存在 }); } res.status(500).json({ code: 1, message: 服务器内部错误 }); }错误处理中间件必须接收四个参数(err, req, res, next)少一个 Express 就不会把它当作错误处理中间件。这个细节很多人不知道。4. 常见问题排查与避坑指南4.1 端口被占用怎么办开发时经常遇到EADDRINUSE错误意思是端口已经被别的程序占了。两个解决办法# Mac/Linux 查看谁占了 3000 端口 lsof -i :3000 # Windows netstat -ano | findstr :3000找到进程号后 kill 掉或者直接换个端口。我习惯在.env里配PORT3001避免和常用端口冲突。4.2 async 错误没被捕获这是 Express 最经典的坑。看这段代码app.get(/test, async (req, res) { throw new Error(出错了); // 这个错误不会被错误处理中间件捕获 });async 函数返回的是一个 PromiseExpress 4.x 不会自动捕获 Promise 的 rejection。解决办法有三种手动 try-catch 然后next(err)、用express-async-errors这个包、或者升级到 Express 5Express 5 原生支持 async 错误捕获。我推荐第一种最可控。4.3 跨域问题前端调用接口时报 CORS 错误解决办法是加cors中间件npm install corsconst cors require(cors); app.use(cors());开发阶段可以允许所有来源生产环境要配置白名单只允许你自己的域名访问。4.4 常见问题速查表问题现象可能原因解决方法Cannot GET /api/weather路由路径不匹配检查路由注册的前缀和请求路径req.body为 undefined没加express.json()在路由之前注册 body 解析中间件错误处理中间件不生效参数不是四个确保是(err, req, res, next)外部接口调用超时没设 timeoutaxios 配置里加timeout: 5000环境变量读不到.env没加载入口文件顶部加require(dotenv).config()修改代码不生效没重启服务用 nodemon 启动或手动重启4.5 几个我踩过的坑dotenv 的加载时机。require(dotenv).config()必须放在最顶部在所有其他 require 之前。因为其他模块可能在加载时就会读取环境变量如果 dotenv 还没执行读到的就是 undefined。路径参数的编码问题。城市名如果包含中文或空格URL 里会被编码。Express 会自动解码req.params但如果你手动拼接 URL 去调外部接口记得用encodeURIComponent()处理。JSON 响应里的中文。Express 默认的res.json()会正确设置Content-Type: application/json; charsetutf-8中文不会乱码。但如果你用res.send()返回对象Express 也会自动转 JSON效果一样。5. 用 AI 辅助开发的正确姿势5.1 AI 能帮你做什么在这个项目里我用 AI 做了这些事生成路由和控制器的样板代码我只需要改业务逻辑写参数校验的正则表达式比如城市名只允许中文、英文和空格生成错误处理的分类逻辑把不同的错误码映射到不同的 HTTP 状态码写单元测试的用例覆盖正常和异常场景AI 生成的代码质量参差不齐关键是要能看懂。看不懂的代码不要用让 AI 解释一遍理解了再决定要不要。5.2 AI 不能替你做什么架构决策、错误处理的边界条件、业务逻辑的细节这些必须自己来。比如“城市名传空字符串应该返回 400 还是 404”这种问题没有标准答案取决于你的 API 设计约定。AI 会给你一个答案但不一定是你想要的。还有一个重要的点AI 生成的代码可能有安全漏洞。比如它可能会把用户输入直接拼接到 SQL 里或者忘记做输入过滤。安全相关的代码一定要自己审查。5.3 我的 AI 协作流程我的习惯是先自己想清楚要做什么用注释把逻辑写出来然后让 AI 把注释翻译成代码。这样 AI 是在执行我的设计而不是替我做设计。代码生成后我会逐行审查改掉不合理的部分加上自己的错误处理和日志。这个流程的好处是AI 提高了编码速度但架构和逻辑仍然在我掌控之中。出了问题我知道去哪找因为代码是我设计的。6. 部署与后续扩展6.1 本地验证清单部署之前我会用 curl 把每个接口都过一遍# 正常请求 curl http://localhost:3000/api/weather/beijing # 空城市名 curl http://localhost:3000/api/weather/ # 不存在的城市 curl http://localhost:3000/api/weather/notacity # 超长城市名 curl http://localhost:3000/api/weather/aaaaaaaaaa...每个接口的正常和异常路径都要覆盖确认返回的状态码和响应格式符合预期。6.2 可以继续扩展的方向这个项目跑通之后可以往几个方向扩展加缓存。天气数据不需要每次请求都去调外部接口可以加一层内存缓存比如 10 分钟内同一个城市的请求直接返回缓存结果。用node-cache这个包几行代码就能搞定。加限流。防止有人恶意刷接口用express-rate-limit限制每个 IP 的请求频率。加接口文档。用 Swagger 自动生成 API 文档前端同事不用问你接口怎么调。加健康检查接口。GET /health返回服务状态部署到云平台后负载均衡器会定期检查这个接口。迁移到 TypeScript。业务逻辑稳定后加上类型定义重构时更有底气。6.3 我个人的体会搭这个 API 服务最大的收获不是学会了 Express 的 API而是理解了一个后端服务的完整生命周期请求进来、经过中间件、到达路由、执行逻辑、调用外部服务、组装响应、返回给客户端、记录日志。这个流程走一遍以后看任何后端框架的文档都能快速上手因为概念是相通的。另外AI 辅助编码确实能提速但前提是你自己得有判断力。AI 给的代码对不对、好不好、安不安全你得能看出来。这个判断力来自你亲手写过的代码量没有捷径。最后分享一个小技巧每次改完代码不要只测你改的那个功能把相关的接口都跑一遍。我遇到过好几次改 A 功能把 B 功能改坏的情况都是因为只测了改动点。养成回归测试的习惯能省掉很多线上排查的时间。
返回列表