2fish 的部落格

歡迎來到我的部落格!這裡是我記錄開發點滴的地方,希望你會喜歡😌

0%

[ 2024 IT 鐵人賽 DAY15 ] 打造自己的 API:CRUD 基本功

大家好!我是2魚,今天我們要深入探討後端開發中兩個核心概念:API(應用程式介面)和CRUD(創建、讀取、更新、刪除)操作。這兩個概念是構建現代Web應用的基石。我們將從MongoDB資料庫設置開始,逐步實現一個完整的API,涵蓋所有CRUD操作。無論你是後端新手還是想複習基礎的開發者,這篇文章都會給你實用的洞見和hands-on經驗。讓我們開始這段精彩的後端之旅吧!

房東不給養鸚鵡 - 那就用 Nuxt + Express + MongoDB 30 天寫一個全端鸚鵡專案。

挑戰人生第一次鐵人賽,就來個佛心分享 side project,從專案發想、規劃、設計、資料庫、開發到部署,技術使用 Nuxt 3、Tailwind CSS、Pinia、Axios、Express、MongoDB,製作一個前後端分離的鸚鵡專案,功能主要介紹食物計算機和聯絡我們,希望可以讓更多人瞭解專案的完整流程 ✨

IT 全文章連結

1. MongoDB Atlas 環境準備

首先,我們需要確保MongoDB Atlas中的測試環境是乾淨的。這個步驟很重要,因為它可以幫助我們避免舊數據干擾新的開發過程。

  1. 登入MongoDB Compass

  2. 找到你的test資料庫
    https://ithelp.ithome.com.tw/upload/images/20240924/20159686Ef1NSznZiX.png

  3. 刪除所有現有的集合
    https://ithelp.ithome.com.tw/upload/images/20240924/20159686nstyYoDZ87.png
    這樣做可以讓我們從一個乾淨的狀態開始開發,避免之前的測試數據影響我們的新功能開發。

2. 定義Mongoose模型

1. 打開 VsCode,在專案根目錄下創建models資料夾

https://ithelp.ithome.com.tw/upload/images/20240924/20159686LIjyxmdmfD.png

2. 參考我們在第七天規劃的資料表 Excel

https://ithelp.ithome.com.tw/upload/images/20240924/201596864i1252skD9.png

KEY 英文欄位名稱 中文欄位名稱 型態 必填 預設值 備註
PK _id ID ObjectID v
contactPerson 名稱 String v
phone 電話 String
email 信箱 String v
content 內容 String v
source 從哪裡得知本網站 Number 1:網路搜尋 2:親友推薦 3:社群媒體 4:其他
createTime 創建時間 Date now

3. 在 models 資料夾內新增feedback.js文件

