T3 StackはNext.js, TypeScrpit, tRPC, Prisma, Tailwind CSS, NextAuthで構成されておりcreate t3-app@latestコマンドを実行してプロジェクトを作成することでType Safeなフルスタックアプリケーションを構築することができます。プロジェクト作成直後からType Safeにアプリケーションを構築するための設定が行われているので構成する技術の基本的な知識があれば効率的にアプリケーションの開発を進めることができます。

本文書ではcrate t3-appでプロジェクトを作成後、T3の理解を深めるためにデフォルトの設定を利用してtRPCやNextAuthではどのような設定を行っているか確認していきます。

T3プロジェクトの環境構築

create t3-appコマンドによるプロジェクト作成

T3 Stackのプロジェクトを作成するためにcreate-t3-appのコマンドを実行します。コマンドを実行するとプロジェクトの名前の設定、TypeScriptとJavaScriptの選択、T3で利用するパッケージの選択(nextAuth, prisma, tailwind, trpc)を行う必要があります。本文書ではTypeScriptを選択し4つのパッケージを選択してプロジェクトの作成を行います。プロジェクト名はデフォルト値のmy-t3-appとしています。


 % npm create t3-app@latest
Need to install the following packages:
  create-t3-app@7.7.0
Ok to proceed? (y) y
   ___ ___ ___   __ _____ ___   _____ ____    __   ___ ___
  / __| _ \ __| /  \_   _| __| |_   _|__ /   /  \ | _ \ _ \
 | (__|   / _| / /\ \| | | _|    | |  |_ \  / /\ \|  _/  _/
  \___|_|_\___|_/‾‾\_\_| |___|   |_| |___/ /_/‾‾\_\_| |_|


? What will your project be called? my-t3-app
? Will you be using TypeScript or JavaScript? TypeScript
Good choice! Using TypeScript!
? Which packages would you like to enable? nextAuth, prisma, tailwind, trpc
? Initialize a new git repository? Yes
Nice one! Initializing repository!
? Would you like us to run 'npm install'? Yes
Alright. We'll install the dependencies for you!
? What import alias would you like configured? ~/

Using: npm

✔ my-t3-app 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 envVariables

Installing dependencies...
✔ Successfully installed dependencies!

Initializing Git...
✔ Successfully initialized and staged git

Next steps:
  cd my-t3-app
  npx prisma db push
  npm run dev

コマンドが完了するとmy-t3-appフォルダが作成されるので移動します。


 % cd my-t3-app

コマンドを実行した際にJavaScriptパッケージのdependenciesのインストールも同時に行うように設定されているので”npm install”コマンドの実行も完了しています。どのようなパッケージがインストールされているのかはpackage.jsonファイルから確認することができます。


{
  "name": "my-t3-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "build": "next build",
    "dev": "next dev",
    "postinstall": "prisma generate",
    "lint": "next lint",
    "start": "next start"
  },
  "dependencies": {
    "@next-auth/prisma-adapter": "^1.0.5",
    "@prisma/client": "^4.9.0",
    "@tanstack/react-query": "^4.20.2",
    "@trpc/client": "^10.9.0",
    "@trpc/next": "^10.9.0",
    "@trpc/react-query": "^10.9.0",
    "@trpc/server": "^10.9.0",
    "next": "^13.2.1",
    "next-auth": "^4.19.0",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "superjson": "1.9.1",
    "zod": "^3.20.6"
  },
  "devDependencies": {
    "@types/eslint": "^8.21.1",
    "@types/node": "^18.14.0",
    "@types/prettier": "^2.7.2",
    "@types/react": "^18.0.28",
    "@types/react-dom": "^18.0.11",
    "@typescript-eslint/eslint-plugin": "^5.53.0",
    "@typescript-eslint/parser": "^5.53.0",
    "autoprefixer": "^10.4.7",
    "eslint": "^8.34.0",
    "eslint-config-next": "^13.2.1",
    "postcss": "^8.4.14",
    "prettier": "^2.8.1",
    "prettier-plugin-tailwindcss": "^0.2.1",
    "prisma": "^4.9.0",
    "tailwindcss": "^3.2.0",
    "typescript": "^4.9.5"
  },
  "ct3aMetadata": {
    "initVersion": "7.7.0"
  }
}

