[後端] API 設計:分頁、篩選、排序的慣例,讓你的 API 更好用!
前言
身為一個後端工程師,我們每天都在跟 API 打交道。設計一個好的 API,不只是讓它能動就好,更重要的是要「好用」、「好理解」、「好維護」。一個友善的 API 能讓前端同事笑咪咪,也能讓未來的自己少掉很多麻煩。
今天 Hikari 我想跟大家聊聊,在設計 API 時,有幾個非常常見但又常常被討論的議題:分頁(Pagination)、篩選(Filtering)和排序(Sorting)。這些功能幾乎是每個列表型資料都會需要,但設計上卻有各種不同的風格。到底怎麼做,才算是比較「慣例」且「好用」的設計呢?我們一起來拆解看看!
分頁(Pagination):資料太多怎麼辦?
想像一下,如果你要顯示一個有幾十萬筆使用者資料的列表,一次全部撈出來回傳給前端,那會發生什麼事?
- 網路延遲爆表:傳輸大量資料需要時間,使用者等到天荒地老。
- 伺服器壓力山大:資料庫一次要撈幾十萬筆,記憶體、CPU 可能都吃不消。
- 前端卡到不行:瀏覽器一次渲染幾十萬筆 DOM 元素,直接死機給你看。
所以,為了效能考量和使用者體驗,我們一定要做分頁。常見的分頁方式有兩種:傳統的「頁碼分頁」(Offset-based)和比較新潮的「游標分頁」(Cursor-based)。
1. 頁碼分頁(Offset-based Pagination)
這是最直覺、最常見的分頁方式,就像書本的頁碼一樣。通常會透過兩個 Query Parameter 來控制:
page(頁碼):從第幾頁開始,通常從 1 或 0 開始算。limit(每頁筆數):每頁要回傳幾筆資料。
範例:
1 | GET /products?page=1&limit=10 // 取得第一頁的 10 筆商品 |
背後原理(SQL 範例):
1 | SELECT * FROM products |
優點:
- 直觀易懂:使用者習慣頁碼,前端開發也簡單。
- 可以跳頁:可以直接跳到第 10 頁或最後一頁。
缺點:
- 深層分頁效能差:
OFFSET會讓資料庫先掃描並跳過前面的N筆資料,如果N很大,效能會非常差。 - 資料異動問題:在不同頁之間,如果資料有新增或刪除,可能導致使用者看到重複的資料,或遺漏某些資料。
什麼時候用?
- 資料量不大,或者使用者不太需要翻到很深層的頁面。
- 需要提供直接跳頁功能,例如後台管理系統。
2. 游標分頁(Cursor-based Pagination)
這種方式不依賴頁碼,而是依賴一個「游標」(Cursor),通常是資料庫中的某個唯一且可排序的欄位值,例如 ID 或時間戳。它會告訴你「從這個點之後,再給我 N 筆資料」。
範例:
1 | GET /products?after=product_id_123&limit=10 // 取得 ID 在 product_id_123 之後的 10 筆商品 |
這裡的 after 或 before 值,通常是前一頁最後一筆資料的 ID,或是一個 Base64 編碼過的字串,裡面包含多個排序欄位的值。
背後原理(SQL 範例):
1 | SELECT * FROM products |
優點:
- 效能好:直接利用索引進行查詢,沒有
OFFSET的效能問題。 - 資料異動影響小:因為是基於特定點後的資料,即使有新資料加入,也不會導致重複或遺漏。
- 適合無限捲動:前端常見的「滑到底自動載入更多」模式很適合。
缺點:
- 無法跳頁:只能「下一頁」、「上一頁」,不能直接跳到特定頁碼。
- 實作較複雜:游標的設計(要包含哪些欄位?要不要編碼?)需要仔細考慮。
什麼時候用?
- 資料量非常大,且使用者多半是連續翻閱(例如社群媒體的動態牆)。
- 追求極致效能的場景。
分頁額外資訊:總筆數(Total Count)
在頁碼分頁中,前端常常會需要知道「總共有多少筆資料」來顯示頁碼導覽列。這時候,API 可能需要回傳一個 total 或 total_count 的欄位。
1 | { |
注意:撈取 total 也是一個 COUNT(*) 的資料庫操作,在資料量極大的情況下,也可能造成效能問題。如果不是非必要,可以考慮只回傳 has_next_page (還有沒有下一頁) 這類布林值即可。
篩選(Filtering):精準找到我要的!
使用者通常不會想看所有資料,他們會想找「某個分類」、「某個價格區間」的資料。這就是篩選功能派上用場的時候了。
最常見的設計方式是利用 Query Parameter。每個參數代表一個篩選條件。
範例:
1 | // 查詢分類為 'electronics' 的商品 |
常見的運算子表示法:
在 Query Parameter 中,我們可以透過參數命名來表示不同的運算子:
_eq: 等於 (通常省略,直接param=value就表示等於)_neq: 不等於_gt: 大於_gte: 大於等於_lt: 小於_lte: 小於等於_like: 模糊比對 (SQL 中的%LIKE%)_in: 包含在列表中 (多個值,例如status=active,pending)
實作考量:
- 安全性:前端傳來的篩選條件務必經過嚴格的參數驗證和淨化,避免 SQL Injection 或其他惡意攻擊。不要直接將參數拼接到 SQL 語句中!
- 索引優化:根據常用的篩選欄位建立資料庫索引,可以大大提升查詢效能。
- 參數數量限制:如果篩選條件非常多,可以考慮將部分複雜篩選包裝成一個 JSON 字串,但會增加解析複雜度,不建議過度使用。
排序(Sorting):資料怎麼排才好看?
資料列表通常需要一個預設的排序方式(例如最新建立的在最前面),但使用者也常常需要自己決定要怎麼排序,例如依價格由高到低,或依名稱字母順序。
排序功能通常也透過 Query Parameter 來設計。
範例:
最常見的設計方式是使用一個 sort 參數,後面接 欄位名稱 和 排序方向。
1 | // 依價格由高到低排序 |
如果需要多欄位排序,可以用逗號分隔:
1 | // 先依價格由高到低,再依名稱字母順序排序 |
另一種常見寫法是分成兩個參數 sortBy 和 sortOrder:
1 | GET /products?sortBy=price&sortOrder=desc |
Hikari 我個人比較偏好 sort=price_desc,name_asc 這種單一參數,因為在前端組裝和後端解析上都比較簡潔,也更容易擴充多欄位排序。
實作考量:
- 預設排序:當前端沒有指定排序時,後端應該提供一個合理的預設排序(例如依建立時間倒序
created_at_desc)。 - 合法欄位檢查:後端必須嚴格檢查傳入的排序欄位是否是合法的、可排序的欄位。千萬不要允許前端指定任何欄位來排序,否則可能會導致資料庫錯誤或洩漏內部結構。可以維護一個白名單 (whitelist)。
綜合應用與眉角
當分頁、篩選、排序這些功能綜合起來時,它們通常都會以 Query Parameters 的形式出現在同一個 API 請求中。
綜合範例:
1 | GET /products?page=2&limit=10&category=electronics&price_gte=100&price_lte=500&sort=price_desc,name_asc |
幾點實作上的眉角:
- 預設值:所有這些參數都應該在後端設定合理的預設值。這樣前端就不需要每次都傳一堆參數,減少溝通成本,也避免前端忘記傳參數導致非預期結果。
page: 1limit: 10 或 20 (看產品需求)sort:created_at_desc或其他合理的預設排序
- 驗證機制:再次強調,所有 Query Parameters 都必須經過嚴格的驗證!包括類型、數值範圍、是否在白名單內等等。這是保護 API 安全和穩定的基本功。
- API 文件:設計再好的 API,如果沒有清楚的 API 文件,等於白搭。詳細說明每個參數的用途、類型、可接受的值、預設值、運算子等,讓前後端都能快速理解和使用。
小結
分頁、篩選、排序這些看似基本的功能,在 API 設計上卻有很多值得玩味的地方。選擇哪種分頁方式、如何命名篩選參數、怎麼處理多欄位排序,都會影響到 API 的可用性和維護性。
其實沒有一套「絕對正確」的設計,而是要根據你的專案需求、資料量大小、效能考量和前端使用情境來做權衡。但只要我們掌握了這些常見的慣例和背後的考量,並且記得保持一致性、提供彈性、做好驗證、寫好文件,相信你就能設計出一個讓大家都說讚的 API!
希望今天這篇分享對你有幫助!如果你有其他更好的設計方式或踩雷經驗,也歡迎在留言區跟我分享喔!
如果您喜歡我寫的文章,幫我按個5下讚吧!感謝您的鼓勵和支持!

![[後端] API 設計:分頁、篩選、排序的慣例,讓你的 API 更好用!](/img/covers/api-design-pagination-filtering-sorting-conventions.jpg)
![[AI] 提示詞注入(Prompt Injection)與防禦:讓你的 LLM 應用不再被綁架!](/img/covers/prompt-injection-defense.jpg)
![[AI] LLM 的 Function Calling / Tool Use 怎麼運作?白話拆解原理與實戰眉角](/img/covers/llm-function-calling-tool-use-how-it-works.jpg)