https://ithelp.ithome.com.tw/upload/images/20240924/201596861WiijIyO7A.png

  1. 引入 Mongoose 套件
    1
    const mongoose = require("mongoose");
    這行程式碼的作用是引入 mongoose 這個工具,它能幫助我們與 MongoDB 資料庫進行溝通,並且管理資料。
  2. 定義聯絡我們模型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    const feedbackSchema = new mongoose.Schema(
    {
    // 名稱
    contactPerson: { type: String, required: [true, "姓名未填寫"] },
    // 電話
    phone: { type: String },
    // 信箱
    email: { type: String, required: [true, "信箱未填寫"] },
    // 內容
    feedback: { type: String, required: [true, "內容未填寫"] },
    // 從哪裡得知此網站
    source: {
    type: String,
    enum: ["網路搜尋", "社群媒體", "親友介紹", "其他"],
    },
    },
    {
    versionKey: false,
    timestamps: true,
    }
    );
    這段程式碼的目的是創建一個「模型」,用來描述我們想在資料庫裡儲存的「聯絡我們」資料的結構,簡而言之,就是這些資料的「樣板」。

    每個欄位的說明:
    - **contactPerson (名稱)**:用於儲存用戶的名字。type: String 表示它是文字格式,而且必須填寫(required: true),如果沒填寫就會顯示「姓名未填寫」的錯誤訊息。
    - **phone (電話)**:用於儲存用戶的電話號碼。它是文字格式,但這個欄位可以不填(沒有 required)。
    - **email (信箱)**:用於儲存用戶的電子郵件地址。它也是文字格式,而且必須填寫,否則會顯示「信箱未填寫」的錯誤訊息。
    - **feedback (內容)**:用於儲存用戶給你的反饋內容。這也是必填項,沒填寫的話會顯示「內容未填寫」。
    - **source (從哪裡得知此網站)**:用於記錄用戶是從哪裡知道你這個網站的。它只能是幾個選項之一(例如「網路搜尋」、「社群媒體」等)。

    其他設定:
    - **versionKey: false**:這個設定讓 Mongoose 不會自動生成一個叫 __v 的版本號欄位。
    - **timestamps: true**:這個設定會自動幫你添加兩個欄位,分別記錄資料創建和最後更新的時間。

  3. 創建並導出模型
    1
    2
    const Feedback = mongoose.model("Feedback", feedbackSchema);
    module.exports = Feedback;
    最後,我們使用 mongoose.model 創建了一個名為 Feedback 的模型,這個模型就像是我們剛剛定義的資料「樣板」的具體實現。
    接著,我們用 module.exports 將這個模型導出,這樣在其他地方的程式碼中也可以使用它。

4. 掛載到 app.js 進行測試

  • 打開 app.js 添加以下程式碼:
    1
    const Feedback = require("./models/feedback");
  • 開啟終端機,輸入 npm start 執行專案
  • 回到 compass 可以看到 test 資料庫多出一個新的 feedbacks 集合
    https://ithelp.ithome.com.tw/upload/images/20240924/201596866yv8AeBEgs.png

💡 補充說明:為什麼新增的是 feedbacks 而不是 feedback

當你使用 Mongoose 創建模型時,Mongoose 會自動將模型名稱轉換為複數形式,用來命名 MongoDB 中的集合。這是 Mongoose 的預設行為。

在上面的範例:

  • 我們定義了一個模型叫 Feedback
  • Mongoose 會把 Feedback 變成 feedbacks,然後在資料庫裡建立這個集合。
  • 所以,當我們把資料存進去時,它會自動進入 feedbacks 這個集合,而不是 feedback
    這樣的設計是因為資料庫通常會存放多筆資料,所以用複數形式來命名會更符合實際情況。

如果你不想要複數形式:

如果你希望集合名稱保持單數,你可以在創建模型時指定具體的集合名稱:

1
2
3
4
5
// 這是上面範例寫法,建立出來的集合會是 feedbacks
const Feedback = mongoose.model("Feedback", feedbackSchema);

// 自訂集合名稱,撰寫在 () 第三個變數,建立出來會是 feedback
const Feedback = mongoose.model("Feedback", feedbackSchema, "feedback");

這樣資料就會存到名為 feedback 的集合裡,而不是 feedbacks
這種自動複數化的行為是 Mongoose 幫你做的,但你也可以輕鬆地控制它。


開發第一個 API:Feedback CRUD 操作

接下來,我們將在 Express 專案中實現 CRUD(創建、讀取、更新、刪除)操作。
我們會以上面定義的 Feedback 模型為例,直接在 routes 中實現這些功能。

Step1. 再次檢視我們的 Feedback 模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const mongoose = require("mongoose");

const feedbackSchema = new mongoose.Schema(
{
contactPerson: { type: String, required: [true, "姓名未填寫"] },
phone: { type: String },
email: { type: String, required: [true, "信箱未填寫"] },
feedback: { type: String, required: [true, "內容未填寫"] },
source: {
type: String,
enum: ["網路搜尋", "社群媒體", "親友介紹", "其他"],
},
},
{
versionKey: false,
timestamps: true,
}
);

