在我們開始將最佳實踐應用到示例項目之前,我想簡要介紹一下我們將構建的內容。

我們將為 CrossFit Training 應用程序構建一個 REST API。如果您對CrossFit不太了解,它是一種結合高強度鍛煉和多項運動(如奧運會舉重、體操等)元素的健身方法和競技運動。

在我們的應用程序中,我們希望創建、讀取、更新和刪除 WOD (Workouts othe Day)。這將幫助我們的用戶(即健身房所有者)制定鍛煉計劃,并在一個應用程序中維護自己的訓練計劃。最重要的是,他們還可以為每次鍛煉添加一些關鍵的訓練技巧。

我們的工作將要求我們為該應用程序設計和實現一個 API。

先決條件

為了確保您能夠順利跟上本教程的學習進度,建議您具備一定的JavaScript、Node.js、Express.js以及后端架構的相關知識和實踐經驗。此外,對于REST和API等概念,您應該有所了解,并熟悉客戶端-服務器模型(Client-Server-Model)。

雖然我們并不要求您是這些領域的專家,但擁有相關的基礎知識和一定的實踐經驗將有助于您更輕松地掌握教程內容。如果您在某些方面還不夠熟悉,也請不要氣餒,因為這里仍然有很多寶貴的知識等待您去探索和學習。

盡管此 API 是用 JavaScript 和 Express 編寫的,但最佳實踐并不局限于這些工具。它們也可以應用于其他編程語言或框架。

架構

如上所述,我們將 Express.js 用于我們的 API。我不想提出一個復雜的架構,所以我想堅持使用 3 層架構:

在 Controller 中,我們將處理所有與 HTTP 相關的事情。包括接收請求和發送響應。這一層之上,Express的路由器會將請求分發到相應的控制器。

整個業務邏輯將位于 Service Layer 中,該層提供了一些服務方法供控制器調用。

數據訪問層位于最底層,負責與數據庫進行交互。我們在這一層定義了一些數據庫操作方法,例如創建可供我們的服務層使用的 WOD。

在我們的示例中,我們沒有使用真正的數據庫,例如 MongoDB 或 PostgreSQL,因為我想更多地關注最佳實踐本身。因此,我們使用一個模擬 Database 的本地 JSON 文件。當然,這些邏輯可以輕松遷移到其他類型的數據庫上。

基本設置

現在我們已經準備好為我們的 API 創建一個基本設置。為了不讓事情過于復雜,我們將構建一個簡單但有條理的項目結構。

首先,讓我們創建包含所有必要文件和依賴項的整體文件夾結構。之后,我們將進行快速測試以檢查是否一切正常:

# Create project folder & navigate into it
mkdir crossfit-wod-api && cd crossfit-wod-api
# Create a src folder & navigate into it
mkdir src && cd src
# Create sub folders
mkdir controllers && mkdir services && mkdir database && mkdir routes
# Create an index file (entry point of our API)
touch index.js
# We're currently in the src folder, so we need to move one level up first 
cd ..

# Create package.json file
npm init -y

安裝基本設置的依賴項:

# Dev Dependencies 
npm i -D nodemon

# Dependencies
npm i express

在Text Editor 中打開項目并配置 Express:

// In src/index.js 
const express = require("express");

const app = express();
const PORT = process.env.PORT || 3000;

// For testing purposes
app.get("/", (req, res) => {
res.send("<h2>It's Working!</h2>");
});

app.listen(PORT, () => {
console.log(API is listening on port ${PORT}); });

在 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"
}
}

該腳本確保開發服務器在我們進行更改時進行自動重啟。

啟動開發服務器:

npm run dev

查看您的終端,應該有一條消息 “API is listening on port 3000” 。

在瀏覽器中訪問 localhost:3000。當一切都設置正確后,您應該會看到以下內容:

太棒了!現在我們已經準備好實施最佳實踐。

REST API 最佳實踐

是的!現在我們已經有一個非常基本的 Express 設置,我們可以使用以下最佳實踐來擴展我們的 API。

讓我們從基本的 CRUD 終端節點開始。之后,我們將使用每個最佳實踐來擴展 API。

版本控制

在編寫任何特定于 API 的代碼之前,我們應該了解版本控制。就像在其他應用程序中一樣,會有改進、新功能和類似的東西。因此,對 API 進行版本控制也很重要。

最大的優勢在于,我們能夠在不影響客戶端正常使用當前版本的情況下,同時在新版本上開發新功能或進行改進。

我們也不會強迫客戶立即使用新版本。他們可以使用當前版本,并在新版本穩定時自行遷移。

當前版本和新版本基本上是并行運行的,不會相互影響。

但是我們如何區分這些版本呢?一種好的做法是將 v1 或 v2 等路徑段添加到 URL 中。

// Version 1 
"/api/v1/workouts"

// Version 2
"/api/v2/workouts"

// ...

這就是我們向外界公開的內容,也是其他開發人員可以使用的內容。但是我們還需要構建我們自己的項目,以便區分每個版本。

在 Express API 中,有許多不同的方法可以處理版本控制。在我們的示例中,我們打算為 src 目錄中的每個版本創建一個名為 v1 的子文件夾。

mkdir src/v1

現在,我們將 routes 文件夾移動到新的 v1 目錄中。

# Get the path to your current directory (copy it) 
pwd

# Move "routes" into "v1" (insert the path from above into {pwd})
mv {pwd}/src/routes {pwd}/src/v1

新目錄 /src/v1/routes 將存儲版本 1 的所有路由。我們稍后會添加內容。但現在讓我們添加一個簡單的 index.js 文件來測試一下。

# In /src/v1/routes 
touch index.js

在里面,我們啟動了一個簡單的路由器。

// In src/v1/routes/index.js
const express = require("express");
const router = express.Router();

router.route("/").get((req, res) => {
res.send(<h2>Hello from ${req.baseUrl}</h2>); }); module.exports = router;

現在我們必須在 src/index.js 的根入口點內連接我們的 v1 路由器。

// In src/index.js
const express = require("express");
// *** ADD ***
const v1Router = require("./v1/routes");

const app = express();
const PORT = process.env.PORT || 3000;

// *** REMOVE ***
app.get("/", (req, res) => {
res.send("<h2>It's Working!</h2>");
});

// *** ADD ***
app.use("/api/v1", v1Router);

app.listen(PORT, () => {
console.log(API is listening on port ${PORT}); });

現在在瀏覽器中訪問 localhost:3000/api/v1,您應該會看到以下內容:

祝賀!您剛剛構建了用于處理不同版本的工程。現在,我們將帶有 “/api/v1” 的傳入請求傳遞給我們的版本 1 路由器,該路由器稍后會將每個請求路由到相應的控制器方法。

在我們繼續之前,我想指出一些事情。

我們剛剛將 routes 文件夾移動到了 v1 目錄中。其他文件夾(如 controllers 或 services)仍保留在我們的 src 目錄中。這目前沒有問題,因為我們正在構建一個相對較小的 API。我們可以在全球每個版本中使用相同的控制器和服務。

例如,當 API 增長并需要特定于 v2 的不同控制器方法時,最好將 controllers 文件夾移動到 v2 目錄中,以便封裝該特定版本的所有特定邏輯。

另一個原因可能是我們可能會更改所有其他版本使用的服務。我們不想破壞其他版本中的內容。因此,將 services 文件夾也移動到特定版本文件夾中將是一個明智的決定。

但正如我所說,在我們的示例中,我可以只區分路由,讓路由器處理其余部分。盡管如此,重要的是要記住這一點,以便在 API 擴展和需要更改時有一個清晰的結構。

以復數形式命名資源

設置完所有內容后,我們現在可以深入了解 API 的真正實現。就像我說的,我想從我們的基本 CRUD 終端節點開始。

換句話說,讓我們開始實現用于創建、讀取、更新和刪除鍛煉的終端節點。

首先,讓我們為我們的鍛煉連接一個特定的控制器、服務和路由器。

touch src/controllers/workoutController.js 

touch src/services/workoutService.js

touch src/v1/routes/workoutRoutes.js

讓我們考慮一下如何命名我們的終端節點。這對我們的最佳實踐至關重要。

我們可以將創建端點命名為 /api/v1/workout,因為我們想添加一個鍛煉,對吧?雖然這種方法沒有錯,但可能會導致誤解。

請記住:您的 API 由其他人使用,因此應該準確無誤。這也適用于命名您的資源。

我總是把資源想象成一個盒子。在我們的示例中,該框是存儲不同鍛煉的集合。

以復數形式命名您的資源有一個很大的好處,即其他人很清楚,這是一個由不同鍛煉組成的集合。

那么,讓我們在鍛煉路由器中定義我們的端點。

// In src/v1/routes/workoutRoutes.js
const express = require("express");
const router = express.Router();

router.get("/", (req, res) => {
res.send("Get all workouts");
});

router.get("/:workoutId", (req, res) => {
res.send("Get an existing workout");
});

router.post("/", (req, res) => {
res.send("Create a new workout");
});

router.patch("/:workoutId", (req, res) => {
res.send("Update an existing workout");
});

router.delete("/:workoutId", (req, res) => {
res.send("Delete an existing workout");
});

module.exports = router;

你可以刪除 src/v1/routes 中的測試文件index.js

現在讓我們跳到我們的入口點并連接我們的 v1 鍛煉路由器。

// In src/index.js
const express = require("express");
// *** REMOVE ***
const v1Router = require("./v1/routes");
// *** ADD ***
const v1WorkoutRouter = require("./v1/routes/workoutRoutes");

