👀

【Next.js / axios】APIへの値の渡し方を整理する〜パスパラメータ、クエリパラメータ、リクエストボディの違い〜

2024/04/05に公開

経緯

Next.jsの勉強として、Clerkによるログイン認証とprismaをデータ管理を実装したTODOアプリを開発中です。APIのGETメソッドを実行して、現在ログインしているユーザーIDに合致するTODOを取得する際、、フロントエンドからバックエンドへどうやってユーザーIDを渡すべきかに困りました。3種類の値の渡し方を整理します。

前回の記事
https://zenn.dev/k_zumi_dev/articles/26fb15263a326a

こちらの記事を参考にさせていただきました。
https://qiita.com/Shokorep/items/b7697a146cbb1c3e9f0b

環境

  • Next.js
  • axios
  • prisma
  • Clerk(本筋には関係ない)

REST APIとは

とてつもなく雑に言うと「みんながこうやってAPIを設計したら、シンプルで便利だよね!」と、取り決められた設計原則に基づいて作られたAPIです。
(なので他の設計原則に基づいて作られたAPIも当然ある。)

原則 説明
ステートレス性 それぞれのリクエストは他と完全に独立しており、以前の通信の内容に左右されることなく処理される。
統一インターフェース 情報の交換にはHTTPプロトコルによる標準化された手法が用いられ、GET、POST、PATCH、PUT、DELETEのメソッドが採用される。データ交換フォーマットにはJSONやXMLが使用される。
アドレス可読性 交換されるすべての情報は、一意に定義されたURIによって識別され、各リソースはIDなどの識別子をURIに含む。
接続性 リンクを通じて他のリソースに簡単にアクセスでき、情報の中には別のリソースへのハイパーリンクが含まれることがある。

こちらの記事が参考になります。
https://qiita.com/masato44gm/items/dffb8281536ad321fb08
https://tech.012grp.co.jp/entry/rest_api_basics
https://zenn.dev/arsaga/articles/4a72774b1c93d2

パスパラメータ、クエリパラメータ、リクエストボディの違い

使用シナリオ パスパラメータ クエリパラメータ リクエストボディ
特定のリソースを識別する
リソースの検索条件を指定する
大量のデータや複雑なデータ構造を送信する
リソースの作成を要求する
リソースの更新を要求する(全体または部分的に)
リソースの削除を要求する
リソースの取得方法や表示方法を指定する

パスパラメータ

パスパラメータは、URLのパスの一部として組み込まれています。リソースを指定するのに使われ、通常は特定のリソースを識別するための一意のIDがパスパラメータとして渡されます。

目的: 特定のリソースのアクセス先を指定する。
特徴: URLの構造の一部となるため、リソースの階層関係を直感的に示すことができる。

https://example.com/api/users/123

クエリパラメータ

クエリパラメータは、URLの末尾に?に続けて追加され、キーと値のペアで構成されます。フィルタリング、ソート、検索条件など、リクエストに付加的な情報を渡すのに使います。

目的: リソースの検索条件や操作オプションを指定する。
特徴: 複数のパラメータを&で連結して送信できる。リソース自体ではなく、その取得方法や表示方法を制御する。

https://example.com/api/users?age=20&country=japan

具体例をあげると、Amazonの検索欄で「お茶」を検索してみると、キーワードが「お茶」になっているURLに飛びます。

https://www.amazon.co.jp/s?k=お茶

リクエストボディ

リクエストボディは、POSTやPUTなどのHTTPメソッドを使用してサーバーにデータを送る際に使用されます。フォームデータやJSON形式のデータを含めることができ、新しいリソースの作成や既存リソースの更新に使われます。

目的: 新しいリソースの作成や既存リソースの更新。
特徴: 大量のデータや複雑なデータ構造を送信するのに適している。

// POSTで "https://example.com/api/users" に以下のJSONを送る
{
  "name": "Taro",
  "age": 30
}

パスパラメータとクエリパラメータの使い分け方

あるリソースを特定するときに必要な場合は→パスパラメータ
操作に条件を追加するとき→クエリパラメータ

この記事が大変わかりやすかったので参照してください。
https://zenn.dev/eri_agri/articles/859a3362db8386

HTTPメソッド(GET, POST, PUT, DELETE)ではどの送信方法を使うべきか?

