Prisma ORMをClerk AuthおよびNext.jsと連携して使用する方法
はじめに
Clerkは、サインアップ、サインイン、ユーザー管理、Webhooksを処理するドロップイン認証プロバイダーで、開発者がこれらの手間を省くことができます。
このガイドでは、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
**に貼り付けます
# Clerk
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=<your-publishable-key>
CLERK_SECRET_KEY=<your-secret-key>
2.2. Clerkミドルウェアでルートを保護する
clerkMiddleware
ヘルパーは認証を有効にし、保護されたルートを設定する場所です。
プロジェクトの/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
コンポーネントを追加します
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を始めるには、いくつかの依存関係をインストールする必要があります
- Prisma Postgres (推奨)
- その他のデータベース
npm install prisma --save-dev
npm install tsx --save-dev
npm install @prisma/extension-accelerate
npm install prisma --save-dev
npm install tsx --save-dev
インストール後、プロジェクトでPrismaを初期化します
npx prisma init --db --output ../src/app/generated/prisma
Prisma Postgresデータベースをセットアップする際に、いくつかの質問に答える必要があります。お住まいの地域に最も近いリージョンと、「My Clerk NextJS Project」のような覚えやすいデータベース名を選択してください。
これにより、以下が作成されます
prisma/
ディレクトリとその中のschema.prisma
ファイル.env
内のDATABASE_URL
3.2. 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())
}
これにより、User
とPost
という2つのモデルが作成され、それらの間に一対多のリレーションシップが設定されます。
次に、以下のコマンドを実行してデータベーステーブルを作成し、Prismaクライアントを生成します
npx prisma migrate dev --name init
/src/app/generated/prisma
を.gitignore
ファイルに追加することをお勧めします。
3.3. 再利用可能なPrismaクライアントを作成する
src/
ディレクトリ内に/lib
ディレクトリを作成し、その中にprisma.ts
ファイルを作成します
- Prisma Postgres (推奨)
- その他のデータベース
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;
import { PrismaClient } from "@/app/generated/prisma";
const globalForPrisma = global as unknown as {
prisma: PrismaClient;
};
const prisma = globalForPrisma.prisma || new PrismaClient();
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ルートを作成します
必要な依存関係をインポートする
import { Webhook } from "svix";
import { WebhookEvent } from "@clerk/nextjs/server";
import { headers } from "next/headers";
import prisma from "@/lib/prisma";
Clerkが呼び出し、ペイロードを検証するPOST
メソッドを作成します。
まず、署名シークレットが設定されていることを確認します
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
メソッドを作成します
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
メソッドを使用して、ユーザーが存在しない場合は新規作成することで行います。
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}`,
},
});
}
}
最後に、webhookが受信されたことをClerkに確認するためのレスポンスを返します
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. Webhooks用にローカルアプリを公開する
ngrokでWebhooks用にローカルアプリを公開する必要があります。これにより、Clerkがuser.created
のようなイベントをプッシュするために/api/webhooks/clerk
ルートに到達できるようになります。
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>
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ルートを作成する必要があります
まず、必要な依存関係をインポートします
import { auth } from "@clerk/nextjs/server";
import prisma from "@/lib/prisma";
認証されたユーザーのclerkId
を取得します。ユーザーがいない場合は、401
Unauthorizedレスポンスを返します
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
Not Foundレスポンスを返します
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
Createdレスポンスを返します
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"
を使用しており、コンポーネントがクライアント側でレンダリングされるようにします。タイトルとコンテンツは、それぞれのuseState
フックに保存されます。
フォームが送信されたときに呼び出される関数を作成します
"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で交流する リポジトリにスターを付けたり、問題を報告したり、問題に貢献したりして。