const app = express();
const PORT = process.env.PORT || 3000;

// *** REMOVE ***
app.use("/api/v1", v1Router);

// *** ADD ***
app.use("/api/v1/workouts", v1WorkoutRouter);

app.listen(PORT, () => {
console.log(API is listening on port ${PORT}); });

現在,我們使用 v1WorkoutRouter 捕獲所有發送到 /api/v1/workouts 的請求。

在我們的路由器中,我們將為每個不同的端點調用一個由控制器處理的不同方法。

讓我們為每個終端節點創建一個方法?,F在只發回一條消息應該沒問題。

// In src/controllers/workoutController.js
const getAllWorkouts = (req, res) => {
res.send("Get all workouts");
};

const getOneWorkout = (req, res) => {
res.send("Get an existing workout");
};

const createNewWorkout = (req, res) => {
res.send("Create a new workout");
};

const updateOneWorkout = (req, res) => {
res.send("Update an existing workout");
};

const deleteOneWorkout = (req, res) => {
res.send("Delete an existing workout");
};

module.exports = {
getAllWorkouts,
getOneWorkout,
createNewWorkout,
updateOneWorkout,
deleteOneWorkout,
};

現在是時候稍微重構我們的鍛煉路由器并使用控制器方法了。

// In 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/2342 來測試我們的 GET /api/v1/workouts/:workoutId 端點。您應該看到如下內容:

我們成功了!我們架構的第一層已經完成。讓我們通過實施下一個最佳實踐來創建我們的服務層。

接受并使用 JSON 格式的數據進行響應

與 API 交互時,您始終隨請求發送特定數據,或者隨響應接收數據。有許多不同的數據格式,但 JSON(Javascript 對象表示法)是一種標準化格式。

盡管 JSON 中有 JavaScript 一詞,但它并不僅限于 JavaScript。您還可以使用 Java、Python 等語言編寫 API,這些 API 同樣可以處理 JSON。

由于 JSON 的標準化特性,API 應該接受 JSON 格式的數據并做出相應的響應。

首先,創建我們的服務層。

// In 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,
};

最好將服務方法命名為與控制器方法相同的名稱,以便在它們之間建立連接。

在我們的鍛煉控制器中,我們可以使用這些方法。

// In src/controllers/workoutController.js
// *** ADD ***
const workoutService = require("../services/workoutService");

const getAllWorkouts = (req, res) => {
// *** ADD ***
const allWorkouts = workoutService.getAllWorkouts();
res.send("Get all workouts");
};

const getOneWorkout = (req, res) => {
// *** ADD ***
const workout = workoutService.getOneWorkout();
res.send("Get an existing workout");
};

const createNewWorkout = (req, res) => {
// *** ADD ***
const createdWorkout = workoutService.createNewWorkout();
res.send("Create a new workout");
};

const updateOneWorkout = (req, res) => {
// *** ADD ***
const updatedWorkout = workoutService.updateOneWorkout();
res.send("Update an existing workout");
};

const deleteOneWorkout = (req, res) => {
// *** ADD ***
workoutService.deleteOneWorkout();
res.send("Delete an existing workout");
};

module.exports = {
getAllWorkouts,
getOneWorkout,
createNewWorkout,
updateOneWorkout,
deleteOneWorkout,
};

目前,我們的回答應該沒有任何變化。但在后臺,我們的控制器層現在與服務層進行對話。

在我們的服務方法中,我們將處理業務邏輯,例如轉換數據結構和與數據庫層通信。

為此,我們需要一個數據庫和一組實際處理數據庫交互的方法。我們的數據庫將是一個預先填充了一些鍛煉數據的簡單 JSON 文件。

# Create a new file called db.json inside src/database 
touch src/database/db.json

# Create a Workout File that stores all workout specific methods in /src/database
touch src/database/Workout.js

將以下內容復制到 db.json:

{
"workouts": [
{
"id": "61dbae02-c147-4e28-863c-db7bd402b2d6",
"name": "Tommy V",
"mode": "For Time",
"equipment": [
"barbell",
"rope"
],
"exercises": [
"21 thrusters",
"12 rope climbs, 15 ft",
"15 thrusters",
"9 rope climbs, 15 ft",
"9 thrusters",
"6 rope climbs, 15 ft"
],
"createdAt": "4/20/2022, 2:21:56 PM",
"updatedAt": "4/20/2022, 2:21:56 PM",
"trainerTips": [
"Split the 21 thrusters as needed",
"Try to do the 9 and 6 thrusters unbroken",
"RX Weights: 115lb/75lb"
]
},
{
"id": "4a3d9aaa-608c-49a7-a004-66305ad4ab50",
"name": "Dead Push-Ups",
"mode": "AMRAP 10",
"equipment": [
"barbell"
],
"exercises": [
"15 deadlifts",
"15 hand-release push-ups"
],
"createdAt": "1/25/2022, 1:15:44 PM",
"updatedAt": "3/10/2022, 8:21:56 AM",
"trainerTips": [
"Deadlifts are meant to be light and fast",
"Try to aim for unbroken sets",
"RX Weights: 135lb/95lb"
]
},
{
"id": "d8be2362-7b68-4ea4-a1f6-03f8bc4eede7",
"name": "Heavy DT",
"mode": "5 Rounds For Time",
"equipment": [
"barbell",
"rope"
],
"exercises": [
"12 deadlifts",
"9 hang power cleans",
"6 push jerks"
],
"createdAt": "11/20/2021, 5:39:07 PM",
"updatedAt": "11/20/2021, 5:39:07 PM",
"trainerTips": [
"Aim for unbroken push jerks",
"The first three rounds might feel terrible, but stick to it",
"RX Weights: 205lb/145lb"
]
}
]
}

如您所見,插入了三個鍛煉。一個鍛煉包括 id、name、mode、equipment、exercises、createdAt、updatedAt 和 trainerTips。

讓我們從最簡單的開始,返回所有存儲的鍛煉,然后開始在我們的數據訪問層 (src/database/Workout.js) 中實現相應的方法。

同樣,我選擇將此處的方法命名為與服務和控制器中的方法相同的名稱,這完全是可選的。

// In src/database/Workout.js
const DB = require("./db.json");

const getAllWorkouts = () => {
return DB.workouts;
};

module.exports = { getAllWorkouts };

直接跳回到我們的鍛煉服務并實現 getAllWorkouts 的邏輯。

// In src/database/workoutService.js
// *** ADD ***
const Workout = require("../database/Workout");
const getAllWorkouts = () => {
// *** ADD ***
const allWorkouts = Workout.getAllWorkouts();
// *** ADD ***
return allWorkouts;
};

const getOneWorkout = () => {
return;
};

const createNewWorkout = () => {
return;
};

const updateOneWorkout = () => {
return;
};

const deleteOneWorkout = () => {
return;
};

module.exports = {
getAllWorkouts,
getOneWorkout,
createNewWorkout,
updateOneWorkout,
deleteOneWorkout,
};

返回所有鍛煉非常簡單,我們不必進行轉換,因為它已經是一個 JSON 文件。我們現在也不需要接受任何爭論,所以這個實現非常簡單。但我們稍后會回到這個問題。

回到我們的鍛煉控制器中,我們從中接收返回值,并將其作為響應發送給客戶端。我們已經通過我們的服務將數據庫響應循環到控制器。

// In src/controllers/workoutControllers.js
const workoutService = require("../services/workoutService");

const getAllWorkouts = (req, res) => {
const allWorkouts = workoutService.getAllWorkouts();
// *** ADD ***
res.send({ status: "OK", data: allWorkouts });
};

const getOneWorkout = (req, res) => {
const workout = workoutService.getOneWorkout();
res.send("Get an existing workout");
};

const createNewWorkout = (req, res) => {
const createdWorkout = workoutService.createNewWorkout();
res.send("Create a new workout");
};

const updateOneWorkout = (req, res) => {
const updatedWorkout = workoutService.updateOneWorkout();
res.send("Update an existing workout");
};

const deleteOneWorkout = (req, res) => {
workoutService.deleteOneWorkout();
res.send("Delete an existing workout");
};

module.exports = {
getAllWorkouts,
getOneWorkout,
createNewWorkout,
updateOneWorkout,
deleteOneWorkout,
};

在瀏覽器中轉到 localhost:3000/api/v1/workouts,您應該會看到響應 JSON。

太好了!我們將以 JSON 格式發回數據。但是如何接收數據呢?讓我們考慮一個終端節點,我們需要從客戶端接收 JSON 數據。用于創建或更新鍛煉的終端節點需要來自客戶端的數據。

在我們的鍛煉控制器中,我們提取請求正文中用于創建新鍛煉的數據,并將其傳遞給鍛煉服務。在鍛煉服務中,我們會將其插入到我們的 DB.json 中,并將新創建的鍛煉發送回給客戶。

為了能夠解析請求正文中發送的 JSON,我們需要先安裝 body-parser 并對其進行配置。

npm i body-parser
// In src/index.js 
const express = require("express");
// *** ADD ***
const bodyParser = require("body-parser");
const v1WorkoutRouter = require("./v1/routes/workoutRoutes");

const app = express();
const PORT = process.env.PORT || 3000;

// *** ADD ***
app.use(bodyParser.json());
app.use("/api/v1/workouts", v1WorkoutRouter);

app.listen(PORT, () => {
console.log(API is listening on port ${PORT}); });

