跳到主要内容

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

25 分钟

简介

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 文件中

.env
# Clerk
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=<your-publishable-key>
CLERK_SECRET_KEY=<your-secret-key>

2.2. 使用 Clerk 中间件保护路由

clerkMiddleware 辅助函数启用身份验证,你将在此配置你的受保护路由。

在你的项目 /src 目录下创建 middleware.ts 文件

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 组件

src/app/layout.tsx
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 组件,它将用于显示登录和注册按钮,以及用户登录后的用户按钮。

src/app/layout.tsx
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,你需要安装一些依赖项

npm install prisma --save-dev
npm install tsx --save-dev
npm install @prisma/extension-accelerate

安装完成后,在你的项目中初始化 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 文件中,添加以下模型

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

这将创建两个模型:UserPost,它们之间具有一对多的关系。

现在,运行以下命令创建数据库表并生成 Prisma Client

npx prisma migrate dev --name init
警告

建议你将 /src/app/generated/prisma 添加到你的 .gitignore 文件中。

3.3. 创建可复用的 Prisma Client

src/ 目录下,创建 /lib 目录并在其中创建一个 prisma.ts 文件

src/lib/prisma.ts
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;

4. 将 Clerk 连接到数据库

4.1. 创建 Clerk webhook 端点

你将使用 Svix 来确保请求安全。Svix 会对每个 webhook 请求进行签名,以便你可以验证其合法性,并确保在传输过程中未被篡改。

安装 svix

npm install svix

src/app/api/webhooks/clerk/route.ts 创建一个新的 API 路由

导入必要的依赖项

src/app/api/webhooks/clerk/route.ts
import { Webhook } from "svix";
import { WebhookEvent } from "@clerk/nextjs/server";
import { headers } from "next/headers";
import prisma from "@/lib/prisma";

创建 Clerk 将调用并验证负载的 POST 方法。

首先,它将检查是否设置了签名密钥

src/app/api/webhooks/clerk/route.ts
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 方法

src/app/api/webhooks/clerk/route.ts
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 方法创建新用户(如果他们不存在)。

src/app/api/webhooks/clerk/route.ts
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

src/app/api/webhooks/clerk/route.ts
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 文件中

.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 路由

首先导入必要的依赖项

src/app/api/posts/route.ts
import { auth } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";

获取已认证用户的 clerkId。如果没有用户,则返回 401 未授权响应

src/app/api/posts/route.ts
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 未找到响应

src/app/api/posts/route.ts
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 已创建响应

src/app/api/posts/route.ts
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 文件

src/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 钩子中。

创建一个将在表单提交时调用的函数

src/app/components/PostInputs.tsx
"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 路由

src/app/page.tsx
"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 中的所有内容,只保留以下内容

src/app/page.tsx
export default function Home() {
return ()
}

导入必要的依赖项

src/app/page.tsx
import { currentUser } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";
import PostInputs from "@/app/components/PostInputs";

export default function Home() {
return ()
}

为了确保只有登录用户才能访问文章功能,更新 Home 组件以检查用户是否存在

src/app/page.tsx
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 ()
}

找到用户后,从数据库中获取该用户的文章

src/app/page.tsx
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 ()
}

最后,渲染表单和文章列表

src/app/page.tsx
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 之旅 我们活跃的社区。保持信息畅通,参与其中,并与其他开发者协作

我们真诚地珍视你的参与,并期待你成为我们社区的一员!

© . All rights reserved.