【Next.js × Supabase × Zod】実践チュートリアル

# Next.js、Zod、Supabaseを用いた高品質なWebアプリケーション構築ガイド

最新のウェブ開発スタックである**Next.jsのApp Router**、**Zod**、**Supabase**を組み合わせて、高品質なWebアプリケーションを構築する方法を詳細に解説します。

本記事では、ディレクトリ構成から各ファイルの役割、ベストプラクティスまでを網羅し、プロジェクトの成功に必要な知識を提供します。

## 最終ディレクトリ構成

まずは、プロジェクトの最終的なディレクトリ構成を確認しましょう。

```
my-app/
├── app/
│   ├── api/
│   │   ├── auth/
│   │   │   └── route.ts
│   │   ├── register/
│   │   ├── users/
│   │   │   ├── route.ts
│   │   │   └── [id]/
│   │   │       └── route.ts
│   │   └── ...その他のAPI
│   ├── components/
│   │   ├── common/
│   │   │   ├── Header.tsx
│   │   │   └── Footer.tsx
│   │   ├── forms/
│   │   │   └── LoginForm.tsx
│   │   ├── layout/
│   │   │   └── Sidebar.tsx
│   │   └── AuthWrapper.tsx
│   ├── hooks/
│   │   ├── useAuth.ts
│   │   ├── useFetchUsers.ts
│   │   └── useResponsive.ts
│   ├── lib/
│   │   ├── supabaseClient.ts
│   │   └── analytics.ts
│   ├── schemas/
│   │   ├── authSchema.ts
│   │   └── userSchema.ts
│   ├── types/
│   │   └── index.d.ts
│   ├── styles/
│   │   └── globals.css
│   ├── about/
│   │   └── page.tsx
│   ├── login/
│   │   └── page.tsx
│   ├── settings/
│   │   └── page.tsx
│   ├── tests/
│   │   └── unit/
│   │       └── example.test.ts
│   ├── users/
│   │   └── page.tsx
│   ├── layout.tsx
│   ├── page.tsx
│   └── globals.css
├── public/
│   ├── assets/
│   │   ├── images/
│   │   │   ├── logo.png
│   │   │   └── banner.jpg
│   │   └── fonts/
│   │       ├── GeistMonoVF.woff
│   │       └── GeistVF.woff
│   └── favicon.ico
├── supabase/
│   ├── config.toml
│   ├── functions/
│   │   └── hello-world/
│   │       ├── index.ts
│   │       ├── import_map.json
│   │       └── supabase.toml
│   ├── migrations/
│   │   └── 20240427_init.sql
│   ├── seed.sql
│   └── supabase.config.ts
├── .eslintrc.json
├── .gitignore
├── next.config.mjs
├── package.json
├── postcss.config.mjs
├── tailwind.config.ts
├── tsconfig.json
└── README.md
```

---

## 各ファイルのサンプルコードと解説

### 1. `app/settings/page.tsx`

```tsx
// app/settings/page.tsx
const SettingsPage = () => {
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">設定</h1>
      <p>ここにユーザー設定のオプションを追加します。</p>
    </div>
  )
}

export default SettingsPage
```

*ユーザー設定ページの基本的な構造。必要に応じて設定オプションを追加します。*

---

### 2. `app/types/index.d.ts`

```typescript
// app/types/index.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    NEXT_PUBLIC_SUPABASE_URL: string
    NEXT_PUBLIC_SUPABASE_ANON_KEY: string
    SUPABASE_PROJECT_ID: string
    SUPABASE_SERVICE_ROLE_KEY: string
    // 他の環境変数をここに追加
  }
}

interface User {
  id: string
  name: string
  email: string
  created_at?: string
}
```

*TypeScriptの型定義を集中管理。環境変数とユーザー型を定義しています。*

---

### 3. `app/tests/unit/example.test.ts`

```typescript
// app/tests/unit/example.test.ts
import { sum } from '../../lib/utils'

test('sum関数のテスト', () => {
    expect(sum(1, 2)).toBe(3)
})
```

*ユニットテストの例。`sum`関数が正しく動作するかをテストします。*

---

### 4. `app/about/page.tsx`

```tsx
// app/about/page.tsx
const AboutPage = () => {
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">私たちについて</h1>
      <p>ここに会社や製品についての情報を記載します。</p>
    </div>
  )
}

export default AboutPage
```

*「私たちについて」ページの基本構造。会社や製品の情報を追加します。*

---

### 5. `app/schemas/authSchema.ts`

```typescript
// app/schemas/authSchema.ts
import { z } from 'zod'

// 認証データのバリデーションスキーマ
export const authSchema = z.object({
  email: z.string().email(),
  password: z.string().min(6),
})
```

*認証データのバリデーションスキーマ。ユーザー登録やログイン時のデータ検証に使用します。*

---

### 6. `app/schemas/userSchema.ts`

```typescript
// app/schemas/userSchema.ts
import { z } from 'zod'

// ユーザーデータのバリデーションスキーマ
export const userSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
  created_at: z.string().optional(),
})

export type User = z.infer<typeof userSchema>
```

*ユーザーデータのバリデーションスキーマおよびTypeScript型。データの整合性を保つために使用します。*

---

### 7. `app/styles/globals.css`

```css
/* app/styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* カスタムスタイルをここに追加 */
body {
  @apply bg-gray-50 text-gray-900;
}
```

*グローバルスタイルおよびTailwind CSSの設定。全体のデザイン基盤を提供します。*

---

### 8. `app/components/forms/LoginForm.tsx`

```tsx
// app/components/forms/LoginForm.tsx
"use client";

import { useState } from 'react'
import { useAuth } from '../../hooks/useAuth'
import { z } from 'zod'

const LoginForm: React.FC = () => {
  const { signIn } = useAuth()
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [errors, setErrors] = useState<string[]>([])

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    const schema = z.object({
      email: z.string().email(),
      password: z.string().min(6),
    })

    const result = schema.safeParse({ email, password })

    if (!result.success) {
      setErrors(result.error.errors.map((err) => err.message))
      return
    }

    try {
      await signIn(email, password)
      // リダイレクトや成功時の処理
    } catch (error: any) {
      setErrors([error.message || '不明なエラーが発生しました'])
    }
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <div>
        <label htmlFor="email" className="block text-sm font-medium text-gray-700">
          メールアドレス
        </label>
        <input
          type="email"
          id="email"
          className="mt-1 block w-full"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
      </div>
      <div>
        <label htmlFor="password" className="block text-sm font-medium text-gray-700">
          パスワード
        </label>
        <input
          type="password"
          id="password"
          className="mt-1 block w-full"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
      </div>
      {errors.length > 0 && (
        <div className="text-red-500">
          {errors.map((error, index) => (
            <p key={index}>{error}</p>
          ))}
        </div>
      )}
      <button type="submit" className="btn-primary">
        ログイン
      </button>
    </form>
  )
}

export default LoginForm
```

