【Next.js × Supabase × Zod】データバリデーション入門

# はじめに

こんにちは！
今回は、**Next.jsの新しいApp Router**、強力なデータバリデーションライブラリである**Zod**、そしてモダンなバックエンドサービスの**Supabase**を組み合わせて、データバリデーションとユーザー管理を実装する方法を詳しく解説します。

このガイドでは、実際のアプリケーションを構築しながら、各ステップでのベストプラクティスやよくある問題の解決方法を学びます。
初心者の方でも安心して取り組めるよう、丁寧に説明していますので、ぜひ最後までお付き合いください。

## 目次

1. [このガイドの目的](#このガイドの目的)
2. [必要な前提知識と環境](#必要な前提知識と環境)
3. [プロジェクトのセットアップ](#プロジェクトのセットアップ)
4. [Zodの導入と活用方法](#zodの導入と活用方法)
5. [Supabaseでのユーザー管理](#supabaseでのユーザー管理)
6. [APIルートでのデータバリデーションとデータ保存](#apiルートでのデータバリデーションとデータ保存)
7. [クライアントサイドでのバリデーションとユーザー体験向上](#クライアントサイドでのバリデーションとユーザー体験向上)
8. [エラー処理とデバッグ方法](#エラー処理とデバッグ方法)
9. [実践的な応用と次のステップ](#実践的な応用と次のステップ)
10. [まとめ](#まとめ)

---

## このガイドの目的

このガイドでは、以下のことを学びます：

- **Next.js App Router**を使った最新のプロジェクト構築方法
- **Zod**を用いた堅牢なデータバリデーションの実装
- **Supabase**を利用したユーザー認証とデータベース操作
- サーバーサイドとクライアントサイド双方での**バリデーション統一**
- **ベストプラクティス**や**セキュリティ考慮**を含む実践的な知識
- **エラー処理**と**デバッグ**の効果的な方法

これらを通じて、実際のプロジェクトで役立つスキルを習得し、自信を持ってアプリケーション開発に取り組めるようになります。

---

## 必要な前提知識と環境

以下の環境と知識が必要です：

- **Node.js**（推奨バージョン：16.x以上）
- **npm**または**yarn**
- **TypeScript**と**React**の基本的な知識
- **Next.js**の基本操作
- **Supabase**アカウント（[Supabase公式サイト](https://supabase.com/)で無料登録可能）

> **注意**：ライブラリのバージョンによっては動かないこともあるかもしれません。

---

## プロジェクトのセットアップ

### 1. Next.jsプロジェクトの作成

まず、Next.jsのプロジェクトをTypeScript対応で作成します。

```bash
npx create-next-app@latest my-app --typescript --experimental-app
cd my-app
```

### 2. 不要なファイルの整理

初期状態では不要なファイルが含まれているため、以下のファイルを削除または編集してクリーンな状態にします。

- `/app/page.tsx` を編集して初期ページをクリア
- `/public` フォルダ内の不要な画像を削除

### 3. 依存関係のインストール

プロジェクトで使用するライブラリをインストールします。

```bash
npm install zod @supabase/supabase-js
```

---

## Zodの導入と活用方法

### 1. Zodとは

Zodは、TypeScriptでスキーマベースのバリデーションを行うためのライブラリです。
型定義とバリデーションを一元管理できるため、フロントエンドとバックエンドで一貫性のあるデータ処理が可能です。

### 2. バリデーションスキーマの定義

`/lib/validation/schema.ts` を作成し、ユーザー登録フォームのスキーマを定義します。

```typescript
// lib/validation/schema.ts
import { z } from 'zod';

export const registrationSchema = z.object({
  name: z.string().min(1, '名前は必須です'),
  email: z.string().email('有効なメールアドレスを入力してください'),
  password: z.string().min(6, 'パスワードは6文字以上必要です'),
});

export type RegistrationData = z.infer<typeof registrationSchema>;
```

### 3. スキーマの活用ポイント

- **再利用性**：スキーマを一度定義すれば、どこでも使い回せます。
- **型安全性**：`z.infer`を使ってTypeScriptの型を自動生成します。

---

## Supabaseでのユーザー管理

### 1. Supabaseとは

Supabaseは、PostgreSQLをベースとしたオープンソースのバックエンドサービスで、認証、データベース、ストレージなどの機能を提供します。

### 2. Supabaseプロジェクトの作成

1. [Supabase公式サイト](https://supabase.com/)でアカウントを作成し、ログインします。
2. 「New Project」から新しいプロジェクトを作成します。
3. プロジェクト名とパスワードを設定し、リージョンを選択して作成します。

### 3. APIキーとURLの取得

1. ダッシュボードの「Settings」 > 「API」を選択します。
2. 「Project URL」と「anon public」キーをコピーします。

### 4. 環境変数の設定

プロジェクトルートに`.env.local`ファイルを作成し、以下を追加します。

```env
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
```

> **注意**：実際の値に置き換えてください。

### 5. Supabaseクライアントの初期化

`/lib/supabaseClient.ts` を作成し、クライアントを初期化します。

```typescript
// lib/supabaseClient.ts
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;

export const supabase = createClient(supabaseUrl, supabaseAnonKey);
```

---

## APIルートでのデータバリデーションとデータ保存

### 1. APIルートの作成

`/app/api/register/route.ts` を作成します。

```typescript// app/api/register/route.ts

// 必要なモジュールをインポート
import { NextResponse } from 'next/server'; // Next.jsのレスポンスオブジェクトを扱うためのモジュール
import { registrationSchema } from '@/lib/validation/schema'; // ユーザー登録データのバリデーションスキーマ
import { supabase } from '@/lib/supabaseClient'; // Supabaseクライアントのインスタンス

// POSTリクエストを処理する関数をエクスポート
export async function POST(request: Request) {
  try {
    // リクエストのボディをJSON形式で取得
    const json = await request.json();

    // バリデーションスキーマを使用してデータを検証
    const data = registrationSchema.parse(json);

    // Supabaseを使用して新規ユーザーを登録
    const { error } = await supabase.auth.signUp({
      email: data.email, // ユーザーのメールアドレス
      password: data.password, // ユーザーのパスワード
      options: {
        data: { name: data.name }, // ユーザーの追加データとして名前を保存
      },
    });

    // 登録時にエラーが発生した場合は例外を投げる
    if (error) throw error;

    // ユーザー登録に成功した場合、成功メッセージとステータスコード201を返す
    return NextResponse.json({ message: 'ユーザー登録に成功しました' }, { status: 201 });
  } catch (error) {
    // エラーがZodError（バリデーションエラー）の場合
    if (error instanceof ZodError) {
      return NextResponse.json({ errors: error.flatten().fieldErrors }, { status: 400 }); // ステータスコード400でエラーメッセージを返す
    }
    // その他のエラーの場合、エラーメッセージとステータスコード500を返す
    return NextResponse.json({ message: error.message }, { status: 500 });
  }
}
```

### 2. エラーハンドリングのポイント

- **ZodErrorのキャッチ**：バリデーションエラーをキャッチし、詳細な情報を返します。
- **Supabaseエラーの処理**：Supabaseからのエラーも適切に処理します。

### 3. セキュリティ考慮

- **データの検証**：サーバーサイドで必ずデータを検証し、信頼できないデータの処理を避けます。
- **エラーメッセージの慎重な取り扱い**：攻撃者に情報を与えないよう、エラーメッセージは必要最小限にします。

---

## クライアントサイドでのバリデーションとユーザー体験向上

### 1. 登録フォームの作成

`/app/register/page.tsx` を作成します。

```tsx// app/register/page.tsx
'use client'; // クライアントサイドでレンダリングすることを指定

import { useState } from 'react'; // ReactのuseStateフックをインポート
import { useForm } from 'react-hook-form'; // react-hook-formのuseFormフックをインポート
import { zodResolver } from '@hookform/resolvers/zod'; // Zodとreact-hook-formを統合するためのresolverをインポート
import { registrationSchema, RegistrationData } from '@/lib/validation/schema'; // バリデーションスキーマと型定義をインポート

// ユーザー登録ページのコンポーネントを定義
export default function RegisterPage() {
  // サーバーエラーを管理するためのステートを初期化
  const [serverError, setServerError] = useState('');

  // react-hook-formのuseFormフックを使用してフォームの管理をセットアップ
  const {
    register, // フォームフィールドを登録する関数
    handleSubmit, // フォーム送信時のハンドラー
    formState: { errors }, // フォームのエラー状態を取得
  } = useForm<RegistrationData>({
    resolver: zodResolver(registrationSchema), // Zodのバリデーションスキーマをresolverとして指定
  });

  // フォーム送信時に呼び出される関数
  const onSubmit = async (data: RegistrationData) => {
    setServerError(''); // 送信前にサーバーエラーをリセット
    try {
      // APIエンドポイントにPOSTリクエストを送信
      const res = await fetch('/api/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' }, // JSON形式のデータを送信
        body: JSON.stringify(data), // フォームデータをJSONに変換
      });

      // レスポンスが正常でない場合、エラーメッセージを取得して例外を投げる
      if (!res.ok) {
        const result = await res.json();
        throw new Error(result.message || 'サーバーエラーが発生しました');
      }

      // 登録成功時にアラートを表示
      alert('ユーザー登録に成功しました');
    } catch (error) {
      // エラー発生時にサーバーエラーメッセージをステートにセット
      setServerError(error.message);
    }
  };

  return (
    <div className="max-w-md mx-auto mt-10">
      {/* ページのタイトル */}
      <h1 className="text-2xl font-bold mb-5">ユーザー登録</h1>
      
      {/* サーバーエラーが存在する場合に表示 */}
      {serverError && <p className="text-red-500">{serverError}</p>}
      
      {/* フォームの定義 */}
      <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
        {/* 名前入力フィールド */}
        <div>
          <label>名前</label>
          <input
            type="text"
            {...register('name')} // フォームフィールドを登録
            className={`w-full p-2 border ${errors.name ? 'border-red-500' : 'border-gray-300'}`} // エラー時に赤色の枠を表示
          />
          {/* 名前フィールドのエラーメッセージを表示 */}
          {errors.name && <p className="text-red-500">{errors.name.message}</p>}
        </div>

        {/* メールアドレス入力フィールド */}
        <div>
          <label>メールアドレス</label>
          <input
            type="email"
            {...register('email')} // フォームフィールドを登録
            className={`w-full p-2 border ${errors.email ? 'border-red-500' : 'border-gray-300'}`} // エラー時に赤色の枠を表示
          />
          {/* メールアドレスフィールドのエラーメッセージを表示 */}
          {errors.email && <p className="text-red-500">{errors.email.message}</p>}
        </div>

        {/* パスワード入力フィールド */}
        <div>
          <label>パスワード</label>
          <input
            type="password"
            {...register('password')} // フォームフィールドを登録
            className={`w-full p-2 border ${errors.password ? 'border-red-500' : 'border-gray-300'}`} // エラー時に赤色の枠を表示
          />
          {/* パスワードフィールドのエラーメッセージを表示 */}
          {errors.password && <p className="text-red-500">{errors.password.message}</p>}
        </div>

        {/* 送信ボタン */}
        <button type="submit" className="w-full bg-blue-500 text-white p-2">
          登録
        </button>
      </form>
    </div>
  );
}
```

### 2. React Hook FormとZodの統合

- **`react-hook-form`の導入**：フォーム状態の管理とバリデーションを簡素化します。

```bash
npm install react-hook-form @hookform/resolvers
```

- **`zodResolver`の使用**：Zodのスキーマをフォームバリデーションに直接利用できます。

### 3. ユーザー体験の向上ポイント

- **リアルタイムバリデーション**：ユーザー入力時に即座にエラーを表示します。
- **視覚的なフィードバック**：エラーがあるフィールドを色付けして強調します。
- **ユーザーフレンドリーなメッセージ**：わかりやすいエラーメッセージを表示します。

---

## エラー処理とデバッグ方法

### 1. よくあるエラーと対処法

- **環境変数の未設定**：SupabaseのURLやキーが正しく設定されているか確認します。
- **CORSエラー**：APIルートのエンドポイントが正しく設定されているか確認します。
- **バリデーションエラー**：Zodのスキーマと入力データが一致しているか確認します。

### 2. デバッグのベストプラクティス

- **ログの活用**：`console.log`やブラウザのデベロッパーツールを使って状態を確認します。
- **エラーメッセージの確認**：詳細なエラーメッセージを一時的に表示して原因を特定します。
- **公式ドキュメントの参照**：問題が解決しない場合、各ライブラリの公式ドキュメントやGitHub Issueを確認します。

---

## 実践的な応用と次のステップ

### 1. サンプルプロジェクトの提供

このガイドで作成したコードは、以下のGitHubリポジトリで公開しています。ぜひクローンして試してみてください。

- [GitHubリポジトリ](https://github.com/yourusername/nextjs-zod-supabase-tutorial)

### 2. 追加機能の実装

- **ユーザーログイン機能**：Supabaseの`signIn`メソッドを使ってログインを実装します。
- **プロフィール編集**：ユーザー情報を更新するフォームとAPIルートを作成します。
- **認証ガード**：認証されたユーザーのみがアクセスできるページを設定します。

### 3. デプロイと運用

- **VercelやNetlifyへのデプロイ**：環境変数の設定に注意してデプロイします。
- **Supabaseの無料プランの制限に注意**：アクセスが増えた場合、有料プランへのアップグレードを検討します。

---

## まとめ

お疲れ様でした！
このガイドを通じて、Next.js、Zod、Supabaseを組み合わせたデータバリデーションとユーザー管理の実装方法を学びました。

### 学んだポイント

- **統一されたデータバリデーション**：Zodを使ってフロントエンドとバックエンドで一貫性のあるバリデーションを実現しました。
- **モダンなバックエンドサービスの活用**：Supabaseを使って手軽にユーザー管理とデータベース操作を行いました。
- **ユーザー体験の向上**：リアルタイムバリデーションや視覚的なフィードバックで使いやすいフォームを作成しました。
- **エラー処理とデバッグ**：よくある問題に対処する方法を学びました。

これらのスキルは、今後のアプリケーション開発に大いに役立つことでしょう。
ぜひ、さらに応用して自分だけのプロジェクトを作ってみてください。

---

**参考リンク**

- [Next.js公式ドキュメント](https://nextjs.org/docs)
- [Zod公式ドキュメント](https://zod.dev/)
- [Supabase公式ドキュメント](https://supabase.com/docs)
- [React Hook Form公式ドキュメント](https://react-hook-form.com/)
- [@supabase/supabase-js GitHubリポジトリ](https://github.com/supabase/supabase-js)

---
### 会社紹介

**株式会社 Mosaica**  
最先端テクノロジーで社会課題を解決し、持続可能な未来を創造する IT カンパニー。
AI ソリューション、クラウド統合、DX 推進、経営コンサルティングなど包括的なサービスでビジネス変革を支援しています。  

詳しくは [公式サイト](https://mosaica.co.jp/) までお気軽にご相談ください。
公式サイト： https://mosaica.co.jp/