Skip to main content

如何将 Prisma ORM 与 Clerk Auth 和 Next.js 结合使用

25 min

介绍

¥Introduction

Clerk 是一个嵌入式身份验证提供程序,可处理注册、登录、用户管理和 Webhook,让你无需再亲自动手。

¥Clerk is a drop-in auth provider that handles sign-up, sign-in, user management, and webhooks so you don't have to.

在本指南中,你将把 Clerk 连接到一个全新的 Next.js 应用,将用户持久化到 Prisma Postgres 数据库中,并公开一个小型的 posts API。你可以在 GitHub 上找到本指南的完整示例。

¥In this guide you'll wire Clerk into a brand-new Next.js app, persist users in a Prisma Postgres database, and expose a tiny posts API. You can find a complete example of this guide on GitHub.

先决条件

¥Prerequisites

1. 设置你的项目

¥ Set up your project

创建应用:

¥Create the app:

npx create-next-app@latest clerk-nextjs-prisma

它会提示你自定义设置。选择默认值:

¥It will prompt you to customize your setup. Choose the defaults:

信息
  • 你想使用 TypeScript 吗?Yes

    ¥Would you like to use TypeScript? Yes

  • 你想使用 ESLint 吗?Yes

    ¥Would you like to use ESLint? Yes

  • 你想使用 Tailwind CSS 吗?Yes

    ¥Would you like to use Tailwind CSS? Yes

  • 你想将代码放在 src/ 目录中吗?No

    ¥Would you like your code inside a src/ directory? No

  • 你想使用 App Router 吗?(推荐)Yes

    ¥Would you like to use App Router? (recommended) Yes

  • 你想为 next dev 使用 Turbopack 吗?Yes

    ¥Would you like to use Turbopack for next dev? Yes

  • 你想自定义导入别名(默认为 @/*)吗?No

    ¥Would you like to customize the import alias (@/* by default)? No

导航到项目目录:

¥Navigate to the project directory:

cd clerk-nextjs-prisma

2. 设置 Clerk

¥ Set up Clerk

2.1.创建一个新的 Clerk 应用

¥2.1. Create a new Clerk application

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

¥Sign in to Clerk and navigate to the home page. From there, press the Create Application button to create a new application. Enter a title, select your sign-in options, and click Create Application.

信息

本指南将使用 Google、Github 和电子邮件登录选项。

¥For this guide, the Google, Github, and Email sign in options will be used.

安装 Clerk Next.js SDK:

¥Install the Clerk Next.js SDK:

npm install @clerk/nextjs

复制你的 Clerk 密钥并将其粘贴到项目根目录下的 .env 文件中:

¥Copy your Clerk keys and paste them into .env in the root of your project:

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

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

¥2.2. Protect routes with Clerk middleware

clerkMiddleware 助手启用身份验证,你可以在其中配置受保护的路由。

¥The clerkMiddleware helper enables authentication and is where you'll configure your protected routes.

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

¥Create a middleware.ts file in the root directory of your project:

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 添加到布局

¥2.3. Add Clerk UI to your layout

接下来,你需要将应用封装到 ClerkProvider 组件中,以使身份验证全局可用。

¥Next, you'll need to wrap your app in the ClerkProvider component to make authentication globally available.

在你的 layout.tsx 文件中,添加 ClerkProvider 组件:

¥In your layout.tsx file, add the ClerkProvider component:

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 组件,该组件将用于在用户登录后显示“登录”和“注册”按钮以及“用户”按钮:

¥Create a Navbar component which will be used to display the Sign In and Sign Up buttons as well as the User Button once a user is signed in:

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

¥ Install and configure Prisma

3.1.安装依赖

¥3.1. Install dependencies

要开始使用 Prisma,你需要安装一些依赖:

¥To get started with Prisma, you'll need to install a few dependencies:

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

安装完成后,请在你的项目中初始化 Prisma:

¥Once installed, initialize Prisma in your project:

npx prisma init --db --output ../app/generated/prisma
信息

在设置 Prisma Postgres 数据库时,你需要回答几个问题。选择距离你位置最近的区域,并为数据库选择一个容易记住的名称,例如 "我的 Clerk NextJS 项目"

¥You'll need to answer a few questions while setting up your Prisma Postgres database. Select the region closest to your location and a memorable name for the database like "My Clerk NextJS Project"

这将造成:

¥This will create:

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

    ¥A prisma/ directory with a schema.prisma file

  • .env 中的 DATABASE_URL

    ¥A DATABASE_URL in .env

3.2.定义 Prisma Schema

¥3.2. Define your Prisma Schema

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

¥In the prisma/schema.prisma file, add the following models:

prisma/schema.prisma
generator client {
provider = "prisma-client-js"
output = "../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,它们之间存在一对多关系。

¥This will create two models: User and Post, with a one-to-many relationship between them.

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

¥Now, run the following command to create the database tables and generate the Prisma Client:

npx prisma migrate dev --name init
警告

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

¥It is recommended that you add /app/generated/prisma to your .gitignore file.

3.3.创建一个可复用的 Prisma 客户端

¥3.3. Create a reusable Prisma Client

/app 目录中,创建 /libprisma.ts 文件:

¥In the /app directory, create /lib and a prisma.ts file inside it:

app/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 连接到数据库

¥ Wire Clerk to the database

4.1.创建 Clerk webhook 端点

¥4.1. Create a Clerk webhook endpoint

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

¥Create a new API route at app/api/webhooks/clerk/route.ts:

导入必要的依赖:

¥Import the necessary dependencies:

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 方法:

¥Create the POST method that Clerk will call and verify the 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}`
);
} catch (err) {
console.error("Error verifying webhook:", err);
return new Response("Error verifying webhook", { status: 400 });
}
}

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

¥When a new user is created, they need to be stored in the database.

你将通过检查事件类型是否为 user.created 来实现这一点,然后使用 Prisma 的 upsert 方法在用户不存在的情况下创建新用户:

¥You'll do that by checking if the event type is user.created and then using Prisma's upsert method to create a new user if they don't exist:

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:

¥Finally, return a response to Clerk to confirm the webhook was received:

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 公开本地应用

¥4.2. Expose your local app for webhooks

你需要使用 ngrok 为 webhook 公开本地应用。这将允许 Clerk 访问你的 /api/webhooks/clerk 路由以推送类似 user.created 的事件。

¥You'll need to expose your local app for webhooks with ngrok. This will allow Clerk to reach your /api/webhooks/clerk route to push events like user.created.

安装 ngrok 并公开你的本地应用:

¥Install ngrok and expose your local app:

npm install --global ngrok
ngrok http 3000

复制 ngrok Forwarding URL。这将用于在 Clerk 中设置 webhook URL。

¥Copy the ngrok Forwarding URL. This will be used to set the webhook URL in Clerk.

导航到 Clerk 应用的“Webhooks”部分,该部分位于“开发者”下“配置”选项卡的底部附近。

¥Navigate to the Webhooks section of your Clerk application located near the bottom of the Configure tab under Developers.

点击“添加端点”,将 ngrok URL 粘贴到“端点 URL”字段中,并在 URL 末尾添加 /api/webhooks/clerk。它应该类似于以下内容:

¥Click Add Endpoint and paste the ngrok URL into the Endpoint URL field and add /api/webhooks/clerk to the end of the URL. It should look similar to this:

https://a60b-99-42-62-240.ngrok-free.app/api/webhooks/clerk

复制签名密钥并将其添加到你的 .env 文件中:

¥Copy the Signing Secret and add it to your .env file:

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

在主页上,点击“注册”并使用任意注册选项创建一个账户

¥On the home page, press Sign Up and create an account using any of the sign-up options

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

¥Open Prisma Studio and you should see a user record.

npx prisma studio
注意

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

¥If you don't see a user record, there are a few things to check:

  • 从 Clerk 的“用户”选项卡中删除你的用户,然后重试。

    ¥Delete your user from the Users tab in Clerk and try again.

  • 检查你的 ngrok URL 并确保其正确(每次重启 ngrok 时,它都会更改)。

    ¥Check your ngrok URL and ensure it's correct (it will change everytime you restart ngrok).

  • 检查你的 Clerk webhook 是否指向正确的 ngrok URL。

    ¥Check your Clerk webhook is pointing to the correct ngrok URL.

  • 确保你已将 /api/webhooks/clerk 添加到 URL 末尾。

    ¥Make sure you've added /api/webhooks/clerk to the end of the URL.

5. 构建帖子 API

¥ Build a posts API

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

¥To create posts under a user, you'll need to create a new API route at app/api/posts/route.ts:

首先导入必要的依赖:

¥Start by importing the necessary dependencies:

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

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

¥Get the clerkId of the authenticated user. If there's no user, return a 401 Unauthorized response:

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

¥Match the Clerk user to a user in the database. If none is found, return a 404 Not Found response:

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

¥Destructure the title and content from the incoming request and create a post. Once done, return a 201 Created response:

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

¥ Add a Post creation UI

/app 中,创建一个 /components 目录并在其中创建一个 PostInputs.tsx 文件:

¥In /app, create a /components directory and a PostInputs.tsx file inside it:

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

¥This component uses "use client" to ensure the component is rendered on the client. The title and content are stored in their own useState hooks.

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

¥Create a function that will be called when a form is submitted:

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

¥You'll be using a form to create a post and call the POST route you created earlier:

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

提交时:

¥On submit:

  • 它将 POST 请求发送到 /api/posts 路由

    ¥It sends a POST request to the /api/posts route

  • 清除输入字段

    ¥Clears the input fields

  • 重新加载页面以显示新帖子

    ¥Reloads the page to show the new post

7. 设置 page.tsx

¥ Set up page.tsx

现在,更新 page.tsx 文件以获取帖子、显示表单并渲染列表。

¥Now, update the page.tsx file to fetch posts, show the form, and render the list.

删除 page.tsx 中的所有内容,仅保留以下内容:

¥Delete everything within page.tsx, leaving only the following:

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

导入必要的依赖:

¥Import the necessary dependencies:

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

¥To ensure only signed-in users can access the post functionality, update the Home component to check for a user:

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

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

¥Once a user is found, fetch that user's posts from the database:

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

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

¥Finally, render the form and post list:

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 应用,为轻松处理用户管理和数据持久化的安全且可扩展的全栈应用奠定了基础。

¥You've successfully built a Next.js application with Clerk authentication and Prisma, creating a foundation for a secure and scalable full-stack application that handles user management and data persistence with ease.

以下是一些后续步骤,以及一些可帮助你开始扩展项目的资源。

¥Below are some next steps to explore, as well as some more resources to help you get started expanding your project.

后续步骤

¥Next Steps

  • 添加删除帖子和用户的功能。

    ¥Add delete functionality to posts and users.

  • 添加搜索栏以过滤帖子。

    ¥Add a search bar to filter posts.

  • 部署到 Vercel 并在 Clerk 中设置你的生产 webhook URL。

    ¥Deploy to Vercel and set your production webhook URL in Clerk.

  • 使用 Prisma Postgres 启用查询缓存以获得更好的性能

    ¥Enable query caching with Prisma Postgres for better performance

更多信息

¥More Info


Stay connected with Prisma

Continue your Prisma journey by connecting with our active community. Stay informed, get involved, and collaborate with other developers:

We genuinely value your involvement and look forward to having you as part of our community!