*ログインフォームコンポーネント。ユーザーの入力を受け取り、認証処理を行います。Zodを用いたバリデーションも実装しています。*

---

### 9. `app/components/layout/Sidebar.tsx`

```tsx
// app/components/layout/Sidebar.tsx
import Link from 'next/link'

const Sidebar: React.FC = () => {
  return (
    <aside className="w-64 bg-gray-100 p-4">
      <nav className="flex flex-col space-y-2">
        <Link href="/" className="text-blue-600">
          Home
        </Link>
        <Link href="/users" className="text-blue-600">
          ユーザー一覧
        </Link>
        <Link href="/settings" className="text-blue-600">
          設定
        </Link>
      </nav>
    </aside>
  )
}

export default Sidebar
```

*サイドバーのナビゲーションメニュー。主要なページへのリンクを提供します。*

---

### 10. `app/components/common/Footer.tsx`

```tsx
// app/components/common/Footer.tsx
import React from 'react';

const Footer: React.FC = () => {
  return (
    <footer className="bg-gray-800 text-white p-4 mt-auto">
      <div className="container mx-auto text-center">
        © {new Date().getFullYear()} MyApp. All rights reserved.
      </div>
    </footer>
  );
};

export default Footer;
```

*サイトのフッター部分。著作権情報などを表示します。*

---

### 11. `app/components/common/Header.tsx`

```tsx
// app/components/common/Header.tsx
import React from 'react';
import Link from 'next/link';

const Header: React.FC = () => {
  return (
    <header className="bg-blue-600 text-white p-4">
      <nav className="container mx-auto flex justify-between">
        <Link href="/" className="text-xl font-bold">
          MyApp
        </Link>
        <div>
          <Link href="/about" className="mr-4">
            About
          </Link>
          <Link href="/login">
            Login
          </Link>
        </div>
      </nav>
    </header>
  );
};

export default Header;
```

*サイトのヘッダー部分。ロゴやナビゲーションリンクを含みます。*

---

### 12. `app/components/AuthWrapper.tsx`

```tsx
// app/components/AuthWrapper.tsx
"use client";

import React from 'react';
import { useAuth } from '../hooks/useAuth';
import LoginForm from './forms/LoginForm';
import Header from './common/Header';
import Footer from './common/Footer';
import Sidebar from './layout/Sidebar';

interface AuthWrapperProps {
  children: React.ReactNode;
}

const AuthWrapper: React.FC<AuthWrapperProps> = ({ children }) => {
  const { user } = useAuth();

  if (!user) {
    return <LoginForm />;
  }

  return (
    <div className="flex flex-col min-h-screen">
      <Header />
      <div className="flex flex-1">
        <Sidebar />
        <main className="flex-1 p-4">{children}</main>
      </div>
      <Footer />
    </div>
  );
};

export default AuthWrapper;
```

*認証ラッパーコンポーネント。ユーザーが認証されていない場合はログインフォームを表示し、認証済みの場合はアプリケーションのメインレイアウトを提供します。*

---

### 13. `app/hooks/useAuth.ts`

```typescript
// app/hooks/useAuth.ts
"use client";

import { useState, useEffect } from 'react';
import { supabase } from '@/lib/supabaseClient';
import { User } from '@/app/schemas/userSchema';

export const useAuth = () => {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (event, session) => {
        setUser(session?.user ?? null);
      }
    );

    return () => {
      subscription.unsubscribe();
    };
  }, []);

  // サインイン機能
  const signIn = async (email: string, password: string) => {
    const { error } = await supabase.auth.signInWithPassword({ email, password });
    if (error) throw error;
  };

  // サインアウト機能
  const signOut = async () => {
    const { error } = await supabase.auth.signOut();
    if (error) throw error;
  };

  return { user, signIn, signOut };
};
```

*認証状態の管理およびサインイン、サインアウトの機能を提供します。Supabaseの認証機能を利用しています。*

---

### 14. `app/hooks/useResponsive.ts`

```typescript
// app/hooks/useResponsive.ts
import { useState, useEffect } from 'react'

export const useResponsive = () => {
    const [isMobile, setIsMobile] = useState(false)

    useEffect(() => {
        const handleResize = () => {
            setIsMobile(window.innerWidth < 768)
        }
        window.addEventListener('resize', handleResize)
        handleResize()
        return () => window.removeEventListener('resize', handleResize)
    }, [])

    return { isMobile }
}
```

*レスポンシブデザインをサポートするためのカスタムフック。画面幅に応じて`isMobile`を更新します。*

---

### 15. `app/layout.tsx`

```tsx
// app/layout.tsx
import { FC, ReactNode } from 'react'
import dynamic from 'next/dynamic'
import './globals.css'

const AuthWrapper = dynamic(() => import('./components/AuthWrapper'), { ssr: false })

interface LayoutProps {
  children: ReactNode
}

const RootLayout: FC<LayoutProps> = ({ children }) => {
  return (
    <html lang="ja">
      <body>
        <AuthWrapper>{children}</AuthWrapper>
      </body>
    </html>
  )
}

export default RootLayout
```

*全体のレイアウトを定義するファイル。認証ラッパーを使用して、認証状態に応じた表示を制御します。*

---

### 16. `app/users/page.tsx`

```tsx
// app/users/page.tsx
'use client'

import { useState, useEffect } from 'react'
import { supabase } from '@/lib/supabaseClient'
import { User } from '@/app/schemas/userSchema'

const UsersPage = () => {
  const [users, setUsers] = useState<User[]>([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetchUsers()
  }, [])

  const fetchUsers = async () => {
    try {
      setLoading(true)
      const { data, error } = await supabase.from('users').select('*')
      if (error) throw error
      setUsers(data as User[])
    } catch (error) {
      console.error('ユーザーの取得中にエラーが発生しました:', error)
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">ユーザー一覧</h1>
      {loading ? (
        <p>読み込み中...</p>
      ) : (
        <ul className="space-y-2">
          {users.map((user) => (
            <li key={user.id} className="border p-2 rounded">
              {user.name} ({user.email})
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}

export default UsersPage
```

*ユーザー一覧ページ。Supabaseからユーザーデータを取得し、表示します。読み込み中の状態管理も行っています。*

---

