如何使用 Prisma ORM 搭配 Clerk Auth 與 Next.js
簡介
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/目錄中嗎?No - 您想使用 App Router 嗎? (推薦)
Yes - 您想在
next dev中使用 Turbopack 嗎?Yes - 您想自訂匯入別名 (預設為
@/*) 嗎?No
導覽至專案目錄
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 檔案中
# Clerk
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=<your-publishable-key>
CLERK_SECRET_KEY=<your-secret-key>
2.2. 使用 Clerk 中介軟體保護路由
clerkMiddleware 輔助工具可啟用身分驗證,您將在此設定受保護的路由。
在您的專案根目錄建立一個 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,您需要安裝一些依賴項目
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 檔案中,新增以下模型
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())
}
這將建立兩個模型:User 和 Post,兩者之間具有一對多關係。
建立 prisma.config.ts 檔案以設定 Prisma:
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 檔案
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 路由
匯入必要的相依套件
import { verifyWebhook } from "@clerk/nextjs/webhooks";
import { NextRequest } from "next/server";
import prisma from "@/lib/prisma";
建立 Clerk 將會呼叫的 POST 方法並驗證 Webhook
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 方法在使用者不存在時建立新使用者來達成目標
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
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 公開您的本地應用程式。這將允許 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>
CLERK_WEBHOOK_SIGNING_SECRET=<your-signing-secret>
在首頁點擊「註冊」並使用任一註冊選項建立帳號
開啟 Prisma Studio,您應該會看到一筆使用者記錄。
npx prisma studio
如果您沒有看到使用者記錄,請檢查以下幾點
- 在 Clerk 的 Users 分頁中刪除您的使用者並再試一次。
- 檢查您的 ngrok URL 並確保它是正確的 (每次重新啟動 ngrok 時它都會變更)。
- 檢查您的 Clerk Webhook 是否指向正確的 ngrok URL。
- 確保您已在 URL 末尾加入
/api/webhooks/clerk。
5. 建立貼文 API
若要在使用者名下建立貼文,您需要在 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 });
}
從傳入的請求中解構 title 和 content 並建立貼文。完成後,回傳 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" 以確保元件在客戶端渲染。title 和 content 儲存在各自的 useState Hook 中。
建立一個在表單提交時會被呼叫的函數
"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 上交流 透過為存放庫加星標、報告問題或為 issue 做出貢獻。