iT邦幫忙

2021 iThome 鐵人賽

DAY 12
0
Modern Web

從零開始學習 Next.js系列 第 12

Day12 - 該來寫 API 了,API routes 簡介

API routes

Next.js 是一個全端框架,除了提供 SSR 與 SSG 的功能之外,還能夠建立 API 提供前端頁面使用。

你可以使用 API routes 建立 REST API,如果有 graphql 的需求,也可以用來建立 graphql API。官方文件提供了許多的範例,讓我們可以快用用一些模板建立服務:

在這篇文章中將以 REST API 作為範例,體驗 Next.js 的 API routes。

如何建立 API routes

API routes 的概念與前端頁面一樣,都是使用 file-based routing,所有的 API 都會放在 pages/api 這個資料夾底下,例如 pages/api/products 即是對應 api/products 這個 endpoint。

在這這資料夾中的所有檔案將不會被當作頁面的 url,因此在 pages/api 中的檔案都不會被打包近客戶端的 bundle 中,如果使用者在瀏覽器的網址列輸入 api/products ,即是跟伺服器端請求 API,而並非是一個頁面。

舉一個例子,在 pages/api/products.ts 中建立一個 API,回傳 json 格式的資料,並且包含 200 的 HTTP 狀態碼:

import { NextApiRequest, NextApiResponse } from "next";

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  res.status(200).json({ products: [{ name: "item" }] });
}

從以上範例中可以看到,API routes 是一個 export default 的 function,可以使用 res.status 指定 HTTP 的狀態碼,並使用 res.json 回傳資料至客戶端。

接著,在瀏覽器中輸入 api/products 就會看到 API 回應的資料,打開 Chrome 的 Network 也可以看到 HTTP 狀態碼為 200。

API response

我們從 reqres 的型別定義中可以看到:

  • req 的型別 NextApiRequest 繼承了 http.IncomingMessage ,是一個 IncomingMessage 的 instance。
  • res 的型別 NextApiResponse 繼承了 http.ServerResponse ,是一個 ServerResponse 的 instance。

但是兩者與原生 node.js 的寫法不太一樣,像是 Next.js 封裝了 req ,讓我們可以用 req.query 取得 url 上的參數,還可以使用 req.body 取得 body 中的內容。此外,Next.js 也封裝了 res 這個物件,讓我們能夠用 chain function 的方式使用 res ,如上方的範例。

如果想要處理不同的 HTTP method,可以透過 req.method 這個屬性判斷:

import { NextApiRequest, NextApiResponse } from "next";

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === "GET") {
    res.status(200).json({ products: [{ name: "item" }] });
  } else if (req.method === "POST") {
    // 建立產品資料
  } else if (req.method === "DELETE") {
    // 刪除產品資料
  }
}

Dynamic API routes

既然 API routes 是基於 file-based routing,所以也能夠處理動態的資源,例如在前面章節實作的「產品詳細頁面」,其對應的頁面是 pages/products/[id].tsx ,在這個頁面中的 id 是動態的,會根據使用者瀏覽的產品對應至不同的值,因此在詳細頁面中也會需要呼叫不同的 API endpoint,像是 api/products/[id]

要建立 dynamic API routes 其概念與「頁面」一樣, api/products/[id] 即是對應 api/products/[id].ts

import { NextApiRequest, NextApiResponse } from "next";

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const { id } = req.query;
  res.status(200).json({ productId: id });
}

接著,在瀏覽器中輸入 api/products/123 就會看到 API 回應的資料,打開 Chrome 的 Network 也可以看到 HTTP 狀態碼為 200。

Api response

Catch all API routes

以部落格的例子來說,一篇貼文的 url 以「年月日」來設計,所以 url 可能這個樣子 /posts/<year>/<month>/<day> , 如果 API 要像下方這樣子建立很多個資料夾,工程師們大概會覺得很麻煩:

pages/
└── api/
		└──posts/
		   └── [year]/
		       └── [month]/
		           └── [day].ts

為了解決這個情況,所以 API routes 也有 catch all routes 的實作,以上方的例子來說,只要定義 /pages/api/[...date].ts 就可以匹配「年月日」的參數,而且甚至可以無限地加上新的參數,例如顆粒度想要細到小時、分鐘、秒,都是可以的:

pages/
└── api/
		└──posts/
		   └── [...date].ts

[...date] 的資料最後會以陣列被儲存在 router.query 中:

