REST API 設計最佳實踐指南 - 如何用 JavaScript、Node.js 和 Express.js 構建 REST API

過去幾年里,我創建并使用過很多 API。在此過程中,我遇到過各種好的和壞的實踐,也在開發和調用 API 時碰到過不少棘手的問題,但也有很多順利的時刻。

網上有很多介紹最佳實踐的文章,但在我看來,其中不少都缺乏實用性。只懂理論、沒幾個實例固然有一定價值,但我總是會想:在更真實的場景中,這些理論該如何落地?

簡單的示例能幫助我們理解概念本身,避免過多復雜性干擾,但實際開發中事情往往沒那么簡單。我相信你肯定懂這種感受 😁

這就是我決定寫這篇教程的原因。我把自己的所有經驗(好的、壞的都有)整合到這篇通俗易懂的文章里,同時提供了可跟著操作的實戰案例。最終,我們會一步步落實最佳實踐,搭建出一個完整的 API。

開始前需要明確幾點: 所謂“最佳實踐”,并非必須嚴格遵守的法律或規則,而是經過時間檢驗、被證明有效的約定或建議。其中一些如今已成為標準,但這并不意味著你必須原封不動地照搬。

它們的核心目的是為你提供方向,幫助你從用戶體驗(包括調用者和開發者)、安全性、性能三個維度優化 API。

但請記住:不同項目需要不同的解決方案。有些情況下,你可能無法或不應該遵循某條約定。因此,最終需要開發者自己或與團隊共同判斷。

好了,廢話不多說,我們開始吧!

目錄

  1. 示例項目介紹
  2. 前置要求
  3. 架構設計
  4. 基礎搭建
  5. REST API 最佳實踐
    1. 版本控制
    2. 資源命名使用復數形式
    3. 接收與返回數據采用 JSON 格式
    4. 用標準 HTTP 錯誤碼響應
    5. 端點名稱避免使用動詞
    6. 關聯資源分組(邏輯嵌套)
    7. 集成過濾、排序與分頁
    8. 用數據緩存提升性能
    9. 良好的安全實踐
    10. 完善 API 文檔
  6. 總結

1. 示例項目介紹

在這里插入圖片描述

在將最佳實踐落地到示例項目前,先簡單介紹一下我們要做什么:

我們將為一個 CrossFit 訓練應用搭建 REST API。如果你不了解 CrossFit,它是一種結合了高強度訓練與奧林匹克舉重、體操等多種運動元素的健身方式和競技運動。

在這個應用中,用戶(健身房經營者)可以創建、查詢、更新和刪除 WOD(每日訓練計劃,Workout of the Day),制定訓練方案并統一管理;此外,還能為每個訓練計劃添加重要的訓練提示。

我們的任務就是為這個應用設計并實現 API。

2. 前置要求

要跟上本教程的節奏,你需要具備以下基礎:

  • JavaScript、Node.js、Express.js 的使用經驗
  • 后端架構的基礎認知
  • 了解 REST、API 的概念,理解客戶端-服務器模型

當然,你不必是這些領域的專家,只要熟悉基本用法、有過實操經驗即可。

如果暫時不滿足這些要求,也不用跳過這篇教程——里面仍有很多值得學習的內容,只是有基礎會更容易跟上步驟。

另外,雖然本 API 用 JavaScript 和 Express 編寫,但這些最佳實踐并不局限于這兩種工具,同樣適用于其他編程語言或框架。

3. 架構設計

如前所述,我們將用 Express.js 搭建 API。為避免過度復雜,我們采用三層架構
在這里插入圖片描述

  • 控制器層:處理所有 HTTP 相關邏輯,負責請求與響應的處理;上層通過 Express 的路由將請求分發到對應的控制器方法。
  • 服務層:包含所有業務邏輯,通過導出方法供控制器調用。
  • 數據訪問層:負責與數據庫交互,導出數據庫操作方法(如創建 WOD)供服務層調用。

本示例中,我們不會使用 MongoDB、PostgreSQL 等真實數據庫(以便聚焦最佳實踐本身),而是用一個本地 JSON 文件模擬數據庫。當然,這里的邏輯也可以無縫遷移到真實數據庫中。

4. 基礎搭建

現在我們開始搭建 API 的基礎框架。不用搞得太復雜,重點是結構清晰。

首先創建項目文件夾、子目錄及必要文件,然后安裝依賴并測試是否能正常運行:

4.1 創建目錄結構

# 創建項目文件夾并進入
mkdir crossfit-wod-api && cd crossfit-wod-api# 創建src文件夾并進入
mkdir src && cd src# 創建子文件夾
mkdir controllers && mkdir services && mkdir database && mkdir routes# 創建入口文件index.js
touch index.js# 返回項目根目錄
cd ..# 創建package.json文件
npm init -y

4.2 安裝依賴

# 開發依賴(熱重載)
npm i -D nodemon # 核心依賴(Express框架)
npm i express

4.3 配置 Express

打開src/index.js,寫入以下代碼:

const express = require("express"); 
const app = express(); 
const PORT = process.env.PORT || 3000; // 測試接口
app.get("/", (req, res) => { res.send("<h2>運行正常!</h2>"); 
}); // 啟動服務
app.listen(PORT, () => { console.log(`API正在監聽 ${PORT} 端口`); 
});

4.4 配置開發腳本

package.json中添加dev腳本(實現代碼修改后自動重啟服務):