### 17. `app/lib/analytics.ts`

```typescript
// app/lib/analytics.ts
export const captureException = (error: any) => {
    // エラーログを外部サービスに送信
    // 例：Sentry.captureException(error)
    console.error('エラーが発生しました:', error)
}
```

*エラーログを外部サービスに送信するためのユーティリティ関数。現在はコンソールにエラーを表示するのみですが、Sentryなどのサービスと連携可能です。*

---

### 18. `app/api/auth/route.ts`

```typescript
// app/api/auth/route.ts
import { NextResponse } from 'next/server'
import { supabase } from '../../lib/supabaseClient'
import { authSchema } from '../../schemas/authSchema'

// ユーザー登録を処理するPOSTエンドポイント
export async function POST(request: Request) {
    const body = await request.json()
    const parseResult = authSchema.safeParse(body)

    if (!parseResult.success) {
        return NextResponse.json({ errors: parseResult.error.errors }, { status: 400 })
    }

    const { email, password } = parseResult.data

    const { user, error } = await supabase.auth.signUp({
        email,
        password,
    })

    if (error) {
        return NextResponse.json({ error: error.message }, { status: 400 })
    }

    return NextResponse.json({ user }, { status: 200 })
}
```

*認証関連のAPIエンドポイント。ユーザー登録を処理します。Zodを用いたバリデーションも実装しています。*

---

### 19. `app/api/users/route.ts`

```typescript
// app/api/users/route.ts
import { NextResponse } from 'next/server'
import { supabase } from '../../lib/supabaseClient'
import { userSchema } from '../../schemas/userSchema'

// ユーザー一覧を取得するGETエンドポイント
export async function GET() {
    const { data, error } = await supabase.from('users').select('*')

    if (error) {
        return NextResponse.json({ error: error.message }, { status: 400 })
    }

    const parseResult = userSchema.array().safeParse(data)

    if (!parseResult.success) {
        return NextResponse.json({ errors: parseResult.error.errors }, { status: 400 })
    }

    return NextResponse.json(parseResult.data, { status: 200 })
}

// 新規ユーザーを追加するPOSTエンドポイント
export async function POST(request: Request) {
    const body = await request.json()
    const parseResult = userSchema.safeParse(body)

    if (!parseResult.success) {
        return NextResponse.json({ errors: parseResult.error.errors }, { status: 400 })
    }

    const { data, error } = await supabase.from('users').insert(parseResult.data)

    if (error) {
        return NextResponse.json({ error: error.message }, { status: 400 })
    }

    return NextResponse.json(data, { status: 201 })
}
```

*ユーザー一覧の取得や新規ユーザーの追加を処理します。データのバリデーションをZodで行っています。*

---

### 20. `app/api/users/[id]/route.ts`

```typescript
// app/api/users/[id]/route.ts
import { NextResponse } from 'next/server';
import { supabase } from '@/app/lib/supabaseClient';
import { userSchema } from '@/app/schemas/userSchema';

// 特定ユーザーの詳細を取得するGETエンドポイント
export async function GET(request: Request, { params }: { params: { id: string } }) {
  const { id } = params;

  const { data, error } = await supabase.from('users').select('*').eq('id', id).single();

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 404 });
  }

  const parseResult = userSchema.safeParse(data);

  if (!parseResult.success) {
    return NextResponse.json({ errors: parseResult.error.errors }, { status: 400 });
  }

  return NextResponse.json(parseResult.data, { status: 200 });
}

// 特定ユーザーを更新するPUTエンドポイント
export async function PUT(request: Request, { params }: { params: { id: string } }) {
  const { id } = params;
  const body = await request.json();
  const parseResult = userSchema.partial().safeParse(body);

  if (!parseResult.success) {
    return NextResponse.json({ errors: parseResult.error.errors }, { status: 400 });
  }

  const { data, error } = await supabase.from('users').update(parseResult.data).eq('id', id);

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 400 });
  }

  return NextResponse.json(data, { status: 200 });
}

// 特定ユーザーを削除するDELETEエンドポイント
export async function DELETE(request: Request, { params }: { params: { id: string } }) {
  const { id } = params;

  const { data, error } = await supabase.from('users').delete().eq('id', id);

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 400 });
  }

  return NextResponse.json(data, { status: 200 });
}
```

*特定ユーザーの詳細取得、更新、削除を処理します。動的ルートを利用してユーザーIDごとの操作を実現しています。*

---

### 21. `app/page.tsx`

```tsx
// app/page.tsx
import { FC } from 'react'
import Header from './components/common/Header'
import Footer from './components/common/Footer'

const HomePage: FC = () => {
    return (
        <div className="flex flex-col min-h-screen">
            <Header />
            <main className="container mx-auto p-4 flex-1">
                <h1 className="text-3xl font-bold">Welcome to MyApp</h1>
                <p className="mt-4">This is the home page.</p>
            </main>
            <Footer />
        </div>
    )
}

export default HomePage
```

*ホームページのエントリーポイント。ヘッダーとフッターを含み、メインコンテンツを表示します。*

---

### 22. `app/globals.css`

```css
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --background: #ffffff;
  --foreground: #171717;
}

@media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;
    --foreground: #ededed;
  }
}

body {
  color: var(--foreground);
  background: var(--background);
  font-family: Arial, sans-serif;
  margin: 0;
  padding: 0;
}

@layer utilities {
  .text-balance {
    text-wrap: balance;
  }
}
```

*グローバルスタイルおよびカスタムCSS変数の設定。ダークモード対応やフォント設定を含みます。*

---

### 23. `app/login/page.tsx`

```tsx
// app/login/page.tsx
import LoginForm from '../components/forms/LoginForm'

const LoginPage = () => {
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">ログイン</h1>
      <LoginForm />
    </div>
  )
}

export default LoginPage
```

*ログインページのエントリーポイント。`LoginForm`コンポーネントを表示します。*

---

### 24. `postcss.config.mjs`

```javascript
// postcss.config.mjs
/** @type {import('postcss-load-config').Config} */
const config = {
  plugins: {
    tailwindcss: {},
  },
};

export default config;
```

*PostCSSの設定ファイル。Tailwind CSSをプラグインとして使用しています。*

---

### 25. `next.config.mjs`

```javascript
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {};

export default nextConfig;
```

*Next.jsの設定ファイル。必要に応じて設定を追加できます。*

---

### 26. `supabase/migrations/20240427_init.sql`

```sql
-- supabase/migrations/20240427_init.sql

-- ユーザーテーブルの作成
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc', now())
);

-- 必要に応じて他のテーブルやインデックスを追加
```