import { NextApiRequest, NextApiResponse } from "next";

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const { date } = req.query;
  res.status(200).json({ date });
}

/api/posts/2021/12/31 這個例子來說, date 會是以下這個模樣:

{
  date: [2021, 12, 31];
}

如果一個 API 的資料夾同時包括 [id].ts[...date].ts 兩種 pattern 的話,當呼叫 /api/posts/abc 會先匹配 /api/posts/[id].ts 這個 API routes,而 2 個以上的參數才會匹配 [...date].ts

Optional catch all API routes

這是 dynamic routes 的最後一個 pattern,前面提到的 [id].ts[...date].ts 都不能用來匹配 /api/products 這種 API endpoint,但是 optional catch all API routes 可以用來匹配所有的 API endpoint,它是以兩個鐘括號作為定義,例如 [[...slug]].ts

所以,如果想要用一個 API routes 定義部落格中所有的貼文 API,則可以定義 /api/posts/[[...slug]].ts ,這一個 API 則可以同時匹配以下幾種 API endpoints:

  • /api/posts
  • /api/posts/123
  • /api/posts/2021/12/31

而這幾個 API endpoints 的 req.query 會是以下這個樣子:

  • {}
  • { slug: [123] }
  • { slug: [2021, 12, 31] }

API routes 的階層關係

Next.js 為了讓 API 定義更彈性一點,提供了各種不同的 API routes 的定義模式,包括以下幾種:

  • 明確定義的 API rotues - /api/posts.ts
  • dynamic API - /api/posts/[id].ts
  • catch all routes API - /api/posts/[...date].ts
  • ⭐ optional catch all routes API - /api/posts/[[...slug]].ts

它們彼此之前的關係是由上到下,後面的 API routes 不會蓋掉前面,舉例來說如果同時在一個 API routes 的資料夾有四種不同的模式:

  • /api/posts/about.ts 匹配 /api/posts/about
  • /api/posts/[id].ts 匹配 /api/posts/123
  • /api/posts/[...date].ts 匹配 /api/posts/2021/12/31
  • 但是 /api/posts/[[...slug]].ts 不會匹配任何的 endpoints

所以用這種方式思考一個特殊的情況,當一個 API routes 的 API routes 只有 [id].ts[[...slug]].ts ,但是沒有 index.ts ,直覺的思考「是不是 [[...slug]].ts 會匹配 /api/posts 這種 endpoint」,但因為有 [id].ts 存在於 API routes 的資料夾中, /api/posts 將會回傳 HTTP 404。

想要讓 [[...slug]].ts 可以匹配 /api/posts ,則要刪除 [id].ts 這個 API routes,讓 [[...slug]].ts 做所有的事情。

產品列表頁面與產品詳細頁面所需要的 API

現在我們了解了幾種不同定義 API routes 的模式後,來嘗試設計「產品列表頁面」與「產品詳細頁面」中需要的 API,已知有兩個 API endpoints :

  • /api/products :回傳產品列表
  • /api/products/[id] :回傳一個產品的詳細資訊

所以,我們可以統整出幾種不同的定義方式:

  • 方法一:
    • /api/products.ts
    • /api/products/[id].ts
  • 方法二:
    • /api/products/index.ts
    • /api/products/[id].ts
  • 方法三:
    • /api/products/[[...slug]].ts

方法一與方法二的 [id].ts 可以用 [...slug].ts[[...slug].ts 取代,但是如果使用[[...slug]].ts 則沒有意義,因為 /api/products 已經由 index.ts 定義了,所以不如使用 [...slug].ts 避免造成 API routes 混亂。

總結

今天我們了解了如何定義 API routes,可以藉由 req.method 判斷 API 的 HTTP method,用以切分不同的實作,想要獲得 endpoint 上的資訊,則可以透過 req.query 取得。當我們要回應 API 請求時,可以使用 res.status 定義 HTTP 狀態碼,並透過 res.json 回傳 JSON 格式的資料。

而 API routes 有四種模式匹配各種路由,基本上是 file-based routing 的概念,只是套用在 API 身上。而每一種模式都有其先後順序,在實作時要注意,否則可能會造成 API routes 看起來很混亂,讓後續維護 API 時感到困擾。

Reference


上一篇
Day11 - 在 Next.js 中使用 CSR - feat. useSWR
下一篇
Day13 - 重構產品頁面 API,使用 API routes - feat. MongoDB
系列文
從零開始學習 Next.js30

尚未有邦友留言

立即登入留言