| レイヤー | Lightest | Light | Middle | Heavy①② |
|---|---|---|---|---|
| フレームワーク | Next.js | Next.js + Hono | Next.js + Django | Next.js + Django/Hono |
| 認証 | Supabase Auth | Supabase Auth / Better Auth | Django Auth / Better Auth | Cognito / Better Auth |
| ORM | Supabase Client | Drizzle | Django ORM | Drizzle / Prisma |
| DB | Supabase (PG) | Supabase (PG) | Supabase / RDS | RDS (PG) |
| CSS | Tailwind CSS | Tailwind CSS | Tailwind CSS | Tailwind CSS |
| UIコンポーネント | shadcn/ui | shadcn/ui | shadcn/ui | shadcn/ui / 独自 |
| 状態管理 | useState + Server Components | Zustand / Jotai | Zustand | Zustand |
| テスト | Vitest | Vitest + Playwright | Pytest + Vitest | Jest / Vitest + Playwright |
| CI/CD | Vercel自動デプロイ | Vercel + GitHub Actions | GitHub Actions | GitHub Actions + AWS |
認証は「作らない」が原則。セキュリティリスクが高い領域のため、 実績あるライブラリ・サービスに乗っかるのが正解。
| ソリューション | 種別 | 向いている構成 | 月額 |
|---|---|---|---|
| Supabase Auth | BaaS | Lightest / Light | 無料〜 |
| Better Auth | ライブラリ(自己ホスト) | Light / Middle | 無料 |
| NextAuth.js (Auth.js) | ライブラリ | Light / Middle | 無料 |
| Clerk | SaaS | Light〜Heavy① | $25〜/月 |
| Django Auth | フレームワーク内蔵 | Middle | 無料 |
| Cognito | AWS BaaS | Heavy① / Heavy② | 従量課金 |
// サインアップ
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'password',
})
// OAuthログイン(Google)
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
})
// セッション取得(Server Component)
const { data: { session } } = await supabase.auth.getSession()
✅ メール/パスワード・OAuth・マジックリンク・MFA
✅ RLSと連携してDBレベルで認可できる
✅ 無料枠:50,000ユーザーまで
❌ 独自の複雑な認証ロジックには向かない
// セットアップ(Hono + Drizzle と相性が良い)
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: 'pg' }),
emailAndPassword: { enabled: true },
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
})
✅ 自己ホストのため外部サービス依存なし
✅ Drizzle / Prisma アダプター完備
✅ セッション管理・OAuth・メール認証が揃っている
✅ TypeScript ネイティブ
❌ Supabase Auth より設定量が多い
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import Google from 'next-auth/providers/google'
export const { handlers, auth } = NextAuth({
providers: [Google],
})
// Server Componentでセッション取得
const session = await auth()
✅ Next.jsとの親和性が最も高い
✅ 多数のOAuthプロバイダーに対応
⚠️ Auth.jsへのリネームで過渡期。ドキュメントが混在している
❌ DBアダプターの設定がやや複雑
// middleware.ts に1行追加するだけで全保護
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware()
// コンポーネントでユーザー取得
import { currentUser } from '@clerk/nextjs/server'
const user = await currentUser()
✅ 設定がほぼゼロ。UIコンポーネントも付属
✅ 組織・チーム管理・MFAが標準搭載
❌ 月$25〜。MAUが増えると高額になる
❌ 外部サービス依存
✅ 企業AD・SAML・SSO連携が必要なとき
✅ MFAが必須要件(医療・金融系)
✅ 既存のCognitoユーザープールがある
❌ 個人開発では過剰。設定が複雑
💡 ポイント: 個人MVPはSupabase Auth一択。Supabase以外のDBを使うならBetter Auth。Clerkは「時間を買う」選択。Cognitoは企業要件が明確なときだけ。
Supabase使う?
└─ YES → Supabase Auth(ほぼ確定)
└─ NO → 時間を買いたい?
└─ YES → Clerk($25〜)
└─ NO → Next.js中心? → NextAuth.js
Hono/Drizzle中心? → Better Auth
Next.js + TypeScript スタックでは型安全なORM選択が重要。 Djangoと異なり、Node.jsエコシステムではORMが分散しているため自分で選定が必要。
| ORM | 型安全性 | マイグレーション | 学習コスト | 向いている構成 |
|---|---|---|---|---|
| Drizzle | ◎ | ◎(SQLに近い) | 低 | Light / Heavy② |
| Prisma | ◎ | ◎(自動生成) | 中 | Light〜Heavy② |
| Supabase Client | △ | Supabase管理 | 最低 | Lightest |
| Django ORM | ○ | ◎(最強) | 中(Python) | Middle |
| 生SQL(postgres.js等) | △ | 手動 | 低 | Heavy②(最適化時) |
// schema.ts
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
export const posts = pgTable('posts', {
id: uuid('id').defaultRandom().primaryKey(),
title: text('title').notNull(),
userId: uuid('user_id').notNull(),
createdAt: timestamp('created_at').defaultNow(),
})
// クエリ(SQLに近い直感的な記法)
const userPosts = await db
.select()
.from(posts)
.where(eq(posts.userId, userId))
.orderBy(desc(posts.createdAt))
.limit(10)
// JOIN
const result = await db
.select({ post: posts, user: users })
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
✅ SQLに近い記法で学習コストが低い
✅ バンドルサイズが小さい(Edge Runtime対応)
✅ Honoとの相性が良い
✅ マイグレーションがシンプル
❌ Prismaほどのエコシステムはまだない
// schema.prisma
model Post {
id String @id @default(cuid())
title String
user User @relation(fields: [userId], references: [id])
userId String
createdAt DateTime @default(now())
}
// クエリ(直感的なオブジェクト記法)
const posts = await prisma.post.findMany({
where: { userId },
include: { user: true }, // JOINが直感的
orderBy: { createdAt: 'desc' },
take: 10,
})
✅ スキーマ定義が読みやすい
✅ `include` でJOINが直感的
✅ マイグレーションが自動生成
✅ 大きなコミュニティ・ドキュメントが豊富
❌ バンドルサイズが大きい(Edge Runtime非対応)
❌ N+1問題が起きやすい
# Django → マイグレーションが最強
python manage.py makemigrations # 差分を自動検出
python manage.py migrate # 適用
# モデル変更 → 自動でSQLを生成してくれる
class Post(models.Model):
title = models.CharField(max_length=200)
# フィールド追加するだけでmakemigrationsが検出
# Drizzle → SQLに近い、差分管理が明示的
npx drizzle-kit generate # マイグレーションファイル生成
npx drizzle-kit migrate # 適用
# Prisma → 自動生成だがDjangoほど賢くない
npx prisma migrate dev --name add_post_table
💡 ポイント: DjangoのORMは「モデルを書けばマイグレーションが自動生成される」点で最強クラス。Node.jsでは Drizzle(軽量・Edge対応)か Prisma(エコシステム豊富)の2択。個人開発のLightest/LightならDrizzle推奨。
// 型安全にするにはsupabase-jsの型生成を使う
import { Database } from '@/types/supabase' // npx supabase gen types で生成
const supabase = createClient<Database>(url, key)
const { data, error } = await supabase
.from('posts')
.select('*, users(name)') // JOINはこの記法
.eq('user_id', userId)
| フレームワーク | アプローチ | 学習コスト | カスタマイズ | 向いている用途 |
|---|---|---|---|---|
| Tailwind CSS | ユーティリティクラス | 中 | ◎ | ほぼ全構成で推奨 |
| CSS Modules | スコープ付きCSS | 低 | ◎ | コンポーネント単位の管理 |
| Styled Components | CSS-in-JS | 中 | ◎ | 動的スタイルが多い場合 |
| vanilla-extract | 型安全CSS-in-JS | 高 | ◎ | ゼロランタイムが必要 |
// ユーティリティクラスで直接スタイリング
export function Card({ title, description }: Props) {
return (
<div className="rounded-xl border border-gray-200 p-6 shadow-sm hover:shadow-md transition-shadow">
<h2 className="text-lg font-semibold text-gray-900">{title}</h2>
<p className="mt-2 text-sm text-gray-500">{description}</p>
</div>
)
}
// レスポンシブ・ダークモードも直感的
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 dark:bg-gray-900">
✅ Next.jsと公式統合済み(設定ゼロ)
✅ コンポーネントとスタイルが一体で管理しやすい
✅ パージ機能で本番バンドルサイズが小さい
❌ クラス名が長くなりがち → cn()ユーティリティで対処
# コンポーネントをプロジェクトにコピーして使う(依存しない設計)
npx shadcn-ui@latest add button
npx shadcn-ui@latest add dialog
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader } from '@/components/ui/dialog'
// Radix UI + Tailwind CSS の組み合わせ
<Button variant="outline" size="sm">キャンセル</Button>
✅ コンポーネントをコピーして自由にカスタマイズできる
✅ アクセシビリティ対応(Radix UI ベース)
✅ Tailwind CSSと完全統合
✅ 個人開発〜SaaSまで幅広く使える
❌ npmパッケージではなくファイルコピー方式のため更新が手動
💡 ポイント: Next.js個人開発では「Tailwind CSS + shadcn/ui」がデファクトスタンダード。UIを1から作らず、shadcn/uiをベースに独自カスタマイズが最速。
// lib/utils.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// 使い方:条件付きクラスの衝突を解決してくれる
<div className={cn(
'rounded-md px-4 py-2',
isActive && 'bg-blue-500 text-white',
isDisabled && 'opacity-50 cursor-not-allowed',
)}>
| フレームワーク | 言語 | レンダリング | 向いている用途 |
|---|---|---|---|
| Next.js | TS/JS | SSR/SSG/ISR/RSC | 個人開発〜大規模全般 |
| Next.js + Hono | TS | SSR + API分離 | APIが複雑な中規模以上 |
| Remix | TS/JS | SSR中心 | フォーム処理・MPA的設計 |
| SvelteKit | Svelte | SSR/SSG | 軽量・Reactを使わない場合 |
| Nuxt | TS/JS | SSR/SSG | Vue.jsユーザー |
| Django(フルスタック) | Python | テンプレート/API | Python資産・管理画面中心 |
【Next.js単体(Route Handlers)】
app/
api/
posts/
route.ts ← GETとPOSTを1ファイルで管理
users/
route.ts
問題:
❌ 認証チェックを毎ルートに書く必要がある
❌ ルートが増えると管理が煩雑
❌ バックエンドだけ別サービスに切り出せない
【Next.js + Hono】
app/
api/
[[...route]]/
route.ts ← Honoがここで全APIを受け取る
// Hono側
const app = new Hono()
app.use('/api/*', authMiddleware) // 認証を一元管理
app.use('/api/*', loggingMiddleware) // ロギングも一元管理
app.route('/api/posts', postsRoute)
app.route('/api/users', usersRoute)
メリット:
✅ ミドルウェアで横断的な処理を一元化
✅ 将来バックエンドをCloudflare Workers等に切り出せる
✅ OpenAPIによる自動ドキュメント生成が可能
Next.js:
✅ RSC(React Server Components)対応
✅ ISRなど豊富なキャッシュ戦略
✅ エコシステムが最大
❌ App RouterとPages Routerの共存が混乱を生む
Remix:
✅ フォーム処理・データミューテーションが直感的
✅ エラーバウンダリが強力
✅ Web標準(FormData等)に準拠
❌ ISR相当の機能がない
❌ コミュニティがNext.jsより小さい
💡 ポイント: 2026年時点で個人開発のデファクトはNext.js。Honoを組み合わせることでAPIの設計品質を上げられる。RemixはフォームヘビーなアプリやWeb標準を重視する場合の選択肢。
| ツール | 種別 | 向いている用途 |
|---|---|---|
| Vitest | ユニット / 統合 | ロジック・ユーティリティ・hooks |
| Playwright | E2E | ユーザーフロー全体の動作確認 |
| Testing Library | コンポーネント | Reactコンポーネントの動作確認 |
| MSW | モック | APIモック(ユニット〜E2E) |
| Pytest | ユニット(Python) | Djangoのロジックテスト |
// utils.test.ts
import { describe, it, expect } from 'vitest'
import { formatPrice } from '@/lib/utils'
describe('formatPrice', () => {
it('整数を日本円フォーマットに変換する', () => {
expect(formatPrice(1000)).toBe('¥1,000')
})
it('小数点以下を切り捨てる', () => {
expect(formatPrice(999.9)).toBe('¥999')
})
})
# 設定(vite.config.ts)
export default defineConfig({
test: {
environment: 'jsdom', // Reactコンポーネントのテスト用
globals: true,
},
})
✅ Jestより高速(Viteを使うため)
✅ Next.js / Viteプロジェクトとの相性が良い
✅ TypeScriptのサポートが標準
// e2e/login.spec.ts
import { test, expect } from '@playwright/test'
test('ログインしてダッシュボードに遷移する', async ({ page }) => {
await page.goto('/login')
await page.fill('[name=email]', 'test@example.com')
await page.fill('[name=password]', 'password')
await page.click('[type=submit]')
await expect(page).toHaveURL('/dashboard')
await expect(page.getByText('ようこそ')).toBeVisible()
})
✅ ブラウザ実機(Chromium・Firefox・Safari)でテスト
✅ スクリーンショット・動画記録が標準搭載
✅ CIとの統合が容易
❌ テストが遅い(E2Eの宿命)
❌ テスト環境のDB管理が必要
// mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/api/posts', () => {
return HttpResponse.json([
{ id: '1', title: 'テスト投稿' },
])
}),
]
// Vitestでも Playwright でも同じモックを使い回せる
💡 ポイント: 個人開発での現実的なテスト戦略は「ユニットテスト(Vitest)で重要ロジックを守り、E2E(Playwright)でクリティカルなユーザーフローだけを守る」。全部テストしようとしない。
優先度高:
✅ 認証フロー(ログイン・ログアウト・セッション切れ)
✅ 課金・決済フロー
✅ ユーティリティ関数・バリデーションロジック
優先度中:
⚠️ 主要なCRUD操作
⚠️ 権限・アクセス制御
優先度低(後回しでOK):
△ UIコンポーネントの見た目
△ 静的ページ
| 構成 | CI/CD | 理由 |
|---|---|---|
| Lightest / Light | Vercel自動デプロイ | pushだけで完結。追加設定不要 |
| Light(テストあり) | GitHub Actions + Vercel | テストを挟んでからデプロイ |
| Middle | GitHub Actions + Render/Railway | バックエンドも自動デプロイ |
| Heavy② | GitHub Actions + AWS(ECR/ECS) | コンテナビルド〜ECSデプロイまで自動化 |
GitHubリポジトリ連携だけで:
main push → 本番デプロイ(production)
feature/xxx → プレビューURL自動生成
staging → ステージング環境として固定運用
設定:ほぼゼロ ✅
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci # npm install ではなく ci(ロックファイルを厳守)
- run: npm run type-check # TypeScriptの型チェック
- run: npm run lint # ESLint
- run: npm run test # Vitest
# テストが通ったらVercelが自動でデプロイ(連携設定済みの場合)
# .github/workflows/deploy.yml
name: Deploy to ECS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
- name: Login to ECR
run: aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_URI
- name: Build & Push Docker image
run: |
docker build -t $ECR_URI/my-app:${{ github.sha }} .
docker push $ECR_URI/my-app:${{ github.sha }}
- name: Update ECS service
run: |
aws ecs update-service \
--cluster my-cluster \
--service my-app \
--force-new-deployment
💡 ポイント:
npm installではなくnpm ciを使う。npm ciはロックファイルを厳守するため、CIでの再現性が保証される(セキュリティ・安定性の観点でも重要)。
main → 本番デプロイ(直接pushは禁止、PRのみ)
staging → ステージング(固定ブランチとして維持)
feature/xxx → 機能開発(プレビューURL生成)
fix/xxx → バグ修正
PR フロー:
feature/xxx → staging(動作確認) → main(本番リリース)
Next.js App Router(RSC時代)では、状態管理の考え方が大きく変わった。 「できるだけサーバーで持つ」が基本方針。
| 状態の種類 | 管理方法 | 例 |
|---|---|---|
| サーバーデータ | React Server Components / TanStack Query | DBのデータ、APIレスポンス |
| URLの状態 | Next.js router / searchParams | フィルター、ページネーション |
| グローバルUI状態 | Zustand / Jotai | モーダル開閉、テーマ、認証状態 |
| ローカルUI状態 | useState | フォーム入力、トグル |
| フォーム状態 | React Hook Form | フォームバリデーション |
// ❌ 旧来のやり方(クライアントでfetch + useState)
'use client'
function PostList() {
const [posts, setPosts] = useState([])
useEffect(() => {
fetch('/api/posts').then(r => r.json()).then(setPosts)
}, [])
return <div>{posts.map(p => <Post key={p.id} post={p} />)}</div>
}
// ✅ RSCのやり方(サーバーで直接fetch、状態不要)
async function PostList() { // 'use client' 不要
const posts = await db.select().from(postsTable)
return <div>{posts.map(p => <Post key={p.id} post={p} />)}</div>
}
// store/ui.ts
import { create } from 'zustand'
interface UIStore {
isModalOpen: boolean
theme: 'light' | 'dark'
openModal: () => void
closeModal: () => void
toggleTheme: () => void
}
export const useUIStore = create<UIStore>((set) => ({
isModalOpen: false,
theme: 'light',
openModal: () => set({ isModalOpen: true }),
closeModal: () => set({ isModalOpen: false }),
toggleTheme: () => set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' })),
}))
// コンポーネントで使う
const { isModalOpen, openModal } = useUIStore()
✅ シンプルなAPI(BoilerplateがReduxより大幅に少ない)
✅ TypeScriptとの相性が良い
✅ DevTools対応
❌ サーバーデータの管理には向かない(TanStack Queryを使う)
// RSCを使わない場面(Pages Router or クライアントfetch)でのサーバーデータ管理
const { data: posts, isLoading, error } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetch(`/api/posts?userId=${userId}`).then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5分間はキャッシュを使う
})
// ミューテーション(更新後に自動でキャッシュ無効化)
const mutation = useMutation({
mutationFn: (newPost) => fetch('/api/posts', { method: 'POST', body: JSON.stringify(newPost) }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
})
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const schema = z.object({
email: z.string().email('有効なメールアドレスを入力してください'),
password: z.string().min(8, 'パスワードは8文字以上です'),
})
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
})
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
</form>
)
}
💡 ポイント: App Router時代の状態管理の優先順位は「RSCでサーバー取得 → URLパラメータ → useState → Zustand」の順。Reduxは個人開発では過剰。
フレームワーク : Next.js 15 (App Router)
認証 : Supabase Auth
DB/ORM : Supabase Client(型生成あり)
CSS : Tailwind CSS + shadcn/ui
状態管理 : useState + RSC(Zustandは必要になったら)
テスト : Vitest(ユーティリティのみ)
CI/CD : Vercel自動デプロイ
フレームワーク : Next.js 15 + Hono
認証 : Supabase Auth / Better Auth
DB/ORM : Drizzle + Supabase(PG)
CSS : Tailwind CSS + shadcn/ui
状態管理 : Zustand + TanStack Query
テスト : Vitest + Playwright(主要フローのみ)
CI/CD : GitHub Actions(テスト)+ Vercel(デプロイ)
フレームワーク : Next.js + Django REST Framework
認証 : Django Auth / Better Auth
DB/ORM : Django ORM + Drizzle(Next.js側)
CSS : Tailwind CSS + shadcn/ui
状態管理 : Zustand + TanStack Query
テスト : Pytest(Django)+ Vitest + Playwright
CI/CD : GitHub Actions + Render/Railway
フレームワーク : Next.js + Hono or Django
認証 : Cognito / Better Auth
DB/ORM : Prisma / Drizzle + RDS
CSS : Tailwind CSS + shadcn/ui / 独自デザインシステム
状態管理 : Zustand + TanStack Query
テスト : Vitest + Playwright + 負荷テスト(k6等)
CI/CD : GitHub Actions + ECR + ECS
| ミス | 原因 | 改善策 |
|---|---|---|
| 最初からReduxを導入する | Reactの状態管理=Reduxという思い込み | RSC + useState + Zustandで十分。Reduxは大規模チーム向け |
| Prismaをフロントで直接呼ぶ | 'use server'の誤解 | Prismaはサーバー側のみ。クライアントから直接呼ばない |
| テストを全部書こうとする | 完璧主義 | 個人開発は「認証・決済・コアロジック」だけ守れば十分 |
| CSSをゼロから書こうとする | shadcn/uiを知らない | shadcn/uiをベースにカスタマイズするのが最速 |
npm installをCIで使う | 習慣 | npm ciでロックファイルを厳守する |
| 認証を自前実装する | 「勉強になるから」 | セキュリティリスクが高い。Supabase Auth / Better Authを使う |
| Cognitoを個人開発に使う | AWSで統一したい | SAML・AD連携が不要ならSupabase Auth / Better Authで十分 |
| ORM選定で迷って実装が止まる | 完璧な選択をしようとする | Lightest/LightはDrizzle。Middle以上はPrismaでも可 |
| E2Eテストを全画面に書く | テストの網羅性を追いすぎる | クリティカルフロー(ログイン・購入)に絞る |
useEffect + fetch + useState のパターンはServer Componentsでほぼ不要になった。Djangoの request.user に近い感覚でサーバー側でデータを取れるnpm installしないので依存関係に縛られず、自由にカスタマイズできる点がポイントnpm ci は癖にする: npm installはロックファイルを無視する場合がある。CIでは必ずnpm ci