*データベースの初期マイグレーションファイル。`users`テーブルを作成します。*

---

### 27. `supabase/.temp/cli-latest`

```
v1.204.3
```

*Supabase CLIのバージョンファイル。CLIのバージョン管理に使用されます。*

---

### 28. `supabase/functions/hello-world/supabase.toml`

```toml
# supabase/functions/hello-world/supabase.toml

name = "hello-world"
runtime = "deno"
entrypoint = "index.ts"

[build]
  command = "deno compile --unstable --import-map=import_map.json --output hello-world index.ts"

[deploy]
  allow_insecure = false
```

*Edge Functionのビルドおよびデプロイ設定。Denoランタイムを使用します。*

---

### 29. `supabase/functions/hello-world/import_map.json`

```json
// supabase/functions/hello-world/import_map.json
{
  "imports": {
    "std/": "https://deno.land/std@0.203.0/"
  }
}
```

*Denoランタイムのインポートマップ。`serve`関数のインポートを定義します。*

---

### 30. `supabase/functions/hello-world/index.ts`

```typescript
// supabase/functions/hello-world/index.ts

import { serve } from 'std/server'

// Edge Functionのエントリーポイント
serve(async (req: Request) => {
  return new Response(JSON.stringify({ message: 'Hello, World!' }), {
    headers: { 'Content-Type': 'application/json' },
  })
})
```

*Edge Functionのエントリーポイント。`/hello-world`エンドポイントでメッセージを返します。*

---

### 31. `supabase/supabase.config.ts`

```typescript
// supabase/supabase.config.ts

import { defineConfig } from 'supabase'

// Supabase CLI用の設定ファイル
export default defineConfig({
  projectId: process.env.SUPABASE_PROJECT_ID,
  apiKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
  // 他の設定オプション
})
```

*Supabase CLIやツールの設定ファイル。プロジェクトIDやAPIキーを読み込みます。*

---

### 32. `supabase/config.toml`

```toml
# supabase/config.toml

# プロジェクトIDの設定
project_id = "my-app"

[api]
enabled = true
port = 54321
schemas = ["public", "graphql_public"]
extra_search_path = ["public", "extensions"]
max_rows = 1000

[api.tls]
enabled = false

[db]
port = 54322
shadow_port = 54320
major_version = 15

[db.pooler]
enabled = false
port = 54329
pool_mode = "transaction"
default_pool_size = 20
max_client_conn = 100

[realtime]
enabled = true

[studio]
enabled = true
port = 54323
api_url = "http://127.0.0.1"
openai_api_key = "env(OPENAI_API_KEY)"

[inbucket]
enabled = true
port = 54324

[storage]
enabled = true
file_size_limit = "50MiB"

[storage.image_transformation]
enabled = true

[auth]
enabled = true
site_url = "http://127.0.0.1:3000"
additional_redirect_urls = ["https://127.0.0.1:3000"]
jwt_expiry = 3600
enable_refresh_token_rotation = true
refresh_token_reuse_interval = 10
enable_signup = true
enable_anonymous_sign_ins = false
enable_manual_linking = false

[auth.email]
enable_signup = true
double_confirm_changes = true
enable_confirmations = false
secure_password_change = false
max_frequency = "1s"

[auth.sms]
enable_signup = true
enable_confirmations = false
template = "Your code is {{ .Code }} ."
max_frequency = "5s"

[auth.sms.twilio]
enabled = false
account_sid = ""
message_service_sid = ""
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"

[auth.mfa]
max_enrolled_factors = 10

[auth.mfa.totp]
enroll_enabled = true
verify_enabled = true

[auth.external.apple]
enabled = false
client_id = ""
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
redirect_uri = ""
url = ""
skip_nonce_check = false

[auth.third_party.firebase]
enabled = false

[auth.third_party.auth0]
enabled = false

[auth.third_party.aws_cognito]
enabled = false

[edge_runtime]
enabled = true
policy = "oneshot"
inspector_port = 8083

[analytics]
enabled = true
port = 54327
backend = "postgres"

[experimental]
orioledb_version = ""
s3_host = "env(S3_HOST)"
s3_region = "env(S3_REGION)"
s3_access_key = "env(S3_ACCESS_KEY)"
s3_secret_key = "env(S3_SECRET_KEY)"
```

*Supabaseの設定ファイル。API、データベース、認証、ストレージなどの各種設定を行います。環境変数を利用して機密情報を管理します。*

---

### 33. `README.md`

```markdown
# my-app

This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
```

*プロジェクトの概要とセットアップ手順を説明するREADMEファイル。開発サーバーの起動方法や学習リソースへのリンクも含まれています。*

---

### 34. `tailwind.config.ts`

```typescript
// tailwind.config.ts
import type { Config } from "tailwindcss";

const config: Config = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      colors: {
        background: "var(--background)",
        foreground: "var(--foreground)",
      },
    },
  },
  plugins: [],
};
export default config;
```

*Tailwind CSSの設定ファイル。どのファイルでTailwindクラスを使用するかを指定し、カスタムカラーを追加しています。*

---

### 35. `package.json`

```json
// package.json
{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@supabase/supabase-js": "^2.45.4",
    "next": "14.2.15",
    "react": "^18",
    "react-dom": "^18",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.2.15",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  }
}
```

*プロジェクトの依存関係やスクリプトを管理。Supabase CLIのスクリプトも含まれています。*

---

### 36. `lib/supabaseClient.ts`