データベースの作成

次のステップとして”create-t3-appコマンド実行後のメッセージにはnpx prisma db push”を実行するように表示されています。”npx prisma db push”はPrismaを利用してデータベースを作成するコマンドですが”npx prisma db push”を実行することで何が行われるのか確認します。

PrismaはORM(Object Relational Mapping)でデータベースを操作する際に利用するツールです。データベースの操作をSQLではなくオブジェクトのメソッドを利用して行い、Prismaを介することでデータベースの違いを吸収してくれるためデータベースの違いを意識することなく操作することができます。データベースを作成する際に設定するスキーマの情報はTypeScriptの型に利用することができます。

プロジェクトフォルダ直下にprismaフォルダが作成されておりその下にPrismaの設定ファイルであるschema.prismaファイルが作成されています。schema(スキーマ)という名前がファイルについている通り、データベースの情報だけではなくテーブルのスキーマ定義を確認することができます。スキーマファイルではテーブルの列だけではなく型の情報も設定しています。この型情報をTypeScriptで利用します。


// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
    provider = "prisma-client-js"
}

datasource db {
    provider = "sqlite"
    // NOTE: When using postgresql, mysql or sqlserver, uncomment the @db.Text annotations in model Account below
    // Further reading:
    // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
    // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string
    url      = env("DATABASE_URL")
}

model Example {
    id        String   @id @default(cuid())
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
}

// Necessary for Next auth
model Account {
    id                String  @id @default(cuid())
    userId            String
    type              String
    provider          String
    providerAccountId String
    refresh_token     String? // @db.Text
    access_token      String? // @db.Text
    expires_at        Int?
    token_type        String?
    scope             String?
    id_token          String? // @db.Text
    session_state     String?
    user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)

    @@unique([provider, providerAccountId])
}

model Session {
    id           String   @id @default(cuid())
    sessionToken String   @unique
    userId       String
    expires      DateTime
    user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
    id            String    @id @default(cuid())
    name          String?
    email         String?   @unique
    emailVerified DateTime?
    image         String?
    accounts      Account[]
    sessions      Session[]
}

model VerificationToken {
    identifier String
    token      String   @unique
    expires    DateTime

    @@unique([identifier, token])
}

“npx prisma db push”コマンドを実行すると利用するデータベースを操作するために必要となるPrisma Clientの作成とデータベースの作成とテーブルの作成を行います。databsource dbのproviderにsqliteが設定されて通りデフォルトではSQLiteデータベースを利用するように設定されています。

SQLiteデータベースはファイルベースのデータベースなのでurlにデータベースファイルのパスを指定します。env関数の引数に設定されているDATABASE_URLの値についてはプロジェクトフォルダ直下に作成されている環境変数ファイルの.envから取得します。.envファイルの中身を確認すると環境変数DATABASE_URLの値を確認することができ、db.sqliteが設定されていることがわかります。


# When adding additional environment variables, the schema in "/src/env.mjs"
# should be updated accordingly.

# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DATABASE_URL="file:./db.sqlite"

# Next Auth
# You can generate a new secret on the command line with:
# openssl rand -base64 32
# https://next-auth.js.org/configuration/options#secret
# NEXTAUTH_SECRET=""
NEXTAUTH_URL="http://localhost:3000"

# Next Auth Discord Provider
DISCORD_CLIENT_ID=""
DISCORD_CLIENT_SECRET=""

npx prisma db pushコマンドを実行してどのようなファイルが作成されるのか確認します。


 % npx prisma db push
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "db.sqlite" at "file:./db.sqlite"

SQLite database db.sqlite created at file:./db.sqlite

🚀  Your database is now in sync with your Prisma schema. Done in 27ms

✔ Generated Prisma Client (4.11.0 | library) to ./node_modules/@prisma/client in 109m

実行後のメッセージを確認しながら作成されたファイルを確認します。prismaフォルダにはdb.sqliteファイルが作成されていることがわかります。Prisma Clientについてはnode_modeus/@prisma/clientのindex.jsファイルからrequireされている.prisma/client/index.jsが作成されそのファイル中で設定が行われいます。

