🚄

[App Router]Route Handlersを利用してサーバーサイドで動的なエンドポイントへfetchする

2024/05/10に公開
2

App RouterでRoute Handlersの実装を行った際に、書き方のお作法が分からず苦戦しました。
ユースケースと共に解説し、皆様の一助になれば幸いです。

なお、参考サイトはnextの公式ドキュメントから引用していますが、古い情報がある場合は教えていたでけると嬉しいです。

ユースケース

  • リモートで鍵の開閉を行うサービス(Akerun API)を利用して、鍵の開閉を行いたい
  • 鍵を開けるフローが二段階ある
    1. 鍵の解錠をリクエスト ⇒ 成功すると job_id が返ってくる
    2. job_id を利用して、解錠ステータスを取得する
  • job_id は解錠リクエストごとに異なるため、解錠ステータスを確認するためのエンドポイントは毎回異なる

要件

  • next14(app router)
  • Akerun APIへリクエストを投げる
  • .envファイルからtokenやapiキーなど秘匿性の高い情報を受け取る(サーバーサイドで実行)
  • 鍵情報のステータスを確認する際、エンドポイントが動的に変わる

実装

まずapp/api 配下に今回のエンドポイントを作成していきます

鍵の解錠リクエスト(POST)

.envファイルから tokenや企業IDなどを取得します。サーバーサイドで実行されるため、.envファイルの読み込みが可能です。

クライアントサイドで行うと、コードに直書きする必要があり、セキュリティ上の問題があります。

app/api/akerun/unlock/route.ts

