跳至主要内容

如何在 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 吗?
  • 你想使用 ESLint 吗?
  • 你想使用 Tailwind CSS 吗?
  • 你想将代码放在 src/ 目录中吗?
  • 你想使用 App Router 吗? (推荐)
  • 你想为 next dev 使用 Turbopack 吗?
  • 你想自定义导入别名(默认为 @/*)吗?

导航到项目目录

cd clerk-nextjs-prisma

2. 设置 Clerk

2.1. 创建一个新的 Clerk 应用程序

登录 到 Clerk 并导航到主页。在那里,按下 Create Application 按钮创建一个新应用程序。输入标题,选择你的登录选项,然后点击 Create Application

信息

本指南将使用 Google、Github 和 Email 登录选项。

安装 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 辅助函数启用身份验证,你可以在其中配置受保护的路由。

在项目根目录中创建一个 middleware.ts 文件

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

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 组件,用于显示登录和注册按钮,以及用户登录后的用户按钮

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 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 ../app/generated/prisma
信息

在设置 Prisma Postgres 数据库时,你需要回答几个问题。选择离你位置最近的区域,并为数据库选择一个易于记忆的名称,例如“My Clerk NextJS Project”

这将创建

  • 一个包含 schema.prisma 文件的 prisma/ 目录
  • .env 中的 DATABASE_URL

3.2. 定义你的 Prisma Schema

prisma/schema.prisma 文件中,添加以下模型

prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../app/generated/prisma"
}

datasource db {
provider = "postgresql"
}

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.config.ts 文件来配置 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

npm install dotenv

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

npx prisma migrate dev --name init
npx prisma generate
警告

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

3.3. 创建一个可重用的 Prisma Client

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

lib/prisma.ts
import { PrismaClient } from "../app/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;

4. 将 Clerk 连接到数据库

4.1. 创建一个 Clerk Webhook 端点

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

导入必要的依赖项

app/api/webhooks/clerk/route.ts
import { verifyWebhook } from "@clerk/nextjs/webhooks";
import { NextRequest } from "next/server";
import prisma from "@/lib/prisma";

创建 Clerk 将调用并验证 Webhook 的 POST 方法

app/api/webhooks/clerk/route.ts
import { verifyWebhook } from "@clerk/nextjs/webhooks";
import { NextRequest } from "next/server";
import prisma from "@/lib/prisma";

export async function POST(req: NextRequest) {
try {
const evt = await verifyWebhook(req);
const { id } = evt.data;
const eventType = evt.type;
console.log(
`Received webhook with ID ${id} and event type of ${eventType}`
);
} catch (err) {
console.error("Error verifying webhook:", err);
return new Response("Error verifying webhook", { status: 400 });
}
}

当创建新用户时,需要将其存储在数据库中。

你将通过检查事件类型是否为 user.created,然后使用 Prisma 的 upsert 方法(如果用户不存在则创建新用户)来完成此操作

app/api/webhooks/clerk/route.ts
import { verifyWebhook } from "@clerk/nextjs/webhooks";
import { NextRequest } from "next/server";
import prisma from "@/lib/prisma";

export async function POST(req: NextRequest) {
try {
const evt = await verifyWebhook(req);
const { id } = evt.data;
const eventType = evt.type;
console.log(
`Received webhook with ID ${id} and event type of ${eventType}`
);

if (eventType === "user.created") {
const { id, email_addresses, first_name, last_name } = evt.data;
await prisma.user.upsert({
where: { clerkId: id },
update: {},
create: {
clerkId: id,
email: email_addresses[0].email_address,
name: `${first_name} ${last_name}`,
},
});
}
} catch (err) {
console.error("Error verifying webhook:", err);
return new Response("Error verifying webhook", { status: 400 });
}
}

最后,向 Clerk 返回响应以确认已收到 Webhook

app/api/webhooks/clerk/route.ts
import { verifyWebhook } from "@clerk/nextjs/webhooks";
import { NextRequest } from "next/server";
import prisma from "@/lib/prisma";

export async function POST(req: NextRequest) {
try {
const evt = await verifyWebhook(req);
const { id } = evt.data;
const eventType = evt.type;
console.log(
`Received webhook with ID ${id} and event type of ${eventType}`
);

if (eventType === "user.created") {
const { id, email_addresses, first_name, last_name } = evt.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("Webhook received", { status: 200 });
} catch (err) {
console.error("Error verifying webhook:", err);
return new Response("Error verifying webhook", { status: 400 });
}
}

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>
CLERK_WEBHOOK_SIGNING_SECRET=<your-signing-secret>

在主页上,按下注册并使用任何注册选项创建一个账户

打开 Prisma Studio,你应该会看到一个用户记录。

npx prisma studio
注意

如果你没有看到用户记录,需要检查以下几点

  • 从 Clerk 的用户选项卡中删除你的用户,然后重试。
  • 检查你的 ngrok URL,确保它是正确的(每次重新启动 ngrok 都会更改)。
  • 检查你的 Clerk Webhook 是否指向正确的 ngrok URL。
  • 确保你已在 URL 末尾添加了 /api/webhooks/clerk

5. 构建帖子 API

要在用户下创建帖子,你需要在 app/api/posts/route.ts 创建一个新的 API 路由

首先导入必要的依赖项

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

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

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 未找到响应

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 已创建响应

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

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

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

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

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

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 中的所有内容,只保留以下内容

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

导入必要的依赖项

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 组件以检查用户

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

找到用户后,从数据库中获取该用户的帖子

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

最后,渲染表单和帖子列表

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 之旅: 我们的活跃社区。保持信息灵通,参与其中,并与其他开发人员协作。

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

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