それぞれのメソッドが想定している値の受け渡し方法が存在しています。

例えばGETメソッドではリクエストボディは使えません。私はこのあたりを理解していなかったので、フロント側から現在のログインユーザーのuserIdをGETメソッドに渡す際に、リクエストボディを使おうとしてエラーを吐きました。

HTTPメソッド / 使用シナリオ パスパラメータ クエリパラメータ リクエストボディ
GET
POST
PUT
DELETE
ChatGPT 非推奨(△マーク)の理由

非推奨(△マーク)に指定されている使用法についての理由を詳しく説明します。

GETメソッドとリクエストボディ
非推奨の理由: GETメソッドはリソースを取得するために使われ、その操作は副作用を持たないべきです。HTTP/1.1仕様ではGETリクエストのボディについて明確な禁止はしていませんが、ボディの内容をリクエストのセマンティクスに含めるべきではないと述べています。つまり、GETリクエストのボディがサーバー側で無視される可能性があり、また多くのHTTPクライアントやキャッシングシステムでは考慮されないため、データ送信には不適切です。
影響: クライアントとサーバー間での予期せぬ振る舞いを引き起こし、キャッシュの問題やセキュリティリスクを生じる可能性があります。

POST、PUTメソッドとクエリパラメータ
限定的使用の理由: POSTとPUTはリソースの作成や更新を目的としたメソッドで、主にリクエストボディを通じてデータを送信します。クエリパラメータはこれらの操作において主要なデータ伝達手段ではありませんが、操作をさらに指定するための追加情報(例: バージョン番号や操作オプション)を提供する補助的な手段として使用することは可能です。
影響: クエリパラメータを使用すること自体が問題ではありませんが、リクエストの主要な内容を伝達するためにリクエストボディを優先するべきです。

DELETEメソッドとリクエストボディ
非推奨の理由: DELETEメソッドは特定のリソースを削除するために使用されます。仕様ではDELETEリクエストのボディがサーバーによって無視される可能性があるとしています。そのため、削除対象のリソースをURLのパスパラメータで指定するのが一般的です。リクエストボディを使用して削除対象の詳細情報を送信することは技術的に可能ですが、この方法は広く受け入れられておらず、予期せぬ振る舞いを引き起こす可能性があるため、非推奨とされています。
影響: サーバーによってはリクエストボディを正しく処理しない場合があり、結果として不適切なリソース管理につながる可能性があります。

DELETEメソッドとクエリパラメータ
非推奨の理由: DELETEメソッドはリソースの削除を目的として使用され、主にURLのパスパラメータを通じて特定のリソースを指定します。クエリパラメータを使って削除対象のリソースを指定する方法は、URLの構造上、操作の意図や対象が不明確になる可能性があるため、非推奨です。また、削除操作に関する重要な情報をクエリパラメータに含めることは、セキュリティ上のリスクを高める可能性があります。たとえば、誤ったクエリパラメータの使用が意図しないリソースの削除を引き起こす恐れがあります。
影響: クエリパラメータの使用は、リクエストの意図が明確でない、またはセキュリティ上の問題を引き起こす可能性があるため、DELETE操作では推奨されません。操作の意図を明確にし、誤ったリソース操作を避けるために、削除対象のリソースをURLのパスパラメータで指定する方法が一般的に採用されます。

実践編:Next.js & axios環境でデータの受け渡しをする場合

私が実際のTODOアプリで実装した方法を紹介します。
prismaでデータベース操作をしています。

事前にClerkからuserを取得しています

// ログイン中のユーザー情報を取得
  const { user } = useUser();

GET:クエリパラメータを使った

userIdに合致するデータを検索して取得したいので、クエリパラメータが適切です。
axiosでクエリパラメータを渡す場合は、paramsオブションを使います。

バックエンドのコードはapi/route.tsに書きます。

フロントエンド
 const response = await axios.get("/api/todo", {
   params: { userId: user.id }, 
 });
バックエンド api/route.ts
"use client";
〜〜〜省略〜〜〜
export async function GET(req: NextRequest) {
  const searchParams = req.nextUrl.searchParams;
  const userId = searchParams.get("userId"); // クエリパラメータからuserIdを取得

    if (!userId) {
    return new NextResponse("userIdがありません", { status: 400 });
  }

  const todos = await db.todo.findMany({
    where: {
      userId: userId, // userIdに一致するTODOを取得
    },
  });

  return NextResponse.json(Todos);
}