const Feedback = mongoose.model("Feedback", feedbackSchema);

module.exports = Feedback;

Step 2. 在 routes 資料夾中創建 feedback.js 檔案,並實現 CRUD 操作:

首先,我們需要設置基本的路由結構:

1
2
3
4
5
6
7
const express = require("express");
const router = express.Router();
const Feedback = require("../models/feedback");

// 這裡我們將添加我們的 CRUD 操作

module.exports = router;

現在,讓我們逐一實現 CRUD 操作:

  1. 新增 feedback

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    // 新增 feedback
    router.post("/", async (req, res) => {
    try {
    // 從請求的 body 中提取出用戶提交的資料(姓名、電話、信箱、反饋內容)
    const { contactPerson, phone, email, feedback } = req.body;

    // 驗證必填欄位,檢查是否提供了姓名、信箱和反饋內容
    if (!contactPerson || !email || !feedback) {
    // 如果缺少必填欄位,回應 400 錯誤狀態和錯誤信息
    return res.status(400).json({
    status: "fail",
    message: "姓名、信箱、內容為必填欄位",
    });
    }

    // 如果必填欄位都有,使用 Feedback 模型創建一條新的反饋記錄
    const newFeedback = await Feedback.create({
    contactPerson: contactPerson, // 設置聯絡人姓名
    phone: phone, // 設置聯絡人電話(可選)
    email: email, // 設置聯絡人信箱
    feedback: feedback, // 設置反饋內容
    });

    // 成功創建後,回應 201 狀態(已創建)和成功信息,包含新創建的反饋數據
    res.status(201).json({
    status: "success",
    data: newFeedback,
    });
    } catch (error) {
    // 如果在創建過程中發生錯誤,回應 400 錯誤狀態和錯誤信息
    res.status(400).json({
    status: "fail",
    message: error.message,
    });
    }
    });

    詳細解釋:

    1. 路由設置router.post("/"...) 表示這是一個處理POST請求的路由,路徑為根路徑”/“。
    2. 非同步處理:使用async/await來處理非同步操作,確保資料庫操作完成後再回應。
    3. 解構請求體const { contactPerson, phone, email, feedback } = req.body; 從請求體中提取所需資料。
    4. 必填欄位驗證:檢查必填欄位是否存在,如果缺少則返回400錯誤。
    5. 創建新記錄:使用Feedback.create()方法創建新的feedback記錄。
    6. 成功回應:如果創建成功,返回201狀態碼和新創建的資料。
    7. 錯誤處理:如果過程中出現錯誤,捕獲錯誤並返回400錯誤狀態。
  2. 獲取所有 Feedback

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 取得所有 feedback
    router.get("/", async (req, res) => {
    try {
    // 使用 find() 方法從資料庫中取得所有 feedbacks
    const feedbacks = await Feedback.find();

    // 成功取得資料後,回應 200 狀態碼和回傳的資料
    res.status(200).json({
    status: "success", // 狀態為成功
    results: feedbacks.length, // 回傳的 feedbacks 數量
    data: feedbacks, // 回傳所有 feedbacks 的資料
    });
    } catch (error) {
    // 如果發生錯誤(如資料庫連接問題),回應 404 錯誤狀態和錯誤訊息
    res.status(404).json({
    status: "fail", // 狀態為失敗
    message: error.message, // 返回錯誤訊息
    });
    }
    });

    詳細解釋:

    1. GET請求處理:這個路由處理GET請求,用於獲取所有feedback。
    2. 查詢所有記錄Feedback.find()方法不帶參數,會返回所有feedback記錄。
    3. 回應結構:回應包含狀態、結果數量和實際數據,為前端提供更多信息。
    4. 錯誤處理:如果查詢過程出錯,返回404錯誤。
  3. 獲取指定 ID 的 Feedback

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // 取得指定 id feedback
    router.get("/:id", async (req, res) => {
    try {
    // 使用 findById 方法從資料庫中取得指定 id 的 feedback
    const feedback = await Feedback.findById(req.params.id);

    // 如果找不到該 id 對應的資料,回應 404 錯誤狀態和錯誤訊息
    if (!feedback) {
    return res.status(404).json({
    status: "fail", // 狀態為失敗
    message: "找不到該 feedback", // 返回錯誤訊息
    });
    }

    // 成功取得資料後,回應 200 狀態碼和回傳的資料
    res.status(200).json({
    status: "success", // 狀態為成功
    data: feedback, // 回傳指定 id 的 feedback 資料
    });
    } catch (error) {
    // 如果發生錯誤(如無效的 id),回應 404 錯誤狀態和錯誤訊息
    res.status(404).json({
    status: "fail", // 狀態為失敗
    message: error.message, // 返回錯誤訊息
    });
    }
    });

    詳細解釋:

    1. 路徑參數:id是一個路徑參數,可以通過req.params.id獲取。
    2. 查找特定記錄:使用Feedback.findById()方法查找特定ID的feedback。
    3. 記錄不存在處理:如果找不到對應記錄,返回404錯誤。
    4. 成功回應:找到記錄後,返回200狀態碼和記錄數據。
  4. 更新指定 ID 的 Feedback

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    // 更新指定 id feedback
    router.patch("/:id", async (req, res) => {
    try {
    // 檢查必填欄位是否存在
    const { contactPerson, email, feedback } = req.body;
    if (!contactPerson || !email || !feedback) {
    return res.status(400).json({
    status: "fail",
    message: "姓名、信箱、內容為必填欄位",
    });
    }

    // 使用 findByIdAndUpdate 更新資料
    const updatedFeedback = await Feedback.findByIdAndUpdate(
    req.params.id, // 從 URL 參數中獲取要更新的資料 ID
    req.body, // 使用請求 body 中的資料進行更新
    {
    new: true, // 返回更新後的資料
    runValidators: true, // 在更新時執行驗證
    }
    );

    // 如果找不到該資料,回應 404 錯誤
    if (!updatedFeedback) {
    return res.status(404).json({
    status: "fail",
    message: "找不到該 feedback",
    });
    }

    // 成功更新後回應 200 狀態和更新後的資料
    res.status(200).json({
    status: "success",
    data: updatedFeedback,
    });
    } catch (error) {
    // 捕獲錯誤並返回 400 錯誤狀態和錯誤信息
    res.status(400).json({
    status: "fail",
    message: error.message,
    });
    }
    });

    詳細解釋:

    1. PATCH請求:使用PATCH方法允許部分更新資源。
    2. 必填欄位驗證:再次檢查必填欄位,確保更新操作的完整性。
    3. 更新操作findByIdAndUpdate方法同時查找和更新文檔。
      • new: true選項返回更新後的文檔。
      • runValidators: true確保更新時執行模型的驗證規則。
    4. 更新失敗處理:如果找不到要更新的文檔,返回404錯誤。
    5. 成功回應:更新成功後,返回更新後的文檔。
  5. 刪除指定 ID 的 Feedback

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // 刪除指定 id feedback
    router.delete("/:id", async (req, res) => {
    try {
    // 使用 findByIdAndDelete 根據 URL 中的 id 參數查找並刪除對應的 feedback
    const feedback = await Feedback.findByIdAndDelete(req.params.id);

    // 如果找不到對應的 feedback(即該 id 不存在),回應 404 錯誤
    if (!feedback) {
    return res.status(404).json({
    status: "fail", // 狀態為失敗
    message: "找不到該 feedback", // 返回錯誤訊息
    });
    }

    // 如果成功刪除,回應 200 狀態碼,表示成功操作
    res.status(200).json({
    status: "success", // 狀態為成功
    message: "刪除成功", // 返回成功訊息
    data: null, // 雖然此處用 200,但不返回具體的資料,data 設為 null
    });
    } catch (error) {
    // 如果發生錯誤(例如無效的 id),回應 400 錯誤狀態
    res.status(400).json({
    status: "fail", // 狀態為失敗
    message: error.message, // 返回錯誤訊息,通常是錯誤的詳細信息
    });
    }
    });

    詳細解釋:

    1. 刪除操作:使用findByIdAndDelete方法查找並刪除指定ID的文檔。
    2. 刪除失敗處理:如果找不到要刪除的文檔,返回404錯誤。
    3. 成功回應:刪除成功後,返回200狀態碼。注意這裡返回data: null,因為資源已被刪除。
    4. 錯誤處理:捕獲可能的錯誤(如無效ID)並返回400錯誤。
  6. 最後,別忘了在 app.js 中引入這個新的路由:

    1
    2
    3
    const feedbackRoute = require("./routes/feedback");

    app.use("/feedbacks", feedbackRoute);

    https://ithelp.ithome.com.tw/upload/images/20240924/20159686idBwmleeX9.png