export async function POST() {
  const endpoint = `https://api.akerun.com/v3/organizations/${process.env.AKERUN_ORG_ID}/akeruns/${process.env.AKERUN_ID}/jobs/unlock`;

  try {
    const response = await fetch(endpoint, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.AKERUN_TOKEN}`,
      },
    });

    const data = await response.json();

    if (!response.ok) {
      return Response.json({ message: "Failed to unlock the door" });
    }

    return Response.json({ message: "Success post unlock the door", jobId: data.job.id });
  } catch (error: any) {
    return Response.json({ message: error });
  }
}

鍵の解錠ステータス確認(GET)

これも.envファイルのtokenや企業IDなどを利用します。また、鍵の解錠リクエストで返ってくる job_id を利用してステータスを確認します。

job_id が毎回変わるので、動的に取得する必要があります。ここがポイントです。

App Routerはダイナミックルーティングを使ってページを動的に出力することができますが、APIディレクトリでも同じことができます。

(ちょっと脱線すると…)

ダイナミックルーティングはCMS管理しているコンテンツの詳細ページを表示する際によく使う技術です。

例えば、newsをCMS管理している場合

  • 一覧ページ ⇒ /news/page.tsx
  • 詳細ページ ⇒ /news/[id]/page.tsx

(本題に戻る)

ドキュメントが見つからなかったのですが、第一引数のrequestがないとparamsを取得できません。どこに載っているのか教えていただけると嬉しいです。

鍵の解錠POSTを行ってから、鍵が開くまでステータスを何回か呼びに行くため、キャッシュをオフにする必要があり、 cache: "no-store" をheadersに渡しています。

app/api/akerun/unlockStatus/[id]/route.ts

// MEMO: 引数のrequestは必須っぽい
export async function GET(request: Request, { params }: { params: { id: string } }) {
  const job_id = params.id;

  const endpoint = `https://api.akerun.com/v3/organizations/${process.env.AKERUN_ORG_ID}/jobs/unlock/${job_id}`;
  try {
    const response = await fetch(endpoint, {
      method: "GET",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.AKERUN_TOKEN}`,
      },
      cache: "no-store",
    });

    const data = await response.json();

    if (!response.ok) {
      return Response.json({ message: "Failed to get unlock status" });
    }

    return Response.json({ message: "Success get unlock status", data: data.job });
  } catch (error: any) {
    return Response.json({ message: error });
  }

次にエンドポイントの利用コードについて記載します。

鍵解錠エンドポイントの利用

hooksに処理を格納している。

fetch("/api/akerun/unlock") のようにapi routesを利用すると、サーバーサイドで行う処理が分離できてスッキリしますね。クライアントからapiを叩きにいかないので、処理速度も速いと言われています。

hooks/key/useAkerun.ts

/**
 * 鍵を開けるAPI
 */
export const postOpenDoor = async (): Promise<string | null> => {
  const response = await fetch("/api/akerun/unlock", {
    method: "POST",
  });
  const data = await response.json();
  return data.jobId;
};

鍵解錠ステータス取得用エンドポイントの利用

同じくhooksに格納している。

point: 解錠ステータスを確認するためには jobId を渡す必要があるので、ダイナミックルーティングで渡します。

ここがポイントです。

fetch(`/api/akerun/unlockStatus/${jobId}`)

また、蛇足ですが、fetchはGETがデフォルトなので、第二引数で method: "GET" のように書く必要はありません。

hooks/key/useAkerun.ts

/**
 * 鍵を開けるAPIのステータスを取得する関数
 */
const fetchStatusOpenDoor = async (jobId: number): Promise<openDoorStatusResponse> => {
  const response = await fetch(`/api/akerun/unlockStatus/${jobId}`);
  const data = await response.json();

  return data.job;
};

ハマったポイント

Route Handlersのファイル名は固定

App Routerにおいて、page.tsxlayout.tsxなどファイル名をNext.jsのルールに揃えなければなりません。サーバーサイドで実行するので、ファイル名は route.ts でなければなりません。

参考)File Conventions

Route Handlersの関数名はHTTPメソッドでなければならない

参考記事を探していた際に関数名が handler となっていた。それに倣って書いていたら、 405 Method Not Allowed となってしまう。
関数名がhandlersとなっている記事はPages Routerでエンドポイントを作成する場合の書き方です。
Route HandlersとAPI Routeが混同していたのが原因です。

  • App Router => Route Handlers
  • Pages Router => API Routes

Pages Routerの場合(API Routesの書き方)

import type { NextApiRequest, NextApiResponse } from 'next'
 
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'POST') {
    // Process a POST request
  } else {
    // Handle any other HTTP method
  }
}

参考)

App RouterでRoute Handlersを使う場合関数名はGET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS のいずれか。それ以外だと 405 Method Not Allowed が返ってくる。

また、名前付きエクスポートなので、 export default async function POST(){ のような形式もダメです🙅‍♂️

参考)Supported HTTP Methods

Route HandlersのレスポンスはFetch APIのResponseを利用する

Response.json() を利用して返却するデータをjson化します。

🙅‍♂️: オブジェクトをそのまま返却する

export async function POST() {
  ...
  return { message: "Success post unlock the door", jobId: data.job.id };
  ...
}

🙆‍♂️: Response メソッドを利用

export async function POST() {
  ...
  return Response.json({ message: "Success post unlock the door", jobId: data.job.id });
  ...
}

鍵解錠エンドポイントの利用でmethodに”POST”を指定しなかったら、値が取得できなかった

fetch apiのHTTPメソッドはデフォルトで GET です。

ですので、 method:"POST" を指定しなかった際には405エラーになります。

参考)Supported HTTP Method

GET http://localhost:3000/api/akerun/unlock/ 405 (Method Not Allowed)

Route Handlersをダイナミックパスで指定できることを知らなかった

api routesでエンドポイントを作るのはわかったけど、 jobId をどのようにして渡すのか分からず、苦戦していました。

また、第一引数にrequestを渡さないと、動的なパラメータを取得できないところにハマりました。いまだに、第一引数にrequestを渡さないといけないのがなぜなのか不明なので、もし知っている方がおりましたら、コメントにてご教示いただけると幸いです。

export async function GET(request: Request, { params }: { params: { id: string } }) {
...
}

参考)Dynamic Route Segments


以上となります。

まだまだひよっこフロントエンドなのですが、少しでもお役に立てれば嬉しいです!

Discussion

Honey32Honey32

失礼します。「api routes」とありますが、この記事で使用されているのは「Route Handler」という App Router の機能です。(「api routes」 は Pages Router の機能の名前です)

default export ではなく GET POST のような関数名にする必要があるのも、この両者の違いによるものです。

https://nextjs.org/docs/app/building-your-application/routing/route-handlers

https://nextjs.org/docs/pages/building-your-application/routing/api-routes

maxmax

Honey32様

コメントありがとうございます!
Route HandlerとAPI Routesを混同しておりました。ドキュメントもありがとうございます。
訂正させていただきます🙏