Next.js 是一個全端框架,除了提供 SSR 與 SSG 的功能之外,還能夠建立 API 提供前端頁面使用。
你可以使用 API routes 建立 REST API,如果有 graphql 的需求,也可以用來建立 graphql API。官方文件提供了許多的範例,讓我們可以快用用一些模板建立服務:
在這篇文章中將以 REST API 作為範例,體驗 Next.js 的 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。

我們從 req 與 res 的型別定義中可以看到:
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") {
// 刪除產品資料
}
}
既然 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。

以部落格的例子來說,一篇貼文的 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 。
這是 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] }
Next.js 為了讓 API 定義更彈性一點,提供了各種不同的 API routes 的定義模式,包括以下幾種:
/api/posts.ts
/api/posts/[id].ts
/api/posts/[...date].ts
/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 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 時感到困擾。