🐕

推しの Next.js のはじまりのスタックを紹介します🙌

に公開

推しの Next.js のはじまりのスタックを紹介します🙌

【ちょっと宣伝】
推しの Next.js のはじまりのスタックを CursorClaude などのコーディングエージェントがさくっとつくれるように、
スターターのプロンプトを実装しました🚀

https://www.kakuco.dev/

ざっくりと紹介

ライブラリ

- リント
    - Biome
- テスト
    - Vitest
- CI/CD
    - Husky
    - GitHub Actions
- スキーマ
    - Zod
- フォーム
    - React Hook Form
    - shadcn/ui
- データベース
    - Prisma
- 認証
    - Clerk
- 決済
    - Stripe
- UI
    - Tailwind CSS
- UI - テーマ切り替え
    - next-themes
- アクセス解析
    - Google Analytics
      - @next/third-parties
    - vanilla-cookieconsent

プラットフォーム

- アプリ
  - Vercel
- ストレージ
  - Vercel Blob
- データベース
  - Neon

ディレクトリ構成

src/
├─ _lib/
├─ _actions/
│  └─ domain/
│     └─ todo.ts
├─ _schemas/
│  └─ domain/
│     └─ todo.ts
├─ _services/
│  ├─ app/
│  └─ domain/
│     └─ todo.ts
├─ _components/
│  ├─ ui/
│  └─ domain/
│     └─ domain/
│        ├─ form.tsx
│        └─ list.tsx
├─ _hooks/
└─ app/
   ├─ examples/
   │  ├─ [id]/
   │  │  └─ page.tsx
   │  └─ page.tsx
   └─ page.tsx

それぞれを選んだ理由

2025年現在においてはデファクトスタンダードっぽいものは説明の割愛をしています🙏

ライブラリ

リント

以下を選択肢にあげて検討しました。

  • ESLint + Prettier
  • Biome

わたしは推しを選んだ理由としては、

  • 設定が簡潔
  • 速度が速い

というものになります。

ただし Tailwind CSS の並び順がまだ組み込みされていないため VSC Extension の Tailwind CSS IntelliSense で並べ替えをしています。

テスト

  • /

CI/CD

  • /

スキーマ

  • /

フォーム

  • /

データベース

以下を選択肢にあげて検討しました。

  • Prisma
  • Drizzle ORM

わたしは推しを選んだ理由としては、

  • 挙動が安定
  • スキーマファイルの見やすい

というものになります。

認証

以下を選択肢にあげて検討しました。

  • プロパイダーがある
    • Auth0
    • Clerk
  • プロパイダーがない
    • NextAuth
    • Better Auth

わたしは推しを選んだ理由としては、

  • プロパイダーがある(個人情報絶対もちたくないマン)
  • 価格が安い
  • 開発の体験が良い

というものになります。

決済

  • /

UI

  • /

UI - テーマ切り替え

テーマ(ダークモードやライトモードなど)の切り替えに対応しています。

"use client";

import {
  MoonIcon as DarkModeIcon,
  SunIcon  as RootModeIcon,
} from "lucide-react";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";

import { Button } from "@/_components/ui/button";

export default function Component() {
  const { resolvedTheme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);

  useEffect(() => setMounted(true), []);

  const next = resolvedTheme === "dark" ? "light" : "dark";

  return (
    <Button onClick={() => setTheme(next)} variant="ghost" size="icon">
      {mounted ? (
        resolvedTheme === "dark" ? (
          <RootModeIcon />
        ) : (
          <DarkModeIcon />
        )
      ) : null}
    </Button>
  );
}

アクセス解析

ユーザーのクッキーの許可をまって解析タグを読み込みます。

"use client";

import { GoogleAnalytics } from "@next/third-parties/google";
import { useEffect, useState } from "react";

import "vanilla-cookieconsent/dist/cookieconsent.css";
import * as CookieConsent from "vanilla-cookieconsent";

export default function Component({ gaId }: { gaId: string | undefined }) {
  const [acceptedAnalytics, setAcceptedAnalytics] = useState(false);

  useEffect(() => {
    CookieConsent.run({
      categories: {
        necessary: { enabled: true, readOnly: true },
        analytics: {},
      },
      language: {
        default: "en",
        translations: {
          en: {
            consentModal: {
              title: "We use cookies",
              description: "Cookie modal description",
              acceptAllBtn: "Accept all",
              acceptNecessaryBtn: "Reject all",
              showPreferencesBtn: "Manage Individual preferences",
            },
            preferencesModal: {
              title: "Manage cookie preferences",
              acceptAllBtn: "Accept all",
              acceptNecessaryBtn: "Reject all",
              savePreferencesBtn: "Accept current selection",
              closeIconLabel: "Close modal",
              sections: [
                {
                  title: "Somebody said ... cookies?",
                  description: "I want one!",
                },
                {
                  title: "Strictly Necessary cookies",
                  description:
                    "These cookies are essential for the proper functioning of the website and cannot be disabled.",

                  //this field will generate a toggle linked to the 'necessary' category
                  linkedCategory: "necessary",
                },
                {
                  title: "Performance and Analytics",
                  description:
                    "These cookies collect information about how you use our website. All of the data is anonymized and cannot be used to identify you.",
                  linkedCategory: "analytics",
                },
                {
                  title: "More information",
                  description:
                    'For any queries in relation to my policy on cookies and your choices, please <a href="#contact-page">contact us</a>',
                },
              ],
            },
          },
        },
      },
      onConsent() {
        setAcceptedAnalytics(CookieConsent.acceptedCategory("analytics"));
      },
    });
  }, []);

  if (gaId == null) {
    return;
  }

  return acceptedAnalytics ? <GoogleAnalytics gaId={gaId} /> : null;
}

さいごに

みなさんの推しのはじまりのスタックも知りたい……!

Discussion