跳至主要内容

如何在 Better Auth 和 Next.js 中使用 Prisma ORM

25 分钟

介绍

Better Auth 是一个现代化的开源 Web 应用程序身份验证解决方案。它采用 TypeScript 构建,提供简单且可扩展的身份验证体验,并支持多种数据库适配器,包括 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 吗?
  • 你想使用 ESLint 吗?
  • 你想使用 Tailwind CSS 吗?
  • 你的代码想放在 src/ 目录中吗?
  • 你想使用 App Router 吗?
  • 你想使用 Turbopack 吗?
  • 你想自定义导入别名吗(默认为 @/*)?

导航到项目目录

cd betterauth-nextjs-prisma

这些选择将创建一个现代的 Next.js 项目,其中包含用于类型安全的 TypeScript、用于代码质量的 ESLint 和用于样式的 Tailwind CSS。使用 src/ 目录和 App Router 是新 Next.js 应用程序的常见约定。

2. 设置 Prisma

接下来,你将向项目添加 Prisma 以管理数据库。

2.1. 安装 Prisma 和依赖项

安装必要的 Prisma 包。依赖项略有不同,具体取决于你是否将 Prisma Postgres 与 Accelerate 或其他数据库一起使用。

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 数据库时,你需要回答几个问题。选择离你位置最近的区域,并为你的数据库选择一个易于记忆的名称,例如“我的 Better Auth 项目”

这将创建

  • 一个包含 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 客户端

运行以下命令创建数据库表并生成 Prisma 客户端

npx prisma generate

2.4. 设置全局 Prisma 客户端

src 目录中,创建一个 lib 文件夹,并在其中创建一个 prisma.ts 文件。此文件将用于创建和导出你的 Prisma 客户端实例。

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

像这样设置 Prisma 客户端

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 将使用它来签署身份验证令牌。这确保了你的令牌不会被篡改。

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 模型添加到你的模式

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 中创建一个全捕获 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);

接下来,你需要一个客户端实用程序来从你的 React 组件与这些端点交互。在 src/lib 目录中,创建一个 auth-client.ts 文件

touch src/lib/auth-client.ts

添加以下代码,它将创建你将在 UI 中使用的 React 钩子和函数

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 中必要的钩子来管理状态和导航。初始化路由器和状态变量来保存任何潜在的错误消息。

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>
);
}

现在,添加状态和路由器钩子,类似于注册页面。

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 钩子。这个钩子是管理客户端身份验证状态的关键。它提供会话数据和挂起状态。

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 钩子。此效果检查会话是否已加载 (!isPending) 以及是否存在未经过身份验证的用户 (!session?.user)。如果两者都为 true,它会将用户重定向到登录页面。

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。你应该会看到带有“注册”和“登录”按钮的主页。

  2. 点击注册,创建一个新帐户,然后你将被重定向到仪表盘。然后你可以注销并重新登录。

  3. 要直接在数据库中查看用户数据,可以使用 Prisma Studio。

npx prisma studio
  1. 这将在浏览器中打开一个新标签页,你可以在其中查看 UserSessionAccount 表及其内容。
成功

恭喜!你现在拥有一个使用 Better Auth、Prisma 和 Next.js 构建的完全正常运行的身份验证系统。

后续步骤

  • 添加社交登录或魔术链接支持
  • 实现密码重置和电子邮件验证
  • 添加用户个人资料和帐户管理页面
  • 部署到 Vercel 并保护你的环境变量
  • 使用自定义应用程序模型扩展你的 Prisma 模式

进一步阅读


与 Prisma 保持联系

通过以下方式与我们保持联系,继续你的 Prisma 之旅: 我们的活跃社区。保持信息灵通,参与其中,并与其他开发人员协作。

我们真诚地感谢你的参与,并期待你成为我们社区的一部分!

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