データベースに作成されているテーブルについてはdb.sqliteファイルに直接開いても確認することができないのでPrismaが持つ機能の一つであるPrisma Studioを利用します。Prisma Studioを起動するためにnpx prisma studioコマンドを実行します。localhost:5555でサーバが起動するのでブラウザからアクセスします。


 % npx prisma studio
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Prisma Studio is up on http://localhost:5555

schema.prismaファイルで定義していたModelがブラウザ上に表示されます。

Prsima Studioからのデータベースへの接続
Prsima Studioからのデータベースへの接続

作成されたテーブルの一つであるUserモデルを確認するために表示されているモデルの中からUserをクリックするとUserテーブルの中身を確認することができます。テーブルの中にデータは存在しないのでテーブルの列名だけ表示されます。列名についてはprisma.schemaで定義したModelと一致することが確認できます。他のモデルについてもprisma.schemaで定義した列名でテーブルが作成されていることが確認できます。

Userテーブルへのアクセス
Userテーブルへのアクセス

prisma.schemaのUserモデルの設定は下記となっています。


model User {
    id            String    @id @default(cuid())
    name          String?
    email         String?   @unique
    emailVerified DateTime?
    image         String?
    accounts      Account[]
    sessions      Session[]
}

開発サーバの起動

データベースの確認ができたので”npm run dev”コマンドで開発サーバを起動します。実行できるコマンドについてはpackage.jsonファイルのscriptsを確認してください。


 % npm run dev

> my-t3-app@0.1.0 dev
> next dev

ready - started server on 0.0.0.0:3000, url: http://localhost:3000
info  - Loaded env from /Users/mac/Desktop/my-t3-app/.env
event - compiled client and server successfully in 2.8s (290 modules)

ブラウザからhttp://localhost:3000にアクセスを行います。Create T3 Appの初期画面が表示されます。

create t3 appの初期画面
create t3 appの初期画面

表示されている内容のコードがどのファイルに記述されているか確認を行います。Next.jsを利用しているので/(ルート)へのアクセスが行われるとsrc/pagesフォルダのindex.tsxファイルが読み込まれます。index.tsxファイルを確認すると初期画面に表示されているコードが記述されています。


import { type NextPage } from "next";
import Head from "next/head";
import Link from "next/link";
import { signIn, signOut, useSession } from "next-auth/react";

import { api } from "~/utils/api";

