[App Router]Route Handlersを利用してサーバーサイドで動的なエンドポイントへfetchする
App RouterでRoute Handlersの実装を行った際に、書き方のお作法が分からず苦戦しました。
ユースケースと共に解説し、皆様の一助になれば幸いです。
なお、参考サイトはnextの公式ドキュメントから引用していますが、古い情報がある場合は教えていたでけると嬉しいです。
ユースケース
- リモートで鍵の開閉を行うサービス(Akerun API)を利用して、鍵の開閉を行いたい
- 鍵を開けるフローが二段階ある
- 鍵の解錠をリクエスト ⇒ 成功すると
job_id
が返ってくる -
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.tsx
やlayout.tsx
などファイル名をNext.jsのルールに揃えなければなりません。サーバーサイドで実行するので、ファイル名は route.ts
でなければなりません。
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(){
のような形式もダメです🙅♂️
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エラーになります。
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 } }) {
...
}
以上となります。
まだまだひよっこフロントエンドなのですが、少しでもお役に立てれば嬉しいです!
Discussion
失礼します。「api routes」とありますが、この記事で使用されているのは「Route Handler」という App Router の機能です。(「api routes」 は Pages Router の機能の名前です)
default export ではなく
GET
POST
のような関数名にする必要があるのも、この両者の違いによるものです。Honey32様
コメントありがとうございます!
Route HandlerとAPI Routesを混同しておりました。ドキュメントもありがとうございます。
訂正させていただきます🙏