```typescript
// lib/supabaseClient.ts
"use client";

import { createClient } from '@supabase/supabase-js';

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

if (!supabaseUrl || !supabaseAnonKey) {
  throw new Error('Supabase環境変数が設定されていません。');
}

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

*Supabaseクライアントの初期化および設定。アプリ全体で共有します。環境変数を用いてSupabaseのURLと匿名キーを設定します。*

---

### 37. `tsconfig.json`

```json
// tsconfig.json
{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
```

*TypeScriptの設定ファイル。プロジェクト全体の型設定を管理します。パスエイリアスも設定されており、`@/`でプロジェクトルートへのアクセスが可能です。*

---

### 38. `.eslintrc.json`

```json
// .eslintrc.json
{
  "extends": ["next/core-web-vitals", "next/typescript"]
}
```

*ESLintの設定ファイル。Next.jsとTypeScriptに適したルールセットを拡張しています。*

---

## 補足: 各ツールの役割

- **Next.js App Router**
  - 最新のルーティングシステムを提供し、`app/`ディレクトリ内でページやAPIルートを構築。ファイルベースのルーティングで直感的なページ構成が可能。

- **Zod**
  - スキーマベースのバリデーションライブラリ。APIエンドポイントやフロントエンドのフォームバリデーションでデータの型安全性を確保。

- **Supabase**
  - バックエンドとしてデータベースや認証機能を提供。`lib/supabaseClient.ts`でクライアントを初期化し、APIルートやクライアントサイドで使用。データベースマイグレーションやEdge Functionsを活用。

- **Tailwind CSS**
  - ユーティリティファーストなCSSフレームワーク。迅速かつ一貫性のあるスタイリングを実現。

- **TypeScript**
  - 型安全性を提供し、コードの品質と保守性を向上。

- **SWR**
  - データフェッチングのためのReactフックライブラリ。キャッシュや再検証機能を提供。

---

## Supabaseの設定と活用方法

1. **Supabase CLIのインストール**
   ```bash
   npm install -g supabase
   ```

2. **Supabaseプロジェクトの初期化**
   プロジェクトルートで実行:
   ```bash
   supabase init
   ```

3. **Supabaseログイン**
   Supabaseにログイン:
   ```bash
   supabase db login
   ```

4. **データベースマイグレーションの管理**
   マイグレーションを適用:
   ```bash
   supabase db push
   ```

5. **Edge Functionsの開発とデプロイ**
   関数をデプロイ:
   ```bash
   supabase functions deploy hello-world
   ```

6. **環境変数の管理**
   `.env.local` にSupabaseのプロジェクトIDやAPIキーを設定。

---

## セキュリティに関する注意点

- **環境変数の管理**
  - `.env.local` は機密情報を含むため、必ず `.gitignore` に追加し、リポジトリに含めないようにします。

- **APIキーの保護**
  - サービスロールキー（`SUPABASE_SERVICE_ROLE_KEY`）などの高権限キーはクライアントサイドで使用せず、サーバーサイドのみで利用します。

---

## スタイリングとUIの構築

- **Tailwind CSSの活用**
  - `app/styles/globals.css` でTailwindをインポートし、ユーティリティクラスを使用して迅速にスタイリングを行います。

- **レスポンシブデザイン**
  - Tailwindのレスポンシブユーティリティを活用し、モバイルファーストのデザインを実現します。

---

## デプロイと運用

- **Vercelへのデプロイ**
  - Next.jsアプリはVercelとの相性が良いです。Vercelダッシュボードからプロジェクトをインポートし、環境変数を設定します。

- **Supabaseの運用**
  - Supabaseダッシュボードからデータベースの監視や管理、Edge Functionsのデプロイ状況を確認できます。

- **モニタリングとログ**
  - アプリケーションのパフォーマンスやエラーログをモニタリングするために、適切なツール（例: Sentry、LogRocket）を導入します。

---

## 拡張性と保守性の向上

- **機能ごとのディレクトリ分割**
  - プロジェクトが拡大するにつれて、機能ごとにディレクトリを分割し、コードの可読性と保守性を向上させます。

- **コードの再利用**
  - 再利用可能なコンポーネントやフックを `app/components/` や `app/hooks/` に配置し、DRY（Don't Repeat Yourself）原則を遵守します。

- **型安全性の確保**
  - Zodを活用して、APIリクエストやレスポンスの型安全性を確保します。これにより、ランタイムエラーを減少させ、開発効率を向上させます。

---

## まとめ

この最終版のディレクトリ構成とサンプルコードは、Next.jsのApp Router、Zod、Supabaseを効果的に組み合わせたプロジェクトの基盤を提供します。以下のポイントを押さえて、プロジェクトをスムーズに進めてください。

- **明確なディレクトリ構造:** 各機能を適切なディレクトリに分離し、コードの可読性と保守性を向上。
- **型安全性:** Zodを使用してデータのバリデーションと型定義を行い、エラーの発生を未然に防止。
- **Supabaseの活用:** データベースマイグレーションやEdge Functionsを活用し、強力なバックエンド機能を実現。
- **スタイリング:** Tailwind CSSを使用して、迅速かつ一貫性のあるUIを構築。
- **セキュリティ:** 機密情報の管理やAPIキーの保護に注意し、セキュリティリスクを最小限に抑制。

このサンプルを基に、プロジェクトの要件に合わせてカスタマイズし、最適な開発環境を構築してください。必要に応じて、追加のライブラリやツールを導入し、プロジェクトの機能を拡張することも可能です。

---

## 付録：リソースと参考文献

- **Next.js ドキュメント**：https://nextjs.org/docs
- **Supabase ドキュメント**：https://supabase.io/docs
- **Zod ドキュメント**：https://zod.dev
- **Tailwind CSS ドキュメント**：https://tailwindcss.com/docs
- **TypeScript ハンドブック**：https://www.typescriptlang.org/docs
- **SWR ドキュメント**：https://swr.vercel.app/ja

---

## 実際のソースコード

以下に、プロジェクト全体のソースコードを示します。各ファイルの詳細な役割や実装方法については、上記の各セクションをご参照ください。

### ディレクトリ構造

```
my-app/
├── app/
│   ├── api/
│   │   ├── auth/
│   │   │   └── route.ts
│   │   ├── register/
│   │   ├── users/
│   │   │   ├── route.ts
│   │   │   └── [id]/
│   │   │       └── route.ts
│   │   └── ...その他のAPI
│   ├── components/
│   │   ├── common/
│   │   │   ├── Header.tsx
│   │   │   └── Footer.tsx
│   │   ├── forms/
│   │   │   └── LoginForm.tsx
│   │   ├── layout/
│   │   │   └── Sidebar.tsx
│   │   └── AuthWrapper.tsx
│   ├── hooks/
│   │   ├── useAuth.ts
│   │   ├── useFetchUsers.ts
│   │   └── useResponsive.ts
│   ├── lib/
│   │   ├── supabaseClient.ts
│   │   └── analytics.ts
│   ├── schemas/
│   │   ├── authSchema.ts
│   │   └── userSchema.ts
│   ├── types/
│   │   └── index.d.ts
│   ├── styles/
│   │   └── globals.css
│   ├── about/
│   │   └── page.tsx
│   ├── login/
│   │   └── page.tsx
│   ├── settings/
│   │   └── page.tsx
│   ├── tests/
│   │   └── unit/
│   │       └── example.test.ts
│   ├── users/
│   │   └── page.tsx
│   ├── layout.tsx
│   ├── page.tsx
│   └── globals.css
├── public/
│   ├── assets/
│   │   ├── images/
│   │   │   ├── logo.png
│   │   │   └── banner.jpg
│   │   └── fonts/
│   │       ├── GeistMonoVF.woff
│   │       └── GeistVF.woff
│   └── favicon.ico
├── supabase/
│   ├── config.toml
│   ├── functions/
│   │   └── hello-world/
│   │       ├── index.ts
│   │       ├── import_map.json
│   │       └── supabase.toml
│   ├── migrations/
│   │   └── 20240427_init.sql
│   ├── seed.sql
│   └── supabase.config.ts
├── .eslintrc.json
├── .gitignore
├── next.config.mjs
├── package.json
├── postcss.config.mjs
├── tailwind.config.ts
├── tsconfig.json
└── README.md
```

### 各ファイルの内容

以下に、各ファイルの内容を詳細に記載します。必要に応じてコードをコピーしてプロジェクトに適用してください。

#### `app/settings/page.tsx`

```tsx
// app/settings/page.tsx
const SettingsPage = () => {
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">設定</h1>
      <p>ここにユーザー設定のオプションを追加します。</p>
    </div>
  )
}

