跳至主要內容

如何使用 Prisma ORM 搭配 Better Auth 和 Next.js

25 分鐘

簡介

Better Auth 是一個現代化的開源 Web 應用程式身份驗證解決方案。它採用 TypeScript 建構,提供了簡單且可擴充的驗證體驗,並支援多種資料庫轉接器(Adapter),包括 Prisma。

在本指南中,您將把 Better Auth 整合至一個全新的 Next.js 應用程式中,並將使用者資料持久化於 Prisma Postgres 資料庫中。您可以在 GitHub 上找到本指南的完整範例。

先決條件

  • Node.js 20+
  • 基本熟悉 Next.js App Router 與 Prisma

1. 設定您的專案

建立一個新的 Next.js 應用程式

npx create-next-app@latest betterauth-nextjs-prisma

它會提示您自訂設定。請選擇預設值

資訊
  • 您想使用 TypeScript 嗎? Yes
  • 您想使用 ESLint 嗎? Yes
  • 您想使用 Tailwind CSS 嗎? Yes
  • 您想要將程式碼放在 src/ 目錄中嗎? Yes
  • 您想要使用 App Router 嗎? Yes
  • 您想要使用 Turbopack 嗎? Yes
  • 您想自訂匯入別名 (預設為 @/*) 嗎? No

導覽至專案目錄

cd betterauth-nextjs-prisma

這些選項將建立一個現代化的 Next.js 專案,具備用於型別安全的 TypeScript、用於程式碼品質的 ESLint,以及用於樣式的 Tailwind CSS。使用 src/ 目錄和 App Router 是新 Next.js 應用程式的常見慣例。

2. 設定 Prisma

接下來,您將把 Prisma 加入您的專案以管理資料庫。

2.1. 安裝 Prisma 與相依套件

安裝必要的 Prisma 套件。相依套件會根據您是否使用搭配 Accelerate 的 Prisma Postgres 或其他資料庫而略有不同。

npm install prisma tsx @types/pg --save-dev
npm install @prisma/client @prisma/adapter-pg dotenv pg
資訊

如果您使用的是不同的資料庫提供者(MySQL、SQL Server、SQLite),請安裝相應的驅動程式適配器套件,而不是 @prisma/adapter-pg。如需更多資訊,請參閱資料庫驅動程式

安裝完成後,在您的專案中初始化 Prisma

npx prisma init --db --output ../src/generated/prisma
資訊

您在設定 Prisma Postgres 資料庫時需要回答幾個問題。請選擇離您最近的地區,並為資料庫取一個容易記住的名稱,例如「My Better Auth Project」。

這將建立:

  • 一個包含 schema.prisma 檔案的 prisma 目錄
  • 一個 Prisma Postgres 資料庫
  • 一個在專案根目錄中包含 DATABASE_URL.env 檔案
  • 一個用於產生 Prisma Client 的 output 目錄,路徑為 better-auth/generated/prisma

2.2. 設定 Prisma

在專案根目錄建立一個包含以下內容的 prisma.config.ts 檔案:

prisma.config.ts
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config';

export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: env('DATABASE_URL'),
},
});
注意

dotenv 套件應該已經安裝,因為它是 Next.js 的相依套件。如果沒有,請使用以下指令安裝

npm install dotenv

2.3. 產生 Prisma Client

執行以下指令以建立資料庫表格並產生 Prisma Client

npx prisma generate

2.4. 設定全域 Prisma Client

src 目錄中,建立一個 lib 資料夾並在其中建立一個 prisma.ts 檔案。此檔案將用於建立並匯出您的 Prisma Client 實例。

mkdir -p src/lib
touch src/lib/prisma.ts

按如下方式設定 Prisma client:

src/lib/prisma.ts
import { PrismaClient } from "@/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";

const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL!,
});

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

const prisma =
globalForPrisma.prisma || new PrismaClient({
adapter,
});

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

export default prisma;
警告

我們建議使用連線池(例如 Prisma Accelerate)來有效率地管理資料庫連線。

如果您選擇不使用,請避免在長效執行環境中全域實例化 PrismaClient。請改為在每個請求中建立並銷毀客戶端,以防止資料庫連線耗盡。

3. 設定 Better Auth

現在是時候整合 Better Auth 進行身份驗證了。

3.1. 安裝並設定 Better Auth

首先,安裝 Better Auth 核心套件

npm install better-auth

接下來,產生一個安全密鑰,Better Auth 將使用它來簽署身份驗證權杖 (tokens)。這能確保您的權杖無法被竄改。

npx @better-auth/cli@latest secret

複製產生的密鑰,並將其與您應用程式的網址 (URL) 一起加入您的 .env 檔案中

.env
# Better Auth
BETTER_AUTH_SECRET=your-generated-secret
BETTER_AUTH_URL=https://:3000

# Prisma
DATABASE_URL="your-database-url"

現在,為 Better Auth 建立一個設定檔。在 src/lib 目錄中,建立一個 auth.ts 檔案

touch src/lib/auth.ts

在此檔案中,您將設定 Better Auth 使用 Prisma 轉接器,這能讓它將使用者與工作階段資料持久化於您的資料庫中。您還將啟用電子郵件與密碼身份驗證。

src/lib/auth.ts
import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import prisma from '@/lib/prisma'

export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: 'postgresql',
}),
})

Better Auth 也支援其他登入方式,例如社群登入(Google、GitHub 等),您可以在他們的 文件 中探索。

src/lib/auth.ts
import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import prisma from '@/lib/prisma'

export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: 'postgresql',
}),
emailAndPassword: {
enabled: true,
},
})
資訊

如果您的應用程式運行在 3000 以外的連接埠,您必須將其加入 auth.ts 設定檔中的 trustedOrigins,以避免在身份驗證請求期間發生 CORS 錯誤。

src/lib/auth.ts
import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import prisma from '@/lib/prisma'

export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: 'postgresql',
}),
emailAndPassword: {
enabled: true,
},
trustedOrigins: ['https://:3001'],
})

3.2. 將 Better Auth 模型加入您的結構 (Schema)

Better Auth 提供了一個 CLI 指令,可以自動將必要的身份驗證模型(UserSessionAccountVerification)加入您的 schema.prisma 檔案中。

執行以下指令

npx @better-auth/cli generate
注意

它會要求確認是否覆寫現有的 Prisma 結構。請選擇 y

這將加入以下模型

model User {
id String @id
name String
email String
emailVerified Boolean
image String?
createdAt DateTime
updatedAt DateTime
sessions Session[]
accounts Account[]

@@unique([email])
@@map("user")
}

model Session {
id String @id
expiresAt DateTime
token String
createdAt DateTime
updatedAt DateTime
ipAddress String?
userAgent String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@unique([token])
@@map("session")
}

model Account {
id String @id
accountId String
providerId String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
password String?
createdAt DateTime
updatedAt DateTime

@@map("account")
}

model Verification {
id String @id
identifier String
value String
expiresAt DateTime
createdAt DateTime?
updatedAt DateTime?

@@map("verification")
}

3.3. 遷移資料庫

隨著結構中加入了新模型,您需要更新資料庫。執行遷移以建立對應的表格

npx prisma migrate dev --name add-auth-models
npx prisma generate

4. 設定 API 路由

Better Auth 需要一個 API 端點來處理登入、註冊和登出等身份驗證請求。您將在 Next.js 中建立一個萬用 (catch-all) API 路由來處理所有發送到 /api/auth/[...all] 的請求。

src/app/api 目錄中,建立 auth/[...all] 資料夾結構,並在其中建立 route.ts 檔案

mkdir -p "src/app/api/auth/[...all]"
touch "src/app/api/auth/[...all]/route.ts"

將以下程式碼加入新建立的 route.ts 檔案。此程式碼使用 Better Auth 提供的輔助函式來建立與 Next.js 相容的 GETPOST 請求處理程式。

import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";

export const { POST, GET } = toNextJsHandler(auth);

接下來,您需要一個客戶端工具來與這些端點進行互動。在 src/lib 目錄中,建立一個 auth-client.ts 檔案

touch src/lib/auth-client.ts

加入以下程式碼,它會建立您在 UI 中使用的 React Hooks 和函式

import { createAuthClient } from 'better-auth/react'

export const { signIn, signUp, signOut, useSession } = createAuthClient()

5. 設定您的頁面

現在,讓我們為身份驗證建立使用者介面。在 src/app 目錄中,建立以下資料夾結構

  • sign-up/page.tsx
  • sign-in/page.tsx
  • dashboard/page.tsx
mkdir -p src/app/{sign-up,sign-in,dashboard}
touch src/app/{sign-up,sign-in,dashboard}/page.tsx

5.1. 註冊頁面

首先,在 src/app/sign-up/page.tsx 中建立基本的 SignUpPage 元件。這將為您的頁面設定主要的容器與標題。

src/app/sign-up/page.tsx
"use client";

export default function SignUpPage() {
return (
<main className="max-w-md mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign Up</h1>
</main>
);
}

接下來,匯入 React 和 Next.js 的必要 Hooks 以管理狀態與導航。初始化路由器和一個狀態變數來存放任何潛在的錯誤訊息。

src/app/sign-up/page.tsx
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";

export default function SignUpPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);

return (
<main className="max-w-md mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign Up</h1>
</main>
);
}

現在,匯入 Better Auth 客戶端中的 signUp 函式並加入 handleSubmit 函式。此函式在表單提交時觸發,並呼叫 Better Auth 提供的 signUp.email 方法,傳入使用者的姓名、電子郵件和密碼。

src/app/sign-up/page.tsx
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
//add-next-lin
import { signUp } from "@/lib/auth-client";

export default function SignUpPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);

async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);

const formData = new FormData(e.currentTarget);

const res = await signUp.email({
name: formData.get("name") as string,
email: formData.get("email") as string,
password: formData.get("password") as string,
});

if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}

return (
<main className="max-w-md mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign Up</h1>
</main>
);
}

為了通知使用者任何問題,請加入一個當 error 狀態不為 null 時進行條件渲染的元素。

src/app/sign-up/page.tsx
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { signUp } from "@/lib/auth-client";

export default function SignUpPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);

async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);

const formData = new FormData(e.currentTarget);

const res = await signUp.email({
name: formData.get("name") as string,
email: formData.get("email") as string,
password: formData.get("password") as string,
});

if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}

return (
<main className="max-w-md mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign Up</h1>

{error && <p className="text-red-500">{error}</p>}
</main>
);
}

最後,加入包含使用者姓名、電子郵件和密碼輸入欄位的 HTML 表單,以及一個提交按鈕。

src/app/sign-up/page.tsx
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { signUp } from "@/lib/auth-client";

export default function SignUpPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);

async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);

const formData = new FormData(e.currentTarget);

const res = await signUp.email({
name: formData.get("name") as string,
email: formData.get("email") as string,
password: formData.get("password") as string,
});

if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}

return (
<main className="max-w-md mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign Up</h1>

{error && <p className="text-red-500">{error}</p>}

<form onSubmit={handleSubmit} className="space-y-4">
<input
name="name"
placeholder="Full Name"
required
className="w-full rounded-md bg-neutral-900 border border-neutral-700 px-3 py-2"
/>
<input
name="email"
type="email"
placeholder="Email"
required
className="w-full rounded-md bg-neutral-900 border border-neutral-700 px-3 py-2"
/>
<input
name="password"
type="password"
placeholder="Password"
required
minLength={8}
className="w-full rounded-md bg-neutral-900 border border-neutral-700 px-3 py-2"
/>
<button
type="submit"
className="w-full bg-white text-black font-medium rounded-md px-4 py-2 hover:bg-gray-200"
>
Create Account
</button>
</form>
</main>
);
}

5.2. 登入頁面

對於登入頁面,從 src/app/sign-in/page.tsx 中的基本結構開始。

src/app/sign-in/page.tsx
"use client";

export default function SignInPage() {
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign In</h1>
</main>
);
}

現在,加入狀態和路由器 Hooks,這與註冊頁面類似。

src/app/sign-in/page.tsx
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";

export default function SignInPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);

return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign In</h1>
</main>
);
}

加入 handleSubmit 函式,這次改為匯入並使用 Better Auth 的 signIn.email 方法。

src/app/sign-in/page.tsx
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { signIn } from "@/lib/auth-client";

export default function SignInPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);

async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);

const formData = new FormData(e.currentTarget);

const res = await signIn.email({
email: formData.get("email") as string,
password: formData.get("password") as string,
});

if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}

return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign In</h1>
</main>
);
}

加入錯誤訊息的條件顯示。

src/app/sign-in/page.tsx
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { signIn } from "@/lib/auth-client";

export default function SignInPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);

async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);

const formData = new FormData(e.currentTarget);

const res = await signIn.email({
email: formData.get("email") as string,
password: formData.get("password") as string,
});

if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}

return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign In</h1>

{error && <p className="text-red-500">{error}</p>}
</main>
);
}

最後,加入電子郵件和密碼的表單欄位以及一個登入按鈕。

src/app/sign-in/page.tsx
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { signIn } from "@/lib/auth-client";

export default function SignInPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);

async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);

const formData = new FormData(e.currentTarget);

const res = await signIn.email({
email: formData.get("email") as string,
password: formData.get("password") as string,
});

if (res.error) {
setError(res.error.message || "Something went wrong.");
} else {
router.push("/dashboard");
}
}

return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Sign In</h1>

{error && <p className="text-red-500">{error}</p>}

<form onSubmit={handleSubmit} className="space-y-4">
<input
name="email"
type="email"
placeholder="Email"
required
className="w-full rounded-md bg-neutral-900 border border-neutral-700 px-3 py-2"
/>
<input
name="password"
type="password"
placeholder="Password"
required
className="w-full rounded-md bg-neutral-900 border border-neutral-700 px-3 py-2"
/>
<button
type="submit"
className="w-full bg-white text-black font-medium rounded-md px-4 py-2 hover:bg-gray-200"
>
Sign In
</button>
</form>
</main>
);
}

5.3. 儀表板頁面

這是給已驗證使用者的受保護頁面。從 src/app/dashboard/page.tsx 中的基本元件開始。

src/app/dashboard/page.tsx
"use client";

export default function DashboardPage() {
return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Dashboard</h1>
</main>
);
}

匯入 Better Auth 客戶端中的 useSession Hook。這個 Hook 是在客戶端管理身份驗證狀態的關鍵。它提供工作階段資料和載入中狀態。

src/app/dashboard/page.tsx
"use client";

import { useRouter } from "next/navigation";
import { useSession } from "@/lib/auth-client";

export default function DashboardPage() {
const router = useRouter();
const { data: session, isPending } = useSession();

return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Dashboard</h1>
</main>
);
}

為了保護此路由,請使用 useEffect Hook。此 Effect 會檢查工作階段是否已載入 (!isPending) 以及是否有已驗證的使用者 (!session?.user)。如果兩者皆為真,它會將使用者重新導向至登入頁面。

src/app/dashboard/page.tsx
"use client";

import { useRouter } from "next/navigation";
import { useSession } from "@/lib/auth-client";
import { useEffect } from "react";

export default function DashboardPage() {
const router = useRouter();
const { data: session, isPending } = useSession();

useEffect(() => {
if (!isPending && !session?.user) {
router.push("/sign-in");
}
}, [isPending, session, router]);

return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Dashboard</h1>
</main>
);
}

為了提供更好的使用者體驗,請在驗證工作階段時加入載入和重新導向狀態。

src/app/dashboard/page.tsx
"use client";

import { useRouter } from "next/navigation";
import { useSession } from "@/lib/auth-client";
import { useEffect } from "react";

export default function DashboardPage() {
const router = useRouter();
const { data: session, isPending } = useSession();

useEffect(() => {
if (!isPending && !session?.user) {
router.push("/sign-in");
}
}, [isPending, session, router]);

if (isPending)
return <p className="text-center mt-8 text-white">Loading...</p>;
if (!session?.user)
return <p className="text-center mt-8 text-white">Redirecting...</p>;

return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Dashboard</h1>
</main>
);
}

最後,如果使用者已驗證,顯示從 session 物件中取得的姓名與電子郵件。同時,匯入 signOut 函式並加入一個按鈕來呼叫它,允許使用者登出。

src/app/dashboard/page.tsx
"use client";

import { useRouter } from "next/navigation";
import { useSession, signOut } from "@/lib/auth-client";
import { useEffect } from "react";

export default function DashboardPage() {
const router = useRouter();
const { data: session, isPending } = useSession();

useEffect(() => {
if (!isPending && !session?.user) {
router.push("/sign-in");
}
}, [isPending, session, router]);

if (isPending)
return <p className="text-center mt-8 text-white">Loading...</p>;
if (!session?.user)
return <p className="text-center mt-8 text-white">Redirecting...</p>;

const { user } = session;

return (
<main className="max-w-md h-screen flex items-center justify-center flex-col mx-auto p-6 space-y-4 text-white">
<h1 className="text-2xl font-bold">Dashboard</h1>
<p>Welcome, {user.name || "User"}!</p>
<p>Email: {user.email}</p>
<button
onClick={() => signOut()}
className="w-full bg-white text-black font-medium rounded-md px-4 py-2 hover:bg-gray-200"
>
Sign Out
</button>
</main>
);
}

5.4. 首頁

最後,更新首頁以提供通往登入與註冊頁面的簡單導航。將 src/app/page.tsx 的內容替換為以下內容

src/app/page.tsx
"use client";

import { useRouter } from "next/navigation";

export default function Home() {
const router = useRouter();

return (
<main className="flex items-center justify-center h-screen bg-neutral-950 text-white">
<div className="flex gap-4">
<button
onClick={() => router.push("/sign-up")}
className="bg-white text-black font-medium px-6 py-2 rounded-md hover:bg-gray-200">
Sign Up
</button>
<button
onClick={() => router.push("/sign-in")}
className="border border-white text-white font-medium px-6 py-2 rounded-md hover:bg-neutral-800">
Sign In
</button>
</div>
</main>
);
}

6. 測試

您的應用程式現在已完全設定完成。

  1. 啟動開發伺服器進行測試
npm run dev
  1. 在瀏覽器中前往 https://:3000。您應該會看到帶有「Sign Up」(註冊)和「Sign In」(登入)按鈕的首頁。

  2. 點擊 Sign Up,建立一個新帳號,您應該會被重新導向至儀表板。接著您可以登出並重新登入。

  3. 要直接在您的資料庫中檢視使用者資料,可以使用 Prisma Studio。

npx prisma studio
  1. 這會在您的瀏覽器中開啟一個新分頁,您可以在那裡看到 UserSessionAccount 表格及其內容。
成功

恭喜!您現在擁有一個由 Better Auth、Prisma 和 Next.js 建構的完整身份驗證系統。

後續步驟

  • 新增對社群登入或 Magic Links 的支援
  • 實作密碼重設與電子郵件驗證
  • 加入使用者個人檔案與帳號管理頁面
  • 部署至 Vercel 並確保您的環境變數安全
  • 使用自訂應用程式模型擴充您的 Prisma 結構

延伸閱讀


與 Prisma 保持聯繫

透過以下方式與我們聯繫,繼續您的 Prisma 旅程: 我們的活躍社群。保持資訊靈通、參與其中,並與其他開發者合作

我們衷心感謝您的參與,並期待您成為我們社群的一份子!

© . This site is unofficial and not affiliated with Prisma Data, Inc.