如何使用 Prisma ORM 搭配 Better Auth 和 Next.js
簡介
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 檔案:
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:
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 檔案中
# 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 轉接器,這能讓它將使用者與工作階段資料持久化於您的資料庫中。您還將啟用電子郵件與密碼身份驗證。
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 等),您可以在他們的 文件 中探索。
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 錯誤。
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 指令,可以自動將必要的身份驗證模型(User、Session、Account 和 Verification)加入您的 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 相容的 GET 和 POST 請求處理程式。
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.tsxsign-in/page.tsxdashboard/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 元件。這將為您的頁面設定主要的容器與標題。
"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 以管理狀態與導航。初始化路由器和一個狀態變數來存放任何潛在的錯誤訊息。
"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 方法,傳入使用者的姓名、電子郵件和密碼。
"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 時進行條件渲染的元素。
"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 表單,以及一個提交按鈕。
"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 中的基本結構開始。
"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,這與註冊頁面類似。
"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 方法。
"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>
);
}
加入錯誤訊息的條件顯示。
"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>
);
}
最後,加入電子郵件和密碼的表單欄位以及一個登入按鈕。
"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 中的基本元件開始。
"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 是在客戶端管理身份驗證狀態的關鍵。它提供工作階段資料和載入中狀態。
"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)。如果兩者皆為真,它會將使用者重新導向至登入頁面。
"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>
);
}
為了提供更好的使用者體驗,請在驗證工作階段時加入載入和重新導向狀態。
"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 函式並加入一個按鈕來呼叫它,允許使用者登出。
"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 的內容替換為以下內容
"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. 測試
您的應用程式現在已完全設定完成。
- 啟動開發伺服器進行測試
npm run dev
-
在瀏覽器中前往
https://:3000。您應該會看到帶有「Sign Up」(註冊)和「Sign In」(登入)按鈕的首頁。 -
點擊 Sign Up,建立一個新帳號,您應該會被重新導向至儀表板。接著您可以登出並重新登入。
-
要直接在您的資料庫中檢視使用者資料,可以使用 Prisma Studio。
npx prisma studio
- 這會在您的瀏覽器中開啟一個新分頁,您可以在那裡看到
User、Session和Account表格及其內容。
恭喜!您現在擁有一個由 Better Auth、Prisma 和 Next.js 建構的完整身份驗證系統。
後續步驟
- 新增對社群登入或 Magic Links 的支援
- 實作密碼重設與電子郵件驗證
- 加入使用者個人檔案與帳號管理頁面
- 部署至 Vercel 並確保您的環境變數安全
- 使用自訂應用程式模型擴充您的 Prisma 結構
延伸閱讀
與 Prisma 保持聯繫
透過以下方式與我們聯繫,繼續您的 Prisma 旅程: 我們的活躍社群。保持資訊靈通、參與其中,並與其他開發者合作
- 在 X 上關注我們 以獲取公告、現場活動和實用技巧。
- 加入我們的 Discord 提出問題、與社群對話,並透過對話獲得積極支援。
- 在 YouTube 上訂閱 查看教學、演示和直播。
- 在 GitHub 上交流 透過為存放庫加星標、報告問題或為 issue 做出貢獻。