export default SettingsPage
```

#### `app/types/index.d.ts`

```typescript
// app/types/index.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    NEXT_PUBLIC_SUPABASE_URL: string
    NEXT_PUBLIC_SUPABASE_ANON_KEY: string
    SUPABASE_PROJECT_ID: string
    SUPABASE_SERVICE_ROLE_KEY: string
    // 他の環境変数をここに追加
  }
}

interface User {
  id: string
  name: string
  email: string
  created_at?: string
}
```

#### `app/tests/unit/example.test.ts`

```typescript
// app/tests/unit/example.test.ts
import { sum } from '../../lib/utils'

test('sum関数のテスト', () => {
    expect(sum(1, 2)).toBe(3)
})
```

#### `app/about/page.tsx`

```tsx
// app/about/page.tsx
const AboutPage = () => {
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">私たちについて</h1>
      <p>ここに会社や製品についての情報を記載します。</p>
    </div>
  )
}

export default AboutPage
```

#### `app/schemas/authSchema.ts`

```typescript
// app/schemas/authSchema.ts
import { z } from 'zod'

// 認証データのバリデーションスキーマ
export const authSchema = z.object({
  email: z.string().email(),
  password: z.string().min(6),
})
```

#### `app/schemas/userSchema.ts`

```typescript
// app/schemas/userSchema.ts
import { z } from 'zod'

// ユーザーデータのバリデーションスキーマ
export const userSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
  created_at: z.string().optional(),
})

export type User = z.infer<typeof userSchema>
```

#### `app/styles/globals.css`

```css
/* app/styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* カスタムスタイルをここに追加 */
body {
  @apply bg-gray-50 text-gray-900;
}
```

#### `app/components/forms/LoginForm.tsx`

```tsx
// app/components/forms/LoginForm.tsx
"use client";

import { useState } from 'react'
import { useAuth } from '../../hooks/useAuth'
import { z } from 'zod'

const LoginForm: React.FC = () => {
  const { signIn } = useAuth()
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [errors, setErrors] = useState<string[]>([])

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    const schema = z.object({
      email: z.string().email(),
      password: z.string().min(6),
    })

    const result = schema.safeParse({ email, password })

    if (!result.success) {
      setErrors(result.error.errors.map((err) => err.message))
      return
    }

    try {
      await signIn(email, password)
      // リダイレクトや成功時の処理
    } catch (error: any) {
      setErrors([error.message || '不明なエラーが発生しました'])
    }
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <div>
        <label htmlFor="email" className="block text-sm font-medium text-gray-700">
          メールアドレス
        </label>
        <input
          type="email"
          id="email"
          className="mt-1 block w-full"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
      </div>
      <div>
        <label htmlFor="password" className="block text-sm font-medium text-gray-700">
          パスワード
        </label>
        <input
          type="password"
          id="password"
          className="mt-1 block w-full"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
      </div>
      {errors.length > 0 && (
        <div className="text-red-500">
          {errors.map((error, index) => (
            <p key={index}>{error}</p>
          ))}
        </div>
      )}
      <button type="submit" className="btn-primary">
        ログイン
      </button>
    </form>
  )
}

export default LoginForm
```

#### `app/components/layout/Sidebar.tsx`

```tsx
// app/components/layout/Sidebar.tsx
import Link from 'next/link'

const Sidebar: React.FC = () => {
  return (
    <aside className="w-64 bg-gray-100 p-4">
      <nav className="flex flex-col space-y-2">
        <Link href="/" className="text-blue-600">
          Home
        </Link>
        <Link href="/users" className="text-blue-600">
          ユーザー一覧
        </Link>
        <Link href="/settings" className="text-blue-600">
          設定
        </Link>
      </nav>
    </aside>
  )
}

export default Sidebar
```

#### `app/components/common/Footer.tsx`

```tsx
// app/components/common/Footer.tsx
import React from 'react';

const Footer: React.FC = () => {
  return (
    <footer className="bg-gray-800 text-white p-4 mt-auto">
      <div className="container mx-auto text-center">
        © {new Date().getFullYear()} MyApp. All rights reserved.
      </div>
    </footer>
  );
};

export default Footer;
```

#### `app/components/common/Header.tsx`

```tsx
// app/components/common/Header.tsx
import React from 'react';
import Link from 'next/link';

const Header: React.FC = () => {
  return (
    <header className="bg-blue-600 text-white p-4">
      <nav className="container mx-auto flex justify-between">
        <Link href="/">
          <a className="text-xl font-bold">MyApp</a>
        </Link>
        <div>
          <Link href="/about">
            <a className="mr-4">About</a>
          </Link>
          <Link href="/login">
            <a>Login</a>
          </Link>
        </div>
      </nav>
    </header>
  )
}

export default Header
```

#### `app/components/AuthWrapper.tsx`

```tsx
// app/components/AuthWrapper.tsx
"use client";

import React from 'react';
import { useAuth } from '../hooks/useAuth';
import LoginForm from './forms/LoginForm';
import Header from './common/Header';
import Footer from './common/Footer';
import Sidebar from './layout/Sidebar';

interface AuthWrapperProps {
  children: React.ReactNode;
}

const AuthWrapper: React.FC<AuthWrapperProps> = ({ children }) => {
  const { user } = useAuth();

  if (!user) {
    return <LoginForm />;
  }

  return (
    <div className="flex flex-col min-h-screen">
      <Header />
      <div className="flex flex-1">
        <Sidebar />
        <main className="flex-1 p-4">{children}</main>
      </div>
      <Footer />
    </div>
  );
};

export default AuthWrapper;
```

#### `app/hooks/useAuth.ts`

```typescript
// app/hooks/useAuth.ts
"use client";

