0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js App Routerで学ぶServer Actions:安全なフォーム処理とバリデーションの実装

0
Posted at

Next.jsのApp Routerでは、Server Actions(Server Functions)を利用することで、フォーム送信やデータ更新などの処理をサーバー側で実行できます。

従来のReactアプリケーションでは、フォームからAPIへリクエストを送信し、サーバー側でデータを処理する構成が一般的でした。

一方、Next.jsではServer Functionsを利用することで、フォームとサーバー処理をより簡潔に接続できます。

ただし、実装が簡単になる一方で、入力値の検証や認可処理を省略してよいわけではありません。

本記事では、Next.js App Routerを利用したフォーム処理の基本と、安全な実装方法を紹介します。

1. Server Actionsとは?

Server Actionsは、サーバー側で実行される非同期関数をフォーム送信などから呼び出す仕組みです。

例えば、ユーザーがフォームへ入力した情報をサーバーで処理する場合、次のような流れになります。

User
 ↓
HTML Form
 ↓
Server Action
 ↓
Validation
 ↓
Database / Business Logic
 ↓
Response

この構成では、クライアント側にデータベース接続情報などを公開せずに処理できます。

ただし、Server Action自体はクライアントから呼び出せるサーバー側の入口になるため、認証・認可の確認が必要です。

2. Next.jsプロジェクトを準備する

まず、新しいNext.jsプロジェクトを作成します。

npx create-next-app@latest next-form-demo

作成時には、TypeScriptとApp Routerを有効にします。

プロジェクトへ移動します。

cd next-form-demo

今回はフォームのバリデーションにZodを利用します。

npm install zod

Zodを利用すると、サーバー側で入力データの形式を検証できます。

3. 基本的なServer Actionを作成する

まず、次のファイルを作成します。

app/actions.ts

"use server";

export async function submitMessage(
  formData: FormData
) {
  const name = formData.get("name");

  console.log("Received:", name);

  return {
    success: true
  };
}

"use server"を指定することで、このファイルからエクスポートされる非同期関数をサーバー側で実行できます。

続いてフォームを作成します。

app/page.tsx

import { submitMessage } from "./actions";

export default function Home() {
  return (
    <main>
      <h1>Contact Form</h1>

      <form action={submitMessage}>
        <input
          type="text"
          name="name"
          placeholder="Your name"
          required
        />

        <button type="submit">
          Send
        </button>
      </form>
    </main>
  );
}

このように、フォームのaction属性へServer Actionを指定できます。

ただし、現時点では入力値を検証していないため、本番環境で利用するには追加の処理が必要です。

4. Zodで入力値を検証する

Webアプリケーションでは、クライアント側の入力制限だけでは不十分です。

HTMLのrequired属性などはユーザー体験の改善には役立ちますが、サーバー側の検証を代替するものではありません。

Zodを利用して入力スキーマを定義します。

import { z } from "zod";

export const messageSchema = z.object({
  name: z.string().trim().min(1).max(50),
  email: z.email(),
  message: z.string().trim().min(1).max(500)
});

この例では、名前、メールアドレス、メッセージに制限を設定しています。

次に、Server Actionへ適用します。

"use server";

import { messageSchema } from "./schema";

export async function submitMessage(
  formData: FormData
) {
  const result = messageSchema.safeParse({
    name: formData.get("name"),
    email: formData.get("email"),
    message: formData.get("message")
  });

  if (!result.success) {
    return {
      success: false,
      message: "Invalid input"
    };
  }

  return {
    success: true,
    message: "Message received"
  };
}

safeParse()を利用すると、検証結果を例外ではなくオブジェクトとして取得できます。

これにより、入力エラーを適切に処理できます。

5. 認証と認可を忘れない

Server Actionsを利用する場合、特に重要なのが認証と認可です。

例えば、ユーザー情報を変更する処理では、ログイン状態を確認する必要があります。

概念的には次のような構成です。

"use server";

import { auth } from "@/lib/auth";

export async function updateProfile(
  formData: FormData
) {
  const session = await auth();

  if (!session?.user?.id) {
    throw new Error("Unauthorized");
  }

  // 入力値を検証する
  // 対象リソースへの更新権限を確認する
  // 許可されたデータのみ更新する
}

ここでのauth()は、利用する認証ライブラリに応じて実装する関数です。

