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


<<:  鬼故事 - 为什麽骇客一直回来

>>:  @Day27 | C# WixToolset + WPF 帅到不行的安装包 [额外的DLL引用]

Thunkable学习笔记 2 - 加入Firebase登入功能(使用EMail)

Firebase提供现成的email/password验证功能供我们使用, thunkable提供s...

[Day12]Die Game

上一篇介绍了Bangla Numbers,由於数字太大,所以选择使用了BigInteger,但各位也...

@Day6 | C# WixToolset + WPF 帅到不行的安装包 [自订页面-官方UI页面结构]

如果 今天想要用 WixToolset 去做自订的页面了话 (ex:我们要设定系统要修改HelloW...

【D12】发现新厨具:Snapshot

前言 有个Snapshot的功能,可以看当下的商品状况,让我们看看这个功能可以做啥吧! 参考网站:S...

资料库连接练习

我们继续熟悉资料库连接的操作吧! 这次是沿用漫画爬虫的程序码,但我目前只要两笔资料:漫画编号、漫画名...