import { useState, useEffect } from 'react';
import { supabase } from '@/lib/supabaseClient';
import { User } from '@/app/schemas/userSchema';

export const useAuth = () => {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (event, session) => {
        setUser(session?.user ?? null);
      }
    );

    return () => {
      subscription.unsubscribe();
    };
  }, []);

  // サインイン機能
  const signIn = async (email: string, password: string) => {
    const { error } = await supabase.auth.signInWithPassword({ email, password });
    if (error) throw error;
  };

  // サインアウト機能
  const signOut = async () => {
    const { error } = await supabase.auth.signOut();
    if (error) throw error;
  };

  return { user, signIn, signOut };
};
```

#### `app/hooks/useResponsive.ts`

```typescript
// app/hooks/useResponsive.ts
import { useState, useEffect } from 'react'

export const useResponsive = () => {
    const [isMobile, setIsMobile] = useState(false)

    useEffect(() => {
        const handleResize = () => {
            setIsMobile(window.innerWidth < 768)
        }
        window.addEventListener('resize', handleResize)
        handleResize()
        return () => window.removeEventListener('resize', handleResize)
    }, [])

    return { isMobile }
}
```

#### `app/layout.tsx`

```tsx
// app/layout.tsx
import { FC, ReactNode } from 'react'
import dynamic from 'next/dynamic'
import './globals.css'

const AuthWrapper = dynamic(() => import('./components/AuthWrapper'), { ssr: false })

interface LayoutProps {
  children: ReactNode
}

const RootLayout: FC<LayoutProps> = ({ children }) => {
  return (
    <html lang="ja">
      <body>
        <AuthWrapper>{children}</AuthWrapper>
      </body>
    </html>
  )
}

export default RootLayout
```

#### `app/users/page.tsx`

```tsx
// app/users/page.tsx
'use client'

import { useState, useEffect } from 'react'
import { supabase } from '@/lib/supabaseClient'
import { User } from '@/app/schemas/userSchema'

const UsersPage = () => {
  const [users, setUsers] = useState<User[]>([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetchUsers()
  }, [])

  const fetchUsers = async () => {
    try {
      setLoading(true)
      const { data, error } = await supabase.from('users').select('*')
      if (error) throw error
      setUsers(data as User[])
    } catch (error) {
      console.error('ユーザーの取得中にエラーが発生しました:', error)
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">ユーザー一覧</h1>
      {loading ? (
        <p>読み込み中...</p>
      ) : (
        <ul className="space-y-2">
          {users.map((user) => (
            <li key={user.id} className="border p-2 rounded">
              {user.name} ({user.email})
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}

export default UsersPage
```

#### `app/lib/analytics.ts`

```typescript
// app/lib/analytics.ts
export const captureException = (error: any) => {
    // エラーログを外部サービスに送信
    // 例：Sentry.captureException(error)
    console.error('エラーが発生しました:', error)
}
```

#### `app/api/auth/route.ts`

```typescript
// app/api/auth/route.ts
import { NextResponse } from 'next/server'
import { supabase } from '../../lib/supabaseClient'
import { authSchema } from '../../schemas/authSchema'

// ユーザー登録を処理するPOSTエンドポイント
export async function POST(request: Request) {
    const body = await request.json()
    const parseResult = authSchema.safeParse(body)

    if (!parseResult.success) {
        return NextResponse.json({ errors: parseResult.error.errors }, { status: 400 })
    }

    const { email, password } = parseResult.data

    const { user, error } = await supabase.auth.signUp({
        email,
        password,
    })

    if (error) {
        return NextResponse.json({ error: error.message }, { status: 400 })
    }

    return NextResponse.json({ user }, { status: 200 })
}
```

#### `app/api/users/route.ts`

```typescript
// app/api/users/route.ts
import { NextResponse } from 'next/server'
import { supabase } from '../../lib/supabaseClient'
import { userSchema } from '../../schemas/userSchema'

// ユーザー一覧を取得するGETエンドポイント
export async function GET() {
    const { data, error } = await supabase.from('users').select('*')

    if (error) {
        return NextResponse.json({ error: error.message }, { status: 400 })
    }

    const parseResult = userSchema.array().safeParse(data)

    if (!parseResult.success) {
        return NextResponse.json({ errors: parseResult.error.errors }, { status: 400 })
    }

    return NextResponse.json(parseResult.data, { status: 200 })
}

// 新規ユーザーを追加するPOSTエンドポイント
export async function POST(request: Request) {
    const body = await request.json()
    const parseResult = userSchema.safeParse(body)

    if (!parseResult.success) {
        return NextResponse.json({ errors: parseResult.error.errors }, { status: 400 })
    }

    const { data, error } = await supabase.from('users').insert(parseResult.data)

    if (error) {
        return NextResponse.json({ error: error.message }, { status: 400 })
    }

    return NextResponse.json(data, { status: 201 })
}
```

#### `app/api/users/[id]/route.ts`

```typescript
// app/api/users/[id]/route.ts
import { NextResponse } from 'next/server';
import { supabase } from '@/app/lib/supabaseClient';
import { userSchema } from '@/app/schemas/userSchema';

// 特定ユーザーの詳細を取得するGETエンドポイント
export async function GET(request: Request, { params }: { params: { id: string } }) {
  const { id } = params;

  const { data, error } = await supabase.from('users').select('*').eq('id', id).single();

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 404 });
  }

  const parseResult = userSchema.safeParse(data);

  if (!parseResult.success) {
    return NextResponse.json({ errors: parseResult.error.errors }, { status: 400 });
  }

  return NextResponse.json(parseResult.data, { status: 200 });
}

// 特定ユーザーを更新するPUTエンドポイント
export async function PUT(request: Request, { params }: { params: { id: string } }) {
  const { id } = params;
  const body = await request.json();
  const parseResult = userSchema.partial().safeParse(body);

  if (!parseResult.success) {
    return NextResponse.json({ errors: parseResult.error.errors }, { status: 400 });
  }

  const { data, error } = await supabase.from('users').update(parseResult.data).eq('id', id);

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 400 });
  }

  return NextResponse.json(data, { status: 200 });
}

// 特定ユーザーを削除するDELETEエンドポイント
export async function DELETE(request: Request, { params }: { params: { id: string } }) {
  const { id } = params;

  const { data, error } = await supabase.from('users').delete().eq('id', id);

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 400 });
  }

  return NextResponse.json(data, { status: 200 });
}
```

#### `app/page.tsx`

```tsx
// app/page.tsx
import { FC } from 'react'
import Header from './components/common/Header'
import Footer from './components/common/Footer'