這樣,我們就完成了 Feedback 的 CRUD 操作!每個操作都有其特定的 HTTP 方法和路徑,使用了適當的 Mongoose 方法來操作數據庫。


API常用HTTP狀態碼

在開發API時,正確使用HTTP狀態碼對於前後端溝通至關重要。以下是一些常用的狀態碼及其含義:

  1. 200 OK:請求成功。通常用於GET和PUT/PATCH請求。
  2. 201 Created:請求成功並創建了新資源。常用於POST請求。
  3. 204 No Content:請求成功,但無返回內容。常用於DELETE請求。
  4. 400 Bad Request:客戶端請求有誤,例如請求體缺少必要參數。
  5. 401 Unauthorized:未經授權的請求,通常是因為缺少或無效的認證。
  6. 403 Forbidden:伺服器理解請求但拒絕執行,通常是權限問題。
  7. 404 Not Found:請求的資源不存在。
  8. 500 Internal Server Error:伺服器遇到未知錯誤,無法完成請求。
    正確使用這些狀態碼可以幫助客戶端更好地理解請求的結果,提高API的可用性和可維護性。

HTTP狀態碼速查表

狀態碼 說明 使用情境 餐廳情境比喻
200 OK 請求成功,資料已回傳。 GET取得資料、PUT/PATCH更新資料成功時。 服務生:「您的餐點已送達,請慢用。」
201 Created 請求成功,新資源已建立。 POST新增資料成功時。 櫃台:「您的訂位已成功,座位已準備好。」
204 No Content 請求成功,但無需回傳資料。 刪除成功,或不需回傳內容的操作。 服務生收走空盤子,點頭示意但不說話。
400 Bad Request 請求有誤,伺服器無法處理。 傳送的資料格式錯誤或缺少必要參數。 廚師:「這個訂單資訊不完整,請確認後再下單。」
401 Unauthorized 需要驗證身份。 未登入或登入已過期。 櫃台:「對不起,這是會員專屬餐廳,請出示會員卡。」
403 Forbidden 已驗證身份,但權限不足。 嘗試存取無權限的資源。 經理:「很抱歉,員工專用廚房區域不對外開放。」
404 Not Found 請求的資源不存在。 訪問不存在的網址或資源。 服務生:「對不起,我們的菜單上沒有這道菜。」
500 Internal Server Error 伺服器內部錯誤。 伺服器端程式錯誤或未預期的異常。 主廚:「非常抱歉,廚房設備突然故障,暫時無法處理訂單。」

