iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
佛心分享-IT 人自學之術

出發吧!後端菜鳥:30 天的後端學習紀錄系列 第 7

Day 07|CRUD 寫完後:搞懂 Params、Query、Body,並修正 ID

  • 分享至 

  • xImage
  •  

前言

上一篇我們已經完成 Notes API 的 CRUD,可以新增、取得、修改與刪除 Note:

GET    /notes
GET    /notes/:id
POST   /notes
PATCH  /notes/:id
DELETE /notes/:id

功能雖然完成了,但在實作過程中,其實已經碰到不少 Request 裡的資料,例如 req.params.idreq.body,只是當時把重點放在 CRUD,沒有特別拆開說明。

這一篇就回頭整理這些資料到底從哪裡來,以及什麼情況適合放在 Params、Query、Body 或 Headers,最後再利用 Query String,替 Notes API 加上一個簡單的搜尋功能。另外,上一篇使用的 notes.length + 1 也留下了一個 ID 重複問題,會放在文章最後一起處理。

Request 的資料可能放在哪裡?

Client 向 Server 發送 Request 時,除了告訴 Server 要使用哪個 HTTP Method、存取哪個 URL,也可能需要附帶其他資料,例如要取得哪一筆 Note、要搜尋什麼關鍵字,或是要新增哪些內容。

以 Notes API 為例,下面三個 Request 帶的資料位置就不一樣:

GET /notes/3
GET /notes?keyword=node
POST /notes

GET /notes/3 把 Note ID 放在 URL 路徑裡,GET /notes?keyword=node 把搜尋條件放在 Query String,而 POST /notes 要新增的標題和內容通常會放在 Request Body。

在 Express 中,最常使用以下三個屬性取得這些資料:

req.params
req.query
req.body

除此之外,HTTP Request 還有 Headers,可以攜帶資料格式、驗證資訊等附加內容。

req.params:取得 Route Parameter

上一篇取得單筆 Note 時,我們建立了這樣的 Route:

app.get("/notes/:id", function (req, res) {
  // ...
});

其中的 :id 稱為 Route Parameter,代表 URL 這個位置可以放入不同的值,因此以下 Request 都會進入同一個 Route:

GET /notes/1
GET /notes/2
GET /notes/15

假設 Client 發送:

GET /notes/3

Express 會把 3 放進 req.params

{
  id: "3"
}

因此可以透過 req.params.id 取得 URL 裡的 ID:

app.get("/notes/:id", function (req, res) {
  const id = Number(req.params.id);

  const note = notes.find(function (note) {
    return note.id === id;
  });

  res.json({
    data: note
  });
});

這裡需要使用 Number(),是因為 URL 裡取得的 Route Parameter 是字串,所以 /notes/3 得到的是 "3",而目前 Notes 裡的 ID 則是數字 3,如果直接使用嚴格比較,會變成:

3 === "3"

結果會是 false,因此要先把 req.params.id 轉成 Number。

Route Parameter 很適合用來表示「要操作哪一個資源」,像是取得、修改或刪除某一筆 Note:

GET    /notes/3
PATCH  /notes/3
DELETE /notes/3

req.query:取得 Query String

如果不是要指定某一筆資料,而是想替一批資料加上查詢條件,就很常使用 Query String。

例如我們希望搜尋標題中包含 node 的 Note,可以設計成:

GET /notes?keyword=node

URL 中 ? 後面的 keyword=node 就是 Query String,其中 keyword 是參數名稱,node 則是它的值。

在 Express 中,可以透過 req.query 取得:

app.get("/notes", function (req, res) {
  console.log(req.query);

  res.json({
    data: notes
  });
});

當 Request 是:

GET /notes?keyword=node

req.query 會得到類似以下內容:

{
  keyword: "node"
}

因此搜尋關鍵字可以透過 req.query.keyword 取得。

Query String 很適合用在搜尋、篩選、排序或分頁,例如:

GET /notes?keyword=node
GET /products?category=book
GET /products?sort=price
GET /notes?page=2

如果有多個條件,可以使用 & 串在一起:

GET /products?category=book&sort=price&page=2

這時 req.query 可能會是:

{
  category: "book",
  sort: "price",
  page: "2"
}