{"name": "crossfit-wod-api","version": "1.0.0","description": "","main": "index.js","scripts": {"dev": "nodemon src/index.js"  // 新增這行},"keywords": [],"author": "","license": "ISC","devDependencies": {"nodemon": "^2.0.15"},"dependencies": {"express": "^4.17.3"}
}

4.5 測試基礎搭建

啟動開發服務器:

npm run dev

終端會顯示“API 正在監聽 3000 端口”,此時在瀏覽器中訪問localhost:3000,若看到“運行正常!”則說明基礎搭建完成。

5. REST API 最佳實踐

有了 Express 的基礎框架后,我們就可以結合以下最佳實踐來擴展 API 了。

先從最基礎的 CRUD 端點開始,再逐步集成各項最佳實踐。

5.1 版本控制(Versioning)

在編寫任何 API 特定代碼前,必須先考慮版本控制。和其他應用一樣,API 也會不斷迭代、新增功能,因此版本控制至關重要。

版本控制的優勢:
  • 開發新版本時,舊版本仍可正常使用,不會因破壞性變更影響現有用戶;
  • 無需強制用戶立即升級到新版本,用戶可在新版本穩定后自行遷移;
  • 新舊版本并行運行,互不干擾。
如何實現版本控制?

一個常用的最佳實踐是在 URL 中添加版本標識(如v1v2):

// 版本1 
"/api/v1/workouts" // 版本2 
"/api/v2/workouts" 

這是對外暴露的 URL 格式,供其他開發者調用。同時,項目結構也需要區分不同版本:

步驟 1:創建版本目錄

src下創建v1文件夾,用于存放版本 1 的代碼:

mkdir src/v1

將之前創建的routes文件夾移動到v1目錄下:

# 先查看當前目錄路徑并復制(例如/Users/xxx/crossfit-wod-api)
pwd # 移動routes文件夾到v1目錄(將{pwd}替換為復制的路徑)
mv {pwd}/src/routes {pwd}/src/v1
步驟 2:創建版本路由測試文件

src/v1/routes下創建index.js,編寫簡單的路由測試代碼:

touch src/v1/routes/index.js
// src/v1/routes/index.js
const express = require("express"); 
const router = express.Router();// 測試路由
router.route("/").get((req, res) => {res.send(`<h2>來自 ${req.baseUrl} 的響應</h2>`); 
});module.exports = router;
步驟 3:關聯根入口文件與版本路由

修改src/index.js,引入 v1 路由并配置訪問路徑:

const express = require("express"); 
// 引入v1路由
const v1Router = require("./v1/routes"); 
const app = express(); 
const PORT = process.env.PORT || 3000; // 移除舊的測試接口
// app.get("/", (req, res) => { 
//     res.send("<h2>運行正常!</h2>"); 
// }); // 配置v1路由的訪問路徑
app.use("/api/v1", v1Router);app.listen(PORT, () => { console.log(`API正在監聽 ${PORT} 端口`); 
});
步驟 4:測試版本路由

訪問localhost:3000/api/v1,若看到“來自 /api/v1 的響應”則說明版本路由配置成功。

注意事項:

目前我們只將routes放入v1目錄,controllersservices等仍在src根目錄——這對小型 API 來說沒問題,可以讓多個版本共享這些通用邏輯。

但如果 API 規模擴大,比如 v2 需要特定的控制器或服務(修改通用邏輯可能影響舊版本),則建議將controllersservices也按版本拆分到對應目錄,實現版本內邏輯的完全封裝。

5.2 資源命名使用復數形式(Name resources in plural)

接下來開始實現 API 的核心功能——為 WOD 設計 CRUD 端點。首先要解決的是資源命名問題。

為什么用復數?

資源可以理解為“一個存放數據的集合”(比如“workouts”是所有訓練計劃的集合)。用復數命名能讓調用者一目了然地知道這是一個“集合”,而非單個資源,避免歧義。

步驟 1:創建 WOD 相關文件

創建控制器、服務和路由文件,分別對應三層架構:

# 控制器(處理HTTP請求/響應)
touch src/controllers/workoutController.js # 服務(處理業務邏輯)
touch src/services/workoutService.js # 路由(分發請求)
touch src/v1/routes/workoutRoutes.js
步驟 2:編寫 WOD 路由(復數命名)

src/v1/routes/workoutRoutes.js中定義 CRUD 端點,注意 URL 使用復數/workouts

// src/v1/routes/workoutRoutes.js
const express = require("express"); 
const router = express.Router();// 獲取所有訓練計劃
router.get("/", (req, res) => {res.send("獲取所有訓練計劃"); 
});// 獲取單個訓練計劃(通過ID)
router.get("/:workoutId", (req, res) => {res.send("獲取單個訓練計劃"); 
});// 創建訓練計劃
router.post("/", (req, res) => {res.send("創建訓練計劃"); 
});// 更新訓練計劃
router.patch("/:workoutId", (req, res) => {res.send("更新訓練計劃"); 
});// 刪除訓練計劃
router.delete("/:workoutId", (req, res) => {res.send("刪除訓練計劃"); 
});module.exports = router;

刪除之前用于測試的src/v1/routes/index.js(已不再需要)。

步驟 3:關聯根入口文件與 WOD 路由

修改src/index.js,替換舊的 v1 路由,改用 WOD 路由:

const express = require("express"); 
// 移除舊的v1路由引入
// const v1Router = require("./v1/routes"); 
// 引入WOD路由
const v1WorkoutRouter = require("./v1/routes/workoutRoutes"); 
const app = express(); 
const PORT = process.env.PORT || 3000; // 移除舊的v1路由配置
// app.use("/api/v1", v1Router); 
// 配置WOD路由的訪問路徑(復數)
app.use("/api/v1/workouts", v1WorkoutRouter);app.listen(PORT, () => { console.log(`API正在監聽 ${PORT} 端口`); 
});
步驟 4:編寫控制器方法

src/controllers/workoutController.js中定義與路由對應的控制器方法:

// src/controllers/workoutController.js
// 獲取所有訓練計劃
const getAllWorkouts = (req, res) => {res.send("獲取所有訓練計劃"); 
}; // 獲取單個訓練計劃
const getOneWorkout = (req, res) => {res.send("獲取單個訓練計劃"); 
}; // 創建訓練計劃
const createNewWorkout = (req, res) => {res.send("創建訓練計劃"); 
}; // 更新訓練計劃
const updateOneWorkout = (req, res) => {res.send("更新訓練計劃"); 
}; // 刪除訓練計劃
const deleteOneWorkout = (req, res) => {res.send("刪除訓練計劃"); 
};module.exports = {getAllWorkouts,getOneWorkout,createNewWorkout,updateOneWorkout,deleteOneWorkout, 
};
步驟 5:路由關聯控制器

修改src/v1/routes/workoutRoutes.js,將路由與控制器方法綁定:

// src/v1/routes/workoutRoutes.js
const express = require("express"); 
// 引入控制器
const workoutController = require("../../controllers/workoutController"); 
const router = express.Router();// 綁定路由與控制器方法
router.get("/", workoutController.getAllWorkouts);
router.get("/:workoutId", workoutController.getOneWorkout);
router.post("/", workoutController.createNewWorkout);
router.patch("/:workoutId", workoutController.updateOneWorkout);
router.delete("/:workoutId", workoutController.deleteOneWorkout);module.exports = router;
測試路由

訪問localhost:3000/api/v1/workouts/123,若看到“獲取單個訓練計劃”則說明路由配置成功。

5.3 接收與返回數據采用 JSON 格式

調用 API 時,請求和響應都需要傳遞數據。JSON(JavaScript 對象表示法) 是通用的標準化格式,不受編程語言限制(Java、Python 等都能處理 JSON),因此 API 應統一使用 JSON 接收和返回數據。

步驟 1:編寫服務層基礎代碼

服務層負責業務邏輯,先在src/services/workoutService.js中創建與控制器對應的方法:

// src/services/workoutService.js
const getAllWorkouts = () => {return; 
}; const getOneWorkout = () => {return; 
}; const createNewWorkout = () => {return; 
}; const updateOneWorkout = () => {return; 
}; const deleteOneWorkout = () => {return; 
};module.exports = {getAllWorkouts,getOneWorkout,createNewWorkout,updateOneWorkout,deleteOneWorkout, 
};
步驟 2:創建模擬數據庫(JSON 文件)

src/database下創建db.json(模擬數據庫)和Workout.js(數據訪問方法):

# 模擬數據庫
touch src/database/db.json # 數據訪問層方法
touch src/database/Workout.js

db.json中添加測試數據(3 個訓練計劃):

{"workouts": [{"id": "61dbae02-c147-4e28-863c-db7bd402b2d6","name": "Tommy V","mode": "計時完成","equipment": ["杠鈴", "繩梯"],"exercises": ["21次火箭推","12次15英尺繩爬","15次火箭推","9次15英尺繩爬","9次火箭推","6次15英尺繩爬"],"createdAt": "2022-04-20 14:21:56","updatedAt": "2022-04-20 14:21:56","trainerTips": ["21次火箭推可拆分完成","9次和6次火箭推盡量不間斷完成","標準重量:115磅/75磅"]},{"id": "4a3d9aaa-608c-49a7-a004-66305ad4ab50","name": "Dead Push-Ups","mode": "10分鐘內盡可能多組","equipment": ["杠鈴"],"exercises": ["15次硬拉","15次釋放式俯臥撐"],"createdAt": "2022-01-25 13:15:44","updatedAt": "2022-03-10 08:21:56","trainerTips": ["硬拉重量宜輕,速度宜快","盡量不間斷完成一組","標準重量:135磅/95磅"]},{"id": "d8be2362-7b68-4ea4-a1f6-03f8bc4eede7","name": "Heavy DT","mode": "5輪計時完成","equipment": ["杠鈴", "繩梯"],"exercises": ["12次硬拉","9次懸掛式力量抓舉","6次推挺"],"createdAt": "2021-11-20 17:39:07","updatedAt": "2021-11-20 17:39:07","trainerTips": ["推挺盡量不間斷","前3輪可能很痛苦,但堅持住","標準重量:205磅/145磅"]}]
}
步驟 3:編寫數據訪問層方法(獲取所有訓練計劃)

src/database/Workout.js中實現從 JSON 文件讀取數據的方法:

// src/database/Workout.js
// 引入模擬數據庫
const DB = require("./db.json");// 獲取所有訓練計劃
const getAllWorkouts = () => {return DB.workouts;
};module.exports = { getAllWorkouts };
步驟 4:服務層調用數據訪問層

修改src/services/workoutService.js,調用數據訪問層方法獲取數據:

// src/services/workoutService.js
// 引入數據訪問層
const Workout = require("../database/Workout");// 獲取所有訓練計劃
const getAllWorkouts = () => {const allWorkouts = Workout.getAllWorkouts();return allWorkouts;
}; // 其他方法暫不修改
const getOneWorkout = () => { return; }; 
const createNewWorkout = () => { return; }; 
const updateOneWorkout = () => { return; }; 
const deleteOneWorkout = () => { return; };module.exports = {getAllWorkouts,getOneWorkout,createNewWorkout,updateOneWorkout,deleteOneWorkout, 
};
步驟 5:控制器返回 JSON 數據

修改src/controllers/workoutController.js,通過服務層獲取數據并以 JSON 格式返回:

// src/controllers/workoutController.js
// 引入服務層
const workoutService = require("../services/workoutService");// 獲取所有訓練計劃(返回JSON)
const getAllWorkouts = (req, res) => {const allWorkouts = workoutService.getAllWorkouts();// 以JSON格式返回數據(包含狀態和數據)res.send({ status: "成功", data: allWorkouts });
}; // 其他方法暫不修改
const getOneWorkout = (req, res) => { res.send("獲取單個訓練計劃"); }; 
const createNewWorkout = (req, res) => { res.send("創建訓練計劃"); }; 
const updateOneWorkout = (req, res) => { res.send("更新訓練計劃"); }; 
const deleteOneWorkout = (req, res) => { res.send("刪除訓練計劃"); };module.exports = {getAllWorkouts,getOneWorkout,createNewWorkout,updateOneWorkout,deleteOneWorkout, 
};
測試返回 JSON

訪問localhost:3000/api/v1/workouts,瀏覽器會顯示 JSON 格式的訓練計劃數據,說明返回 JSON 配置成功。

步驟 6:配置 API 接收 JSON 請求

創建或更新訓練計劃時,需要接收客戶端發送的 JSON 數據。需安裝body-parser解析請求體:

npm i body-parser

修改src/index.js,配置解析 JSON 請求體:

const express = require("express"); 
// 引入body-parser
const bodyParser = require("body-parser");
const v1WorkoutRouter = require("./v1/routes/workoutRoutes"); 
const app = express(); 
const PORT = process.env.PORT || 3000; // 配置解析JSON請求體
app.use(bodyParser.json());app.use("/api/v1/workouts", v1WorkoutRouter);app.listen(PORT, () => { console.log(`API正在監聽 ${PORT} 端口`); 
});
步驟 7:實現“創建訓練計劃”(接收并存儲 JSON)

要實現創建功能,需先添加“保存數據到 JSON 文件”的工具方法:

  1. 創建工具方法:在src/database下創建utils.js,實現寫入 JSON 文件的邏輯:
touch src/database/utils.js
// src/database/utils.js
const fs = require("fs");// 保存數據到JSON文件
const saveToDatabase = (DB) => {fs.writeFileSync("./src/database/db.json", JSON.stringify(DB, null, 2), {encoding: "utf-8",});
};module.exports = { saveToDatabase };

2.更新數據訪問層:修改src/database/Workout.js,添加創建訓練計劃的方法:

// src/database/Workout.js
const DB = require("./db.json");
// 引入保存工具
const { saveToDatabase } = require("./utils");// 獲取所有訓練計劃
const getAllWorkouts = () => {return DB.workouts;
};// 創建訓練計劃
const createNewWorkout = (newWorkout) => {// 檢查是否已存在同名訓練計劃const isAlreadyExists = DB.workouts.findIndex(w => w.name === newWorkout.name) > -1;if (isAlreadyExists) {return; // 已存在則返回空}// 新增訓練計劃并保存DB.workouts.push(newWorkout);saveToDatabase(DB);return newWorkout;
};module.exports = { getAllWorkouts, createNewWorkout };
  1. 更新服務層:安裝uuid生成唯一 ID,修改src/services/workoutService.js
npm i uuid
// src/services/workoutService.js
const { v4: uuid } = require("uuid"); // 生成唯一ID
const Workout = require("../database/Workout");// 獲取所有訓練計劃(不變)
const getAllWorkouts = () => {const allWorkouts = Workout.getAllWorkouts();return allWorkouts;
}; // 創建訓練計劃(添加ID、時間戳)
const createNewWorkout = (newWorkout) => {// 補充必要字段(ID、創建時間、更新時間)const workoutToAdd = {...newWorkout,id: uuid(), // 唯一IDcreatedAt: new Date().toLocaleString("zh-CN", { timeZone: "UTC" }),updatedAt: new Date().toLocaleString("zh-CN", { timeZone: "UTC" }),};const createdWorkout = Workout.createNewWorkout(workoutToAdd);return createdWorkout;
}; // 其他方法暫不修改
const getOneWorkout = () => { return; }; 
const updateOneWorkout = () => { return; }; 
const deleteOneWorkout = () => { return; };module.exports = {getAllWorkouts,getOneWorkout,createNewWorkout,updateOneWorkout,deleteOneWorkout, 
};
  1. 更新控制器:修改src/controllers/workoutController.js,接收 JSON 請求并驗證:
// src/controllers/workoutController.js
const workoutService = require("../services/workoutService");// 獲取所有訓練計劃(不變)
const getAllWorkouts = (req, res) => {const allWorkouts = workoutService.getAllWorkouts();res.send({ status: "成功", data: allWorkouts });
}; // 其他方法暫不修改
const getOneWorkout = (req, res) => { res.send("獲取單個訓練計劃"); }; // 創建訓練計劃(接收JSON并驗證)
const createNewWorkout = (req, res) => {const { body } = req;// 驗證必填字段if (!body.name || !body.mode || !body.equipment || !body.exercises || !body.trainerTips) {res.status(400).send({ status: "失敗", data: { error: "請求體缺少以下必填字段:'name'、'mode'、'equipment'、'exercises'、'trainerTips'" } });return;}// 調用服務層創建訓練計劃const createdWorkout = workoutService.createNewWorkout(body);// 返回201(創建成功)和新訓練計劃res.status(201).send({ status: "成功", data: createdWorkout });
}; const updateOneWorkout = (req, res) => { res.send("更新訓練計劃"); }; 
const deleteOneWorkout = (req, res) => { res.send("刪除訓練計劃"); };module.exports = {getAllWorkouts,getOneWorkout,createNewWorkout,updateOneWorkout,deleteOneWorkout, 
};
測試接收 JSON

用 Postman 或 Apifox 發送POST請求到localhost:3000/api/v1/workouts,請求體為 JSON:

{"name": "核心爆發","mode": "20分鐘內盡可能多組","equipment": ["架子", "杠鈴", "腹肌墊"],"exercises": ["15次舉腿觸杠","10次火箭推","30次腹肌墊卷腹"],"trainerTips": ["舉腿觸杠最多分兩組完成","火箭推盡量不間斷","卷腹時調整呼吸節奏"]
}

若返回狀態 201 和包含 ID、時間戳的新訓練計劃,則說明 API 成功接收并存儲了 JSON 數據。再訪問localhost:3000/api/v1/workouts,可看到新增的訓練計劃。

5.4 用標準 HTTP 錯誤碼響應

實際開發中,API 難免出現錯誤(如參數缺失、資源不存在等)。使用標準 HTTP 錯誤碼并返回清晰的錯誤信息,能幫助調用者快速定位問題。

常見 HTTP 錯誤碼及場景:

  • 400:請求錯誤(如參數缺失、格式錯誤)
  • 404:資源不存在(如查詢的訓練計劃 ID 不存在)
  • 500:服務器內部錯誤(如數據庫操作失敗)
步驟 1:完善“創建訓練計劃”的錯誤處理

修改數據訪問層、服務層和控制器,添加錯誤拋出和捕獲:

  1. 數據訪問層(拋出錯誤):修改src/database/Workout.js
// src/database/Workout.js
const DB = require("./db.json");
const { saveToDatabase } = require("./utils");// 獲取所有訓練計劃(添加錯誤捕獲)
const getAllWorkouts = () => {try {return DB.workouts;} catch (error) {throw { status: 500, message: "獲取訓練計劃失敗:" + error.message };}
};// 創建訓練計劃(拋出錯誤)
const createNewWorkout = (newWorkout) => {try {const isAlreadyExists = DB.workouts.findIndex(w => w.name === newWorkout.name) > -1;if (isAlreadyExists) {throw { status: 400, message: `訓練計劃"${newWorkout.name}"已存在` };}DB.workouts.push(newWorkout);saveToDatabase(DB);return newWorkout;} catch (error) {throw { status: error.status || 500, message: error.message || "創建訓練計劃失敗" };}
};module.exports = { getAllWorkouts, createNewWorkout };

2.服務層(捕獲并拋出錯誤):修改src/services/workoutService.js

// src/services/workoutService.js
const { v4: uuid } = require("uuid");
const Workout = require("../database/Workout");// 獲取所有訓練計劃(錯誤處理)
const getAllWorkouts = () => {try {const allWorkouts = Workout.getAllWorkouts();return allWorkouts;} catch (error) {throw error;}
}; // 創建訓練計劃(錯誤處理)
const createNewWorkout = (newWorkout) => {try {const workoutToAdd = {...newWorkout,id: uuid(),createdAt: new Date().toLocaleString("zh-CN", { timeZone: "UTC" }),updatedAt: new Date().toLocaleString("zh-CN", { timeZone: "UTC" }),};const createdWorkout = Workout.createNewWorkout(workoutToAdd);return createdWorkout;} catch (error) {throw error;}
}; // 其他方法暫不修改
const getOneWorkout = () => { return; }; 
const updateOneWorkout = () => { return; }; 
const deleteOneWorkout = () => { return; };module.exports = {getAllWorkouts,getOneWorkout,createNewWorkout,updateOneWorkout,deleteOneWorkout, 
};

3.控制器(捕獲錯誤并返回錯誤碼):修改src/controllers/workoutController.js

// src/controllers/workoutController.js
const workoutService = require("../services/workoutService");// 獲取所有訓練計劃(錯誤處理)
const getAllWorkouts = (req, res) => {try {const allWorkouts = workoutService.getAllWorkouts();res.send({ status: "成功", data: allWorkouts });} catch (error) {res.status(error.status || 500).send({ status: "失敗", data: { error: error.message } });}
}; // 其他方法暫不修改
const getOneWorkout = (req, res) => { res.send("獲取單個訓練計劃"); }; // 創建訓練計劃(錯誤處理)
const createNewWorkout = (req, res) => {const { body } = req;// 驗證必填字段(400錯誤)if (!body.name || !body.mode || !body.equipment || !body.exercises || !body.trainerTips) {res.status(400).send({ status: "失敗", data: { error: "請求體缺少以下必填字段:'name'、'mode'、'equipment'、'exercises'、'trainerTips'" } });return;}try {const createdWorkout = workoutService.createNewWorkout(body);res.status(201).send({ status: "成功", data: createdWorkout });} catch (error) {res.status(error.status || 500).send({ status: "失敗", data: { error: error.message } });}
}; const updateOneWorkout = (req, res) => { res.send("更新訓練計劃"); }; 
const deleteOneWorkout = (req, res) => { res.send("刪除訓練計劃"); };module.exports = {getAllWorkouts,getOneWorkout,createNewWorkout,updateOneWorkout,deleteOneWorkout, 
};
測試錯誤處理
  1. 重復創建同名訓練計劃:發送相同名稱的 POST 請求,返回 400 錯誤和“訓練計劃已存在”的信息;
  2. 缺失必填字段:請求體不包含name,返回 400 錯誤和“缺少必填字段”的信息。

5.5 端點名稱避免使用動詞

端點 URL 應指向資源,而非描述“動作”——因為 HTTP 方法(GET/POST/PATCH/DELETE)已經明確了動作含義,再在 URL 中加動詞會顯得冗余且混亂。

錯誤示例(含動詞):
GET "/api/v1/getAllWorkouts"  // 冗余:GET已表示“獲取”
POST "/api/v1/createWorkout"  // 冗余:POST已表示“創建”
DELETE "/api/v1/deleteWorkout/123"  // 冗余:DELETE已表示“刪除”
正確示例(無動詞,僅資源):
GET "/api/v1/workouts"  // 獲取所有訓練計劃(GET+復數資源)
POST "/api/v1/workouts"  // 創建訓練計劃(POST+復數資源)
DELETE "/api/v1/workouts/123"  // 刪除ID為123的訓練計劃(DELETE+資源+ID)

我們之前的實現已經遵循了這個最佳實踐,無需修改——核心原則是:HTTP 方法描述動作,URL 描述資源

5.6 關聯資源分組(邏輯嵌套)

當資源之間存在關聯關系時(如“訓練計劃”與“訓練記錄”),可通過 URL 嵌套實現邏輯分組,讓 API 結構更清晰。

例如,我們要為每個訓練計劃添加“會員記錄”(記錄會員完成該訓練的時間),可設計嵌套 URL:

// 獲取ID為123的訓練計劃的所有記錄
GET "/api/v1/workouts/123/records"
步驟 1:擴展模擬數據庫(添加會員數據)

修改src/database/db.json,添加members(會員)和records(記錄)字段:

{"workouts": [// 原有訓練計劃數據不變...],"members": [{"id": "12a410bc-849f-4e7e-bfc8-4ef283ee4b19","name": "Jason Miller","gender": "男","dateOfBirth": "1990-04-23","email": "jason@mail.com","password": "666349420ec497c1dc890c45179d44fb13220239325172af02d1fb6635922956"},{"id": "2b9130d4-47a7-4085-800e-0144f6a46059","name": "Tiffany Brookston","gender": "女","dateOfBirth": "1996-06-09","email": "tiffy@mail.com","password": "8a1ea5669b749354110dcba3fac5546c16e6d0f73a37f35a84f6b0d7b3c22fcc"}],"records": [{"id": "r1","workoutId": "61dbae02-c147-4e28-863c-db7bd402b2d6","memberId": "12a410bc-849f-4e7e-bfc8-4ef283ee4b19","time": "12:30","date": "2022-04-21"},{"id": "r2","workoutId": "61dbae02-c147-4e28-863c-db7bd402b2d6","memberId": "2b9130d4-47a7-4085-800e-0144f6a46059","time": "14:15","date": "2022-04-21"}]
}
步驟 2:創建記錄相關文件
# 記錄控制器
touch src/controllers/recordController.js # 記錄服務
touch src/services/recordService.js # 記錄路由
touch src/v1/routes/recordRoutes.js
步驟 3:編寫記錄數據訪問層方法

src/database下創建Record.js

touch src/database/Record.js
// src/database/Record.js
const DB = require("./db.json");// 根據訓練計劃ID獲取記錄
const getRecordsByWorkoutId = (workoutId) => {return DB.records.filter(record => record.workoutId === workoutId);
};module.exports = { getRecordsByWorkoutId };
步驟 4:編寫記錄服務層
// src/services/recordService.js
const Record = require("../database/Record");// 根據訓練計劃ID獲取記錄
const getRecordsByWorkoutId = (workoutId) => {return Record.getRecordsByWorkoutId(workoutId);
};module.exports = { getRecordsByWorkoutId };
步驟 5:編寫記錄控制器
// src/controllers/recordController.js
const recordService = require("../services/recordService");// 根據訓練計劃ID獲取記錄
const getRecordsByWorkoutId = (req, res) => {const { workoutId } = req.params;if (!workoutId) {res.status(400).send({ status: "失敗", data: { error: "workoutId不能為空" } });return;}try {const records = recordService.getRecordsByWorkoutId(workoutId);res.send({ status: "成功", data: records });} catch (error) {res.status(500).send({ status: "失敗", data: { error: error.message } });}
};module.exports = { getRecordsByWorkoutId };
步驟 6:編寫嵌套路由
// src/v1/routes/recordRoutes.js
const express = require("express");
const recordController = require("../../controllers/recordController");
const router = express.Router({ mergeParams: true }); // 允許訪問父路由參數// 嵌套路由:/api/v1/workouts/:workoutId/records
router.get("/", recordController.getRecordsByWorkoutId);module.exports = router;
步驟 7:關聯訓練計劃路由與記錄路由

修改src/v1/routes/workoutRoutes.js,引入記錄路由并配置嵌套:

// src/v1/routes/workoutRoutes.js
const express = require("express"); 
const workoutController = require("../../controllers/workoutController"); 
// 引入記錄路由
const recordRouter = require("./recordRoutes");
const router = express.Router();// 嵌套路由:將/records掛載到/workouts/:workoutId下
router.use("/:workoutId/records", recordRouter);// 原有CRUD路由不變
router.get("/", workoutController.getAllWorkouts);
router.get("/:workoutId", workoutController.getOneWorkout);
router.post("/", workoutController.createNewWorkout);
router.patch("/:workoutId", workoutController.updateOneWorkout);
router.delete("/:workoutId", workoutController.deleteOneWorkout);module.exports = router;
測試嵌套路由

訪問localhost:3000/api/v1/workouts/61dbae02-c147-4e28-863c-db7bd402b2d6/records,可獲取該訓練計劃的所有會員記錄,說明嵌套路由配置成功。

5.7 其他最佳實踐(簡要說明)

由于篇幅限制,以下最佳實踐簡要介紹核心思路,可參考上述方法自行實現:

1. 集成過濾、排序與分頁

當資源數量龐大時,需支持過濾、排序和分頁,減輕服務器壓力:

  • 過濾GET /api/v1/workouts?mode=計時完成(篩選“計時完成”的訓練計劃);
  • 排序GET /api/v1/workouts?sort=createdAt&order=desc(按創建時間倒序);
  • 分頁GET /api/v1/workouts?page=1&limit=10(第 1 頁,每頁 10 條)。

實現思路:在服務層解析req.query中的參數,對數據進行過濾、排序或切片處理。

2. 用數據緩存提升性能

對頻繁訪問且更新不頻繁的數據(如熱門訓練計劃),可使用 Redis 緩存,減少數據庫查詢次數:

  • 首次請求:從數據庫獲取數據,存入 Redis;
  • 后續請求:直接從 Redis 獲取數據,若數據過期則重新從數據庫加載。
3. 良好的安全實踐
  • 身份驗證:用 JWT 或 OAuth2.0 驗證用戶身份(如僅登錄用戶可創建訓練計劃);
  • 權限控制:區分管理員和普通用戶權限(如僅管理員可刪除訓練計劃);
  • 輸入驗證:用express-validator驗證請求參數,防止 SQL 注入或 XSS 攻擊;
  • HTTPS:生產環境強制使用 HTTPS,加密傳輸數據;
  • 限流:用express-rate-limit限制接口調用頻率,防止惡意請求。
4. 完善 API 文檔

API 文檔是調用者的使用指南,推薦用 Swagger/OpenAPI 自動生成文檔:

  • 安裝swagger-jsdocswagger-ui-express
  • 在代碼中添加 JSDoc 風格的注釋(描述端點、參數、響應等);
  • 配置 Swagger 路由,訪問/api-docs即可查看交互式文檔。

6. 總結

REST API 的最佳實踐并非一成不變的規則,而是基于“提升可用性、安全性和性能”的設計原則。本文通過一個 CrossFit 訓練應用的示例,落地了以下核心實踐:

  1. 版本控制:URL 添加版本標識,支持新舊版本并行;
  2. 資源命名:用復數形式命名資源,避免歧義;
  3. 數據格式:統一使用 JSON 接收和返回數據;
  4. 錯誤處理:用標準 HTTP 錯誤碼+清晰信息,便于調試;
  5. 端點設計:URL 指向資源,不包含動詞;
  6. 關聯資源:用嵌套路由分組關聯資源。

實際開發中,需根據項目規模和需求靈活調整(如小型 API 可簡化版本控制,大型 API 需嚴格區分版本內邏輯)。掌握這些實踐,能讓你的 API 更易于維護和使用。

擴展鏈接

數據同步功能

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/98148.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/98148.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/98148.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

MyCat

文章目錄18.1 MySQL 讀寫分離概述18.1.1 工作原理18.1.2 為什么要讀寫分離18.1.3 實現方式18.2 什么是 MyCat18.3 MyCat 安裝與配置1. 下載與解壓2. 創建用戶并修改權限3. 目錄說明4. Java 環境要求18.4 MyCat 啟動與配置1. 配置環境變量2. 配置 hosts&#xff08;多節點集群&a…

使用 Spring Boot 搭建和部署 Kafka 消息隊列系統

使用 Spring Boot 搭建和部署 Kafka 消息隊列系統 摘要 本文將引導您在 Kafka 上搭建一個消息隊列系統&#xff0c;并整合到您的 Spring Boot 項目中。我們將逐步實現這一方案&#xff0c;探討其中的關鍵原理&#xff0c;避開可能遇到的坑&#xff0c;并最終將其部署到 Kuberne…

daily notes[45]

文章目錄basic knowledgereferencesbasic knowledge the variable in Rust is not changed. let x5; x6;Rust language promotes the concept that immutable variables are safer than variables in other programming language such as python and and are in favour of th…

技術奇點爆發周:2025 年 9 月科技突破全景掃描

技術奇點爆發周&#xff1a;2025 年 9 月科技突破全景掃描當中國 "祖沖之三號" 量子計算機在特定任務上超越經典超級計算機一千萬億倍的算力新聞&#xff0c;與 OpenAI 宣布 100 億美元定制芯片量產協議的消息在同一周密集爆發時&#xff0c;我們真切感受到了技術革命…

分布式專題——10.3 ShardingSphere實現原理以及內核解析

1 ShardingSphere-JDBC 內核工作原理當往 ShardingSphere 提交一個邏輯SQL后&#xff0c;ShardingSphere 到底做了哪些事情呢&#xff1f;首先要從 ShardingSphere 官方提供的這張整體架構圖說起&#xff1a;1.1 配置管控在 SQL 進入 ShardingSphere 內核處理&#xff08;如解析…

移動語義的里里外外:從 std::move 的幻象到性能的現實

我們都已經聽過這樣的建議&#xff1a;“使用 std::move 來避免昂貴的拷貝&#xff0c;提升性能。” 這沒錯&#xff0c;但如果你對它的理解僅止于此&#xff0c;那么你可能正在黑暗中揮舞著一把利劍&#xff0c;既可能披荊斬棘&#xff0c;也可能傷及自身。 移動語義是 C11 帶…

selenium完整版一覽

selenium 庫驅動瀏覽器selenium庫是一種用于Web應用程序測試的工具,它可以驅動瀏覽器執行特定操作,自動按照腳本代碼做出單擊、輸入、打開、驗證等操作,支持的瀏覽器包括IE、Firefox、Safari、Chrome、Opera等。而在辦公領域中如果經常需要使用瀏覽器操作某些內容,就可以使用se…

[Linux]學習筆記系列 -- lib/kfifo.c 內核FIFO實現(Kernel FIFO Implementation) 高效的無鎖字節流緩沖區

文章目錄lib/kfifo.c 內核FIFO實現(Kernel FIFO Implementation) 高效的無鎖字節流緩沖區歷史與背景這項技術是為了解決什么特定問題而誕生的&#xff1f;它的發展經歷了哪些重要的里程碑或版本迭代&#xff1f;目前該技術的社區活躍度和主流應用情況如何&#xff1f;核心原理與…

MFC_Install_Create

1. 安裝MFC 編寫MFC窗口應用程序需要用到Visual Studiohttps://visualstudio.microsoft.com/zh-hans/&#xff0c;然后安裝&#xff0c;要選擇使用C的桌面開發&#xff0c;再點擊右邊安裝詳細信息中的使用C的桌面開發&#xff0c;往下滑&#xff0c;有一個適用于最新的v143生成…

Langchain4j開發之AI Service

學習基于Langchain4j的大模型開發需要學習其中Ai Service的開發模式。里面對大模型做了一層封裝&#xff0c;提供一些可以方便調用的api。其中有兩種使用Ai Service的方式。一.編程式開發1.首先引入Langchain4的依賴。<dependency><groupId>dev.langchain4j</gr…

認識神經網絡和深度學習

什么是神經網絡&#xff1f;什么又是深度學習&#xff1f;二者有什么關系&#xff1f;……帶著這些疑問&#xff0c;進入本文的學習。什么是神經網絡神經網絡&#xff08;Neural Network&#xff09;是一種模仿生物神經系統&#xff08;如大腦神經元連接方式&#xff09;設計的…

醫療行業安全合規數據管理平臺:構建高效協作與集中化知識沉淀的一體化解決方案

在醫療行業中&#xff0c;數據不僅是日常運營的基礎&#xff0c;更是患者安全、服務質量和合規管理的核心載體。隨著醫療業務的復雜化和服務模式的多元化&#xff0c;各類機構——從大型醫院到科研中心——都面臨著海量文檔、報告、影像資料和政策文件的管理需求。這些資料往往…

Day25_【深度學習(3)—PyTorch使用(5)—張量形狀操作】

reshape() squeeze()unsqueeze()transpose()permute()view() reshape() contiguous() reshape() 一、reshape() 函數保證張量數據不變的前提下改變數據的維度&#xff0c;將其轉換成指定的形狀。def reshape_tensor():data torch.tensor([[1, 2, 3], [4, 5, 6]])print(data…

第十八篇 開發網頁教學:實現畫布、繪畫、簡易 PS 方案

在網頁開發領域&#xff0c;畫布功能是實現交互創作的重要基礎&#xff0c;無論是簡單的繪畫工具&#xff0c;還是具備基礎修圖能力的簡易 PS 方案&#xff0c;都能為用戶帶來豐富的視覺交互體驗。本篇教學將圍繞 “學習 - 實踐 - 實操” 的核心思路&#xff0c;從技術原理講解…

封裝形成用助焊劑:電子制造“隱形橋梁”的技術突圍與全球產業重構

在5G通信、人工智能、新能源汽車等新興技術驅動下&#xff0c;全球電子制造業正以年均6.8%的增速重構產業鏈。作為電子元件焊接的核心輔料&#xff0c;封裝形成用助焊劑&#xff08;又稱電子封裝用助焊劑&#xff09;憑借其“優化焊接質量、提升可靠性、降低制造成本”的核心價…

【完整源碼+數據集+部署教程】零件實例分割系統源碼和數據集:改進yolo11-GhostHGNetV2

背景意義 研究背景與意義 隨著工業自動化和智能制造的迅速發展&#xff0c;零件的高效識別與分割在生產線上的重要性日益凸顯。傳統的圖像處理方法在處理復雜場景時往往面臨著準確性不足和實時性差的問題&#xff0c;而深度學習技術的引入為這一領域帶來了新的機遇。特別是基于…

墨色規則與血色節點:C++紅黑樹設計與實現探秘

前言? 前幾天攻克了AVL樹&#xff0c;我們已然是平衡二叉樹的強者。但旅程還未結束&#xff0c;下一個等待我們的&#xff0c;是更強大、也更傳奇的**終極BOSS**——紅黑樹。它不僅是map和set的強大心臟&#xff0c;更是C STL皇冠上的明珠。準備好了嗎&#xff1f;讓我們一…

大數據時代時序數據庫選型指南:為何 Apache IoTDB 成優選(含實操步驟)

在數字經濟加速滲透的今天&#xff0c;工業物聯網&#xff08;IIoT&#xff09;、智慧能源、金融交易、城市運維等領域每天產生海量 “帶時間戳” 的數據 —— 從工業設備的實時溫度、電壓&#xff0c;到電網的負荷波動&#xff0c;再到金融市場的每秒行情&#xff0c;這類 “時…

MAZANOKE+cpolar讓照片存儲無上限

文章目錄前言1. 關于MAZANOKE2. Docker部署3. 簡單使用MAZANOKE4. 安裝cpolar內網穿透5. 配置公網地址6. 配置固定公網地址總結當工具開始理解用戶的需求痛點時&#xff0c;MAZANOKE與cpolar這對搭檔給出了“輕量化”的解決方案。它不追求浮夸的功能堆砌&#xff0c;卻用扎實的…

正則表達式 - 元字符

正則表達式中的元字符是具有特殊含義的字符&#xff0c;它們不表示字面意義&#xff0c;而是用于控制匹配模式。基本元字符. (點號)匹配除換行符(\n)外的任意單個字符示例&#xff1a;a.b 匹配 "aab", "a1b", "a b" 等^ (脫字符)匹配字符串的開始…