まくろぐ

Next.js の API Routes 機能で Web API を作成する

更新:
作成:

Next.js の Web API 機能

Next.js では、pages/api ディレクトリ以下に TypeScript (JavaScript) コードを配置するだけで、クライアントサイド JavaScript から呼び出せる API を定義することができます。

例えば、次のようなファイルを作成します。

pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next'

type Response = {
  name: string
}

export default (req: NextApiRequest, res: NextApiResponse<Response>) => {
  res.status(200).json({ name: 'John Doe' })

  // チェーン呼び出しせずに次のように記述しても OK
  // res.statusCode = 200
  // res.json({ name: 'John Doe'})
}

あとは、Next.js サーバーを起動した状態で、/api/hello というエンドポイントにアクセスすると、次のような JSON データを取得できます。

{"name":"John Doe"}

API 機能は次のような用途に使用することができます。

  • フォームに入力された値が POST されたときにサーバーサイドで DB に保存する
  • 3rd パーティ製の Web API の呼び出しを中継する

このような機能を実装するには、データベースのパスワードや、3rd パーティ製 Web API のアクセスキーなどが必要になりますが、そういった情報は Next.js サーバ側の環境変数などに保存しておくことができます。 そうすれば、API の実装コードから process.env.XXX_ACCESS_KEY のように参照できます。 pages/api ディレクトリ以下の実装内容が、クライアントに見られてしまうことはありません。

API のコードは Next.js サーバー上で実行されるため、この API 機能を使用するには、Web サイトのホスティング時に Next.js サーバー (next start) が必要です。 必然的に、Vercel のサービス などを使ってホスティングすることになるため、静的サーバー用の HTML ファイル群を生成する next exports コマンドは実行できなくなります(pages/api 以下にファイルを作成すると、next build までしか成功しなくなります)。

クエリパラメーターに対応する

例えば、ゲームの情報を取得する API として /api/games というエンドポイントを定義するとします。 パラメーターとして 1 などのゲーム ID を指定する場合、次のような 2 通りの指定方法が考えられます。

  • /api/games/1 (REST 形式の URL にする)
  • /api/games?id=1 (クエリ文字列を付加する)

以下、それぞれの実装方法を説明します。

REST 形式

/api/games/1 という REST API 風の URL でアクセスしたいときは、通常のページコンポーネントと同様のダイナミックルーティングの機能を使って API を実装します。 例えば、/api/games/1/api/games/2 のような URL をハンドルするには、pages/api/games/[id].ts というファイルを作成します。

id 部分に指定されたパラメーターの値は、ハンドラー関数に渡される NextApiRequest オブジェクトを使って、req.query.id のように参照することができます。

pages/api/games/[id].ts
import type { NextApiRequest, NextApiResponse } from 'next'

export type Game = {
  id: string
  title: string
  genre: string
}

// API のレスポンス型
export type GamesApiResponse = {
  game?: Game
  debugMessage?: string
}

// API のエントリポイント
export default function gamesApi(
  req: NextApiRequest,
  res: NextApiResponse<GamesApiResponse>
): void {
  const id = req.query.id as string
  const game = fetchGameData(id)
  if (game) {
    res.status(200).json({ game })
  } else {
    res.status(400).json({ debugMessage: `Game [id=${id}] not found` })
  }
}

// 擬似的なデータフェッチ関数
function fetchGameData(id: string): Game | undefined {
  const games: Game[] = [
    { id: '1', title: 'ドンキーコング', genre: 'アクション' },
    { id: '2', title: 'ゼビウス', genre: 'シューティング' },
    { id: '3', title: 'ロードランナー', genre: 'パズル' },
  ]
  return games.find((game) => game.id === id)
}

例えば、/api/games/3 というアドレスでアクセスすると、次のような JSON データが返されます。

{"game":{"id":"3","title":"ロードランナー","genre":"パズル"}}

クエリ文字列形式

/api/games?id=1 といったクエリ文字列の形で指定されたパラメータ (?id=1) を取得するには、API を実装するファイルを、pages/api/games.ts あるいは pages/api/games/index.ts という名前で作成します。

パラメーターの参照方法は前述の方法と同じで、req.query.id のように参照できます。

games/api/games.ts
// ...実装方法はまったく同じ...
export default function gamesApi(
  req: NextApiRequest,
  res: NextApiResponse<GamesApiResponse>
): void {
  const id = req.query.id as string
  const game = fetchGameData(id)
  if (game) {
    res.status(200).json({ game })
  } else {
    res.status(400).json({ debugMessage: `Game [id=${id}] not found` })
  }
}

例えば、/api/games?id=2 というアドレスでアクセスすると、次のような JSON データが返されます。

{"game":{"id":"2","title":"ゼビウス","genre":"シューティング"}}

どちらの形式を使うべきか?

どちらでもよいですが、パラメーターが 1 つの場合は REST 形式 (pages/api/games/[id].ts) で定義するとシンプルです。 API を呼び出すときに、いちいち id のようなパラメーター名を指定する必要がありません(呼び出し例: /api/games/1)。

逆にパラメーターを複数指定する可能性があって、その指定順序に制約がない場合は、クエリ文字列を使った形式 (pages/api/games.tsx) で定義するのがよいと思います。 呼び出し時にキー&バリューの形でパラメーターを指定するので、間違った値を指定してしまうミスが減ります(呼び出し例: /api/games?genre=ACT&year=1990)。

React コンポーネントから API を呼び出す

上記のように定義した API を React コンポーネントの実装から呼び出すには、useSWR フックを使用するのが簡単です。 このフックは Vercel が swr パッケージとして提供しています。

swr パッケージのインストール
### yarn の場合
$ yarn add swr

### npm の場合
$ npm install swr

先に、Game インタフェースを共有できるように、ライブラリファイルとして抽出しておきます。

libs/types.ts
export type Game = {
  id: string
  title: string
  genre: string
}

次のコンポーネントでは、クライアントサイド JavaScript で /api/games/1 というエンドポイントの API を呼び出しています。 useSWR フックの型パラメーターとして Game を指定することで、戻り値の data 変数の型が Game | undefined になります(データ取得が完了するまで undefined になる)。

pages/home.tsx
import { NextPage } from 'next'
import useSWR from 'swr'
import type { GamesApiResponse } from '../api/games/[id]'

const HomePage: NextPage = () => {
  const { data, error } = useSWR<GamesApiResponse, Error>('/api/games/1', fetcher)
  if (error) return <p>Error: {error.message}</p>
  if (!data) return <p>Loading...</p>

  return (
    <>
      {data.game && <b>{data.game.title}</b>}
      {data.debugMessage}
    </>
  )
}

export default HomePage
const fetcher = (url: string) => fetch(url).then((r) => r.json())

関連記事

まくろぐ
サイトマップまくへのメッセージ