如何在 Clerk Auth 和 Next.js 中使用 Prisma ORM
简介
Clerk 是一个即插即用的身份验证提供商,它处理注册、登录、用户管理和 webhook,让你无需费心。
在本指南中,你将把 Clerk 集成到一个全新的 Next.js 应用中,将用户持久化到 Prisma Postgres 数据库中,并公开一个简单的文章 API。你可以在 GitHub 上找到本指南的完整示例。
先决条件
1. 设置你的项目
创建应用
npx create-next-app@latest clerk-nextjs-prisma
它会提示你自定义设置。选择默认值即可
- 你想使用 TypeScript 吗?
Yes
- 你想使用 ESLint 吗?
Yes
- 你想使用 Tailwind CSS 吗?
Yes
- 你想把代码放在
src/
目录下吗?Yes
- 你想使用 App Router 吗? (推荐)
Yes
- 你想在
next dev
中使用 Turbopack 吗?Yes
- 你想自定义导入别名(默认为
@/*
)吗?No
导航到项目目录
cd clerk-nextjs-prisma
2. 设置 Clerk
2.1. 创建新的 Clerk 应用
登录 到 Clerk 并导航到主页。在那里,点击 Create Application
按钮创建一个新应用。输入标题,选择登录选项,然后点击 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 中间件保护路由
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 项目”
这将创建
- 一个包含
schema.prisma
文件的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
方法。
首先,它将检查是否设置了签名密钥
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 });
}
签名密钥可在 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>
在主页上,点击“注册”并使用任何注册选项创建一个账户
打开 Prisma Studio,你应该会看到一条用户记录。
npx prisma studio
如果你没有看到用户记录,有几点需要检查
- 从 Clerk 的“用户”标签页中删除你的用户,然后重试。
- 检查你的 ngrok URL 并确保它正确 (每次重启 ngrok 都会改变)。
- 检查你的 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
未授权响应
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
未找到响应
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 });
}
从传入请求中解构标题和内容并创建一篇文章。完成后,返回 201
已创建响应
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"
以确保组件在客户端渲染。标题和内容存储在它们各自的 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 上关注我们 获取公告、实时活动和实用技巧。
- 加入我们的 Discord 提问、与社区交流,并通过对话获得积极支持。
- 在 YouTube 上订阅 获取教程、演示和直播。
- 在 GitHub 上参与 通过点赞仓库、报告问题或为问题贡献代码。