💨

App Router の Route Handlers をうまくプロダクトに組み込む

2024/08/23に公開

この記事でやること

  • Next.js の App Router の基本的な学習
  • Route Handlers の基本的な学習

この記事でやらないこと

  • 外部との連携

モチベーション

せっかく Route Handlers という便利そうな機能が存在するのに、その機能についての知見が足りていないと感じたので、この機会に少し学習しておこうと考えました。

いざ実践

今回実現すること

  • Route Handlers 経由でデータを取得する
  • Server Actions を使って Route Handlers に対して POST リクエストを送信する

データを取得する方法

まずはデータを取得するための API を作成します。今回は以下のようなデータ構造が存在するということにして、実装してみます。

// User
{
  "id": number,
  "name": string,
  "email": string,
}

ディレクトリ構成とファイル配置はこのような感じです。

/src/app/api/users/route.ts

Route Handlers は命名規約として、 route.ts という名前を使います。

src/app/api/users/route.ts
export type User = {
  id: number;
  name: string;
  email: string;
};

export const GET = async () => {
  // 今回は簡単に、関数内でデータを用意する
  const users: User[] = [
    {
      id: 1,
      name: "John Doe",
      email: "john@example.com",
    },
    {
      id: 2,
      name: "Jane Doe",
      email: "jane@example.com",
    },
  ];

  return Response.json(users);
}

このように、 GET という名前の関数を作ります。例えばこれが POST リクエストを受け付ける API を作りたいなら、 POST という名前の関数を作ることになります。
return するのは Response に生えている json という関数です。この ResponseFetch API の一部なのですが、 Next.js で使う場合はこれ自体が Next.js 用にオーバーライドされた特殊なものになっています。

続いて、フロントエンドの実装です。

src/app/users/layout.tsx

import { User } from "@/app/api/users/route";
import Link from "next/link";

const fetchUsers = async (): Promise<User[]> => {
  const response = await fetch("http://localhost:3000/api/users");
  return response.json();
}

export default async function Home({
  children,
}: {
  children: React.ReactNode;
}) {
  const users = await fetchUsers();

  return (
    <main>
      <div className="container left">
        <header>
          <h1>ユーザー一覧</h1>
        </header>
        <div>
          <ul>
            {users.map((user) => (
              <li key={`user-${user.id}`}>
                {user.name}
              </li>
            ))}
          </ul>
        </div>
        <div className="container right">
          {children}
        </div>
      </div>
    </main>
  );
}

今回は2カラムにして、左カラムにユーザー一覧、左カラムで選択したものが右カラムに表示される、というような UI にしようと考えています。なので、レイアウトを利用してその UI を実現します。
データ取得のところがキモで、このような fetch の利用方法は、Server Components / Route Handlers / Server Actions で利用できます。

ここまでで、 Server Components でのデータ取得に Route Handlers を使う、という部分が完成です。楽ちんすぎる。

POST リクエストを送る方法

その前に、ユーザーの詳細情報を取得する API と UI を作っておきます。

src/app/data/users.ts
export const users = [
  {
    id: 1,
    name: "John Doe",
    email: "john@example.com",
  },
  {
    id: 2,
    name: "Jane Doe",
    email: "jane@example.com",
  },
];
src/app/api/users/[id]/route.ts
import { users } from "@/app/data/users";

export const GET = async (
  req: Request,
  { params }: { params: { id: string } }
) => {
  const user = users.find((user) => user.id === Number(params.id));
  if (user == null) {
    return Response.json({ error: "User not found" }, { status: 404 });
  }

  return Response.json(user);
};
src/app/users/[id]/page.tsx
import Link from "next/link";

const fetchUser = async (id: string) => {
  const response = await fetch(`http://localhost/api/users/${id}`);
  return response.json();
};

export default async function UserPage({
  params: { id },
}: {
  params: { id: string };
}) {
  const user = await fetchUser(id);

  return (
    <>
      <h1>{user.name}</h1>
      <div>
        <dl>
          <dt>Email
          <dd>{user.email}</dd>
        </dl>
      </div>
      <div>
        <Link href={`/users/${user.id}/edit`}>Edit</Link>
      </div>
    <>
}

このようにして、 Link コンポーネントを使って、編集画面へのリンクを作成します。
続いて、「ユーザー情報を編集する画面」と、「データを送信する API 」を実装していきます。
実際のデータ更新はそれぞれの実装があると思うのでご自由にやっていただくとして、リクエストが成功した場合は元の詳細画面にリダイレクトすることにします。

src/app/api/users/[id]/route.ts
// 先に作ったものの下に追加する

export const POST = async (req: Request) => {
  const formData = await req.formData();
  const data = {
    name: formData.get("name"),
    email: formData.get("email"),
  };

  // なんらかのデータミューテーション
  return Response.json({ data });
};
src/app/users/[id]/edit/page.ts
import { redirect } from "next/navigation";

const fetchUser = async (id: string) => {
  // 詳細画面と同じ実装なのでスルー
};

const action = async (id: string, form: FormData) => {
  "use server";

  await fetch(`http://localhost:3000/api/users/${id}`, {
    method: "POST",
    body: form,
  });

  redirect(`/users/${id}`);
};

export default async function UserEdit({
  params: { id },
}: {
  params: { id: string };
}) {
  const user = await fetchUser(id);
  const updateUser = action.bind(null, user.id);

  return (
    <>
      <h1>ユーザー情報を編集する</h1>
      <form action={updateUser}>
        <div>
          <label htmlFor="user_name">名前</label>
          <input
            type="text"
            name="user_name"
            id="user_name"
            defaultValue={user.name}
          />
        </div>
        <div>
          <label htmlFor="user_email">メールアドレス</label>
          <input
            type="text"
            name="user_email"
            id="user_email"
            defaultValue={user.email}
          />
        </div>
        <div>
          <button type="submit">送信</button>
        </div>
      </form>
    </>
  );
}

今回は Server Actions でフォームを実装しました。
Server Actions な関数を作るためには、 "use server" ディレクティブを関数内に書いてあげる必要があります。

const action = async () => {
  "use server";

  ...
}

要するに、「サーバーサイドでクライアントサイドみたいな非同期処理を動かす仕組み」ということです。
また、 formaction には、デフォルトでは引数をこちらから挿入することはできません。なので、 JavaScript 的な機能の bind を用いて、「ユーザーのID」を渡しています。

const updateUser = action.bind(null, user.id);

...

<form action={updateUser}>

これで action 関数の方では、引数として idformData を受け取ることが可能になる、ということです。

まとめ

今回は僕が思う App RouterServer ActionsRoute Handlers を扱うベターなプラクティスを実践してみました。
一応これらのコードはローカル開発中に動くことが確認されたコードになりますが、もし動かないぜ!などありましたらコメントなどでご連絡いただければと思います。

Discussion