:從零搭建天氣查詢 API 服務(wù))
1. 項目緣起與整體設(shè)計思路1.1 為什么選這個題目練手我一直覺得學(xué)一門技術(shù)最快的路徑不是看文檔而是動手做一個能跑起來的東西。API 服務(wù)就是這樣一個絕佳的練手項目——它足夠小小到一個人一兩天就能搞定又足夠完整完整到能覆蓋后端開發(fā)的核心鏈路接收請求、處理邏輯、返回響應(yīng)、錯誤處理、日志記錄。這次我選的技術(shù)棧是Node.js Express。原因很直接JavaScript 一門語言從前端寫到后端不用切換思維Express 的生態(tài)成熟到幾乎任何需求都能找到現(xiàn)成的中間件再加上現(xiàn)在有 AI 輔助編碼很多樣板代碼可以直接生成省下來的時間可以花在真正需要思考的架構(gòu)設(shè)計上。這個項目適合誰如果你已經(jīng)會一點 JavaScript 基礎(chǔ)語法知道什么是函數(shù)、什么是對象但從來沒自己從零搭過一個后端服務(wù)那這篇內(nèi)容就是寫給你的。如果你已經(jīng)寫過 Express 但一直是復(fù)制粘貼別人的代碼不清楚每一行在干什么那這篇也能幫你把知識串起來。1.2 這個 API 服務(wù)到底要做什么我給自己定的目標(biāo)很明確做一個天氣查詢 API 服務(wù)。用戶傳一個城市名服務(wù)返回這個城市的天氣信息。聽起來簡單但麻雀雖小五臟俱全需要一個 HTTP 服務(wù)器接收請求需要路由來區(qū)分不同的接口需要參數(shù)校驗防止用戶傳亂七八糟的東西需要調(diào)用外部數(shù)據(jù)源獲取天氣需要統(tǒng)一的響應(yīng)格式需要錯誤處理不能一報錯就崩需要日志方便排查問題這七個需求基本上就是一個生產(chǎn)級 API 服務(wù)的骨架。把這個項目吃透以后換任何業(yè)務(wù)場景套路都是一樣的。1.3 技術(shù)選型的幾個關(guān)鍵決策為什么用 Express 而不是 Fastify這個問題我被問過很多次。Fastify 性能確實更好基準(zhǔn)測試數(shù)據(jù)擺在那里。但對于小項目來說Express 的優(yōu)勢在于中間件生態(tài)最豐富、文檔最全、遇到問題搜一下就有答案。Fastify 的插件體系雖然設(shè)計得更現(xiàn)代但學(xué)習(xí)曲線更陡。我的建議是先把 Express 用熟理解 HTTP 服務(wù)的本質(zhì)再去嘗試 Fastify 不遲。為什么不用 TypeScript小項目實戰(zhàn)的目的是快速驗證想法TypeScript 的類型定義在項目初期反而是一種負(fù)擔(dān)。等你把業(yè)務(wù)邏輯跑通了再遷移到 TypeScript 也不遲。當(dāng)然如果你已經(jīng)熟悉 TypeScript直接用也沒問題。AI 在這個項目里扮演什么角色我的用法是讓 AI 生成樣板代碼和重復(fù)性邏輯比如路由注冊、錯誤處理中間件、參數(shù)校驗規(guī)則。但核心的業(yè)務(wù)邏輯和架構(gòu)決策必須自己來。AI 生成的代碼你要能看懂、能改、能調(diào)試否則出了問題你連從哪下手都不知道。2. 環(huán)境搭建與項目初始化2.1 Node.js 安裝的坑與正確姿勢Node.js 的安裝看起來簡單但版本選擇有講究。我推薦用LTS 版本長期支持版不要追最新的 Current 版本。LTS 版本經(jīng)過充分測試生態(tài)兼容性最好。截至我寫這篇內(nèi)容的時候Node.js 20.x 和 22.x 都是 LTS選哪個都行。安裝方式我強(qiáng)烈建議用nvmNode Version Manager而不是直接去官網(wǎng)下載安裝包。原因很簡單不同項目可能依賴不同的 Node.js 版本nvm 讓你可以在版本之間一鍵切換。Windows 用戶可以用 nvm-windowsMac 和 Linux 用戶直接用官方的 nvm 腳本。安裝完成后打開終端驗證一下node -v npm -v兩個命令都能輸出版本號說明安裝成功。如果提示“command not found”大概率是環(huán)境變量沒配好檢查一下 nvm 的安裝路徑是否加到了 PATH 里。注意不要用 sudo 安裝全局 npm 包這會導(dǎo)致權(quán)限問題。如果遇到權(quán)限報錯正確做法是配置 npm 的全局目錄到用戶目錄下而不是加 sudo。2.2 項目目錄結(jié)構(gòu)設(shè)計很多人寫小項目習(xí)慣把所有代碼塞進(jìn)一個index.js一開始確實爽但改到第三天就痛苦了。我建議從一開始就按職責(zé)分目錄weather-api/ ├── src/ │ ├── routes/ # 路由定義 │ │ └── weather.js │ ├── controllers/ # 業(yè)務(wù)邏輯 │ │ └── weatherController.js │ ├── services/ # 外部服務(wù)調(diào)用 │ │ └── weatherService.js │ ├── middlewares/ # 中間件 │ │ ├── errorHandler.js │ │ └── requestLogger.js │ ├── utils/ # 工具函數(shù) │ │ └── response.js │ └── app.js # Express 應(yīng)用配置 ├── .env # 環(huán)境變量 ├── .gitignore ├── package.json └── server.js # 入口文件這個結(jié)構(gòu)的好處是路由只管 URL 和 HTTP 方法的映射控制器管業(yè)務(wù)邏輯服務(wù)層管數(shù)據(jù)獲取。三層各司其職以后要換數(shù)據(jù)源只改服務(wù)層要加新接口只加路由和控制器。2.3 初始化項目與依賴安裝mkdir weather-api cd weather-api npm init -y npm install express dotenv axios npm install -D nodemon這里解釋一下每個依賴的作用expressWeb 框架處理 HTTP 請求的核心dotenv讀取.env文件里的環(huán)境變量比如端口號、API 密鑰axios發(fā) HTTP 請求用來調(diào)用外部天氣數(shù)據(jù)接口nodemon開發(fā)依賴監(jiān)聽文件變化自動重啟服務(wù)開發(fā)時必備在package.json里加兩個腳本{ scripts: { start: node server.js, dev: nodemon server.js } }開發(fā)時用npm run dev部署時用npm start。3. 核心代碼實現(xiàn)與關(guān)鍵細(xì)節(jié)3.1 入口文件與 Express 應(yīng)用分離很多教程把a(bǔ)pp.listen()直接寫在app.js里我不推薦這種做法。把應(yīng)用配置和啟動邏輯分開好處是測試的時候可以直接導(dǎo)入 app 而不啟動服務(wù)器。server.js只做一件事——啟動服務(wù)const app require(./src/app); const PORT process.env.PORT || 3000; app.listen(PORT, () { console.log(服務(wù)已啟動監(jiān)聽端口 ${PORT}); });src/app.js負(fù)責(zé)組裝中間件和路由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拿不到數(shù)據(jù)錯誤處理中間件必須在所有路由之后否則捕獲不到路由里拋出的錯誤。3.2 路由層只做映射不寫邏輯路由層的職責(zé)非常單一把 URL 和 HTTP 方法映射到對應(yīng)的控制器函數(shù)。不要在路由里寫業(yè)務(wù)邏輯這是新手最容易犯的錯誤。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查當(dāng)前天氣GET /api/weather/:city/forecast查未來幾天預(yù)報。:city是路徑參數(shù)Express 會自動把它解析到req.params.city。3.3 控制器層參數(shù)校驗與響應(yīng)組裝控制器是業(yè)務(wù)邏輯的入口它要做三件事校驗參數(shù)、調(diào)用服務(wù)層、組裝響應(yīng)。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 };幾個關(guān)鍵點參數(shù)校驗要前置。不要等到調(diào)用外部服務(wù)了才發(fā)現(xiàn)參數(shù)不對那樣浪費一次網(wǎng)絡(luò)請求。校驗規(guī)則要具體比如城市名長度限制、特殊字符過濾。用next(err)傳遞錯誤。在 async 函數(shù)里throw的錯誤不會自動被 Express 捕獲必須手動傳給next()。這是 Express 的一個經(jīng)典坑很多人在這里栽過跟頭。響應(yīng)格式要統(tǒng)一。我定義了一個response.js工具function success(data, message ok) { return { code: 0, message, data }; } function error(message, code 1) { return { code, message, data: null }; }這樣前端拿到響應(yīng)后只需要判斷code是否為 0不用去猜每個接口的返回結(jié)構(gòu)。3.4 服務(wù)層外部數(shù)據(jù)獲取與容錯服務(wù)層負(fù)責(zé)真正去拿數(shù)據(jù)。我用的是一個公開的天氣數(shù)據(jù)接口通過 axios 調(diào)用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 };這里有幾個實戰(zhàn)經(jīng)驗一定要設(shè) timeout。不設(shè)超時的話外部接口掛了你的服務(wù)也會跟著掛請求會一直掛在那里直到客戶端超時。5 秒是個合理的值。返回數(shù)據(jù)要裁剪。外部接口返回的字段可能幾十個但你只需要其中幾個。在服務(wù)層就把數(shù)據(jù)裁剪成你需要的結(jié)構(gòu)控制器和前端都不用關(guān)心原始數(shù)據(jù)結(jié)構(gòu)。API 密鑰放環(huán)境變量。絕對不要把密鑰硬編碼在代碼里然后提交到代碼倉庫。.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()前打印是因為要等響應(yīng)完成才能拿到狀態(tài)碼和耗時。全局錯誤處理中間件是最后一道防線function errorHandler(err, req, res, next) { console.error(未捕獲錯誤:, err.message); if (err.code ECONNABORTED) { return res.status(504).json({ code: 1, message: 外部服務(wù)超時 }); } if (err.response err.response.status 404) { return res.status(404).json({ code: 1, message: 城市不存在 }); } res.status(500).json({ code: 1, message: 服務(wù)器內(nèi)部錯誤 }); }錯誤處理中間件必須接收四個參數(shù)(err, req, res, next)少一個 Express 就不會把它當(dāng)作錯誤處理中間件。這個細(xì)節(jié)很多人不知道。4. 常見問題排查與避坑指南4.1 端口被占用怎么辦開發(fā)時經(jīng)常遇到EADDRINUSE錯誤意思是端口已經(jīng)被別的程序占了。兩個解決辦法# Mac/Linux 查看誰占了 3000 端口 lsof -i :3000 # Windows netstat -ano | findstr :3000找到進(jìn)程號后 kill 掉或者直接換個端口。我習(xí)慣在.env里配PORT3001避免和常用端口沖突。4.2 async 錯誤沒被捕獲這是 Express 最經(jīng)典的坑。看這段代碼app.get(/test, async (req, res) { throw new Error(出錯了); // 這個錯誤不會被錯誤處理中間件捕獲 });async 函數(shù)返回的是一個 PromiseExpress 4.x 不會自動捕獲 Promise 的 rejection。解決辦法有三種手動 try-catch 然后next(err)、用express-async-errors這個包、或者升級到 Express 5Express 5 原生支持 async 錯誤捕獲。我推薦第一種最可控。4.3 跨域問題前端調(diào)用接口時報 CORS 錯誤解決辦法是加cors中間件npm install corsconst cors require(cors); app.use(cors());開發(fā)階段可以允許所有來源生產(chǎn)環(huán)境要配置白名單只允許你自己的域名訪問。4.4 常見問題速查表問題現(xiàn)象可能原因解決方法Cannot GET /api/weather路由路徑不匹配檢查路由注冊的前綴和請求路徑req.body為 undefined沒加express.json()在路由之前注冊 body 解析中間件錯誤處理中間件不生效參數(shù)不是四個確保是(err, req, res, next)外部接口調(diào)用超時沒設(shè) timeoutaxios 配置里加timeout: 5000環(huán)境變量讀不到.env沒加載入口文件頂部加require(dotenv).config()修改代碼不生效沒重啟服務(wù)用 nodemon 啟動或手動重啟4.5 幾個我踩過的坑dotenv 的加載時機(jī)。require(dotenv).config()必須放在最頂部在所有其他 require 之前。因為其他模塊可能在加載時就會讀取環(huán)境變量如果 dotenv 還沒執(zhí)行讀到的就是 undefined。路徑參數(shù)的編碼問題。城市名如果包含中文或空格URL 里會被編碼。Express 會自動解碼req.params但如果你手動拼接 URL 去調(diào)外部接口記得用encodeURIComponent()處理。JSON 響應(yīng)里的中文。Express 默認(rèn)的res.json()會正確設(shè)置Content-Type: application/json; charsetutf-8中文不會亂碼。但如果你用res.send()返回對象Express 也會自動轉(zhuǎn) JSON效果一樣。5. 用 AI 輔助開發(fā)的正確姿勢5.1 AI 能幫你做什么在這個項目里我用 AI 做了這些事生成路由和控制器的樣板代碼我只需要改業(yè)務(wù)邏輯寫參數(shù)校驗的正則表達(dá)式比如城市名只允許中文、英文和空格生成錯誤處理的分類邏輯把不同的錯誤碼映射到不同的 HTTP 狀態(tài)碼寫單元測試的用例覆蓋正常和異常場景AI 生成的代碼質(zhì)量參差不齊關(guān)鍵是要能看懂??床欢拇a不要用讓 AI 解釋一遍理解了再決定要不要。5.2 AI 不能替你做什么架構(gòu)決策、錯誤處理的邊界條件、業(yè)務(wù)邏輯的細(xì)節(jié)這些必須自己來。比如“城市名傳空字符串應(yīng)該返回 400 還是 404”這種問題沒有標(biāo)準(zhǔn)答案取決于你的 API 設(shè)計約定。AI 會給你一個答案但不一定是你想要的。還有一個重要的點AI 生成的代碼可能有安全漏洞。比如它可能會把用戶輸入直接拼接到 SQL 里或者忘記做輸入過濾。安全相關(guān)的代碼一定要自己審查。5.3 我的 AI 協(xié)作流程我的習(xí)慣是先自己想清楚要做什么用注釋把邏輯寫出來然后讓 AI 把注釋翻譯成代碼。這樣 AI 是在執(zhí)行我的設(shè)計而不是替我做設(shè)計。代碼生成后我會逐行審查改掉不合理的部分加上自己的錯誤處理和日志。這個流程的好處是AI 提高了編碼速度但架構(gòu)和邏輯仍然在我掌控之中。出了問題我知道去哪找因為代碼是我設(shè)計的。6. 部署與后續(xù)擴(kuò)展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...每個接口的正常和異常路徑都要覆蓋確認(rèn)返回的狀態(tài)碼和響應(yīng)格式符合預(yù)期。6.2 可以繼續(xù)擴(kuò)展的方向這個項目跑通之后可以往幾個方向擴(kuò)展加緩存。天氣數(shù)據(jù)不需要每次請求都去調(diào)外部接口可以加一層內(nèi)存緩存比如 10 分鐘內(nèi)同一個城市的請求直接返回緩存結(jié)果。用node-cache這個包幾行代碼就能搞定。加限流。防止有人惡意刷接口用express-rate-limit限制每個 IP 的請求頻率。加接口文檔。用 Swagger 自動生成 API 文檔前端同事不用問你接口怎么調(diào)。加健康檢查接口。GET /health返回服務(wù)狀態(tài)部署到云平臺后負(fù)載均衡器會定期檢查這個接口。遷移到 TypeScript。業(yè)務(wù)邏輯穩(wěn)定后加上類型定義重構(gòu)時更有底氣。6.3 我個人的體會搭這個 API 服務(wù)最大的收獲不是學(xué)會了 Express 的 API而是理解了一個后端服務(wù)的完整生命周期請求進(jìn)來、經(jīng)過中間件、到達(dá)路由、執(zhí)行邏輯、調(diào)用外部服務(wù)、組裝響應(yīng)、返回給客戶端、記錄日志。這個流程走一遍以后看任何后端框架的文檔都能快速上手因為概念是相通的。另外AI 輔助編碼確實能提速但前提是你自己得有判斷力。AI 給的代碼對不對、好不好、安不安全你得能看出來。這個判斷力來自你親手寫過的代碼量沒有捷徑。最后分享一個小技巧每次改完代碼不要只測你改的那個功能把相關(guān)的接口都跑一遍。我遇到過好幾次改 A 功能把 B 功能改壞的情況都是因為只測了改動點。養(yǎng)成回歸測試的習(xí)慣能省掉很多線上排查的時間。