const HomePage: FC = () => {
    return (
        <div className="flex flex-col min-h-screen">
            <Header />
            <main className="container mx-auto p-4 flex-1">
                <h1 className="text-3xl font-bold">Welcome to MyApp</h1>
                <p className="mt-4">This is the home page.</p>
            </main>
            <Footer />
        </div>
    )
}

export default HomePage
```

#### `app/globals.css`

```css
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --background: #ffffff;
  --foreground: #171717;
}

@media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;
    --foreground: #ededed;
  }
}

body {
  color: var(--foreground);
  background: var(--background);
  font-family: Arial, sans-serif;
  margin: 0;
  padding: 0;
}

@layer utilities {
  .text-balance {
    text-wrap: balance;
  }
}
```

#### `app/login/page.tsx`

```tsx
// app/login/page.tsx
import LoginForm from '../components/forms/LoginForm'

const LoginPage = () => {
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">ログイン</h1>
      <LoginForm />
    </div>
  )
}

export default LoginPage
```

#### `postcss.config.mjs`

```javascript
// postcss.config.mjs
/** @type {import('postcss-load-config').Config} */
const config = {
  plugins: {
    tailwindcss: {},
  },
};

export default config;
```

#### `next.config.mjs`

```javascript
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {};

export default nextConfig;
```

#### `supabase/migrations/20240427_init.sql`

```sql
-- supabase/migrations/20240427_init.sql

-- ユーザーテーブルの作成
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc', now())
);

-- 必要に応じて他のテーブルやインデックスを追加
```

#### `supabase/.temp/cli-latest`

```
v1.204.3
```

#### `supabase/functions/hello-world/index.ts`

```typescript
// supabase/functions/hello-world/index.ts

import { serve } from 'std/server'

// Edge Functionのエントリーポイント
serve(async (req: Request) => {
  return new Response(JSON.stringify({ message: 'Hello, World!' }), {
    headers: { 'Content-Type': 'application/json' },
  })
})
```

#### `supabase/functions/hello-world/import_map.json`

```json
// supabase/functions/hello-world/import_map.json
{
  "imports": {
    "std/": "https://deno.land/std@0.203.0/"
  }
}
```

#### `supabase/functions/hello-world/supabase.toml`

```toml
# supabase/functions/hello-world/supabase.toml

name = "hello-world"
runtime = "deno"
entrypoint = "index.ts"

[build]
  command = "deno compile --unstable --import-map=import_map.json --output hello-world index.ts"

[deploy]
  allow_insecure = false
```

#### `supabase/supabase.config.ts`

```typescript
// supabase/supabase.config.ts

import { defineConfig } from 'supabase'

// Supabase CLI用の設定ファイル
export default defineConfig({
  projectId: process.env.SUPABASE_PROJECT_ID,
  apiKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
  // 他の設定オプション
})
```

#### `.env.local`

```env
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-supabase-url.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_PROJECT_ID=your-supabase-project-id
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# 他の環境変数をここに追加
```

*環境変数の定義。SupabaseのURLやAPIキーなどを設定します。*

---

#### `.gitignore`

```gitignore
# .gitignore
node_modules/
.next/
supabase/.env
.env.local
.DS_Store
*.log
```

*バージョン管理に含めないファイルやディレクトリを指定。機密情報を含むファイルを除外します。*

---

#### `next.config.js`

```javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  // 必要に応じて他の設定を追加
}

module.exports = nextConfig
```

*Next.jsの設定ファイル。ReactのStrictモードを有効にしています。必要に応じて他の設定を追加できます。*

---

#### `postcss.config.js`

```javascript
// postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}
```

*PostCSSの設定ファイル。Tailwind CSSとAutoprefixerをプラグインとして使用します。*

---

#### `tailwind.config.js`

```javascript
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

*Tailwind CSSの設定ファイル。どのファイルでTailwindクラスを使用するかを指定します。カスタムテーマの拡張も可能です。*

---

#### `tsconfig.json`

```json
// tsconfig.json
{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "types": ["node"]
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}
```

*TypeScriptの設定ファイル。プロジェクト全体の型設定を管理します。*

---

#### `.eslintrc.json`

```json
// .eslintrc.json
{
  "extends": ["next/core-web-vitals", "next/typescript"]
}
```

*ESLintの設定ファイル。Next.jsとTypeScriptに適したルールセットを拡張しています。*

---

## 付録：リソースと参考文献

- **Next.js ドキュメント**：https://nextjs.org/docs
- **Supabase ドキュメント**：https://supabase.io/docs
- **Zod ドキュメント**：https://zod.dev
- **Tailwind CSS ドキュメント**：https://tailwindcss.com/docs
- **TypeScript ハンドブック**：https://www.typescriptlang.org/docs
- **SWR ドキュメント**：https://swr.vercel.app/ja

---

この最終版のディレクトリ構成とサンプルコードは、Next.jsのApp Router、Zod、Supabaseを効果的に組み合わせたプロジェクトの基盤を提供します。以下のポイントを押さえて、プロジェクトをスムーズに進めてください。

- **明確なディレクトリ構造:** 各機能を適切なディレクトリに分離し、コードの可読性と保守性を向上。
- **型安全性:** Zodを使用してデータのバリデーションと型定義を行い、エラーの発生を未然に防止。
- **Supabaseの活用:** データベースマイグレーションやEdge Functionsを活用し、強力なバックエンド機能を実現。
- **スタイリング:** Tailwind CSSを使用して、迅速かつ一貫性のあるUIを構築。
- **セキュリティ:** 機密情報の管理やAPIキーの保護に注意し、セキュリティリスクを最小限に抑制。

このサンプルを基に、プロジェクトの要件に合わせてカスタマイズし、最適な開発環境を構築してください。必要に応じて、追加のライブラリやツールを導入し、プロジェクトの機能を拡張することも可能です。

---

## 付録：参考資料

- **Next.js公式サイト**: [https://nextjs.org/](https://nextjs.org/)
- **Supabase公式サイト**: [https://supabase.io/](https://supabase.io/)
- **Zod公式ドキュメント**: [https://zod.dev/](https://zod.dev/)
- **Tailwind CSS公式サイト**: [https://tailwindcss.com/](https://tailwindcss.com/)
- **TypeScript公式ドキュメント**: [https://www.typescriptlang.org/docs/](https://www.typescriptlang.org/docs/)
- **SWR公式サイト**: [https://swr.vercel.app/ja](https://swr.vercel.app/ja)

---
### 会社紹介

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

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