為什麼正確使用狀態碼很重要?

正確使用 HTTP 狀態碼能大幅提升 API 的可用性和可維護性。它不只幫助開發者快速理解請求的結果,也有助於前端更有效地處理不同情況。選擇適當的狀態碼,就像在 API 和使用者之間建立了一個清晰的溝通橋樑。
記住,優秀的 API 設計不僅僅是實現功能,更重要的是提供清晰的溝通。適當使用狀態碼,就像為您的 API 安裝了一個高效的訊息系統,讓每次的請求和回應都能一目了然,大大提升了開發效率和用戶體驗。

💡 輕鬆搞懂:HTTP狀態碼 200 OK 和 204 No Content 的差別

在處理API刪除操作時常見的兩種狀態碼:200 OK 和 204 No Content。它們到底有啥不一樣?來,讓我給你講個簡單的例子:

200 OK:「欸,搞定了啦!來,我跟你說說細節」

想像你請你的好麻吉幫忙刪掉一張尷尬的路邊攤醉倒照片。你麻吉不只幫你刪掉,還很熱心地跟你說:「搞定囉!那張照片是上個月吃宵夜時拍的,檔案有5MB大欸!」
在API裡面,用200 OK就是這種感覺,不只說刪掉了,還會給你一堆有的沒的:

1
2
3
4
5
6
7
8
res.status(200).json({
message: "刪掉囉,放心啦",
deletedItem: {
name: "醉倒路邊攤.jpg",
date: "2023-05-01",
size: "5MB"
}
});

204 No Content:「搞定了啦,就這樣」

現在想像你的麻吉只是簡單回你:「OK啦」,什麼都沒多說。
在API裡,用204 No Content就是醬子的:

1
res.status(204).send();

這基本上就是跟客戶端說:「刪掉了啦,沒事了,掰掰」。

什麼時候用哪個咧?

  1. 用 200 OK 的時機:
    - 你想跟前端多嘴幾句,講講刪掉什麼東西
    - 前端需要這些資訊來更新畫面
  2. 用 204 No Content 的時機:
    - 你只想說「刪掉了啦」,不想多講
    - 你想省點網路流量(因為204的回應通常是空的)

實際一點的例子

  • PTT刪除推文(用204)
1
2
3
4
app.delete('/posts/:id/comments/:commentId', (req, res) => {
// 刪除推文的程式碼
res.status(204).send();
});

這裡啊,我們只要知道推文刪掉了就好,不用知道更多有的沒的。

  • 刪除雲端硬碟的檔案(用200)
1
2
3
4
5
6
7
8
9
10
11
app.delete('/files/:id', (req, res) => {
// 刪除檔案的程式碼
res.status(200).json({
message: "檔案刪掉囉",
deletedFile: {
name: "期末報告.docx",
size: "2.5MB",
lastEdited: "2023-06-15"
}
});
});

這個例子中,回傳被刪掉的檔案資訊可能對使用者有幫助,萬一誤刪還可以知道刪掉什麼。

總結

  • 200 OK:「搞定了啦,而且我還知道這個那個」
  • 204 No Content:「搞定了啦,沒什麼好講的」
    選哪個主要看你的API到底要不要在刪除後多嘴幾句。記住喔,保持一致很重要 - 你的API決定要用哪種方式就要貫徹始終,不要三心二意喔!

結語

好啦!我們的 API 小冒險暫時告一段落。從資料庫設置到 CRUD 操作,我們跑了一圈後端開發的小obstacle course。現在的你,就像剛學會騎腳踏車的小孩,已經可以自己平穩前進了!
記住,一個好的 API 就像一個好朋友:容易相處(易用性)、守信用(可靠性),而且總是能給出清楚的回應(狀態碼和錯誤訊息)。你現在已經認識了這位新朋友,接下來就是多多練習,讓你們的友誼更加深厚。
下一站,我們要認識一位新朋友:Postman。它就像是你的 API 健身教練,幫你檢查每個動作是否到位。準備好要再次揮灑汗水了嗎?
繼續保持熱情,開發的世界總有新奇等著你。現在,是時候歇口氣,喝杯咖啡,然後準備迎接下一個挑戰了!
大家有沒有想要更詳細補充的部分呢?歡迎在下方留言分享喔!我們一起學習、一起成長。明天見啦,掰掰~




希望這篇文章有幫助到你,謝謝你的觀看,若有想看的系列也歡迎告訴我 😉