現在我們能夠在控制器中的 req.body 下接收 JSON 數據。

為了正確測試它,只需打開您最喜歡的 HTTP 客戶端(我正在使用 Postman),創建對 localhost:3000/api/v1/workouts 的 POST 請求和JSON格式的請求正文,如下所示:

{
"name": "Core Buster",
"mode": "AMRAP 20",
"equipment": [
"rack",
"barbell",
"abmat"
],
"exercises": [
"15 toes to bars",
"10 thrusters",
"30 abmat sit-ups"
],
"trainerTips": [
"Split your toes to bars into two sets maximum",
"Go unbroken on the thrusters",
"Take the abmat sit-ups as a chance to normalize your breath"
]
}

你可能已經注意到了,缺少一些屬性,比如 “id”、“createdAt” 和 “updatedAt”。這就是我們的 API 的工作,即在插入之前添加這些屬性。我們稍后會在我們的鍛煉服務中處理它。

在鍛煉控制器的 createNewWorkout 方法中,我們可以從 request 對象中提取 body,進行一些驗證,并將其作為參數傳遞給我們的鍛煉服務。

// In src/controllers/workoutController.js
...

const createNewWorkout = (req, res) => {
const { body } = req;
// *** ADD ***
if (
!body.name ||
!body.mode ||
!body.equipment ||
!body.exercises ||
!body.trainerTips
) {
return;
}
// *** ADD ***
const newWorkout = {
name: body.name,
mode: body.mode,
equipment: body.equipment,
exercises: body.exercises,
trainerTips: body.trainerTips,
};
// *** ADD ***
const createdWorkout = workoutService.createNewWorkout(newWorkout);
// *** ADD ***
res.status(201).send({ status: "OK", data: createdWorkout });
};

...

為了改進請求驗證,您通常會使用第三方包,例如 express-validator。

讓我們進入我們的鍛煉服務,并在 createNewWorkout 方法中接收數據。

之后,我們將缺少的屬性添加到對象中,并將其傳遞給數據訪問層中的新方法,以將其存儲在數據庫中。

首先,我們創建一個簡單的 Util 函數來覆蓋我們的 JSON 文件以保留數據。

# Create a utils file inside our database directory 
touch src/database/utils.js
// In src/database/utils.js
const fs = require("fs");

const saveToDatabase = (DB) => {
fs.writeFileSync("./src/database/db.json", JSON.stringify(DB, null, 2), {
encoding: "utf-8",
});
};

module.exports = { saveToDatabase };

然后我們可以在我們的 Workout.js 文件中使用這個函數。

// In src/database/Workout.js
const DB = require("./db.json");
// *** ADD ***
const { saveToDatabase } = require("./utils");

const getAllWorkouts = () => {
return DB.workouts;
};

// *** ADD ***
const createNewWorkout = (newWorkout) => {
const isAlreadyAdded =
DB.workouts.findIndex((workout) => workout.name === newWorkout.name) > -1;
if (isAlreadyAdded) {
return;
}
DB.workouts.push(newWorkout);
saveToDatabase(DB);
return newWorkout;
};

module.exports = {
getAllWorkouts,
// *** ADD ***
createNewWorkout,
};

下一步是使用我們 workout 服務中的數據庫方法。

# Install the uuid package 
npm i uuid
// In src/services/workoutService.js
// *** ADD ***
const { v4: uuid } = require("uuid");
// *** ADD ***
const Workout = require("../database/Workout");

const getAllWorkouts = () => {
const allWorkouts = Workout.getAllWorkouts();
return allWorkouts;
};

const getOneWorkout = () => {
return;
};

const createNewWorkout = (newWorkout) => {
// *** ADD ***
const workoutToInsert = {
...newWorkout,
id: uuid(),
createdAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
};
// *** ADD ***
const createdWorkout = Workout.createNewWorkout(workoutToInsert);
return createdWorkout;
};

const updateOneWorkout = () => {
return;
};

const deleteOneWorkout = () => {
return;
};

module.exports = {
getAllWorkouts,
getOneWorkout,
createNewWorkout,
updateOneWorkout,
deleteOneWorkout,
};

現在,您可以轉到 HTTP 客戶端,再次發送 POST 請求,您應該會收到一個以JSON格式返回的新創建的鍛煉信息。

如果您再次嘗試添加相同的鍛煉,您仍會收到 201 狀態代碼,但不會收到新插入的鍛煉。

這意味著我們的 database 方法暫時取消了插入,并且只返回任何內容。這是因為我們的數據庫方法暫時取消了插入操作,并且只返回了現有內容。這是由于我們的if語句用于檢查是否已經存在同名的鍛煉。接下來,我們將在下一個最佳實踐中處理這種情況。

現在,請向localhost:3000/api/v1/workouts發送GET請求以讀取所有鍛煉信息。我選擇使用瀏覽器進行此操作。您應該會看到我們的鍛煉已成功插入并持久存在。

您可以自己實現其他方法,也可以直接復制。

首先,鍛煉控制器(您可以復制整個內容):

// In src/controllers/workoutController.js
const workoutService = require("../services/workoutService");

const getAllWorkouts = (req, res) => {
const allWorkouts = workoutService.getAllWorkouts();
res.send({ status: "OK", data: allWorkouts });
};

const getOneWorkout = (req, res) => {
const {
params: { workoutId },
} = req;
if (!workoutId) {
return;
}
const workout = workoutService.getOneWorkout(workoutId);
res.send({ status: "OK", data: workout });
};

const createNewWorkout = (req, res) => {
const { body } = req;
if (
!body.name ||
!body.mode ||
!body.equipment ||
!body.exercises ||
!body.trainerTips
) {
return;
}
const newWorkout = {
name: body.name,
mode: body.mode,
equipment: body.equipment,
exercises: body.exercises,
trainerTips: body.trainerTips,
};
const createdWorkout = workoutService.createNewWorkout(newWorkout);
res.status(201).send({ status: "OK", data: createdWorkout });
};

const updateOneWorkout = (req, res) => {
const {
body,
params: { workoutId },
} = req;
if (!workoutId) {
return;
}
const updatedWorkout = workoutService.updateOneWorkout(workoutId, body);
res.send({ status: "OK", data: updatedWorkout });
};

const deleteOneWorkout = (req, res) => {
const {
params: { workoutId },
} = req;
if (!workoutId) {
return;
}
workoutService.deleteOneWorkout(workoutId);
res.status(204).send({ status: "OK" });
};

module.exports = {
getAllWorkouts,
getOneWorkout,
createNewWorkout,
updateOneWorkout,
deleteOneWorkout,
};

然后,鍛煉服務(您可以復制整個內容):

// In src/services/workoutServices.js
const { v4: uuid } = require("uuid");
const Workout = require("../database/Workout");

const getAllWorkouts = () => {
const allWorkouts = Workout.getAllWorkouts();
return allWorkouts;
};

const getOneWorkout = (workoutId) => {
const workout = Workout.getOneWorkout(workoutId);
return workout;
};

const createNewWorkout = (newWorkout) => {
const workoutToInsert = {
...newWorkout,
id: uuid(),
createdAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
};
const createdWorkout = Workout.createNewWorkout(workoutToInsert);
return createdWorkout;
};

const updateOneWorkout = (workoutId, changes) => {
const updatedWorkout = Workout.updateOneWorkout(workoutId, changes);
return updatedWorkout;
};

const deleteOneWorkout = (workoutId) => {
Workout.deleteOneWorkout(workoutId);
};

module.exports = {
getAllWorkouts,
getOneWorkout,
createNewWorkout,
updateOneWorkout,
deleteOneWorkout,
};

最后是我們在數據訪問層中的數據庫方法(你可以復制整個內容):

// In src/database/Workout.js
const DB = require("./db.json");
const { saveToDatabase } = require("./utils");

const getAllWorkouts = () => {
return DB.workouts;
};

const getOneWorkout = (workoutId) => {
const workout = DB.workouts.find((workout) => workout.id === workoutId);
if (!workout) {
return;
}
return workout;
};

const createNewWorkout = (newWorkout) => {
const isAlreadyAdded =
DB.workouts.findIndex((workout) => workout.name === newWorkout.name) > -1;
if (isAlreadyAdded) {
return;
}
DB.workouts.push(newWorkout);
saveToDatabase(DB);
return newWorkout;
};

const updateOneWorkout = (workoutId, changes) => {
const indexForUpdate = DB.workouts.findIndex(
(workout) => workout.id === workoutId
);
if (indexForUpdate === -1) {
return;
}
const updatedWorkout = {
...DB.workouts[indexForUpdate],
...changes,
updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
};
DB.workouts[indexForUpdate] = updatedWorkout;
saveToDatabase(DB);
return updatedWorkout;
};

const deleteOneWorkout = (workoutId) => {
const indexForDeletion = DB.workouts.findIndex(
(workout) => workout.id === workoutId
);
if (indexForDeletion === -1) {
return;
}
DB.workouts.splice(indexForDeletion, 1);
saveToDatabase(DB);
};

module.exports = {
getAllWorkouts,
createNewWorkout,
getOneWorkout,
updateOneWorkout,
deleteOneWorkout,
};

讓我們繼續下一個最佳實踐,看看我們如何正確處理錯誤。

使用標準 HTTP 錯誤代碼進行響應

我們已經取得了很大的進展,但我們的任務還沒有完成。我們的 API 已經能夠處理基本的 CRUD 操作,這很好,但還有改進的空間。

