Prisma ORMをReact Router 7で使用する方法
はじめに
このガイドでは、宣言的なルーティングからフルスタックフレームワークのような全機能まで、様々な戦略に対応するルーターであるReact Router 7でPrisma ORMを使用する方法を説明します。
Prisma ORMとPrisma PostgresをReact Router 7でセットアップし、マイグレーションを処理する方法を学びます。デプロイ可能なサンプルがGitHubにあります。
前提条件
1. プロジェクトをセットアップする
プロジェクトを作成したいディレクトリでcreate-react-router
を実行し、このガイドで使用する新しいReact Routerアプリを作成します。
npx create-react-router@latest react-router-7-prisma
以下の選択肢が表示されるので、両方ともYes
を選択してください
- 新しいgitリポジトリを初期化しますか?
Yes
- npmで依存関係をインストールしますか?
Yes
次に、プロジェクトディレクトリに移動します
cd react-router-7-prisma
2. Prismaのインストールと設定
2.1. 依存関係のインストール
Prismaを使い始めるには、いくつかの依存関係をインストールする必要があります
- Prisma Postgres (推奨)
- その他のデータベース
npm install prisma tsx --save-dev
npm install @prisma/extension-accelerate @prisma/client
npm install prisma tsx --save-dev
npm install @prisma/client
インストール後、プロジェクトでPrismaを初期化します
npx prisma init --db --output ../app/generated/prisma
Prisma Postgresデータベースのセットアップ中にいくつか質問に答える必要があります。お住まいの地域に最も近いリージョンを選択し、「My React Router 7 Project」のような覚えやすい名前をデータベースに付けます。
これにより、以下が作成されます
schema.prisma
ファイルを含むprisma
ディレクトリ。- Prisma Postgresデータベース。
- プロジェクトルートに
DATABASE_URL
を含む.env
ファイル。 - 生成されたPrisma Clientの
output
ディレクトリとしてapp/generated/prisma
。
2.2. Prismaスキーマの定義
prisma/schema.prisma
ファイルに、以下のモデルを追加し、ジェネレーターをprisma-client
プロバイダーを使用するように変更します
generator client {
provider = "prisma-client"
output = "../app/generated/prisma"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
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])
}
これにより、User
とPost
の2つのモデルが作成され、それらの間には1対多のリレーションシップが設定されます。
2.3. Prisma Clientジェネレーターの設定
次に、データベーステーブルを作成し、Prisma Clientを生成するために以下のコマンドを実行します
npx prisma migrate dev --name init
2.4. データベースのシード
サンプルユーザーと投稿でデータベースを埋めるためのシードデータを追加します。
prisma/
ディレクトリにseed.ts
という新しいファイルを作成します
import { PrismaClient, Prisma } from "../app/generated/prisma/client.js";
const prisma = new PrismaClient();
const userData: Prisma.UserCreateInput[] = [
{
name: "Alice",
email: "alice@prisma.io",
posts: {
create: [
{
title: "Join the Prisma Discord",
content: "https://pris.ly/discord",
published: true,
},
{
title: "Prisma on YouTube",
content: "https://pris.ly/youtube",
},
],
},
},
{
name: "Bob",
email: "bob@prisma.io",
posts: {
create: [
{
title: "Follow Prisma on Twitter",
content: "https://www.twitter.com/prisma",
published: true,
},
],
},
},
];
export async function main() {
for (const u of userData) {
await prisma.user.create({ data: u });
}
}
main();
次に、package.json
を更新して、Prismaにこのスクリプトを実行する方法を伝えます
{
"name": "react-router-7-prisma",
"private": true,
"type": "module",
"scripts": {
"build": "react-router build",
"dev": "react-router dev",
"start": "react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"dependencies": {
"@react-router/node": "^7.3.0",
"@react-router/serve": "^7.3.0",
"isbot": "^5.1.17",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.3.0"
},
"devDependencies": {
"@react-router/dev": "^7.3.0",
"@tailwindcss/vite": "^4.0.0",
"@types/node": "^20",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.1",
"prisma": "^6.5.0",
"react-router-devtools": "^1.1.0",
"tailwindcss": "^4.0.0",
"tsx": "^4.19.3",
"typescript": "^5.7.2",
"vite": "^5.4.11",
"vite-tsconfig-paths": "^5.1.4"
}
}
シードスクリプトを実行する
npx prisma db seed
Prisma Studioを開いてデータを検査します
npx prisma studio
3. PrismaをReact Router 7に統合する
3.1. Prisma Clientの作成
app
ディレクトリ内に、新しいlib
ディレクトリを作成し、その中にprisma.ts
ファイルを追加します。このファイルは、Prisma Clientインスタンスを作成およびエクスポートするために使用されます。
Prismaクライアントを次のように設定します
- Prisma Postgres (推奨)
- その他のデータベース
import { PrismaClient } from "../generated/prisma/client.js";
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 "../generated/prisma/client.js";
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
データベース接続を効率的に管理するために、コネクションプーラー(Prisma Accelerateなど)を使用することをお勧めします。
コネクションプーラーを使用しない場合は、長期間稼働する環境でPrismaClient
をグローバルにインスタンス化することは避けてください。代わりに、データベース接続を使い果たさないように、リクエストごとにクライアントを作成して破棄してください。
次のセクションでこのクライアントを使用して最初のクエリを実行します。
3.2. Prismaでデータベースをクエリする
Prisma Clientが初期化され、データベースへの接続があり、初期データが用意できたので、Prisma ORMでデータのクエリを開始できます。
この例では、アプリケーションの「ホーム」ページにすべてのユーザーが表示されるようにします。
app/routes/home.tsx
ファイルを開き、既存のコードを以下に置き換えます
import type { Route } from "./+types/home";
export function meta({}: Route.MetaArgs) {
return [
{ title: "New React Router App" },
{ name: "description", content: "Welcome to React Router!" },
];
}
export default function Home({ loaderData }: Route.ComponentProps) {
return (
<div className="min-h-screen flex flex-col items-center justify-center -mt-16">
<h1 className="text-4xl font-bold mb-8 font-[family-name:var(--font-geist-sans)]">
Superblog
</h1>
<ol className="list-decimal list-inside font-[family-name:var(--font-geist-sans)]">
<li className="mb-2">Alice</li>
<li>Bob</li>
</ol>
</div>
);
}
最初の行でimport type { Route } from "./+types/home";
のようなエラーが表示された場合は、React Routerが必要な型を生成するようにnpm run dev
を実行してください。
これにより、タイトルとユーザーリストを含む基本的なページが得られます。しかし、ユーザーリストは静的です。データベースからユーザーを取得して、ページを動的に更新してください。
import type { Route } from "./+types/home";
import prisma from '~/lib/prisma'
export function meta({}: Route.MetaArgs) {
return [
{ title: "New React Router App" },
{ name: "description", content: "Welcome to React Router!" },
];
}
export async function loader() {
const users = await prisma.user.findMany();
return { users };
}
export default function Home({ loaderData }: Route.ComponentProps) {
const { users } = loaderData;
return (
<div className="min-h-screen flex flex-col items-center justify-center -mt-16">
<h1 className="text-4xl font-bold mb-8 font-[family-name:var(--font-geist-sans)]">
Superblog
</h1>
<ol className="list-decimal list-inside font-[family-name:var(--font-geist-sans)]">
{users.map((user) => (
<li key={user.id} className="mb-2">
{user.name}
</li>
))}
</ol>
</div>
);
}
これでクライアントをインポートし、React Routerのローダーを使用してUser
モデルのすべてのユーザーをクエリし、リストに表示しています。
これでホームぺージは動的になり、データベースからユーザーが表示されるようになります。
3.4 データの更新 (オプション)
データが更新されたときに何が起こるかを確認したい場合は、以下を行うことができます
- 任意のSQLブラウザを介して
User
テーブルを更新する - より多くのユーザーを追加するために
seed.ts
ファイルを変更する prisma.user.findMany
の呼び出しを変更して、ユーザーの順序を並べ替えたり、ユーザーをフィルタリングしたりする。
ページをリロードするだけで変更が確認できます。
4. 新しい投稿リストページを追加する
ホームぺージは動作していますが、すべての投稿を表示する新しいページを追加する必要があります。
まず、app/routes
ディレクトリの下に新しいposts
ディレクトリを作成し、home.tsx
ファイルを追加します
mkdir -p app/routes/posts && touch app/routes/posts/home.tsx
次に、app/routes/posts/home.tsx
ファイルに以下のコードを追加します
import type { Route } from "./+types/home";
import prisma from "~/lib/prisma";
export default function Home() {
return (
<div className="min-h-screen flex flex-col items-center justify-center -mt-16">
<h1 className="text-4xl font-bold mb-8 font-[family-name:var(--font-geist-sans)]">
Posts
</h1>
<ul className="font-[family-name:var(--font-geist-sans)] max-w-2xl space-y-4">
<li>My first post</li>
</ul>
</div>
);
}
次に、app/routes.ts
ファイルを更新して、/posts
ルートにアクセスしたときにposts/home.tsx
ページが表示されるようにします
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("posts", "routes/posts/home.tsx"),
] satisfies RouteConfig;
これでlocalhost:5173/posts
がロードされますが、コンテンツは静的です。ホームぺージと同様に動的に更新します
import type { Route } from "./+types/home";
import prisma from "~/lib/prisma";
export async function loader() {
const posts = await prisma.post.findMany({
include: {
author: true,
},
});
return { posts };
}
export default function Posts({ loaderData }: Route.ComponentProps) {
const { posts } = loaderData;
return (
<div className="min-h-screen flex flex-col items-center justify-center -mt-16">
<h1 className="text-4xl font-bold mb-8 font-[family-name:var(--font-geist-sans)]">
Posts
</h1>
<ul className="font-[family-name:var(--font-geist-sans)] max-w-2xl space-y-4">
{posts.map((post) => (
<li key={post.id}>
<span className="font-semibold">{post.title}</span>
<span className="text-sm text-gray-600 ml-2">
by {post.author.name}
</span>
</li>
))}
</ul>
</div>
);
}
これはホームぺージと同様に機能しますが、ユーザーを表示する代わりに投稿を表示します。また、Prisma Clientクエリでinclude
を使用して各投稿の著者を取得し、著者の名前を表示できるようにしていることがわかります。
この「リストビュー」は、Webアプリケーションで最も一般的なパターンの1つです。アプリケーションには、他にも一般的に必要となる「詳細ビュー」と「作成ビュー」の2つのページを追加します。
5. 新しい投稿詳細ページを追加する
投稿リストページを補完するために、投稿詳細ページを追加します。
routes/posts
ディレクトリに、新しいpost.tsx
ファイルを作成します。
touch app/routes/posts/post.tsx
このページには、単一の投稿のタイトル、コンテンツ、および著者が表示されます。他のページと同様に、以下のコードをapp/routes/posts/post.tsx
ファイルに追加します
import type { Route } from "./+types/post";
import prisma from "~/lib/prisma";
export default function Post({ loaderData }: Route.ComponentProps) {
return (
<div className="min-h-screen flex flex-col items-center justify-center -mt-16">
<article className="max-w-2xl space-y-4 font-[family-name:var(--font-geist-sans)]">
<h1 className="text-4xl font-bold mb-8">My first post</h1>
<p className="text-gray-600 text-center">by Anonymous</p>
<div className="prose prose-gray mt-8">
No content available.
</div>
</article>
</div>
);
}
次に、このページに新しいルートを追加します
export default [
index("routes/home.tsx"),
route("posts", "routes/posts/home.tsx"),
route("posts/:postId", "routes/posts/post.tsx"),
] satisfies RouteConfig;
以前と同様に、このページは静的です。ページに渡されるparams
に基づいて動的に更新します
import { data } from "react-router";
import type { Route } from "./+types/post";
import prisma from "~/lib/prisma";
export async function loader({ params }: Route.LoaderArgs) {
const { postId } = params;
const post = await prisma.post.findUnique({
where: { id: parseInt(postId) },
include: {
author: true,
},
});
if (!post) {
throw data("Post Not Found", { status: 404 });
}
return { post };
}
export default function Post({ loaderData }: Route.ComponentProps) {
const { post } = loaderData;
return (
<div className="min-h-screen flex flex-col items-center justify-center -mt-16">
<article className="max-w-2xl space-y-4 font-[family-name:var(--font-geist-sans)]">
<h1 className="text-4xl font-bold mb-8">{post.title}</h1>
<p className="text-gray-600 text-center">by {post.author.name}</p>
<div className="prose prose-gray mt-8">
{post.content || "No content available."}
</div>
</article>
</div>
);
}
ここには多くの変更があるので、分解して説明します
- Prisma Clientを使用して、
params
オブジェクトから取得したid
で投稿をフェッチしています。 - 投稿が存在しない場合(削除されたか、間違ったIDを入力した場合など)は、エラーをスローして404ページを表示します。
- 次に、投稿のタイトル、コンテンツ、および著者を表示します。投稿にコンテンツがない場合は、プレースホルダーメッセージを表示します。
最も美しいページではありませんが、良いスタートです。localhost:5173/posts/1
およびlocalhost:5173/posts/2
に移動して試してみてください。localhost:5173/posts/999
に移動して404ページもテストできます。
6. 新しい投稿作成ページを追加する
アプリケーションを完成させるために、投稿の「作成」ページを追加します。これにより、自分で投稿を書き、データベースに保存することができます。
他のページと同様に、まず静的ページから始め、それを動的に更新します。
touch app/routes/posts/new.tsx
次に、以下のコードをapp/routes/posts/new.tsx
ファイルに追加します
import type { Route } from "./+types/new";
import { Form } from "react-router";
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const title = formData.get("title") as string;
const content = formData.get("content") as string;
}
export default function NewPost() {
return (
<div className="max-w-2xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">Create New Post</h1>
<Form method="post" className="space-y-6">
<div>
<label htmlFor="title" className="block text-lg mb-2">
Title
</label>
<input
type="text"
id="title"
name="title"
placeholder="Enter your post title"
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<div>
<label htmlFor="content" className="block text-lg mb-2">
Content
</label>
<textarea
id="content"
name="content"
placeholder="Write your post content here..."
rows={6}
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-3 rounded-lg hover:bg-blue-600"
>
Create Post
</button>
</Form>
</div>
);
}
まだアプリでposts/new
ページを開くことはできません。そのためには、再びroutes.tsx
に追加する必要があります
export default [
index("routes/home.tsx"),
route("posts", "routes/posts/home.tsx"),
route("posts/:postId", "routes/posts/post.tsx"),
route("posts/new", "routes/posts/new.tsx"),
] satisfies RouteConfig;
これで新しいURLでフォームを表示できます。見た目は良いですが、まだ何も動作しません。action
を更新して、投稿をデータベースに保存するようにします
import type { Route } from "./+types/new";
import { Form, redirect } from "react-router";
import prisma from "~/lib/prisma";
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const title = formData.get("title") as string;
const content = formData.get("content") as string;
try {
await prisma.post.create({
data: {
title,
content,
authorId: 1,
},
});
} catch (error) {
console.error(error);
return Response.json({ error: "Failed to create post" }, { status: 500 });
}
return redirect("/posts");
}
export default function NewPost() {
return (
<div className="max-w-2xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">Create New Post</h1>
<Form method="post" className="space-y-6">
<div>
<label htmlFor="title" className="block text-lg mb-2">
Title
</label>
<input
type="text"
id="title"
name="title"
placeholder="Enter your post title"
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<div>
<label htmlFor="content" className="block text-lg mb-2">
Content
</label>
<textarea
id="content"
name="content"
placeholder="Write your post content here..."
rows={6}
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-3 rounded-lg hover:bg-blue-600"
>
Create Post
</button>
</Form>
</div>
);
}
このページには機能するフォームができました!フォームを送信すると、新しい投稿がデータベースに作成され、投稿リストページにリダイレクトされます。
localhost:5173/posts/new
に移動してフォームを送信し、試してみてください。
7. 次のステップ
Prisma ORMを使用した動作するReact Routerアプリケーションができたので、アプリケーションを拡張および改善する方法をいくつか紹介します
- ルートを保護するために認証を追加する
- 投稿を編集および削除する機能を追加する
- 投稿にコメントを追加する
- 視覚的なデータベース管理にはPrisma Studioを使用する
詳細情報と更新情報
- Prisma ORMドキュメント
- Prisma Client APIリファレンス
- React Routerドキュメント
- 私たちのDiscordコミュニティに参加する
- TwitterとYouTubeでフォローしてください