Zenn
🚀

T3 Stackの環境構築 + めちゃくちゃ便利なセットアップ

2025/03/22に公開
3
8

この記事でできること

  • 5分でGoogleログイン+Discordログイン
  • T3 Stack(TypeScript, NextAuth, Prisma, Tailwind CSS)の環境構築
  • 認証認可
  • クライアント側でのユーザー情報取得

やること

  • T3 Stackの環境構築
    • Bunを利用
  • 便利なセットアップ
    • 認証
      • Googleログインの追加
      • 独自ログインボタンの作成(画面遷移の改良)
      • Client Componentsでセッション情報取得

環境構築

新規作成

$ bun create t3-app@latest

   ___ ___ ___   __ _____ ___   _____ ____    __   ___ ___
  / __| _ \ __| /  \_   _| __| |_   _|__ /   /  \ | _ \ _ \
 | (__|   / _| / /\ \| | | _|    | |  |_ \  / /\ \|  _/  _/
  \___|_|_\___|_/‾‾\_\_| |___|   |_| |___/ /_/‾‾\_\_| |_|


│
◇  What will your project be called?
│  book-graph
│
◇  Will you be using TypeScript or JavaScript?
│  TypeScript
│
◇  Will you be using Tailwind CSS for styling?
│  Yes
│
◇  Would you like to use tRPC?
│  Yes
│
◇  What authentication provider would you like to use?
│  NextAuth.js
│
◇  What database ORM would you like to use?
│  Prisma
│
◇  Would you like to use Next.js App Router?
│  Yes
│
◇  What database provider would you like to use?
│  PostgreSQL
│
◇  Would you like to use ESLint and Prettier or Biome for linting and
formatting?
│  ESLint/Prettier
│
◇  Should we initialize a Git repository and stage the changes?
│  Yes
│
◇  Should we run 'bun install' for you?
│  Yes
│
◇  What import alias would you like to use?
│  @/

Using: bun

✔ book-graph scaffolded successfully!

Adding boilerplate...
✔ Successfully setup boilerplate for nextAuth
✔ Successfully setup boilerplate for prisma
✔ Successfully setup boilerplate for tailwind
✔ Successfully setup boilerplate for trpc
✔ Successfully setup boilerplate for dbContainer
✔ Successfully setup boilerplate for envVariables
✔ Successfully setup boilerplate for eslint

Installing dependencies...
✔ Successfully installed dependencies!

Formatting project with eslint...
✔ Successfully formatted project
Initializing Git...
✔ Successfully initialized and staged git

Next steps:
  cd book-graph
  Start up a database, if needed using './start-database.sh'
  bun run db:push
  Fill in your .env with necessary values. See https://create.t3.gg/en/usage/first-steps for more info.
  bun run dev
  git commit -m "initial commit"
  • 認証はNextAuthを利用
  • import aliasは @/を利用
    • nextjsやshadcnがデフォルトで採用している
    • 私はこちらの方が好み
cd book-graph

DBの起動

./start-database.sh
You are using the default database password
Should we generate a random password for you? [y/N]: y
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Database container 'book-graph-postgres' was successfully created
  • DockerでDB作成するコマンドを実行しているわけなので、Dockerを起動しておく
bun run db:push

起動

$ bun run dev

以下のエラーが出た

$ next dev --turbo

✘ next.js v15.2.3 is not yet supported in the Community edition of Console Ninja.
We are working hard on it for you https://tinyurl.com/3h9mtwra.

Estimated release dates:
  - Community users: around 15th June, 2025 (subject to team availability)
  - PRO users:       priority access is available now

❌ Invalid environment variables: [
  {
    code: 'invalid_type',
    expected: 'string',
    received: 'undefined',
    path: [ 'AUTH_DISCORD_ID' ],
    message: 'Required'
  },
  {
    code: 'invalid_type',
    expected: 'string',
    received: 'undefined',
    path: [ 'AUTH_DISCORD_SECRET' ],
    message: 'Required'
  }
]
file:///Users/nano/workspace/book-graph/node_modules/@t3-oss/env-core/dist/index.js:61
        throw new Error("Invalid environment variables");
              ^

Error: Invalid environment variables
    at onValidationError (file:///Users/nano/workspace/book-graph/node_modules/@t3-oss/env-core/dist/index.js:61:15)
    at createEnv (file:///Users/nano/workspace/book-graph/node_modules/@t3-oss/env-core/dist/index.js:67:16)
    at createEnv (file:///Users/nano/workspace/book-graph/node_modules/@t3-oss/env-nextjs/dist/index.js:12:12)
    at file:///Users/nano/workspace/book-graph/src/env.js:4:20
    at ModuleJob.run (node:internal/modules/esm/module_job:271:25)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:547:26)
    at async loadConfig (/Users/nano/workspace/book-graph/node_modules/next/dist/server/config.js:964:36)
    at async Module.nextDev (/Users/nano/workspace/book-graph/node_modules/next/dist/cli/next-dev.js:190:14)

Node.js v22.13.0
error: script "dev" exited with code 1

デフォルトでは新規登録+ログインにDiscordを使っているらしく、そのコードがないとのこと。以下を参考にして設定する。
(NextAuthなので簡単にGoogle アカウントも使えるが、Discordの方が簡単にクライアントやシークレットコードを取得できるので使われているのだろう)
https://create.t3.gg/en/usage/next-auth#setting-up-the-default-discordprovider

.env
# Next Auth Discord Provider
AUTH_DISCORD_ID=""
AUTH_DISCORD_SECRET=""

.envに取得したIDとシークレットを貼り付ける。

bun run dev

起動成功

ログイン確認

  • signinボタンを押してログインする
  • デフォルトだとNextAuthが用意した画面( http://localhost:3000/api/auth/signin )にとばされてしまう
    • クリック数が増えてUXが悪いため、後半で改良



無事にログインとuser data取得の成功

投稿確認

  • submit buttonで投稿できる
  • 投稿成功

git commit

git commit -m "initial commit"

便利なセットアップ

認証

Googleログインの追加

src/env.js
export const env = createEnv({
  /**
   * Specify your server-side environment variables schema here. This way you can ensure the app
   * isn't built with invalid env vars.
   */
  server: {
    AUTH_SECRET:
      process.env.NODE_ENV === "production"
        ? z.string()
        : z.string().optional(),
    AUTH_DISCORD_ID: z.string(),
    AUTH_DISCORD_SECRET: z.string(),
+   AUTH_GOOGLE_ID: z.string().optional(),
+   AUTH_GOOGLE_SECRET: z.string().optional(),
    DATABASE_URL: z.string().url(),
    NODE_ENV: z
      .enum(["development", "test", "production"])
      .default("development"),
  },

  runtimeEnv: {
    AUTH_SECRET: process.env.AUTH_SECRET,
    AUTH_DISCORD_ID: process.env.AUTH_DISCORD_ID,
    AUTH_DISCORD_SECRET: process.env.AUTH_DISCORD_SECRET,
+   AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID,
+   AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
    DATABASE_URL: process.env.DATABASE_URL,
    NODE_ENV: process.env.NODE_ENV,
  },
src/server/auth/config.ts
+ import GoogleProvider from "next-auth/providers/google";
+ import { env } from "@/env";

export const authConfig = {
  providers: [
+    DiscordProvider({
+      clientId: env.AUTH_DISCORD_ID,
+      clientSecret: env.AUTH_DISCORD_SECRET,
+    }),
+    GoogleProvider({
+      clientId: env.AUTH_GOOGLE_ID,
+      clientSecret: env.AUTH_GOOGLE_SECRET,
+    }),
  • 以下のように、Googleログインが可能になった


独自ログインボタンの作成(画面遷移の改良)

  • 課題

    • 自作のログインボタンを用意したい
    • ログイン&ログアウトする時に /api/auth/signin や /api/auth/signoutに飛ばされてしまう(ページ移動が多くて困る)
  • ログイン、ログアウトボタンの作成

src/app/_components/AuthButtons.tsx
"use client";

import { signIn } from "next-auth/react";
import Image from "next/image";

export function OAuthButtons() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-4">
      <button
        onClick={() => signIn("google", { callbackUrl: "/" })}
        className="flex items-center justify-center gap-2 rounded-md bg-white px-4 py-2 text-black shadow-sm transition-colors hover:bg-gray-100"
      >
        <Image src="/google-logo.svg" alt="Google" width={20} height={20} />
        <span>Googleでログイン</span>
      </button>

      <button
        onClick={() => signIn("discord", { callbackUrl: "/" })}
        className="flex items-center justify-center gap-2 rounded-md bg-[#5865F2] px-4 py-2 text-white shadow-sm transition-colors hover:bg-[#4752c4]"
      >
        <Image src="/discord-logo.svg" alt="Discord" width={20} height={20} />
        <span>Discordでログイン</span>
      </button>
    </div>
  );
}
src/app/_components/SignOutButton.tsx
"use client";

import { signOut } from "next-auth/react";

export function SignOutButton() {
  return (
    <button
      onClick={() => signOut({ callbackUrl: "/" })}
      className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20"
    >
      サインアウト
    </button>
  );
}
public/discord-logo.svg
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M19.9998 5.00001C19.9998 5.00001 17.8568 3.19001 15.2998 3.00001L15.0998 3.36001C17.3998 3.90001 18.4998 4.74001 19.1998 5.76001C17.2498 4.84501 15.2998 4.00001 12.9998 4.00001C10.6998 4.00001 8.74982 4.84501 6.79982 5.76001C7.49982 4.74001 8.69982 3.86001 10.8998 3.36001L10.6998 3.00001C8.04982 3.20001 6.00982 5.00001 6.00982 5.00001C6.00982 5.00001 3.58982 8.66001 3.00982 15.42C5.34982 18.12 8.99982 18 8.99982 18L9.79982 16.92C8.36982 16.44 6.79982 15.54 5.99982 14C7.19982 14.84 9.13982 15.9 12.9998 15.9C16.8598 15.9 18.7998 14.84 19.9998 14C19.1998 15.54 17.6298 16.44 16.1998 16.92L16.9998 18C16.9998 18 20.6498 18.12 22.9898 15.42C22.4098 8.66001 19.9998 5.00001 19.9998 5.00001ZM9.49982 13C8.69982 13 7.99982 12.22 7.99982 11.3C7.99982 10.38 8.67982 9.60001 9.49982 9.60001C10.3198 9.60001 10.9998 10.38 10.9998 11.3C10.9998 12.22 10.3198 13 9.49982 13ZM16.4998 13C15.6998 13 14.9998 12.22 14.9998 11.3C14.9998 10.38 15.6798 9.60001 16.4998 9.60001C17.3198 9.60001 17.9998 10.38 17.9998 11.3C17.9998 12.22 17.3198 13 16.4998 13Z" fill="white"/>
</svg> 
public/google-logo.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
  <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
  <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
  <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
  <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg> 
  • トップページにログインボタン配置
src/app/page.tsx
import Link from "next/link";

import { LatestPost } from "@/app/_components/post";
import { OAuthButtons } from "@/app/_components/AuthButtons";
import { SignOutButton } from "@/app/_components/SignOutButton";
import { auth } from "@/server/auth";
import { api, HydrateClient } from "@/trpc/server";

export default async function Home() {
  const hello = await api.post.hello({ text: "from tRPC" });
  const session = await auth();

  if (session?.user) {
    void api.post.getLatest.prefetch();
  }

  return (
    <HydrateClient>
      <main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white">
        <div className="container flex flex-col items-center justify-center gap-12 px-4 py-16">
          <h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]">
            Create <span className="text-[hsl(280,100%,70%)]">T3</span> App
          </h1>
          <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-8">
            <Link
              className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20"
              href="https://create.t3.gg/en/usage/first-steps"
              target="_blank"
            >
              <h3 className="text-2xl font-bold">First Steps →</h3>
              <div className="text-lg">
                Just the basics - Everything you need to know to set up your
                database and authentication.
              </div>
            </Link>
            <Link
              className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20"
              href="https://create.t3.gg/en/introduction"
              target="_blank"
            >
              <h3 className="text-2xl font-bold">Documentation →</h3>
              <div className="text-lg">
                Learn more about Create T3 App, the libraries it uses, and how
                to deploy it.
              </div>
            </Link>
          </div>
          <div className="flex flex-col items-center gap-2">
            <p className="text-2xl text-white">
              {hello ? hello.greeting : "Loading tRPC query..."}
            </p>

            <div className="flex flex-col items-center justify-center gap-4">
              <p className="text-center text-2xl text-white">
                {session && <span>Logged in as {session.user?.name}</span>}
              </p>

              {/* 以下変更点 */}
              {session ? (
                <SignOutButton />
              ) : (
                <div className="flex flex-col items-center gap-4">
                  <OAuthButtons />
                  <p className="text-sm text-gray-400">または</p>
                  <Link
                    href="/api/auth/signin"
                    className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20"
                  >
                    他の方法でサインイン
                  </Link>
                </div>
              )}
            </div>
          </div>

          {session?.user && <LatestPost />}
        </div>
      </main>
    </HydrateClient>
  );
}

  • 結果
    • 自作ログインボタンの追加
    • /api/auth/signin や /api/auth/signoutなどの画面に移動せずにログイン&ログアウトが可能に

Client Componentでセッション情報を取得

  • 課題

    • defaultではt3 stackが用意したauth()関数でサーバーサイドでセッションを取得
    • use clientなどの環境ではこの関数は動かない
    • server sideで取得したsession情報をclient componentsにpropsで渡しても良いが、NextAuthが用意しているclient向けのuseSession()を使いたい
  • 解決策

    • useSessionを使えるようにproviderを利用する
export default async function Home() {
  const hello = await api.post.hello({ text: "from tRPC" });
  const session = await auth(); //server sideでしか動かない
  • Providersをレイアウトに配置
src/app/layout.tsx
import "@/styles/globals.css";

import { type Metadata } from "next";
import { Geist } from "next/font/google";

import { Providers } from "@/app/_components/Providers";

export const metadata: Metadata = {
  title: "Create T3 App",
  description: "Generated by create-t3-app",
  icons: [{ rel: "icon", url: "/favicon.ico" }],
};

const geist = Geist({
  subsets: ["latin"],
  variable: "--font-geist-sans",
});

export default function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en" className={`${geist.variable}`}>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}
src/app/_components/Providers.tsx
"use client";

import { type ReactNode } from "react";
import { TRPCReactProvider } from "@/trpc/react";
import { AuthProvider } from "@/app/_components/AuthProvider";

interface ProvidersProps {
  children: ReactNode;
}

export function Providers({ children }: ProvidersProps) {
  return (
    <AuthProvider>
      <TRPCReactProvider>{children}</TRPCReactProvider>
    </AuthProvider>
  );
}

src/app/_components/AuthProvider.tsx
"use client";

import { SessionProvider } from "next-auth/react";
import { type ReactNode } from "react";

interface AuthProviderProps {
  children: ReactNode;
}

export function AuthProvider({ children }: AuthProviderProps) {
  return <SessionProvider>{children}</SessionProvider>;
}
src/app/page.tsx

+ import { UserProfile } from "@/app/_components/UserProfile";

// 省略 //
              {session ? (
+                <>
+                  <UserProfile />
+                  <SignOutButton />
+                </>
              ) : (
src/app/_components/UserProfile.tsx
"use client";

import { useSession } from "next-auth/react";

export function UserProfile() {
  const { data: session, status } = useSession();

  if (status === "loading") {
    return <div>ローディング中...</div>;
  }

  if (status === "unauthenticated") {
    return <div>ログインしていません</div>;
  }

  return (
    <div className="rounded-xl bg-white/10 p-4">
      <h2 className="mb-2 text-xl font-bold">ユーザープロフィール</h2>
      <div className="flex items-center gap-3">
        {session?.user?.image && (
          <img
            src={session.user.image}
            alt={session.user.name ?? "プロフィール画像"}
            className="h-12 w-12 rounded-full"
          />
        )}
        <div>
          <p className="font-medium">{session?.user?.name}</p>
          <p className="text-sm text-gray-300">{session?.user?.email}</p>
        </div>
      </div>
    </div>
  );
}
  • これで、Client Componentsでもsession情報を取得することができるようになった

まとめ

  • T3 Stackの環境構築
  • 認証周り(NextAuth)を改良し、
    • Googleログインの追加
    • カスタムログインボタン
    • クライアント側でのuseSession()を使ったsession取得

環境構築自体はbun create t3-app@latestがやってくれるため、かなり楽だったが、Auth周りは自分でカスタマイズが必要。(最初は薄くしてカスタマイズ性を持たせてくれている)

実際、tRPCにはTanstack Queryが統合されていたり、Prisma, Tailwind CSSなど、全て準備された状態で始めるのはとても楽だった。これを使いながら個人開発を進めていきたい。

8

Discussion

ひものひもの

Googleログイン認証の実装ってこんなイージーに終わるものなんですの...?
ちょっと便利すぎるのではなくて?
かなり汎用性高いですし使えるなら(パフォーマンス的にどういいのか無知なので調べてから)使ってみたい

手羽先手羽先

コメントありがとうございます!NextAuth(最新版:Auth.js)だとこんなに簡単に実装できます。ただ、もちろんNextAuthが便利なのはそうなのですが、実際にはtRPCやPrismaとの連携、NextAuthの導入、初期設定などをT3 Stackが全て事前準備してくれているのでクライアントIDを設定するだけで作業が終わっている、というのが楽に構築できる大きな理由です。

その代わり、OAuth2.0のフローやJWT認証などを知っておかないとブラックボックスになりすぎて痛い目を見ます。

現在はPrisma+PostgresでセッションとUser Dataを管理していますが、Firebase Authなども組みわせることが容易なので、Firebase側にユーザーデータを持たせて、NextAuthではセッションや認証認可だけ行わせることもできますので、調べてみると面白いかもしれません。

ひものひもの

なるほどなるほど、コメント書いた後にT3 Stackの仕組みと思想を軽く調べたのですが予め決められた最小限のスタックを利用し開発を促進、その後必要であれば追加でプラグイン等を付与していく流れなんですね。

情報処理安全確保支援士でOAuth2.0やJWT認証については学習したので、現状の私が利用するケースを考えても問題はなさそうです。

認証サービスの組み合わせですか、少々複雑性は上がりますがそれぞれの利点の良いとこ取りができるなら良さそうです
Next.jsを用いた個人開発をやってみようと思っていたので大変参考になります!ありがとうございます!

ログインするとコメントできます