如何在 Next.js 中将 Prisma ORM 与 Clerk Auth 结合使用
引言
Clerk 是一个即插即用的身份验证提供商,它处理注册、登录、用户管理和 webhook,让您无需亲自处理这些繁琐的工作。
在本指南中,您将把 Clerk 集成到一个全新的 Next.js 应用中,将用户持久化到 Prisma Postgres 数据库,并提供一个简单的文章 API。您可以在 GitHub 上找到本指南的完整示例。
先决条件
1. 设置您的项目
创建应用
npx create-next-app@latest clerk-nextjs-prisma
它将提示您自定义设置。选择默认选项
- 您想使用 TypeScript 吗?
是
- 您想使用 ESLint 吗?
是
- 您想使用 Tailwind CSS 吗?
是
- 您想将代码放在
src/
目录中吗?是
- 您想使用 App Router 吗?(推荐)
是
- 您想为
next dev
使用 Turbopack 吗?是
- 您想自定义导入别名吗?(默认是
@/*
)否
进入项目目录
cd clerk-nextjs-prisma
2. 设置 Clerk
2.1. 创建一个新的 Clerk 应用
前往 Clerk 的 New App 页面,创建一个新的应用。输入标题,选择您的登录选项,然后点击 Create Application
。
在本指南中,将使用 Google、Github 和电子邮件登录选项。
安装 Clerk Next.js SDK
npm install @clerk/nextjs
复制您的 Clerk 密钥,并将其粘贴到项目根目录下的 .env 文件中
# Clerk
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=<your-publishable-key>
CLERK_SECRET_KEY=<your-secret-key>
2.2. 使用 Clerk middleware 保护路由
clerkMiddleware
辅助函数启用身份验证,并在此处配置您的受保护路由。
在项目 /src
目录下创建一个 middleware.ts
文件
import { clerkMiddleware } from "@clerk/nextjs/server";
export default clerkMiddleware();
export const config = {
matcher: [
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
'/(api|trpc)(.*)',
],
};
2.3. 将 Clerk UI 添加到您的布局
接下来,您需要将您的应用包裹在 ClerkProvider
组件中,以使身份验证全局可用。
在您的 layout.tsx
文件中,添加 ClerkProvider
组件
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ClerkProvider } from "@clerk/nextjs";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<ClerkProvider>
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
{children}
</body>
</html>
</ClerkProvider>
);
}
创建一个 Navbar
组件,用于显示登录和注册按钮,以及用户登录后的用户按钮
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import {
ClerkProvider,
UserButton,
SignInButton,
SignUpButton,
SignedOut,
SignedIn,
} from "@clerk/nextjs";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<ClerkProvider>
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<Navbar />
{children}
</body>
</html>
</ClerkProvider>
);
}
const Navbar = () => {
return (
<header className="flex justify-end items-center p-4 gap-4 h-16">
<SignedOut>
<SignInButton />
<SignUpButton />
</SignedOut>
<SignedIn>
<UserButton />
</SignedIn>
</header>
);
};
3. 安装和配置 Prisma
3.1. 安装依赖
要开始使用 Prisma,您需要安装一些依赖项
- Prisma Postgres(推荐)
- 其他数据库
npm install prisma --save-dev
npm install tsx --save-dev
npm install @prisma/extension-accelerate
npm install prisma --save-dev
npm install tsx --save-dev
安装完成后,在您的项目中初始化 Prisma
npx prisma init --db --output ../src/app/generated/prisma
在设置 Prisma Postgres 数据库时,您需要回答几个问题。选择离您位置最近的区域,并为数据库选择一个容易记住的名称,例如“我的 Clerk NextJS 项目”
这将创建
- 一个
prisma/
目录,其中包含一个schema.prisma
文件 .env
文件中的DATABASE_URL
3.2. 定义您的 Prisma Schema
在 prisma/schema.prisma
文件中,添加以下模型
generator client {
provider = "prisma-client-js"
output = "../src/app/generated/prisma"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
clerkId String @unique
email String @unique
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
}
这将创建两个模型:User
和 Post
,它们之间存在一对多关系。
现在,运行以下命令来创建数据库表并生成 Prisma Client
npx prisma migrate dev --name init
建议您将 /src/app/generated/prisma
添加到您的 .gitignore
文件中。
3.3. 创建一个可复用的 Prisma Client
在 src/
目录下,创建 /lib
目录并在其中创建一个 prisma.ts
文件
- Prisma Postgres(推荐)
- 其他数据库
import { PrismaClient } from "@/app/generated/prisma";
import { withAccelerate } from "@prisma/extension-accelerate";
const globalForPrisma = global as unknown as {
prisma: PrismaClient;
};
const prisma =
globalForPrisma.prisma || new PrismaClient().$extends(withAccelerate());
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
export default prisma;
import { PrismaClient } from "@/app/generated/prisma";
const globalForPrisma = global as unknown as {
prisma: PrismaClient;
};
const prisma = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
export default prisma;
4. 将 Clerk 连接到数据库
4.1. 创建一个 Clerk webhook 端点
您将使用 Svix 来确保请求是安全的。Svix 会签署每个 webhook 请求,以便您可以验证其合法性,并确保在传输过程中未被篡改。
安装 svix
包
npm install svix
在 src/app/api/webhooks/clerk/route.ts
创建一个新的 API 路由
导入必要的依赖项
import { Webhook } from "svix";
import { WebhookEvent } from "@clerk/nextjs/server";
import { headers } from "next/headers";
import prisma from "@/lib/prisma";
创建 Clerk 将调用的 POST
方法并验证有效载荷。
首先,它会检查 Signing Secret 是否已设置
import { Webhook } from "svix";
import { WebhookEvent } from "@clerk/nextjs/server";
import { headers } from "next/headers";
import prisma from "@/lib/prisma";
export async function POST(req: Request) {
const secret = process.env.SIGNING_SECRET;
if (!secret) return new Response("Missing secret", { status: 500 });
}
Signing Secret 在您的 Clerk 应用的 Webhooks 部分中可用。目前您应该还没有这个,您将在接下来的几个步骤中进行设置。
现在,创建 Clerk 将调用的 POST
方法并验证有效载荷
import { Webhook } from "svix";
import { WebhookEvent } from "@clerk/nextjs/server";
import { headers } from "next/headers";
import prisma from "@/lib/prisma";
export async function POST(req: Request) {
const secret = process.env.SIGNING_SECRET;
if (!secret) return new Response("Missing secret", { status: 500 });
const wh = new Webhook(secret);
const body = await req.text();
const headerPayload = await headers();
const event = wh.verify(body, {
"svix-id": headerPayload.get("svix-id")!,
"svix-timestamp": headerPayload.get("svix-timestamp")!,
"svix-signature": headerPayload.get("svix-signature")!,
}) as WebhookEvent;
}
当创建新用户时,需要将他们存储到数据库中。
您可以通过检查事件类型是否为 user.created
,然后使用 Prisma 的 upsert
方法在用户不存在时创建新用户来实现。
import { Webhook } from "svix";
import { WebhookEvent } from "@clerk/nextjs/server";
import { headers } from "next/headers";
import prisma from "@/lib/prisma";
export async function POST(req: Request) {
const secret = process.env.SIGNING_SECRET;
if (!secret) return new Response("Missing secret", { status: 500 });
const wh = new Webhook(secret);
const body = await req.text();
const headerPayload = await headers();
const event = wh.verify(body, {
"svix-id": headerPayload.get("svix-id")!,
"svix-timestamp": headerPayload.get("svix-timestamp")!,
"svix-signature": headerPayload.get("svix-signature")!,
}) as WebhookEvent;
if (event.type === "user.created") {
const { id, email_addresses, first_name, last_name } = event.data;
await prisma.user.upsert({
where: { clerkId: id },
update: {},
create: {
clerkId: id,
email: email_addresses[0].email_address,
name: `${first_name} ${last_name}`,
},
});
}
}
最后,向 Clerk 返回响应以确认已接收到 webhook
import { Webhook } from "svix";
import { WebhookEvent } from "@clerk/nextjs/server";
import { headers } from "next/headers";
import prisma from "@/lib/prisma";
export async function POST(req: Request) {
const secret = process.env.SIGNING_SECRET;
if (!secret) return new Response("Missing secret", { status: 500 });
const wh = new Webhook(secret);
const body = await req.text();
const headerPayload = await headers();
const event = wh.verify(body, {
"svix-id": headerPayload.get("svix-id")!,
"svix-timestamp": headerPayload.get("svix-timestamp")!,
"svix-signature": headerPayload.get("svix-signature")!,
}) as WebhookEvent;
if (event.type === "user.created") {
const { id, email_addresses, first_name, last_name } = event.data;
await prisma.user.upsert({
where: { clerkId: id },
update: {},
create: {
clerkId: id,
email: email_addresses[0].email_address,
name: `${first_name} ${last_name}`,
},
});
}
return new Response("OK");
}
4.2. 暴露您的本地应用以供 webhook 使用
您需要使用 ngrok 暴露您的本地应用以供 webhook 使用。这将允许 Clerk 访问您的 /api/webhooks/clerk
路由来推送 user.created
等事件。
安装 ngrok 并暴露您的本地应用
npm install --global ngrok
ngrok http 3000
复制 ngrok 的 Forwarding URL
。这将用于在 Clerk 中设置 webhook URL。
导航到您的 Clerk 应用中的 Webhooks 部分,该部分位于 Developers 下方的 Configure 选项卡底部附近。
点击 Add Endpoint 并将 ngrok URL 粘贴到 Endpoint URL 字段中,然后在 URL 的末尾添加 /api/webhooks/clerk
。它应该看起来类似于这样
https://a60b-99-42-62-240.ngrok-free.app/api/webhooks/clerk
复制 Signing Secret 并将其添加到您的 .env
文件中
# Prisma
DATABASE_URL=<your-database-url>
# Clerk
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=<your-publishable-key>
CLERK_SECRET_KEY=<your-secret-key>
SIGNING_SECRET=<your-signing-secret>
在主页上,点击 Sign Up 并使用任意注册选项创建一个账户
打开 Prisma Studio,您应该会看到一条用户记录。
npx prisma studio
如果您没有看到用户记录,有几件事需要检查:
- 从 Clerk 的 Users 选项卡中删除您的用户,然后重试。
- 检查您的 ngrok URL 并确保其正确(每次重启 ngrok 时 URL 都会改变)。
- 检查您的 Clerk webhook 是否指向正确的 ngrok URL。
- 确保您已在 URL 的末尾添加了
/api/webhooks/clerk
。
5. 构建文章 API
要在用户下创建文章,您需要在 src/app/api/posts/route.ts
创建一个新的 API 路由
首先导入必要的依赖项
import { auth } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";
获取已验证用户的 clerkId
。如果不存在用户,则返回 401 Unauthorized
响应。
import { auth } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";
export async function POST(req: Request) {
const { userId: clerkId } = await auth();
if (!clerkId) return new Response("Unauthorized", { status: 401 });
}
将 Clerk 用户与数据库中的用户进行匹配。如果未找到用户,则返回 404 Not Found
响应。
import { auth } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";
export async function POST(req: Request) {
const { userId: clerkId } = await auth();
if (!clerkId) return new Response("Unauthorized", { status: 401 });
const user = await prisma.user.findUnique({
where: { clerkId },
});
if (!user) return new Response("User not found", { status: 404 });
}
从传入请求中解构 title
和 content
并创建一篇文章。完成后,返回 201 Created
响应。
import { auth } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";
export async function POST(req: Request) {
const { userId: clerkId } = await auth();
if (!clerkId) return new Response("Unauthorized", { status: 401 });
const { title, content } = await req.json();
const user = await prisma.user.findUnique({
where: { clerkId },
});
if (!user) return new Response("User not found", { status: 404 });
const post = await prisma.post.create({
data: {
title,
content,
authorId: user.id,
},
});
return new Response(JSON.stringify(post), { status: 201 });
}
6. 添加文章创建 UI
在 /app
目录下,创建一个 /components
目录并在其中创建一个 PostInputs.tsx
文件
"use client";
import { useState } from "react";
export default function PostInputs() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
}
此组件使用 "use client"
来确保组件在客户端渲染。title
和 content
存储在它们各自的 useState
钩子中。
创建一个函数,当表单提交时将被调用
"use client";
import { useState } from "react";
export default function PostInputs() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
async function createPost(e: React.FormEvent) {
e.preventDefault();
if (!title || !content) return;
await fetch("/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, content }),
});
setTitle("");
setContent("");
location.reload();
}
}
您将使用一个表单来创建文章并调用之前创建的 POST
路由
"use client";
import { useState } from "react";
export default function PostInputs() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
async function createPost(e: React.FormEvent) {
e.preventDefault();
if (!title || !content) return;
await fetch("/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, content }),
});
setTitle("");
setContent("");
location.reload();
}
return (
<form onSubmit={createPost} className="space-y-2">
<input
type="text"
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full p-2 border border-zinc-800 rounded"
/>
<textarea
placeholder="Content"
value={content}
onChange={(e) => setContent(e.target.value)}
className="w-full p-2 border border-zinc-800 rounded"
/>
<button className="w-full p-2 border border-zinc-800 rounded">
Post
</button>
</form>
);
}
提交时
- 它向
/api/posts
路由发送一个POST
请求 - 清除输入字段
- 重新加载页面以显示新文章
7. 设置 page.tsx
现在,更新 page.tsx
文件以获取文章、显示表单并渲染列表。
删除 page.tsx
中的所有内容,只保留以下内容
export default function Home() {
return ()
}
导入必要的依赖项
import { currentUser } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";
import PostInputs from "@/app/components/PostInputs";
export default function Home() {
return ()
}
为确保只有已登录用户才能访问文章功能,请更新 Home
组件以检查是否存在用户
import { currentUser } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";
import PostInputs from "@/app/components/PostInputs";
export default async function Home() {
const user = await currentUser();
if (!user) return <div className="flex justify-center">Sign in to post</div>;
return ()
}
找到用户后,从数据库中获取该用户的文章
import { currentUser } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";
import PostInputs from "@/app/components/PostInputs";
export default async function Home() {
const user = await currentUser();
if (!user) return <div className="flex justify-center">Sign in to post</div>;
const posts = await prisma.post.findMany({
where: { author: { clerkId: user.id } },
orderBy: { createdAt: "desc" },
});
return ()
}
最后,渲染表单和文章列表
import { currentUser } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";
import PostInputs from "@/app/components/PostInputs";
export default async function Home() {
const user = await currentUser();
if (!user) return <div className="flex justify-center">Sign in to post</div>;
const posts = await prisma.post.findMany({
where: { author: { clerkId: user.id } },
orderBy: { createdAt: "desc" },
});
return (
<main className="max-w-2xl mx-auto p-4">
<PostInputs />
<div className="mt-8">
{posts.map((post) => (
<div
key={post.id}
className="p-4 border border-zinc-800 rounded mt-4">
<h2 className="font-bold">{post.title}</h2>
<p className="mt-2">{post.content}</p>
</div>
))}
</div>
</main>
);
}
您已成功使用 Clerk 身份验证和 Prisma 构建了一个 Next.js 应用,为安全、可扩展的全栈应用奠定了基础,该应用能够轻松处理用户管理和数据持久化。
以下是接下来可以探索的一些步骤,以及更多有助于您开始扩展项目的资源。
后续步骤
- 为文章和用户添加删除功能。
- 添加一个搜索栏来过滤文章。
- 部署到 Vercel 并在 Clerk 中设置您的生产环境 webhook URL。
- 使用 Prisma Postgres 启用查询缓存以获得更好的性能
更多信息
与 Prisma 保持联系
通过以下方式继续您的 Prisma 之旅: 我们活跃的社区。保持了解,积极参与,并与其他开发者协作
- 在 X (原 Twitter) 上关注我们 获取公告、直播活动和实用技巧。
- 加入我们的 Discord 提问、与社区交流,并通过对话获得积极支持。
- 在 YouTube 上订阅 获取教程、演示和直播。
- 在 GitHub 上参与 通过点赞仓库、报告问题或贡献于某个问题。