跳至主要內容

如何使用 Prisma ORM 搭配 Clerk Auth 與 Next.js

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/ 目錄中嗎? 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 檔案中

.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 將會呼叫的 POST 方法並驗證 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 });
}
}

當建立新使用者時,他們需要被儲存在資料庫中。

您可以透過檢查事件類型是否為 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 公開您的本地應用程式。這將允許 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 的 Users 分頁中刪除您的使用者並再試一次。
  • 檢查您的 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 });
}

從傳入的請求中解構 title 和 content 並建立貼文。完成後,回傳 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" 以確保元件在客戶端渲染。title 和 content 儲存在各自的 useState Hook 中。

建立一個在表單提交時會被呼叫的函數

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.