為什么?讓我解釋一下。

在一個完美的世界里,一切都很順利,沒有任何錯誤。然而,在現實世界中,無論是人為因素還是技術問題,都可能導致錯誤發生。

當一切順利時,你可能會感到奇怪。這確實很棒,也很有趣,但作為開發人員,我們更習慣于面對問題和錯誤。??

我們的 API 也是如此。我們應該處理某些可能出錯或引發錯誤的情況。這也將強化我們的 API。

當出現問題時(來自請求或我們的 API 內部),我們會發回 HTTP 錯誤代碼。我見過并使用過 API,當請求有問題時,它們總是返回 400 錯誤代碼,而沒有任何關于此錯誤發生原因或錯誤是什么的具體消息。這使得調試變得非常困難。

因此,針對不同情況返回正確的 HTTP 錯誤代碼始終是一個好習慣。這有助于使用者或構建 API 的工程師更輕松地識別問題。

為了改善用戶體驗,我們還可以將簡短的錯誤消息與錯誤響應一起發送。然而,正如我在引言中提到的那樣,這并不總是明智的做法,工程師應該自己權衡利弊。

例如,返回類似 “The username is already signup” 的內容應該經過深思熟慮,因為提供有關用戶的信息實際上可能涉及隱私問題。

在我們的 Crossfit API 中,我們將查看創建端點,看看可能會出現哪些錯誤以及我們如何處理它們。在本提示的末尾,您將再次找到其他終端節點的完整實現。

讓我們開始看看鍛煉控制器中的 createNewWorkout 方法:

// In src/controllers/workoutController.js
...

const createNewWorkout = (req, res) => {
const { body } = req;
if (
!body.name ||
!body.mode ||
!body.equipment ||
!body.exercises ||
!body.trainerTips
) {
return;
}
const newWorkout = {
name: body.name,
mode: body.mode,
equipment: body.equipment,
exercises: body.exercises,
trainerTips: body.trainerTips,
};
const createdWorkout = workoutService.createNewWorkout(newWorkout);
res.status(201).send({ status: "OK", data: createdWorkout });
};

...

我們已經捕獲到請求正文未正確構建并丟失我們期望的鍵的情況。

這是一個很好的示例,可以發回 400 HTTP 錯誤并帶有相應的錯誤消息。

// In src/controllers/workoutController.js
...

const createNewWorkout = (req, res) => {
const { body } = req;
if (
!body.name ||
!body.mode ||
!body.equipment ||
!body.exercises ||
!body.trainerTips
) {
res
.status(400)
.send({
status: "FAILED",
data: {
error:
"One of the following keys is missing or is empty in request body: 'name', 'mode', 'equipment', 'exercises', 'trainerTips'",
},
});
return;
}
const newWorkout = {
name: body.name,
mode: body.mode,
equipment: body.equipment,
exercises: body.exercises,
trainerTips: body.trainerTips,
};
const createdWorkout = workoutService.createNewWorkout(newWorkout);
res.status(201).send({ status: "OK", data: createdWorkout });
};

...

如果我們嘗試添加新的鍛煉,但忘記在請求正文中提供 “mode” 屬性,我們應該會看到錯誤消息以及 400 HTTP 錯誤代碼。

現在,使用 API 的開發人員可以更好地理解他們需要查找的內容。他們立刻明白要檢查請求正文,看看是否遺漏了某個必需的屬性。

現在,對于所有屬性來說,返回通用的錯誤消息是可行的。通常,你會使用 schema 驗證器來處理這個問題。

讓我們更深入地了解我們的鍛煉服務,看看可能發生哪些潛在錯誤。

// In src/services/workoutService.js
...

const createNewWorkout = (newWorkout) => {
const workoutToInsert = {
...newWorkout,
id: uuid(),
createdAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
};
const createdWorkout = Workout.createNewWorkout(workoutToInsert);
return createdWorkout;
};

...

可能出錯的一件事是數據庫插入 Workout.createNewWorkout()。我喜歡將這個東西包裝在 try/catch 塊中,以便在錯誤發生時捕獲它。

// In src/services/workoutService.js
...

const createNewWorkout = (newWorkout) => {
const workoutToInsert = {
...newWorkout,
id: uuid(),
createdAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
};
try {
const createdWorkout = Workout.createNewWorkout(workoutToInsert);
return createdWorkout;
} catch (error) {
throw error;
}
};

...

在我們的 Workout.createNewWorkout() 方法中拋出的每個錯誤都會被捕獲在我們的 catch 塊中。我們只是將其扔回去,以便我們稍后可以在控制器中調整我們的響應。

讓我們在 Workout.js 中定義我們的錯誤:

// In src/database/Workout.js
...

const createNewWorkout = (newWorkout) => {
const isAlreadyAdded =
DB.workouts.findIndex((workout) => workout.name === newWorkout.name) > -1;
if (isAlreadyAdded) {
throw {
status: 400,
message: Workout with the name '${newWorkout.name}' already exists, }; } try { DB.workouts.push(newWorkout); saveToDatabase(DB); return newWorkout; } catch (error) { throw { status: 500, message: error?.message || error }; } }; ...

如您所見,錯誤由兩部分組成:狀態和消息。我在這里只使用 throw 關鍵字來發送與字符串不同的數據結構,這在 throw new Error() 中是必需的。

僅 throwing 的一個缺點使我們無法獲得堆棧跟蹤。但通常,此錯誤引發將由我們選擇的第三方庫(例如,如果您使用 MongoDB 數據庫,則為 Mongoose)處理。但對于本教程的目的,這應該沒問題。

現在,我們能夠在服務和數據訪問層中引發和捕獲錯誤。接下來,我們將進入鍛煉控制器,并捕獲其中的錯誤,然后做出相應的響應。

// In src/controllers/workoutController.js
...

const createNewWorkout = (req, res) => {
const { body } = req;
if (
!body.name ||
!body.mode ||
!body.equipment ||
!body.exercises ||
!body.trainerTips
) {
res
.status(400)
.send({
status: "FAILED",
data: {
error:
"One of the following keys is missing or is empty in request body: 'name', 'mode', 'equipment', 'exercises', 'trainerTips'",
},
});
return;
}
const newWorkout = {
name: body.name,
mode: body.mode,
equipment: body.equipment,
exercises: body.exercises,
trainerTips: body.trainerTips,
};
// *** ADD ***
try {
const createdWorkout = workoutService.createNewWorkout(newWorkout);
res.status(201).send({ status: "OK", data: createdWorkout });
} catch (error) {
res
.status(error?.status || 500)
.send({ status: "FAILED", data: { error: error?.message || error } });
}
};

...

您可以通過添加兩次具有相同名稱的鍛煉或不在請求正文中提供 required 屬性來測試內容。您應該會收到相應的 HTTP 錯誤代碼以及錯誤消息。

要總結此內容并轉到下一個提示,您可以將其他實現的方法復制到以下文件中,也可以自行嘗試:

// In src/controllers/workoutController.js
const workoutService = require("../services/workoutService");

const getAllWorkouts = (req, res) => {
try {
const allWorkouts = workoutService.getAllWorkouts();
res.send({ status: "OK", data: allWorkouts });
} catch (error) {
res
.status(error?.status || 500)
.send({ status: "FAILED", data: { error: error?.message || error } });
}
};

const getOneWorkout = (req, res) => {
const {
params: { workoutId },
} = req;
if (!workoutId) {
res
.status(400)
.send({
status: "FAILED",
data: { error: "Parameter ':workoutId' can not be empty" },
});
}
try {
const workout = workoutService.getOneWorkout(workoutId);
res.send({ status: "OK", data: workout });
} catch (error) {
res
.status(error?.status || 500)
.send({ status: "FAILED", data: { error: error?.message || error } });
}
};

const createNewWorkout = (req, res) => {
const { body } = req;
if (
!body.name ||
!body.mode ||
!body.equipment ||
!body.exercises ||
!body.trainerTips
) {
res
.status(400)
.send({
status: "FAILED",
data: {
error:
"One of the following keys is missing or is empty in request body: 'name', 'mode', 'equipment', 'exercises', 'trainerTips'",
},
});
return;
}
const newWorkout = {
name: body.name,
mode: body.mode,
equipment: body.equipment,
exercises: body.exercises,
trainerTips: body.trainerTips,
};
try {
const createdWorkout = workoutService.createNewWorkout(newWorkout);
res.status(201).send({ status: "OK", data: createdWorkout });
} catch (error) {
res
.status(error?.status || 500)
.send({ status: "FAILED", data: { error: error?.message || error } });
}
};

const updateOneWorkout = (req, res) => {
const {
body,
params: { workoutId },
} = req;
if (!workoutId) {
res
.status(400)
.send({
status: "FAILED",
data: { error: "Parameter ':workoutId' can not be empty" },
});
}
try {
const updatedWorkout = workoutService.updateOneWorkout(workoutId, body);
res.send({ status: "OK", data: updatedWorkout });
} catch (error) {
res
.status(error?.status || 500)
.send({ status: "FAILED", data: { error: error?.message || error } });
}
};

const deleteOneWorkout = (req, res) => {
const {
params: { workoutId },
} = req;
if (!workoutId) {
res
.status(400)
.send({
status: "FAILED",
data: { error: "Parameter ':workoutId' can not be empty" },
});
}
try {
workoutService.deleteOneWorkout(workoutId);
res.status(204).send({ status: "OK" });
} catch (error) {
res
.status(error?.status || 500)
.send({ status: "FAILED", data: { error: error?.message || error } });
}
};

