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
// 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:
- Next.js Documentation - learn about Next.js features and API.
- Learn Next.js - an interactive Next.js tutorial.
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の設定と活用方法
-
Supabase CLIのインストール
npm install -g supabase
-
Supabaseプロジェクトの初期化
プロジェクトルートで実行:supabase init
-
Supabaseログイン
Supabaseにログイン:supabase db login
-
データベースマイグレーションの管理
マイグレーションを適用:supabase db push
-
Edge Functionsの開発とデプロイ
関数をデプロイ:supabase functions deploy hello-world
-
環境変数の管理
.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
// 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 ドキュメント: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/
- Supabase公式サイト: https://supabase.io/
- Zod公式ドキュメント: https://zod.dev/
- Tailwind CSS公式サイト: https://tailwindcss.com/
- TypeScript公式ドキュメント: https://www.typescriptlang.org/docs/
- SWR公式サイト: https://swr.vercel.app/ja