重要なのは、フォームから送信されたユーザーIDをそのまま信用しないことです。

ユーザーの識別情報は、サーバー側で検証済みのセッションから取得する方が安全です。

また、認証済みであることと、特定のデータを更新する権限があることは別問題です。

6. Pending状態を表示する

フォーム送信中に何も変化がないと、ユーザーは処理が実行されているのか判断できません。

ReactのuseFormStatusを利用すると、フォーム送信中の状態を取得できます。

app/submit-button.tsx

"use client";

import { useFormStatus } from "react-dom";

export function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button
      type="submit"
      disabled={pending}
    >
      {pending ? "Sending..." : "Send"}
    </button>
  );
}

フォーム内で利用します。

<form action={submitMessage}>
  <input name="name" required />
  <input name="email" type="email" required />
  <textarea name="message" required />

  <SubmitButton />
</form>

これにより、送信中の状態をユーザーへ伝えられます。

なお、ボタンを無効化するだけでは重複リクエストを完全に防げません。

重要な更新処理では、サーバー側でも冪等性や重複処理への対策を検討する必要があります。

7. データベース更新時の注意点

Server Actionsはデータベース更新処理にも利用できます。

ただし、データベースへアクセスする場合は、以下の点を確認します。

  • 入力値を検証する
  • 更新対象へのアクセス権限を確認する
  • パラメータ化されたクエリを利用する
  • エラー情報を適切に処理する
  • 必要に応じてトランザクションを利用する

特に複数のテーブルを同時に更新する場合は、途中で処理が失敗したときの整合性を考慮する必要があります。

Server Actionsは便利な実装手段ですが、データベース設計の基本原則が不要になるわけではありません。

8. キャッシュと画面更新を考える

フォーム送信後にデータが更新された場合、画面へ最新情報を反映する必要があります。

Next.jsでは、利用しているキャッシュ方式に応じてrevalidatePath()などを利用できます。

"use server";

import { revalidatePath } from "next/cache";

export async function updateContent() {
  // 認証・認可・入力検証
  // データベース更新処理

  revalidatePath("/dashboard");
}

この例では、更新処理の成功後に指定したパスのキャッシュを再検証します。

ただし、すべてのデータ取得処理が自動的にキャッシュされるわけではありません。

利用中のNext.jsのバージョンやキャッシュ設定を確認することが重要です。

9. デジタルプラットフォームでの活用

モバイルユーザーを意識したWebサービスでは、フォーム操作の簡潔さと応答性が重要です。

例えば、WOW88 のようなデジタルプラットフォームを技術的な設計例として考える場合、問い合わせフォーム、プロフィール設定、コンテンツ検索条件の保存などにServer Actionsの考え方を応用できます。

特に、フォーム送信中の状態表示、入力エラーの明確化、サーバー側での検証を組み合わせることで、操作性と安全性の両方を考慮した設計が可能になります。

ただし、すべての処理をServer Actionsへ集約する必要はありません。

外部サービス向けAPIやWebhookなどでは、Route Handlersを利用する方が適切な場合もあります。

10. 実装時のチェックリスト

Server Actionsを本番環境で利用する前に、次の項目を確認すると便利です。

□ Server Actionで入力値を検証している
□ 認証が必要な処理でセッションを確認している
□ リソース単位で認可を実施している
□ 機密情報をクライアントへ公開していない
□ エラー内容を適切に処理している
□ フォーム送信中の状態を表示している
□ 重複送信への対策を検討している
□ 更新後のキャッシュ動作を確認している
□ データベース操作の整合性を確認している

このようなチェックリストをコードレビューに取り入れることで、実装上の問題を発見しやすくなります。

まとめ

Next.jsのServer Actionsは、フォーム処理やデータ更新をシンプルに実装するための便利な仕組みです。

しかし、実装が簡潔になることと、セキュリティが自動的に保証されることは異なります。

特に重要なのは、以下のポイントです。

  • Server Actionsの基本的な仕組みを理解する
  • Zodなどを利用して入力値を検証する
  • サーバー側で認証と認可を実施する
  • useFormStatusで送信状態を表示する
  • データベース更新時の整合性を考慮する
  • キャッシュの更新方法を適切に選択する

Next.jsの機能を活用しながら、セキュリティ、パフォーマンス、ユーザー体験をバランスよく設計することが、保守しやすいWebアプリケーションの開発につながります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?