module.exports = {
getAllWorkouts,
getOneWorkout,
createNewWorkout,
updateOneWorkout,
deleteOneWorkout,
getRecordsForWorkout,
};
// In 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 getOneWorkout = (workoutId) => {
try {
const workout = Workout.getOneWorkout(workoutId);
return workout;
} catch (error) {
throw error;
}
};

const createNewWorkout = (newWorkout) => {
const workoutToInsert = {
...newWorkout,
id: uuid(),
createdAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }),
};
try {
const createdWorkout = Workout.createNewWorkout(workoutToInsert);
return createdWorkout;
} catch (error) {
throw error;
}
};

const updateOneWorkout = (workoutId, changes) => {
try {
const updatedWorkout = Workout.updateOneWorkout(workoutId, changes);
return updatedWorkout;
} catch (error) {
throw error;
}
};

const deleteOneWorkout = (workoutId) => {
try {
Workout.deleteOneWorkout(workoutId);
} catch (error) {
throw error;
}
};

module.exports = {
getAllWorkouts,
getOneWorkout,
createNewWorkout,
updateOneWorkout,
deleteOneWorkout,
};
// In 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 };
}
};

const getOneWorkout = (workoutId) => {
try {
const workout = DB.workouts.find((workout) => workout.id === workoutId);
if (!workout) {
throw {
status: 400,
message: Can't find workout with the id '${workoutId}', }; } return workout; } catch (error) { throw { status: error?.status || 500, message: error?.message || error }; } }; const createNewWorkout = (newWorkout) => { try { const isAlreadyAdded = DB.workouts.findIndex((workout) => workout.name === newWorkout.name) > -1; if (isAlreadyAdded) { throw { status: 400, message: Workout with the name '${newWorkout.name}' already exists, }; } DB.workouts.push(newWorkout); saveToDatabase(DB); return newWorkout; } catch (error) { throw { status: error?.status || 500, message: error?.message || error }; } }; const updateOneWorkout = (workoutId, changes) => { try { const isAlreadyAdded = DB.workouts.findIndex((workout) => workout.name === changes.name) > -1; if (isAlreadyAdded) { throw { status: 400, message: Workout with the name '${changes.name}' already exists, }; } const indexForUpdate = DB.workouts.findIndex( (workout) => workout.id === workoutId ); if (indexForUpdate === -1) { throw { status: 400, message: Can't find workout with the id '${workoutId}', }; } const updatedWorkout = { ...DB.workouts[indexForUpdate], ...changes, updatedAt: new Date().toLocaleString("en-US", { timeZone: "UTC" }), }; DB.workouts[indexForUpdate] = updatedWorkout; saveToDatabase(DB); return updatedWorkout; } catch (error) { throw { status: error?.status || 500, message: error?.message || error }; } }; const deleteOneWorkout = (workoutId) => { try { const indexForDeletion = DB.workouts.findIndex( (workout) => workout.id === workoutId ); if (indexForDeletion === -1) { throw { status: 400, message: Can't find workout with the id '${workoutId}', }; } DB.workouts.splice(indexForDeletion, 1); saveToDatabase(DB); } catch (error) { throw { status: error?.status || 500, message: error?.message || error }; } }; module.exports = { getAllWorkouts, createNewWorkout, getOneWorkout, updateOneWorkout, deleteOneWorkout, };

避免在終端節點名稱中使用動詞

在端點內使用動詞沒有多大意義,事實上,它毫無用處。通常,每個 URL 都應該指向一個資源(記住上面的 box 示例)。

在 URL 中使用動詞表示資源本身不能具有的特定行為。

我們已經成功地實現了端點,而沒有在 URL 中使用動詞?,F在讓我們看看如果我們使用動詞,URL 會是什么樣子。

// Current implementations (without verbs)
GET "/api/v1/workouts"
GET "/api/v1/workouts/:workoutId"
POST "/api/v1/workouts"
PATCH "/api/v1/workouts/:workoutId"
DELETE "/api/v1/workouts/:workoutId"

// Implementation using verbs
GET "/api/v1/getAllWorkouts"
GET "/api/v1/getWorkoutById/:workoutId"
CREATE "/api/v1/createWorkout"
PATCH "/api/v1/updateWorkout/:workoutId"
DELETE "/api/v1/deleteWorkout/:workoutId"

您是否注意到了區別?為每種行為提供完全不同的 URL 可能會很快變得令人困惑和不必要的復雜。

假設我們有 300 個不同的終端節點。為每個 URL 使用單獨的 URL 可能是一個開銷(和文檔)地獄。

我想指出的另一個原因是,在 URL 中不使用動詞的原因是 HTTP 動詞本身已經表示了操作。

像 “GET /api/v1/getAllWorkouts” 或 “DELETE api/v1/deleteWorkout/workoutId” 這樣的內容是不必要的。

當你看一下我們當前的實現時,它變得更加清晰,因為我們只使用了兩個不同的 URL,并且實際行為是通過 HTTP 動詞和相應的請求有效負載來處理的。

我總是想象 HTTP 動詞描述操作(我們想要做什么),而 URL 本身(指向資源)是目標。“GET /api/v1/workouts” 的人類語言也更流利。

將關聯的資源分組在一起(邏輯嵌套)

在設計 API 時,可能會出現您擁有與其他資源關聯的資源的情況。最好將它們分組到一個終端節點中并正確嵌套它們。

讓我們考慮一下,在我們的 API 中,我們還有一個在 CrossFit 框中注冊的會員列表(“box”是 CrossFit 健身房的名稱)。為了激勵我們的會員,我們會跟蹤每次鍛煉的總體記錄。

例如,有一項鍛煉,要求會員盡快按照一定順序完成一系列動作。我們記錄所有成員的時間,以列出每個完成此鍛煉的成員的時間。

現在,前端需要一個終端節點,該節點能夠響應特定鍛煉的所有記錄,以便在用戶界面中顯示這些數據。

鍛煉、成員和記錄存儲在數據庫中的不同位置。所以我們需要的是另一個盒子(鍛煉)里面的盒子(記錄),對吧?

該終端節點的 URI 將為 /api/v1/workouts/:workoutId/records。這是允許 URL 的邏輯嵌套的好做法。URL 本身不一定必須鏡像數據庫結構。

讓我們開始實現該端點。

首先,將一個名為 “members” 的新表添加到您的db.json中。將其放在 “workouts” 下。

{
"workouts": [ ...
],
"members": [
{
"id": "12a410bc-849f-4e7e-bfc8-4ef283ee4b19",
"name": "Jason Miller",
"gender": "male",
"dateOfBirth": "23/04/1990",
"email": "jason@mail.com",
"password": "666349420ec497c1dc890c45179d44fb13220239325172af02d1fb6635922956"
},
{
"id": "2b9130d4-47a7-4085-800e-0144f6a46059",
"name": "Tiffany Brookston",
"gender": "female",
"dateOfBirth": "09/06/1996",
"email": "tiffy@mail.com",
"password": "8a1ea5669b749354110dcba3fac5546c16e6d0f73a37f35a84f6b0d7b3c22fcc"
},
{
"id": "11817fb1-03a1-4b4a-8d27-854ac893cf41",
"name": "Catrin Stevenson",
"gender": "female",
"dateOfBirth": "17/08/2001",
"email": "catrin@mail.com",
"password": "18eb2d6c5373c94c6d5d707650d02c3c06f33fac557c9cfb8cb1ee625a649ff3"
},
{
"id": "6a89217b-7c28-4219-bd7f-af119c314159",
"name": "Greg Bronson",
"gender": "male",
"dateOfBirth": "08/04/1993",
"email": "greg@mail.com",
"password": "a6dcde7eceb689142f21a1e30b5fdb868ec4cd25d5537d67ac7e8c7816b0e862"
}
]
}

在您開始詢問之前 – 是的,密碼已經過哈希處理。??

之后,在 “members” 下添加一些 “records”。

{
"workouts": [ ...
],
"members": [ ...
],
"records": [
{
"id": "ad75d475-ac57-44f4-a02a-8f6def58ff56",
"workout": "4a3d9aaa-608c-49a7-a004-66305ad4ab50",
"record": "160 reps"
},
{
"id": "0bff586f-2017-4526-9e52-fe3ea46d55ab",
"workout": "d8be2362-7b68-4ea4-a1f6-03f8bc4eede7",
"record": "7:23 minutes"
},
{
"id": "365cc0bb-ba8f-41d3-bf82-83d041d38b82",
"workout": "a24d2618-01d1-4682-9288-8de1343e53c7",
"record": "358 reps"
},
{
"id": "62251cfe-fdb6-4fa6-9a2d-c21be93ac78d",
"workout": "4a3d9aaa-608c-49a7-a004-66305ad4ab50",
"record": "145 reps"
}
],
}

為了確保您獲得與我使用相同的 ID 相同的鍛煉,請同時復制鍛煉:

{
"workouts": [
{
"id": "61dbae02-c147-4e28-863c-db7bd402b2d6",
"name": "Tommy V",
"mode": "For Time",
"equipment": [
"barbell",
"rope"
],
"exercises": [
"21 thrusters",
"12 rope climbs, 15 ft",
"15 thrusters",
"9 rope climbs, 15 ft",
"9 thrusters",
"6 rope climbs, 15 ft"
],
"createdAt": "4/20/2022, 2:21:56 PM",
"updatedAt": "4/20/2022, 2:21:56 PM",
"trainerTips": [
"Split the 21 thrusters as needed",
"Try to do the 9 and 6 thrusters unbroken",
"RX Weights: 115lb/75lb"
]
},
{
"id": "4a3d9aaa-608c-49a7-a004-66305ad4ab50",
"name": "Dead Push-Ups",
"mode": "AMRAP 10",
"equipment": [
"barbell"
],
"exercises": [
"15 deadlifts",
"15 hand-release push-ups"
],
"createdAt": "1/25/2022, 1:15:44 PM",
"updatedAt": "3/10/2022, 8:21:56 AM",
"trainerTips": [
"Deadlifts are meant to be light and fast",
"Try to aim for unbroken sets",
"RX Weights: 135lb/95lb"
]
},
{
"id": "d8be2362-7b68-4ea4-a1f6-03f8bc4eede7",
"name": "Heavy DT",
"mode": "5 Rounds For Time",
"equipment": [
"barbell",
"rope"
],
"exercises": [
"12 deadlifts",
"9 hang power cleans",
"6 push jerks"
],
"createdAt": "11/20/2021, 5:39:07 PM",
"updatedAt": "4/22/2022, 5:49:18 PM",
"trainerTips": [
"Aim for unbroken push jerks",
"The first three rounds might feel terrible, but stick to it",
"RX Weights: 205lb/145lb"
]
},
{
"name": "Core Buster",
"mode": "AMRAP 20",
"equipment": [
"rack",
"barbell",
"abmat"
],
"exercises": [
"15 toes to bars",
"10 thrusters",
"30 abmat sit-ups"
],
"trainerTips": [
"Split your toes to bars in two sets maximum",
"Go unbroken on the thrusters",
"Take the abmat sit-ups as a chance to normalize your breath"
],
"id": "a24d2618-01d1-4682-9288-8de1343e53c7",
"createdAt": "4/22/2022, 5:50:17 PM",
"updatedAt": "4/22/2022, 5:50:17 PM"
}
],
"members": [ ...
],
"records": [ ...
]
}

好的,讓我們花幾分鐘時間考慮一下我們的實現。

我們一邊是名為 “workouts” 的資源,另一邊是 “records” 的資源。

為了繼續完善我們的架構,建議新增一個控制器、一個服務,以及一套負責處理記錄的數據庫方法。

考慮到未來可能需要添加、更新或刪除記錄,實現記錄的 CRUD 端點也是很有可能必要的,但這還不是目前的主要任務。

此外,我們還需要創建一個記錄路由器來捕獲針對記錄的特定請求,盡管現在可能還用不到它。這可以是一個利用自定義路由實現記錄的 CRUD 操作并進行一些實踐的好機會。

# Create records controller 
touch src/controllers/recordController.js

# Create records service
touch src/services/recordService.js

# Create records database methods
touch src/database/Record.js

這很容易。讓我們繼續前進,從實現我們的數據庫方法開始。

// In src/database/Record.js
const DB = require("./db.json");

const getRecordForWorkout = (workoutId) => {
try {
const record = DB.records.filter((record) => record.workout === workoutId);
if (!record) {
throw {
status: 400,
message: Can't find workout with the id '${workoutId}', }; } return record; } catch (error) { throw { status: error?.status || 500, message: error?.message || error }; } }; module.exports = { getRecordForWorkout };

很簡單,對吧?我們從 query 參數中篩選出與鍛煉 ID 相關的所有記錄。

下一個是我們的記錄服務:

// In src/services/recordService.js
const Record = require("../database/Record");

const getRecordForWorkout = (workoutId) => {
try {
const record = Record.getRecordForWorkout(workoutId);
return record;
} catch (error) {
throw error;
}
};
module.exports = { getRecordForWorkout };

現在,我們可以在鍛煉路由器中創建新路線,并將請求定向到我們的記錄服務。

// In src/v1/routes/workoutRoutes.js
const express = require("express");
const workoutController = require("../../controllers/workoutController");
// *** ADD ***
const recordController = require("../../controllers/recordController");

const router = express.Router();

router.get("/", workoutController.getAllWorkouts);

router.get("/:workoutId", workoutController.getOneWorkout);

// *** ADD ***
router.get("/:workoutId/records", recordController.getRecordForWorkout);

router.post("/", workoutController.createNewWorkout);

router.patch("/:workoutId", workoutController.updateOneWorkout);

router.delete("/:workoutId", workoutController.deleteOneWorkout);

module.exports = router;

讓我們在瀏覽器中測試一下。

首先,我們獲取所有鍛煉以獲取鍛煉 ID。

讓我們看看是否可以獲取該記錄的所有記錄:

如您所見,當您擁有可以組合在一起的資源時,邏輯嵌套是有意義的。理論上,您可以根據需要將其嵌套任意深度,但根據經驗,你最多可以嵌套三層。

如果您希望進一步嵌套,可以在數據庫記錄中進行一些調整。讓我給您展示一個簡單的例子。

想象一下,前端還需要一個終端節點來獲取有關哪個成員確切持有當前記錄并希望接收有關它們的元數據的信息。

當然,我們可以實現以下 URI:

GET /api/v1/workouts/:workoutId/records/members/:memberId

現在,我們向終端節點添加的嵌套越多,它就越難管理。因此,最好將 URI 存儲起來,以便將有關成員的信息直接接收到記錄中。

請考慮數據庫中的以下內容:

{
"workouts": [ ...
],
"members": [ ...
],
"records": [ ... {
"id": "ad75d475-ac57-44f4-a02a-8f6def58ff56",
"workout": "4a3d9aaa-608c-49a7-a004-66305ad4ab50",
"record": "160 reps",
"memberId": "11817fb1-03a1-4b4a-8d27-854ac893cf41",
"member": "/members/:memberId"
},
]
}

如您所見,我們已經在數據庫中的記錄里添加了兩個屬性:“memberId”和“member”。這樣做有一個顯著的優勢:我們無需進一步嵌套現有的終端節點。

前端只需要調用 GET /api/v1/workouts/:workoutId/records 即可自動接收與該 Workout 相關的所有記錄。

最重要的是,它獲取成員 ID 和終端節點以獲取有關該成員的信息。因此,我們避免了端點的更深嵌套。

當然,這只有在我們可以處理對 “/members/:memberId” 的請求時才有效。??這聽起來像是你實施這種情況的一個很好的培訓機會!

集成過濾、排序和分頁

目前,我們的API已經能夠執行許多操作,這無疑是一個巨大的進步。然而,我們還有更多工作要做。

在最后幾節中,我們專注于改善開發人員體驗以及如何與我們的 API 進行交互。但是,我們 API 的整體性能是我們應該努力的另一個關鍵因素。

因此,集成過濾、排序和分頁功能也是我計劃中的一個重要部分。

想象一下,我們的數據庫中存儲了 2,000 次鍛煉、450 條記錄和 500 個成員。在調用我們的終端節點以獲取所有鍛煉時,我們不希望一次發送所有 2,000 個鍛煉。當然,這將是一個非常緩慢的響應,或者它會使我們的系統宕機(可能有 200,000 ?? )。

過濾和分頁在我們的API中非常重要。過濾允許我們從整個集合中獲取特定數據,例如,所有具有 “For Time” 模式的鍛煉。

而分頁是另一種將我們的整個鍛煉集合拆分為多個“頁面”的機制,例如,每個頁面僅包含 20 個鍛煉。這種技術可以幫助我們確保我們不會在回復客戶時同時發送超過 20 個鍛煉。

排序也是一個復雜的任務,但在API中執行此操作并將排序后的數據發送到客戶端會更加高效。

讓我們從集成一些過濾機制開始,通過接受filter參數來升級我們的API端點。通常,在 GET 請求中,我們將篩選條件添加為查詢參數。

當我們只想獲取處于“AMRAP”模式的鍛煉時,我們的新 URI 將如下所示 (AMany Rounds APossible):/api/v1/workouts?mode=amrap。

為了讓這一切更有趣,我們需要增加一些鍛煉。將這些鍛煉粘貼到 db.json 中的“workouts”集合中:

{
"name": "Jumping (Not) Made Easy",
"mode": "AMRAP 12",
"equipment": [
"jump rope"
],
"exercises": [
"10 burpees",
"25 double-unders"
],
"trainerTips": [
"Scale to do 50 single-unders, if double-unders are too difficult"
],
"id": "8f8318f8-b869-4e9d-bb78-88010193563a",
"createdAt": "4/25/2022, 2:45:28 PM",
"updatedAt": "4/25/2022, 2:45:28 PM"
},
{
"name": "Burpee Meters",
"mode": "3 Rounds For Time",
"equipment": [
"Row Erg"
],
"exercises": [
"Row 500 meters",
"21 burpees",
"Run 400 meters",
"Rest 3 minutes"
],
"trainerTips": [
"Go hard",
"Note your time after the first run",
"Try to hold your pace"
],
"id": "0a5948af-5185-4266-8c4b-818889657e9d",
"createdAt": "4/25/2022, 2:48:53 PM",
"updatedAt": "4/25/2022, 2:48:53 PM"
},
{
"name": "Dumbbell Rower",
"mode": "AMRAP 15",
"equipment": [
"Dumbbell"
],
"exercises": [
"15 dumbbell rows, left arm",
"15 dumbbell rows, right arm",
"50-ft handstand walk"
],
"trainerTips": [
"RX weights for women: 35-lb",
"RX weights for men: 50-lb"
],
"id": "3dc53bc8-27b8-4773-b85d-89f0a354d437",
"createdAt": "4/25/2022, 2:56:03 PM",
"updatedAt": "4/25/2022, 2:56:03 PM"
}

之后,我們必須接受并處理查詢參數。我們的鍛煉控制器將是正確的起點:

// In src/controllers/workoutController.js
...

const getAllWorkouts = (req, res) => {
// *** ADD ***
const { mode } = req.query;
try {
// *** ADD ***
const allWorkouts = workoutService.getAllWorkouts({ mode });
res.send({ status: "OK", data: allWorkouts });
} catch (error) {
res
.status(error?.status || 500)
.send({ status: "FAILED", data: { error: error?.message || error } });
}
};

...

我們從 req.query 對象中提取 “mode” 并定義 workoutService.getAllWorkouts 的參數。這將是一個包含我們的 filter 參數的對象。

我在這里使用速記語法,在對象內創建一個名為 “mode” 的新鍵,其值為 “req.query.mode” 中的任何內容。這可能是 truey 值,如果沒有名為 “mode” 的查詢參數,則為 undefined。我們想要接受的 filter 參數越多,就可以擴展這個對象。

在我們的 workout 服務中,將其傳遞給您的數據庫方法:

// In src/services/workoutService.js
...

const getAllWorkouts = (filterParams) => {
try {
// *** ADD ***
const allWorkouts = Workout.getAllWorkouts(filterParams);
return allWorkouts;
} catch (error) {
throw error;
}
};

...

現在我們可以在數據庫方法中使用它并應用過濾:

// In src/database/Workout.js
...

const getAllWorkouts = (filterParams) => {
try {
let workouts = DB.workouts;
if (filterParams.mode) {
return DB.workouts.filter((workout) =>
workout.mode.toLowerCase().includes(filterParams.mode)
);
}
// Other if-statements will go here for different parameters
return workouts;
} catch (error) {
throw { status: 500, message: error };
}
};

...

很簡單,對吧?我們在這里所做的只是檢查我們的 “filterParams” 中的鍵 “mode” 是否真的有一個真值。如果這是真的,我們就會過濾所有具有相同 “mode” 的鍛煉。如果不是這樣,則沒有名為 “mode” 的查詢參數,我們返回所有鍛煉,因為我們不需要過濾。

我們在這里將 “workouts” 定義為 “let” 變量,因為當為不同的過濾器添加更多 if 語句時,我們可以覆蓋 “workouts” 并鏈接過濾器。

在瀏覽器中,您可以訪問 localhost:3000/api/v1/workouts?mode=amrap,您將收到存儲的所有 “AMRAP” 鍛煉:

如果省略 query 參數,則應像以前一樣獲取所有鍛煉。您可以通過添加“for%20time”作為“mode”參數的值來進一步嘗試(記住 –>“%20”表示“空白”),您應該會收到所有具有“For Time”模式的鍛煉(如果有存儲)。

鍵入未存儲的值時,您應該會收到一個空數組。

排序和分頁的參數遵循相同的原理。讓我們看看我們可以實現的一些功能:

使用數據緩存提高性能

使用數據緩存也是改善 API 整體體驗和性能的好方法。

當數據是經常請求的資源時,使用緩存提供數據非常有意義,或者/或從數據庫中查詢該數據是一項繁重的工作,可能需要幾秒鐘。

您可以將此類數據存儲在緩存中,并從那里提供數據,而不是每次都訪問數據庫查詢數據。

在使用緩存提供數據時必須記住的一件重要事情是,這些數據可能會過時。因此,您必須確保緩存中的數據始終是最新的。

市面上有許多不同的解決方案。一個合適的示例是使用 redis 或快速中間件 apicache。

我想使用 apicache,但如果你想使用 Redis,我強烈建議你查看他們的優秀文檔。

讓我們考慮一下 API 中緩存有意義的場景。我認為請求接收所有鍛煉將有效地從我們的緩存中提供。

首先,讓我們安裝我們的中間件:

npm i apicache

現在,我們必須將其導入我們的鍛煉路由器并對其進行配置。

// In src/v1/routes/workoutRoutes.js
const express = require("express");
// *** ADD ***
const apicache = require("apicache");
const workoutController = require("../../controllers/workoutController");
const recordController = require("../../controllers/recordController");

const router = express.Router();
// *** ADD ***
const cache = apicache.middleware;

// *** ADD ***
router.get("/", cache("2 minutes"), workoutController.getAllWorkouts);

router.get("/:workoutId", workoutController.getOneWorkout);

router.get("/:workoutId/records", recordController.getRecordForWorkout);

router.post("/", workoutController.createNewWorkout);

router.patch("/:workoutId", workoutController.updateOneWorkout);

router.delete("/:workoutId", workoutController.deleteOneWorkout);

module.exports = router;

入門非常簡單,對吧?我們可以通過調用 apicache.middleware 來定義一個新的緩存,并將其用作 get 路由中的中間件。您只需將其作為實際路徑和我們的鍛煉控制器之間的參數。

在那里,您可以定義數據應緩存多長時間。在本教程中,我選擇了兩分鐘。該時間取決于緩存中數據更改的速度或頻率。

現在,讓我們進行測試!

Postman 或您選擇的其他 HTTP 客戶端中,定義一個獲取所有鍛煉的新請求。到目前為止,我一直在瀏覽器中完成此操作,但我想更好地為您可視化響應時間。這就是我現在通過 Postman 請求資源的原因。

讓我們第一次調用我們的請求:

如您所見,我們的API響應時間為22.93毫秒。一旦緩存在兩分鐘后失效并需要重新填充時,第一個請求就會受到影響。

因此,在上面的例子中,數據不是從緩存中提供的,而是通過“常規”方式直接從數據庫獲取并填滿了緩存。

對于第二個請求,我們收到的響應時間更短,因為它是直接從緩存中提供的。

我們的服務速度比之前的要求快了三倍!這一切都歸功于我們的緩存。

在我們的示例中,我們只緩存了一個路由,但你也可以通過像這樣實現它來緩存所有路由:

// In src/index.js
const express = require("express");
const bodyParser = require("body-parser");
// *** ADD ***
const apicache = require("apicache");
const v1WorkoutRouter = require("./v1/routes/workoutRoutes");

const app = express();
// *** ADD ***
const cache = apicache.middleware;
const PORT = process.env.PORT || 3000;

app.use(bodyParser.json());
// *** ADD ***
app.use(cache("2 minutes"));
app.use("/api/v1/workouts", v1WorkoutRouter);

app.listen(PORT, () => {
console.log(API is listening on port ${PORT}); });

當涉及到緩存時,我想強調一件重要的事情。盡管緩存似乎能解決很多問題,但它也可能給您的應用程序帶來一些困擾。

使用緩存時必須注意以下幾點:

以下是通常的做法:

我喜歡從我構建的所有內容盡可能簡單和干凈開始。API 也是如此。

當我開始構建 API 并且沒有特別的理由立即使用緩存時,我會將其省略,看看隨著時間的推移會發生什么。當有理由使用緩存時,我可以在那時實現它。

良好的安全實踐

我們已經經歷了一段非常充實的旅程,探討了許多關鍵要點并相應地擴展了我們的API。

我們已經討論了提高 API 的可用性和性能的最佳實踐。安全性也是 API 的一個關鍵因素。您可以構建最好的 API,如果它在服務器上運行時容易受到攻擊,那么它不僅會變得無用,還會帶來安全風險。

使用SSL/TLS是絕對必要的,因為它已成為當今互聯網通信的標準。對于API來說,保護客戶端和API之間傳輸的私人數據尤為重要。

如果您的資源應該只對經過身份驗證的用戶可用,則應使用身份驗證檢查來保護它們。

例如,在 Express 中,您可以將其實現為中間件,就像我們對特定路由的緩存所做的那樣,并在請求訪問資源之前首先檢查請求是否經過身份驗證。

此外,可能存在一些API資源或交互,我們不希望每個用戶都進行請求。在這種情況下,您應該為用戶設計一個角色系統。因此,您必須向該路由添加額外的檢查邏輯,并驗證用戶是否具有訪問此資源的權限。

在我們的用例中,當我們只希望特定用戶(如教練)創建、更新和刪除我們的鍛煉和記錄時,用戶角色也很有意義。閱讀可以適合所有人(也包括“普通 ”成員)。

這可以在我們用于要保護的路由的另一個 middleware 中處理。例如,我們向 /api/v1/workouts 發送 POST 請求以創建新的鍛煉。

在第一個 middleware 中,我們將檢查用戶是否經過身份驗證。如果這是真的,我們將轉到下一個 middleware,這將是檢查用戶角色的 middleware。如果用戶具有訪問此資源的適當角色,則請求將傳遞給相應的控制器。

在路由處理程序中,它看起來像這樣:

// In src/v1/routes/workoutRoutes.js
...

// Custom made middlewares
const authenticate = require("../../middlewares/authenticate");
const authorize = require("../../middlewares/authorize");

router.post("/", authenticate, authorize, workoutController.createNewWorkout);

...

要進一步閱讀并獲得有關該主題的更多最佳實踐,我建議您閱讀本文。

正確記錄您的 API

我知道文檔絕對不是開發人員最喜歡的任務,但這是必須做的事情。尤其是在 API 方面

我認為這種說法有很多道理,因為如果 API 沒有得到很好的記錄,它就無法正確使用,因此變得毫無用處。該文檔也有助于使開發人員的工作更輕松。

請始終記住,文檔通常是使用者與您的 API 的第一次交互。用戶理解文檔的速度越快,他們使用 API 的速度就越快。

因此,我們的任務是實施良好且精確的文檔。幸運的是,有一些出色的工具可以讓我們的生活變得更加輕松。

與計算機科學的其他領域一樣,也有某種用于記錄 API 的標準,稱為 OpenAPI 規范。

讓我們看看如何創建一些文檔來證明該規范的合理性。我們將使用 swagger-ui-express 和 swagger-jsdoc 包來完成此操作。您會在一秒鐘內驚訝于這是多么棒!

首先,我們為文檔設置裸體結構。因為我們計劃使用不同版本的 API,所以文檔也會有所不同。這就是為什么我想定義我們的 swagger 文件以在相應的版本文件夾中啟動我們的文檔的原因。

# Install required npm packages 
npm i swagger-jsdoc swagger-ui-express

# Create a new file to setup the swagger docs
touch src/v1/swagger.js
// In src/v1/swagger.js
const swaggerJSDoc = require("swagger-jsdoc");
const swaggerUi = require("swagger-ui-express");

// Basic Meta Informations about our API
const options = {
definition: {
openapi: "3.0.0",
info: { title: "Crossfit WOD API", version: "1.0.0" },
},
apis: ["./src/v1/routes/workoutRoutes.js", "./src/database/Workout.js"],
};

// Docs in JSON format
const swaggerSpec = swaggerJSDoc(options);

// Function to setup our docs
const swaggerDocs = (app, port) => {
// Route-Handler to visit our docs
app.use("/api/v1/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// Make our docs in JSON format available
app.get("/api/v1/docs.json", (req, res) => {
res.setHeader("Content-Type", "application/json");
res.send(swaggerSpec);
});
console.log(
Version 1 Docs are available on http://localhost:${port}/api/v1/docs ); }; module.exports = { swaggerDocs };

所以,設置非常簡單。我們已經定義了 API 的一些基本元數據,以 JSON 格式創建了文檔,并創建了一個使我們的文檔可用的函數。

為了控制一切是否正常運行,我們將一條簡單的消息記錄到控制臺,我們可以在其中找到我們的文檔。

這將是我們將在根文件中使用的函數,我們在其中創建了 Express 服務器以確保文檔也啟動。

// In src/index.js
const express = require("express");
const bodyParser = require("body-parser");
const v1WorkoutRouter = require("./v1/routes/workoutRoutes");
// *** ADD ***
const { swaggerDocs: V1SwaggerDocs } = require("./v1/swagger");

const app = express();
const PORT = process.env.PORT || 3000;

app.use(bodyParser.json());
app.use("/api/v1/workouts", v1WorkoutRouter);

app.listen(PORT, () => {
console.log(API is listening on port ${PORT}); /// *** ADD *** V1SwaggerDocs(app, PORT); });

現在你應該看到你的終端內部運行你的開發服務器的地方:

當你訪問 localhost:3000/api/v1/docs 時,你應該已經看到我們的文檔頁面:

我每次都驚訝于它的效果如此之好?,F在,基本結構已設置完畢,我們可以開始為端點實現文檔。我們開始吧!

當您查看 swagger.js 文件中的 options.apis 時,您會發現我們已經在數據庫文件夾中包含了鍛煉路線和鍛煉文件的路徑。這是設置中最重要的部分,它將使整個奇跡發生。

在我們的 swagger 選項中定義這些文件將允許我們使用引用 OpenAPI 的注釋,并且具有類似于 yaml 文件的語法,這是設置我們的文檔所必需的。

現在我們準備好為我們的第一個端點創建文檔了!讓我們直接進入它。

// In src/v1/routes/workoutRoutes.js
...

/**
* @openapi
* /api/v1/workouts:
* get:
* tags:
* - Workouts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* example: OK
* data:
* type: array
* items:
* type: object
*/
router.get("/", cache("2 minutes"), workoutController.getAllWorkouts);

...

這基本上就是向我們的 swagger 文檔添加端點的全部魔術。您可以在他們的優秀文檔中查找所有規范來描述終端節點。

當您重新加載文檔頁面時,您應該會看到以下內容:

如果您已經使用過具有 OpenAPI 文檔的 API,那么這應該看起來非常熟悉。這是將列出所有終端節點的視圖,您可以擴展每個終端節點以獲取有關它的更多信息。

當你仔細查看我們的響應時,你會發現我們沒有定義正確的返回值,因為我們只是說我們的 “data” 屬性將是一個空對象的數組。

這就是 Schema 發揮作用的地方。

// In src/databse/Workout.js
...

/**
* @openapi
* components:
* schemas:
* Workout:
* type: object
* properties:
* id:
* type: string
* example: 61dbae02-c147-4e28-863c-db7bd402b2d6
* name:
* type: string
* example: Tommy V
* mode:
* type: string
* example: For Time
* equipment:
* type: array
* items:
* type: string
* example: ["barbell", "rope"]
* exercises:
* type: array
* items:
* type: string
* example: ["21 thrusters", "12 rope climbs, 15 ft", "15 thrusters", "9 rope climbs, 15 ft", "9 thrusters", "6 rope climbs, 15 ft"]
* createdAt:
* type: string
* example: 4/20/2022, 2:21:56 PM
* updatedAt:
* type: string
* example: 4/20/2022, 2:21:56 PM
* trainerTips:
* type: array
* items:
* type: string
* example: ["Split the 21 thrusters as needed", "Try to do the 9 and 6 thrusters unbroken", "RX Weights: 115lb/75lb"]
*/

...

在上面的示例中,我們創建了第一個 schema。通常,此定義將位于定義數據庫模型的架構或模型文件中。

如您所見,它也非常簡單。我們已經定義了構成鍛煉的所有屬性,包括類型和示例。

您可以再次訪問我們的文檔頁面,我們將收到另一個包含我們架構的部分。

現在可以在終端節點的響應中引用此架構。

// In src/v1/routes/workoutRoutes.js
...

/**
* @openapi
* /api/v1/workouts:
* get:
* tags:
* - Workouts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* example: OK
* data:
* type: array
* items:
* $ref: "#/components/schemas/Workout"
*/
router.get("/", cache("2 minutes"), workoutController.getAllWorkouts);

...

仔細查看我們在 “items” 下的評論底部。我們使用 “$ref” 創建引用,并引用我們在鍛煉文件中定義的架構的路徑。

現在,我們可以在回復中顯示完整的鍛煉。

很酷,對吧?您可能會認為“手動輸入這些評論可能是一項乏味的任務”。

這或許是真的,但請這樣想。代碼庫中的那些注釋對于作為 API 開發人員的您自己來說也是一個很棒的文檔。當您想知道特定終端節點的文檔時,您不必一直訪問文檔。您可以在源代碼中的一個位置查找它。

記錄終端節點還可以幫助您更好地理解它們,并 “迫使” 您考慮您可能忘記實現的任何內容。

正如你所看到的,我確實忘記了一些事情。可能的錯誤響應和查詢參數仍然缺失!

沒問題,以下是按照您的要求進行整理后的內容:

讓我們來解決這個問題:

// In src/v1/routes/workoutRoutes.js
...

/**
* @openapi
* /api/v1/workouts:
* get:
* tags:
* - Workouts
* parameters:
* - in: query
* name: mode
* schema:
* type: string
* description: The mode of a workout
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* example: OK
* data:
* type: array
* items:
* $ref: "#/components/schemas/Workout"
* 5XX:
* description: FAILED
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* example: FAILED
* data:
* type: object
* properties:
* error:
* type: string
* example: "Some error message"
*/
router.get("/", cache("2 minutes"), workoutController.getAllWorkouts);

...

當您查看 “tags” 下注釋的頂部時,您可以看到我添加了另一個名為 “parameters” 的鍵,我在其中定義了用于篩選的查詢參數。

我們的文檔現在可以正確顯示它:

為了記錄可能的錯誤情況,我們此時只拋出一個 5XX 錯誤。因此,在 “responses” 下,您可以看到我還為此定義了另一個文檔。

在我們的文檔頁面上,它看起來像這樣:

我們剛剛為一個終端節點創建了完整的文檔。我強烈建議您自己實現其余的終端節點,以便自己動手操作。在這個過程中,你會學到很多東西!

你可能已經看到,記錄 API 有時會讓人頭疼。我認為我向您介紹的工具可以減少您的整體工作量,并且設置它們非常簡單。

因此,我們可以專注于重要的事情,即文檔本身。在我看來,swagger/OpenAPI 的文檔非常好,互聯網上有很多很好的例子。

因為太多的 “額外” 工作而沒有文檔不應該再成為理由。

結論

這真是一次有趣的旅程。我非常喜歡為您撰寫這篇文章,也從中學到了很多。

可能有一些最佳實踐很重要,而另一些最佳實踐似乎不適用于您當前的情況。這沒關系,因為正如我之前所說,每個工程師都有責任挑選出可應用于他們當前情況的最佳實踐。

我盡我所能將迄今為止所做的所有最佳實踐整合在一起,同時在此過程中構建我們自己的 API。這對我來說很有趣!

下次見!

原文鏈接:https://www.freecodecamp.org/news/rest-api-design-best-practices-build-a-rest-api/

上一篇:

設計可用、靈活、持久的 API

下一篇:

如何設計和開發Web API:開發人員的基本指南
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費