バックエンドでクエリパラメータを受け取る場合は、useSearchParamsを使います。
今回はクライアントコンポーネントの中で実行しているので簡単ですが、サーバーコンポーネントの中で実行する場合はまた別の書き方が必要になるようです。(私は未検証)
https://zenn.dev/igz0/articles/e5f6f08b6cbe1d
https://nextjs.org/docs/app/api-reference/functions/use-search-params

また、userIdの存在確認も入れています。
これを入れておかないと、db.todo.findMany内で型エラーが出ます。

バックエンド
    if (!userId) {
    return new NextResponse("userIdがありません", { status: 400 });
  }

POST:リクエストボディを使った

更新内容自体を送るときはリクエストボディが適切です。
今回は全体のTODOを管理するTodoテーブルに対してデータを新規作成するので、パスパラメータは不要です。

axiosでリクエストボディを送る場合は、第二引数にオブジェクトを渡します。

GETと同様にバックエンドのコードはapi/route.tsに書きます。

フロントエンド
const response = await axios.post("/api/todo", {
  text,
  userId: user.id,
});
バックエンド api/route.ts
export async function POST(req: NextRequest) {
  const { text, userId } = await req.json();
  console.log(text);

  const todo = await db.todo.create({
    data: {
      text: text,
      userId: userId,
    },
  });
  return NextResponse.json(todo);
}

PUT:パスパラメータとリクエストボディを使った

TODOの完了未完了を切り替えるコードです。
idは特定のリソースを指定するものなのでパスパラメータ、completedはリソースの状態を表すものなのでリクエストボディで送るのが適切です。

パスパラメータを使う場合はaxiosでリクエストを送るURL自体がこれまでと変わります。ダイナミックルートを使って、api/[id]/route.tsにバックエンドのコードを置きます。

https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes

フロントエンド
const response = await axios.put(`/api/todo/${id}`, {
   completed,
});
バックエンド api/[id]/route.ts
export async function PUT(
  req: NextRequest,
  { params }: { params: { id: string } } // パスパラメータからidを取得
) {
  const id = params.id;

  const { completed } = await req.json(); // リクエストボディからcompletedを取得

  console.log(id, completed);

  if (!id) {
    return new NextResponse("idがありません", { status: 400 });
  }

  const changeFlag = await db.todo.update({
    where: {
      id: id,
    },
    data: {
      completed: {
        set: !completed,
      },
    },
  });
  return NextResponse.json(changeFlag);
}

DELETE:パスパラメータを使った

TODOの表示の削除ボタンをクリックしたときの操作です。
ここではクリックされたTODO自体のidを指定して、削除したいのでパスパラメータが適切です。

PUTと同様に、ダイナミックルートを使って、api/[id]/route.tsにバックエンドのコードを置きます。

フロントエンド
const response = await axios.delete(`/api/todo/${id}`);
バックエンド api/[id]/route.ts
export async function DELETE(
  req: NextRequest,
  { params }: { params: { id: string } } // パスパラメータからidを取得
) {
  const id = params.id;

  if (!id) {
    return;
  }

  const deleteTodo = await db.todo.delete({
    where: {
      id: id,
    },
  });
  return NextResponse.json(deleteTodo);
}

APIへのRequest URLの確認方法

Chromeの開発者モード→上部のNetwork→確認したいリクエストをクリック→真ん中あたりのHeaders→Request URLが確認できるので、正しいURLにリクエストが送れているかが確認できます。

まとめ

  • パスパラメータ
    • /の後
    • クエリパラメータと同時に使った場合は?の前まで
    • 特定のリソースを識別するために必要な情報
  • クエリパラメータ
    • ?の後
    • 特定のリソース操作して取得する際に必要な情報
  • リクエストボディ
    • JSONで送る
    • データ自体を送るときに使う

これらは組み合わせて使うことがある。

一度知ってしまえば当たり前のことかもしれませんが、Webに馴染みがない状態からスタートするとなかなか苦労するポイントですね。
また、Next.js, axios, prismaの環境での参考になるコードが少なく、適切な書き方を探すのにも一苦労でしたので、これが誰かの参考になれば幸いです。

Discussion