0
1

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 × Supabase × Zod】実践チュートリアル

Last updated at Posted at 2024-10-12

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

最新のウェブ開発スタックであるNext.jsのApp RouterZodSupabaseを組み合わせて、高品質な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

// 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

// 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

// 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

// 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

// 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

// 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

/* 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

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

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


18. app/api/auth/route.ts

// 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

// 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

// 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

// 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

/* 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

// 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

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

export default config;

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


25. next.config.mjs

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

export default nextConfig;

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


26. supabase/migrations/20240427_init.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

# 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

// 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

// 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

// 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

# 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

# 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 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 to automatically optimize and load Geist, a new font family for Vercel.

Learn More

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

You can check out the Next.js GitHub repository - your feedback and contributions are welcome!

Deploy on Vercel

The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.

Check out our Next.js deployment documentation 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

// 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

// 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

// 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

// .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のインストール

    npm install -g supabase
    
  2. Supabaseプロジェクトの初期化
    プロジェクトルートで実行:

    supabase init
    
  3. Supabaseログイン
    Supabaseにログイン:

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

    supabase db push
    
  5. Edge Functionsの開発とデプロイ
    関数をデプロイ:

    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キーの保護に注意し、セキュリティリスクを最小限に抑制。

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


付録:リソースと参考文献


実際のソースコード

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

ディレクトリ構造

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

// 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

// 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

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

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

app/about/page.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

// 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

// 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

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

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

app/components/forms/LoginForm.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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

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

app/api/auth/route.ts

// 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

// 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

// 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

// 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

/* 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

// 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

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

export default config;

next.config.mjs

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

export default nextConfig;

supabase/migrations/20240427_init.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

// 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

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

supabase/functions/hello-world/supabase.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

// 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.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
node_modules/
.next/
supabase/.env
.env.local
.DS_Store
*.log

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


next.config.js

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

module.exports = nextConfig

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


postcss.config.js

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

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


tailwind.config.js

// 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

// 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

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

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


付録:リソースと参考文献


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

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

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


付録:参考資料

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?