const Home: NextPage = () => {
  const hello = api.example.hello.useQuery({ text: "from tRPC" });
  const examples = api.example.getAll.useQuery();

  return (
    <>
      <Head>
        <title>Create T3 App</title>
//略

tRPCの設定

index.tsxファイルのコードを見ると見慣れないコードが確認できます。これはtRPCに関するコードです。tRPCと一緒にuseQueryも利用しています。


const hello = api.example.hello.useQuery({ text: "from tRPC" });

tRPCはTypeScript Remote Procedure Callの略でバックエンドで定義したProcedureをクライアントで実行することができます。REST APIやGraphQLの代わりになるものでtRPCを利用することでバックエンドとクライアントで型情報を共有することで型安全なアプリケーションを構築する際に利用されます。

tRPCでリクエストを送信し戻された値を保存したhelloの値は初期ページで表示されている”Hello from tRPC”に利用されておりindex.tsxファイルでは下記のコードに対応します。


<p className="text-2xl text-white">
  {hello.data ? hello.data.greeting : "Loading tRPC query..."}
</p>

pタグにはclassNameが設定されていますが設定されているclassはすべてTailwind CSSが持つutility classです。Tailwind CSSの設定は完了しているのでTailwind CSSを利用するための追加設定は必要ありません。

tRPCを利用した場合にはサーバ(Next.jsではAPI Route)へのネットワークリクエストが送信されているはずなのでブラウザのデベロッパーツールのネットワークタブで確認することができます。Reqest URLを見るとAPIエンドポイントであるhttp://localhost:3000/api/trpc/example.helloであることがわかります。

ネットワークアクセスの確認
ネットワークアクセスの確認

APIエンドポイント/api/trpc/example.helloから戻される値も確認しておきます。dataオブジェクトの中のgreetingプロパティに”Hello from tRPC”を確認することができます。

APIエンドポイントから戻される値
APIエンドポイントから戻される値

Next.jsでは/pages/apiフォルダはAPIエンドポイントの/apiに対応しており、サーバ側で処理が行われます(API Route)。/api/trpc/example.helloへのアクセスが行われていたので/page/api/trpcフォルダを確認します。

tprcフォルダの中には[trpc].tsファイルが存在し、[trpc].tsの中でtRPCの設定が行われています。ファイル名にはブラケット[]が利用されているのでDyanamic Routingとして設定されています。/api/trpc/以下に続く文字列が何であろうと[trpc].tsファイルが実行され処理されることになります。そのため/api/trpc/exampleへのアクセスでは[trpc].tsファイルが実行されることになります。


import { createNextApiHandler } from "@trpc/server/adapters/next";

import { env } from "~/env.mjs";
import { createTRPCContext } from "~/server/api/trpc";
import { appRouter } from "~/server/api/root";

// export API handler
export default createNextApiHandler({
  router: appRouter,
  createContext: createTRPCContext,
  onError:
    env.NODE_ENV === "development"
      ? ({ path, error }) => {
          console.error(
            `❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
          );
        }
      : undefined,
});

/api/trpc/以下のルーティングについてはappRouterに記述されているのでappRouterをimportしている/server/api/root.tsファイルを確認します。root.tsファイルではさらにルーティングとしてexampleRouterがimportされているのでserver/api/routests/exampleファイルを確認します。


import { createTRPCRouter } from "~/server/api/trpc";
import { exampleRouter } from "~/server/api/routers/example";

/**
 * This is the primary router for your server.
 *
 * All routers added in /api/routers should be manually added here.
 */
export const appRouter = createTRPCRouter({
  example: exampleRouter,
});

// export type definition of API
export type AppRouter = typeof appRouter;

example.tsファイルの中身を確認してようやく/api/trpc/exmaple.helloへのリクエスト後に戻されているデータの内容を確認することができます。


import { z } from "zod";

import {
  createTRPCRouter,
  publicProcedure,
  protectedProcedure,
} from "~/server/api/trpc";

export const exampleRouter = createTRPCRouter({
  hello: publicProcedure
    .input(z.object({ text: z.string() }))
    .query(({ input }) => {
      return {
        greeting: `Hello ${input.text}`,
      };
    }),

  getAll: publicProcedure.query(({ ctx }) => {
    return ctx.prisma.example.findMany();
  }),

  getSecretMessage: protectedProcedure.query(() => {
    return "you can now see this secret message!";
  }),
});

ファイルの冒頭でimportしているzodはフォームなどの入力値のバリデーションに利用されるバリデーションライブラリでtRPCではリクエストに含まれる値をチェックするに利用しています。inputメソッドの中ではzodを利用してリクエストに含まれるtextが文字列かどうかチェックを行っています。textの値についてはindex.tsxファイルのuseQueryの引数で設定をしているのでこの値をチェックしています。


const hello = api.example.hello.useQuery({ text: "from tRPC" });

zodのバリデーションにパスした値のみクライアント側に”Hello ${input.text}”として戻されます。

tRPCではzodでバリデーションで利用した情報は型情報としてフロントエンドで共有されているためtextの値を文字列ではなく数値で設定した場合にはTypeScriptによりメッセージが表示されるため型安全に値を設定することができます。

型が一致しないことによるエラー
型が一致しないことによるエラー

もしそれでも数値にしたままリクエストを行うとブラウザのコンソールにはTRPCClientErrorによりエラーの原因を把握することができます。

エラーメッセージの確認
エラーメッセージの確認

バックエンドで設定した型情報は/server/api/root.tsファイルでexportされています。


//略
// export type definition of API
export type AppRouter = typeof appRouter;

exportした型のAppRouterはindex.tsxファイルでimportしているapiの中でimportが行われています。


//略
import { api } from "~/utils/api";
//略

utils/api.tsファイルを確認するとAppRouterが/server/api/root.tsファイルからimportされていることが確認できます。


//略
import { type AppRouter } from "~/server/api/root";
//略

データベースからのデータ取得

tRPCのルーティングファイルであるexmpale.tsファイルにはhelloの他にgetAllとgetSecretMessageが設定されていることがわかります。getSecretMessageについてはNextAuthを設定後に確認します。


import { z } from "zod";

import {
  createTRPCRouter,
  publicProcedure,
  protectedProcedure,
} from "~/server/api/trpc";

export const exampleRouter = createTRPCRouter({
  hello: publicProcedure
    .input(z.object({ text: z.string() }))
    .query(({ input }) => {
      return {
        greeting: `Hello ${input.text}`,
      };
    }),

  getAll: publicProcedure.query(({ ctx }) => {
    return ctx.prisma.example.findMany();
  }),

  getSecretMessage: protectedProcedure.query(() => {
    return "you can now see this secret message!";
  }),
});

デフォルトではgetAllはindex.tsxファイルで利用されていませんが動作確認のためにクライアント側で設定を行います。getAllではPrismaを経由してExampleテーブルにアクセスを行い、findManyメソッドでExampleテーブルのすべての情報を取得しています。

index.tsxファイルの”const hello = api.exmaple.hello…”の下にコードを追加します。api.example.と入力するとexmaple.tsファイルで設定したgetAll, getSecretMessage, helloが表示されます。

設定したルーティングの選択
設定したルーティングの選択

getAllを選択してuseQueryを設定してexamplesにどんな値が設定されているのか確認します。


const examples = api.example.getAll.useQuery();
console.log(examples)

ブラウザから初期画面を開くとブラウザのコンソールにexmplesの値が表示されます。dataプロパティが空の配列であることが確認できます。

dataプロパティの確認
dataプロパティの確認

index.tsxファイルでexamplesに含まれている値をブラウザ上に表示するためにexamples.と入力するとdataが候補として表示されるのでdataを選択します。dataが持つオブジェクトの型はExample[]かundefinedであることもわかります。

dataの候補が表示
dataの候補が表示

配列なのでmapメソッドを設定するとdataには自動で”?”がつきます。

map関数の設定
map関数の設定

map関数で展開し、example .と入力するとPrismaからアクセスを行うExampleテーブルをを構成する列情報が表示されそこから選択を行うことができるため存在しない列を設定することはなくなります。

Exampleのプロパティの選択
Exampleのプロパティの選択

バックエンドのAPIから取得したexamplesのidを表示するように設定を行いますがExampleテーブルには何もデータが入っていないのでブラウザ上には何も表示されません。


<ul>
  {examples.data?.map((example) => (
    <li className="text-white" key={example.id}>{example.id}</li>
  ))}
</ul>

Exampleテーブルには何もデータが入っていないため実際にPrismaを経由してデータ取得の処理が行われているのか確認したい人もいるかと思います。src/server/db.tsファイルのでPrisma Clientの設定が行われていますがPrismaClientのインスタンスを作成する際に引数にオプションを設定していることでPrismで行っているQueryの内容が”npm run dev”を実行したターミナルに表示されます。


import { PrismaClient } from "@prisma/client";

import { env } from "~/env.mjs";

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

export const prisma =
  globalForPrisma.prisma ||
  new PrismaClient({
    log:
      env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
  });

if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

ターミナルには以下のQueryが表示されます。このように実行するQueryの内容を確認することができます。


prisma:query SELECT `main`.`Example`.`id`, `main`.`Example`.`createdAt`, `main`.`Example`.`updatedAt` FROM `main`.`Example` WHERE 1=1 LIMIT ? OFFSET ?

Queryの表示が必要でない場合はPrismaClientの引数に設定したオブジェクトのlogプロパティの値に設定されている配列から”query”を削除してください。

Exampleテーブルにあるデータをブラウザ上に表示させるためnpx prisma studioコマンドを実行してPrisma Studioを起動してExampleテーブルにデータを登録してください。ここでは2件のデータを登録しています。

Prisma Studioからのデータ登録
Prisma Studioからのデータ登録

Exampleテーブルにデータを登録後、ブラウザで確認するとPrisma Studioで登録したデータのidが表示されることが確認できます。useQueryを利用しているので画面に触れるとリフェッチが行われidが表示されます。

Exampleテーブルに登録されたデータのidを表示
Exampleテーブルに登録されたデータのidを表示

tRPCとPrismaを利用することでデータベースのテーブルに登録されたデータを取得して表示することができます。

NextAuthの設定

NextAuthはAuthentication(認証)に利用するライブラリでNext.js専用で作成されていましたが現在はAuth.jsとしてSvelteKitなど他のフレームワークでも利用することができます。NextAuthはOAuth, magic links, email/passwordなど複数の認証方式を利用することができますがOAuthを利用して動作確認を行います。

通常NextAuthをインストールを行い、設定を行う場合は/pages/api/authフォルダに[…nextAuth].tsファイルを作成することから開始します。T3ではNextAuthをプロジェクト作成時に選択してインストールすると事前に設定は行われています。設定済みの[…nextAuth].tsファイルには以下のコードが記述されています。


import NextAuth from "next-auth";
import { authOptions } from "~/server/auth";

export default NextAuth(authOptions);

NextAuthの引数に設定されているauthOptionsの内容を確認するためにはserver/auth.tsファイルを確認します。OAuthを利用する場合はOAuth Providerの設定を行う必要があります。auth.tsファイルを確認するとデフォルトのOAuth ProviderにはDiscordが設定されていることがわかります。


export const authOptions: NextAuthOptions = {
  callbacks: {
    session({ session, user }) {
      if (session.user) {
        session.user.id = user.id;
        // session.user.role = user.role; <-- put other properties on the session here
      }
      return session;
    },
  },
  adapter: PrismaAdapter(prisma),
  providers: [
    DiscordProvider({
      clientId: env.DISCORD_CLIENT_ID,
      clientSecret: env.DISCORD_CLIENT_SECRET,
    }),
    /**
     * ...add more providers here.
     *
     * Most other providers require a bit more work than the Discord provider. For example, the
     * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
     * model. Refer to the NextAuth.js docs for the provider you want to use. Example:
     *
     * @see https://next-auth.js.org/providers/github
     */
  ],
};

利用可能なOAuth Providerについてはhttps://authjs.dev/reference/providers/oauth-builtinから確認することができます。

本文書でOAuth Providerはみなさんが取得している確率が非常に高いGithubを利用します。

auth.tsファイルではdiscordの設定が行われているのでdiscordからgithubに変更を行います。


//略
import GithubProvider from "next-auth/providers/github";
//略
  adapter: PrismaAdapter(prisma),
  providers: [
    GithubProvider({
      clientId: env.GITHUB_CLIENT_ID,
      clientSecret: env.GITHUB_CLIENT_SECRET,
    }),

clientIdとclientSecretに設定する環境変数の名前をDISCORD_CLIENT_IDからGITHUB_CLIENT_IDに変更するとTypeScriptのメッセージが表示されます。

TypeScriptのメッセージ
TypeScriptのメッセージ

TypeScriptの型情報についてはenv.mjsからimportしているのでenv.mjsファイルを開いてDISCORDの文字列をGITHUBに変更します。env.mjsファイルでもzodが利用されています。


const server = z.object({
  DATABASE_URL: z.string().url(),
 //略
  // Add `.min(1) on ID and SECRET if you want to make sure they're not empty
  GITHUB_CLIENT_ID: z.string(),
  GITHUB_CLIENT_SECRET: z.string(),
});
//略
const processEnv = {
  DATABASE_URL: process.env.DATABASE_URL,
  NODE_ENV: process.env.NODE_ENV,
  NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
  NEXTAUTH_URL: process.env.NEXTAUTH_URL,
  GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
  GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
  // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
};

文字列の変更が完了するとauth.tsで表示されていたTypeScriptのメッセージも非表示となります。

GitHubでのClient IDの取得

GitHubを利用してOAuthを設定するためにGitHubのアカウントを持ち、Client IDとClient Secretを取得する必要があります。

GitHubのアカウントでログイン後に右上のアイコンをクリックしてドロップダウンメニューを表示して”Settings”を選択します。

メニューからSettingを選択
メニューからSettingを選択

左のサイドメニューの一番下にあるDeveloper settingsをクリックします。

Developer settingsの選択
Developer settingsの選択

Developer Settingの画面の左メニューから”OAuth Apps”を選択して”Register a new application”ボタンをクリックします。

OAuth Appsの選択
OAuth Appsの選択

OAuth applicationの登録を行います。Application Nameは任意の名前、Homepage URLは開発サーバのURLを設定、Authorization callback URLにはhttp://localhost:3000/api/auth/callback/githubを設定してください。入力が終わったら”Register application”ボタンをクリックしてください。

OAuth applicationの登録
OAuth applicationの登録

Client IDとClient Secretsを作成することができます。Client Secretsは”Generate a new client secret”ボタンをクリックすると作成できます。下記の図の黒塗りの部分がsecretの情報です。

Client IDとSecret
Client IDとSecret

取得した情報は.envファイルに設定しますが環境変数名がDISCORDになっているのでGITHUBに変更してClient IDとsecretを設定します。各自が取得した値を設定してください。下記の値は利用できません。


GITHUB_CLIENT_ID="9bfad65611f8b9e8e1f9"
GITHUB_CLIENT_SECRET="4ec3ce3765ffe1aacfc3670e3996621f52829f3e"

環境変数を設定後にブラウザからhttp://localhost:3000/api/auth/providersにアクセスするとOAuthを設定したProviderの情報が表示されます。ここではGithubの設定に関する情報が表示されます。env.mjsファイルの更新や.envファイルの環境変数名がGITHUBに変更されていない場合にはエラーが表示されるので修正を行なってください。Client IDやsecretの値の誤りではエラーは表示されません。

callbackへのアクセス
callbackへのアクセス

.envファイルにNEXTAUTH_SECRETを設定する必要があるので下記のコマンドを利用してランダムな文字列を生成します。


% openssl rand -base64 32
f4cQSY7vyM8nqMY7Nb0oAROZubqNSq+w7ZOXxoi4PU0=

生成した値を.envファイルのNEXTAUTH_SECRETに設定してください。


NEXTAUTH_SECRET="f4cQSY7vyM8nqMY7Nb0oAROZubqNSq+w7ZOXxoi4PU0="
NEXTAUTH_URL="http://localhost:3000"

NextAuthに関する設定は完了です。動作確認は初期画面に表示されている”Sign In”をクリックすることで行うことができます。

Exampleテーブルに登録されたデータのidを表示
Sign Inの確認

“Sign in with GitHub”ボタンが表示されるのでクリックします。

"Sign in Github"のボタン表示
“Sign in Github”のボタン表示

GitHubにログインが完了している場合はGitHubアカウントへのアクセスを許可するかどうか聞かれるので”Authorize ユーザ名”ボタンをクリックします。

Authorize画面
Authorize画面

OAuthを介してサインインに成功した場合にはGitHubから取得したユーザ名がブラウザ上に表示されます。

GItHubアカウントユーザ名の表示
GItHubアカウントユーザ名の表示

サインイン後にPrisma Studioを利用してテーブルの中身を確認するとAccount, User, Sessionに情報が登録されていることが確認できます。

UserテーブルにはGitHubから取得したユーザ情報が表示されます。

Userテーブルに登録された情報
Userテーブルに登録された情報

ユーザ名が表示されるまでの流れ

GitHubでのサインインが完了するとユーザ名が初期画面に表示されましたがユーザ名が表示されるまでに内部でどのような設定/処理が行なっているか確認しておきます。

“Sign In”ボタン、サインイン後の表示についてはindex.tsxファイルのAuthShowcaseコンポーネントで行われています。

AuthShowcaseコンポーネントの中身を確認すると以下のコードが記述されています。カスタムフックのuseSessionから戻されるdata(sessionData)を条件分岐に利用して表示させる内容を変えています。sessionDataに値がない場合は”Sign In”のみ表示されます。


const AuthShowcase: React.FC = () => {
  const { data: sessionData } = useSession();

  const { data: secretMessage } = api.example.getSecretMessage.useQuery(
    undefined, // no input
    { enabled: sessionData?.user !== undefined }
  );

  return (
    <div className="flex flex-col items-center justify-center gap-4">
      <p className="text-center text-2xl text-white">
        {sessionData && <span>Logged in as {sessionData.user?.name}</span>}
        {secretMessage && <span> - {secretMessage}</span>}
      </p>
      <button
        className="rounded-full bg-white/10 px-10 py-3 font-semibold text-white no-underline transition hover:bg-white/20"
        onClick={sessionData ? () => void signOut() : () => void signIn()}
      >
        {sessionData ? "Sign out" : "Sign in"}
      </button>
    </div>
  );
};

サインインが完了した後のsessionDataには以下のオブジェクトが含まれています。


{
    "user": {
        "name": "John Doe",
        "email": "gayoureamkefai@gmail.com",
        "image": "https://avatars.githubusercontent.com/u/70009875?v=4",
        "id": "clf7mff4i0000m1hvabsiu5rb"
    },
    "expires": "2023-04-13T02:15:15.296Z"
}

サインインが完了していない場合にはnullが戻されます。

sessionDataにuserオブジェクトが含まれている場合のみtRPCによりリクエストが行われています。つまりサインインが完了していない場合はリクエストは送信されずsecretMessageの値はundefinedになります。useQueryのオプションのenabledsをfalseにするとリクエストは行われません。


const { data: secretMessage } = api.example.getSecretMessage.useQuery(
  undefined, // no input
  { enabled: sessionData?.user !== undefined }
);

tRPCのリクエストの中身を確認するためにgetSecretMessageを調べます。getSecretMessageはsrc/server/api/routers/example.tsファイルの中に記述されています。


import { z } from "zod";

import {
  createTRPCRouter,
  publicProcedure,
  protectedProcedure,
} from "~/server/api/trpc";

export const exampleRouter = createTRPCRouter({
  hello: publicProcedure
    .input(z.object({ text: z.string() }))
    .query(({ input }) => {
      return {
        greeting: `Hello ${input.text}`,
      };
    }),

  getAll: publicProcedure.query(({ ctx }) => {
    return ctx.prisma.example.findMany();
  }),

  getSecretMessage: protectedProcedure.query(() => {
    return "you can now see this secret message!";
  }),
});

サインインをしない状態でも利用できるhello, getAllではpublicProcedureが設定されていますがgetSecretMessageではprotectedProcedureが設定されているという違いがあることがコードからわかります。publicProcedureはserver/api/trpc.tsファイルからimportされているので確認すると以下の通り特別な設定はありません。


export const publicProcedure = t.procedure;

しかしprotectedProcedureではmiddlewareを利用してcontext(ctx)にsession情報が含まれているかチェックを行い、含まれている場合のみ次の処理に進むようなコードになっています。middlewareを設定することでprotectedProcedureが設定されたエンドポイントでは必ずsessionのチェックが行われることになります。


/** Reusable middleware that enforces users are logged in before running the procedure. */
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.session || !ctx.session.user) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }
  return next({
    ctx: {
      // infers the `session` as non-nullable
      session: { ...ctx.session, user: ctx.session.user },
    },
  });
});

/**
 * Protected (authenticated) procedure
 *
 * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
 * the session is valid and guarantees `ctx.session.user` is not null.
 *
 * @see https://trpc.io/docs/procedures
 */
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);

getSecretMessageではmiddlewareでsessionのチェックが行われパスすると文字列の”you can now see this secret message!”をクライアントに戻すように設定されています。

サインインしていない状態でuseQueryを実行できるようにオプションのenabledの値をコメントします。


const { data: secretMessage } = api.example.getSecretMessage.useQuery(
  undefined, // no input
 // { enabled: sessionData?.user !== undefined }
);

tRPCによるリクエストは行われ”TRPCClientError: UNAUTHORIZED”がブラウザのコンソールに表示されます。サインインが行われていないとprotectedRouteで設定したエンドポイントは実行されてないということも確認できました。

APIエンドポイントを追加する場合、サインインしていない状態でもリクエストによる処理が可能な場合はpubicProcedure, サインインしている状態のみリクエストの処理を行う場合にはprotectedProcedureを設定することでアクセスの制限を行うことができます。

ここまで読み進めてもらえたならtRPCによるリクエストとレスポンス、データベースからのデータ取得、ユーザの認証機能に関するT3 Stackのコア部分の基本的の設定方法の理解が深まったと思います。