要注意 Query String 取得的值通常也是字串,因此如果 page 後面要拿來做數字運算,仍然需要自行轉換:

const page = Number(req.query.page);

req.body:取得 Request Body

Params 和 Query 都會直接出現在 URL 上,但新增或修改資料時,通常需要傳送比較完整的內容,這時就會把資料放在 Request Body。

例如建立新的 Note,可以發送:

POST /notes

Request Body 則帶上:

{
  "title": "學習 Node.js",
  "content": "今天練習 Express"
}

Express 可以透過 req.body 取得:

app.post("/notes", function (req, res) {
  const newNote = {
    id: notes.length + 1,
    title: req.body.title,
    content: req.body.content
  };

  notes.push(newNote);

  res.status(201).json({
    data: newNote
  });
});

因此 req.body.title 會取得 "學習 Node.js"req.body.content 則會取得 "今天練習 Express"

Request Body 很常用在 POST 和 PATCH,POST 通常用來傳送建立資料所需要的內容,而 PATCH 則可以只傳送這次要修改的欄位,例如:

{
  "title": "修改後的標題"
}

上一篇已經介紹過 express.json(),這裡只要記得,如果 Client 傳送的是 JSON Request Body,就需要先經過它的解析,後面的 Route 才能透過 req.body 取得資料。

Headers 是什麼?

除了 URL 和 Body 之外,HTTP Request 還可以透過 Headers 攜帶一些描述這次 Request 的額外資訊。

目前先認識兩個之後會經常遇到的 Header:Content-TypeAuthorization

當 Request Body 傳送 JSON 時,通常會看到:

Content-Type: application/json

Content-Type 用來告訴 Server Request Body 使用什麼資料格式,這裡的 application/json 就代表內容是 JSON。

另一個常見的是 Authorization,未來學到 JWT 時可能會看到:

Authorization: Bearer xxxxx.yyyyy.zzzzz

它通常用來攜帶身分驗證資訊,Server 收到 Request 後,可以再根據其中的 Token 判斷使用者身分與權限,目前先知道它是放在 Header 裡即可,後面學驗證機制時再深入處理。

Params、Query、Body 有什麼差異?

整理前面的內容,可以先用這個方式區分:

Params → 指定要操作哪一個資源
Query  → 描述要怎麼查詢資源
Body   → 傳送要建立或修改的內容

例如取得 ID 為 3 的 Note:

GET /notes/3

3 放在 Params。

如果要從所有 Notes 中搜尋 node

GET /notes?keyword=node

keyword=node 放在 Query。

如果要建立一筆新的 Note:

POST /notes

要建立的資料則放在 Body:

{
  "title": "學習 Node.js",
  "content": "今天練習 Express"
}

這個分類不是所有 API 都必須完全照著使用的硬性規則,但在設計一般 REST API 時,可以作為很實用的判斷方式。

加入 Notes 搜尋功能

理解 req.query 之後,就可以替目前的 GET /notes 加上一個簡單的搜尋功能。

假設目前有以下資料:

const notes = [
  {
    id: 1,
    title: "學習 Node.js",
    content: "認識 Runtime"
  },
  {
    id: 2,
    title: "學習 Express",
    content: "建立 Express Server"
  },
  {
    id: 3,
    title: "學習 CSS",
    content: "整理 Flexbox"
  }
];

我們希望 Client 可以使用:

GET /notes?keyword=node

搜尋標題中包含 node 的 Note,因此可以把原本的 GET /notes 改成:

app.get("/notes", function (req, res) {
  const keyword = req.query.keyword;

  if (!keyword) {
    return res.json({
      data: notes
    });
  }

  const filteredNotes = notes.filter(function (note) {
    return note.title
      .toLowerCase()
      .includes(keyword.toLowerCase());
  });

  res.json({
    data: filteredNotes
  });
});

如果 Client 只發送 GET /notes,因為沒有 keyword,API 就直接回傳全部 Notes;如果發送 GET /notes?keyword=node,程式會取得 "node",再透過 filter() 找出符合條件的資料。

這裡同時把標題和搜尋關鍵字使用 toLowerCase() 轉成小寫,是為了避免 "Node""node" 因為大小寫不同而搜尋不到。

搜尋結果可能會是:

{
  "data": [
    {
      "id": 1,
      "title": "學習 Node.js",
      "content": "認識 Runtime"
    }
  ]
}

之後如果想繼續擴充,也可以加入分頁、排序等 Query:

GET /notes?keyword=node&page=1
GET /notes?keyword=node&sort=desc&page=1

最後補充:ID 為什麼不能一直用 notes.length + 1

前面的 POST API 目前還是使用:

const newNote = {
  id: notes.length + 1,
  title: req.body.title,
  content: req.body.content
};

這個寫法在資料只新增、不刪除時不太容易出現問題,假設目前有三筆 Note,notes.length3,下一筆 ID 就會是 4

但如果刪除其中一筆,例如原本的 ID 是 1、2、3,刪除 ID 2 後只剩:

const notes = [
  {
    id: 1,
    title: "學習 Node.js"
  },
  {
    id: 3,
    title: "學習 API"
  }
];

這時 notes.length2,下一次使用 notes.length + 1 又會得到 3,結果就會出現兩筆相同 ID。

原因很簡單,notes.length 只代表目前有幾筆資料,並不代表目前最大的 ID。

練習階段:使用最大 ID + 1

目前 Notes 還只是放在 JavaScript 陣列裡,可以先找出最大的 ID,再加上 1

const maxId = Math.max(...notes.map(note => note.id), 0);

接著修改 POST API:

app.post("/notes", function (req, res) {
  const maxId = Math.max(...notes.map(note => note.id), 0);

  const newNote = {
    id: maxId + 1,
    title: req.body.title,
    content: req.body.content
  };

  notes.push(newNote);

  res.status(201).json({
    data: newNote
  });
});

假設目前的 ID 是 13Math.max() 會找到最大的 3,所以下一筆資料會使用 4,即使中間刪過資料,也不會因為陣列長度縮短而重複使用 ID。

Math.max() 中加入 0,則是為了處理空陣列,如果目前沒有任何 Note,最大值會以 0 計算,因此第一筆資料的 ID 會是 1

這種方式很適合目前用陣列練習,但正式專案通常不會自己掃過所有資料再尋找最大的 ID。

正式專案通常怎麼產生 ID?

真正接上資料庫之後,常見做法之一是讓資料庫自己產生流水號 ID,例如 PostgreSQL 可以使用 Identity 或 Sequence。

概念上可以寫成:

id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY

新增資料時,就不需要自己提供 ID:

INSERT INTO notes (title, content)
VALUES ('學習 Node.js', '今天練習 Express');

資料庫會負責產生下一個值,因此不需要在 Node.js 中使用 Math.max() 自己計算,而且資料庫也有相應機制處理多個 Request 同時新增資料的情況。

另一種正式專案也很常見的方式是 UUID,全名為 Universally Unique Identifier,產生的 ID 可能像:

550e8400-e29b-41d4-a716-446655440000

Node.js 可以使用內建的 crypto.randomUUID()

const crypto = require("crypto");

const id = crypto.randomUUID();

UUID 不需要知道目前最大的 ID,也不依賴上一筆資料,因此不同 Server 或服務可以各自產生識別碼,而產生相同 UUID 的機率也非常低。

目前可以先簡單區分:

最大 ID + 1
→ 目前使用陣列練習時的處理方式

資料庫 Identity / Sequence
→ 正式專案常見的數字流水號

UUID
→ 正式專案常見的另一種唯一識別方式

結語

這一篇主要補上一篇 CRUD 中已經使用、但還沒有仔細拆開說明的 Request 資料來源,req.params 適合取得 URL 路徑中的動態資料,req.query 常用來接收搜尋、篩選、排序與分頁條件,而 req.body 則用來取得 Client 傳送的主要內容,Headers 則負責攜帶 Content-TypeAuthorization 等附加資訊。

理解這些資料放在哪裡之後,也就能進一步替 GET /notes 加入搜尋功能,而上一篇留下的 ID 問題則可以先用「最大 ID + 1」處理,等之後接上資料庫,再交給 Identity、Sequence 或 UUID 等更正式的方式管理。


上一篇
Day 06|CRUD 集合!完成第一組 RESTful API
下一篇
Day 08|API 不一定會成功:錯誤處理
系列文
出發吧!後端菜鳥:30 天的後端學習紀錄10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言