メインコンテンツへスキップ

Prisma ORMをClerk AuthおよびNext.jsと連携して使用する方法

25分

はじめに

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**に貼り付けます

.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データベースをセットアップする際に、いくつかの質問に答える必要があります。お住まいの地域に最も近いリージョンと、「My Clerk NextJS Project」のような覚えやすいデータベース名を選択してください。

これにより、以下が作成されます

  • prisma/ディレクトリとその中のschema.prismaファイル
  • .env内のDATABASE_URL

3.2. Prismaスキーマを定義する

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という2つのモデルが作成され、それらの間に一対多のリレーションシップが設定されます。

次に、以下のコマンドを実行してデータベーステーブルを作成し、Prismaクライアントを生成します

npx prisma migrate dev --name init
警告

/src/app/generated/prisma.gitignoreファイルに追加することをお勧めします。

3.3. 再利用可能なPrismaクライアントを作成する

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

最後に、webhookが受信されたことをClerkに確認するためのレスポンスを返します

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. 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ファイルに追加します

.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 Unauthorizedレスポンスを返します

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 Not Foundレスポンスを返します

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 Createdレスポンスを返します

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を使い続けるには、以下の方法でつながりましょう。 活発なコミュニティに参加しましょう。情報を受け取り、関わり、他の開発者と協力しましょう

  • Xをフォローする アナウンス、ライブイベント、役立つヒントを受け取るために。
  • Discordに参加する 質問したり、コミュニティと話したり、会話を通じて積極的にサポートを受けたりするために。
  • YouTubeを購読する チュートリアル、デモ、ストリームを見るために。
  • GitHubで交流する リポジトリにスターを付けたり、問題を報告したり、問題に貢献したりして。
皆様のご参加を心より歓迎し、コミュニティの一員としてお迎えできることを楽しみにしています!

© . All rights reserved.