跳到主要內容

如何在 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 之旅 我們活躍的社群。保持資訊暢通,參與其中,並與其他開發者協作

我們真誠地珍視你的參與,並期待你成為我們社群的一員!

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