大家好!我是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中的測試環境是乾淨的。這個步驟很重要,因為它可以幫助我們避免舊數據干擾新的開發過程。
登入MongoDB Compass
找到你的test資料庫
刪除所有現有的集合
這樣做可以讓我們從一個乾淨的狀態開始開發,避免之前的測試數據影響我們的新功能開發。
2. 定義Mongoose模型
1. 打開 VsCode,在專案根目錄下創建models
資料夾
2. 參考我們在第七天規劃的資料表 Excel
KEY | 英文欄位名稱 | 中文欄位名稱 | 型態 | 必填 | 預設值 | 備註 |
---|---|---|---|---|---|---|
PK | _id | ID | ObjectID | v | ||
contactPerson | 名稱 | String | v | |||
phone | 電話 | String | ||||
信箱 | String | v | ||||
content | 內容 | String | v | |||
source | 從哪裡得知本網站 | Number | 1:網路搜尋 2:親友推薦 3:社群媒體 4:其他 | |||
createTime | 創建時間 | Date | now |
3. 在 models 資料夾內新增feedback.js
文件
- 引入 Mongoose 套件 這行程式碼的作用是引入
1
const mongoose = require("mongoose");
mongoose
這個工具,它能幫助我們與 MongoDB 資料庫進行溝通,並且管理資料。 - 定義聯絡我們模型 這段程式碼的目的是創建一個「模型」,用來描述我們想在資料庫裡儲存的「聯絡我們」資料的結構,簡而言之,就是這些資料的「樣板」。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21const 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
**:這個設定會自動幫你添加兩個欄位,分別記錄資料創建和最後更新的時間。 - 創建並導出模型 最後,我們使用
1
2const 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 集合
💡 補充說明:為什麼新增的是
feedbacks
而不是feedback
當你使用 Mongoose 創建模型時,Mongoose 會自動將模型名稱轉換為複數形式,用來命名 MongoDB 中的集合。這是 Mongoose 的預設行為。
在上面的範例:
- 我們定義了一個模型叫
Feedback
。 - Mongoose 會把
Feedback
變成feedbacks
,然後在資料庫裡建立這個集合。 - 所以,當我們把資料存進去時,它會自動進入
feedbacks
這個集合,而不是feedback
。
這樣的設計是因為資料庫通常會存放多筆資料,所以用複數形式來命名會更符合實際情況。
如果你不想要複數形式:
如果你希望集合名稱保持單數,你可以在創建模型時指定具體的集合名稱:
1 | // 這是上面範例寫法,建立出來的集合會是 feedbacks |
這樣資料就會存到名為 feedback
的集合裡,而不是 feedbacks
。
這種自動複數化的行為是 Mongoose 幫你做的,但你也可以輕鬆地控制它。
開發第一個 API:Feedback CRUD 操作
接下來,我們將在 Express 專案中實現 CRUD(創建、讀取、更新、刪除)操作。
我們會以上面定義的 Feedback 模型為例,直接在 routes 中實現這些功能。
Step1. 再次檢視我們的 Feedback 模型:
1 | const mongoose = require("mongoose"); |
Step 2. 在 routes
資料夾中創建 feedback.js
檔案,並實現 CRUD 操作:
首先,我們需要設置基本的路由結構:
1 | const express = require("express"); |
現在,讓我們逐一實現 CRUD 操作:
新增 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,
});
}
});詳細解釋:
- 路由設置:
router.post("/"...)
表示這是一個處理POST請求的路由,路徑為根路徑”/“。 - 非同步處理:使用
async/await
來處理非同步操作,確保資料庫操作完成後再回應。 - 解構請求體:
const { contactPerson, phone, email, feedback } = req.body;
從請求體中提取所需資料。 - 必填欄位驗證:檢查必填欄位是否存在,如果缺少則返回400錯誤。
- 創建新記錄:使用
Feedback.create()
方法創建新的feedback記錄。 - 成功回應:如果創建成功,返回201狀態碼和新創建的資料。
- 錯誤處理:如果過程中出現錯誤,捕獲錯誤並返回400錯誤狀態。
- 路由設置:
獲取所有 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, // 返回錯誤訊息
});
}
});詳細解釋:
- GET請求處理:這個路由處理GET請求,用於獲取所有feedback。
- 查詢所有記錄:
Feedback.find()
方法不帶參數,會返回所有feedback記錄。 - 回應結構:回應包含狀態、結果數量和實際數據,為前端提供更多信息。
- 錯誤處理:如果查詢過程出錯,返回404錯誤。
獲取指定 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, // 返回錯誤訊息
});
}
});詳細解釋:
- 路徑參數:
:id
是一個路徑參數,可以通過req.params.id
獲取。 - 查找特定記錄:使用
Feedback.findById()
方法查找特定ID的feedback。 - 記錄不存在處理:如果找不到對應記錄,返回404錯誤。
- 成功回應:找到記錄後,返回200狀態碼和記錄數據。
- 路徑參數:
更新指定 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,
});
}
});詳細解釋:
- PATCH請求:使用PATCH方法允許部分更新資源。
- 必填欄位驗證:再次檢查必填欄位,確保更新操作的完整性。
- 更新操作:
findByIdAndUpdate
方法同時查找和更新文檔。new: true
選項返回更新後的文檔。runValidators: true
確保更新時執行模型的驗證規則。
- 更新失敗處理:如果找不到要更新的文檔,返回404錯誤。
- 成功回應:更新成功後,返回更新後的文檔。
刪除指定 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, // 返回錯誤訊息,通常是錯誤的詳細信息
});
}
});詳細解釋:
- 刪除操作:使用
findByIdAndDelete
方法查找並刪除指定ID的文檔。 - 刪除失敗處理:如果找不到要刪除的文檔,返回404錯誤。
- 成功回應:刪除成功後,返回200狀態碼。注意這裡返回
data: null
,因為資源已被刪除。 - 錯誤處理:捕獲可能的錯誤(如無效ID)並返回400錯誤。
- 刪除操作:使用
最後,別忘了在
app.js
中引入這個新的路由:1
2
3const feedbackRoute = require("./routes/feedback");
app.use("/feedbacks", feedbackRoute);
這樣,我們就完成了 Feedback 的 CRUD 操作!每個操作都有其特定的 HTTP 方法和路徑,使用了適當的 Mongoose 方法來操作數據庫。
API常用HTTP狀態碼
在開發API時,正確使用HTTP狀態碼對於前後端溝通至關重要。以下是一些常用的狀態碼及其含義:
- 200 OK:請求成功。通常用於GET和PUT/PATCH請求。
- 201 Created:請求成功並創建了新資源。常用於POST請求。
- 204 No Content:請求成功,但無返回內容。常用於DELETE請求。
- 400 Bad Request:客戶端請求有誤,例如請求體缺少必要參數。
- 401 Unauthorized:未經授權的請求,通常是因為缺少或無效的認證。
- 403 Forbidden:伺服器理解請求但拒絕執行,通常是權限問題。
- 404 Not Found:請求的資源不存在。
- 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 | res.status(200).json({ |
204 No Content:「搞定了啦,就這樣」
現在想像你的麻吉只是簡單回你:「OK啦」,什麼都沒多說。
在API裡,用204 No Content就是醬子的:
1 | res.status(204).send(); |
這基本上就是跟客戶端說:「刪掉了啦,沒事了,掰掰」。
什麼時候用哪個咧?
- 用 200 OK 的時機:
- 你想跟前端多嘴幾句,講講刪掉什麼東西
- 前端需要這些資訊來更新畫面 - 用 204 No Content 的時機:
- 你只想說「刪掉了啦」,不想多講
- 你想省點網路流量(因為204的回應通常是空的)
實際一點的例子
- PTT刪除推文(用204)
1 | app.delete('/posts/:id/comments/:commentId', (req, res) => { |
這裡啊,我們只要知道推文刪掉了就好,不用知道更多有的沒的。
- 刪除雲端硬碟的檔案(用200)
1 | app.delete('/files/:id', (req, res) => { |
這個例子中,回傳被刪掉的檔案資訊可能對使用者有幫助,萬一誤刪還可以知道刪掉什麼。
總結
- 200 OK:「搞定了啦,而且我還知道這個那個」
- 204 No Content:「搞定了啦,沒什麼好講的」
選哪個主要看你的API到底要不要在刪除後多嘴幾句。記住喔,保持一致很重要 - 你的API決定要用哪種方式就要貫徹始終,不要三心二意喔!
結語
好啦!我們的 API 小冒險暫時告一段落。從資料庫設置到 CRUD 操作,我們跑了一圈後端開發的小obstacle course。現在的你,就像剛學會騎腳踏車的小孩,已經可以自己平穩前進了!
記住,一個好的 API 就像一個好朋友:容易相處(易用性)、守信用(可靠性),而且總是能給出清楚的回應(狀態碼和錯誤訊息)。你現在已經認識了這位新朋友,接下來就是多多練習,讓你們的友誼更加深厚。
下一站,我們要認識一位新朋友:Postman。它就像是你的 API 健身教練,幫你檢查每個動作是否到位。準備好要再次揮灑汗水了嗎?
繼續保持熱情,開發的世界總有新奇等著你。現在,是時候歇口氣,喝杯咖啡,然後準備迎接下一個挑戰了!
大家有沒有想要更詳細補充的部分呢?歡迎在下方留言分享喔!我們一起學習、一起成長。明天見啦,掰掰~
希望這篇文章有幫助到你,謝謝你的觀看,若有想看的系列也歡迎告訴我 😉