Prisma ORMをReact Router 7で使用する方法
はじめに
このガイドでは、宣言的なルーティングのように最小限にも、フルスタックフレームワークのようにフル機能にもできるマルチストラテジー ルーターであるReact Router 7でPrisma ORMを使用する方法を紹介します。
このガイドでは、React RouterでPrisma ORMとPrisma Postgresをセットアップし、マイグレーションを処理する方法を学びます。GitHubでデプロイ準備完了のサンプルを見つけることができます。deployment-ready example on GitHub。
前提条件
このガイドを開始する前に、以下をご確認ください
- Node.js 20+ がインストールされていること
- Aアカウント
1. プロジェクトのセットアップ
プロジェクトを作成したいディレクトリから `create-react-router` を実行して、このガイドで使用する新しいReact Routerアプリを作成します。
npx create-react-router@latest my-app
新しいgitリポジトリを初期化し、npmで依存関係をインストールするかどうかを尋ねられます。「はい」を両方選択してください。
次に、プロジェクトディレクトリに移動します
cd my-app
2. Prisma ORMのセットアップ
2.1 Prisma ORMのインストールと最初のモデルの作成
まず、いくつかの依存関係をインストールする必要があります。プロジェクトのルートディレクトリのターミナルで、以下を実行します。
npm install prisma --save-dev
npm install tsx --save-dev
次に、`prisma init --db` を実行して、Prisma ORMとプロジェクト用のPrisma Postgresデータベースを初期化します。
npx prisma init --db --output ../app/generated/prisma
Prisma Postgresデータベースのセットアップ中に、いくつかの質問に答える必要があります。最寄りの地域と「My React Router Project」のような覚えやすいデータベース名を選択してください。
これにより、プロジェクトに新しい `prisma` ディレクトリが作成され、その中に `schema.prisma` ファイルが作成されます。`schema.prisma` ファイルは、データベースモデルを定義する場所です。
`prisma init` コマンドは、データベース接続文字列を格納するために使用される `.env` ファイルもプロジェクトルートに作成し、生成されたPrisma Clientを `/app/generated/prisma` ディレクトリに出力します。
次に、ジェネレーターを `prisma-client` プロバイダーを使用するように変更し、`schema.prisma` ファイルに2つのモデルを追加しましょう。`User` モデルと `Post` モデルです。
generator client {
provider = "prisma-client"
provider = "prisma-client-js"
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])
}
これは、ユーザーと投稿を持つシンプルなブログを表しています。各 `Post` は `User` を著者として持つことができ、各 `User` は多くの `Post` を持つことができます。
Prisma Schemaとモデルができたので、Prisma Postgresデータベースをマイグレーションしましょう。
2.2 データベーススキーマの更新
既存のデータを持つデータベースに接続する場合は、`prisma db pull` コマンドを使用し、「Prisma Clientのセットアップ」までスキップしてください。
`prisma migrate dev` コマンドを使用して、新しいスキーマをデータベースに適用できます。
npx prisma migrate dev --name init
これにより、`User` および `Post` テーブルを作成する初期マイグレーションが作成され、そのマイグレーションがデータベースに適用されます。
次に、初期データをデータベースに追加しましょう。
2.3 データベースのシード
Prisma ORMには、データベースに初期データをシードするための組み込みサポートがあります。これを行うには、`prisma` ディレクトリに `seed.ts` という名前の新しいファイルを作成します。
import { PrismaClient, Prisma } from '../app/generated/prisma'
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()
次に、`prisma.seed` 設定を `package.json` ファイルに追加します。
{
"name": "my-app",
"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"
}
}
最後に、`prisma db seed` を実行して、`seed.ts` ファイルで定義した初期データでデータベースをシードします。
npx prisma db seed
これで、初期データを含むデータベースができました! `prisma studio` を実行して、データベース内のデータを確認できます。
npx prisma studio
2.4 Prisma Clientのセットアップ
初期データを含むデータベースができたので、Prisma Clientをセットアップしてデータベースに接続できます。
`app` ディレクトリの中に、新しい `lib` ディレクトリを作成し、その中に `prisma.ts` ファイルを追加します。
mkdir -p app/lib && touch app/lib/prisma.ts
次に、以下のコードを `app/lib/prisma.ts` ファイルに追加します
import { PrismaClient } from "../generated/prisma";
declare global {
// avoid multiple instances when hot-reloading
var prismaClient: PrismaClient;
}
globalThis.prismaClient ??= new PrismaClient();
const prisma = globalThis.prismaClient;
export default prisma;
このファイルはPrisma Clientを作成し、グローバルオブジェクトにアタッチすることで、アプリケーション内でクライアントのインスタンスが1つだけ作成されるようにします。これは、開発中にPrisma ORMを使用する際に発生する可能性のあるホットリロードの問題を解決するのに役立ちます。
このクライアントを次のセクションで使用して、最初のクエリを実行します。
3. Prisma ORMでデータベースをクエリする
初期化された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 bg-gray-50 flex flex-col items-center justify-center -mt-16">
<h1 className="text-4xl font-bold mb-8 font-[family-name:var(--font-geist-sans)] text-[#333333]">
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";` でエラーが表示される場合は、`npm run dev` を実行して、React Routerが必要な型を生成するようにしてください。
これにより、タイトルとユーザーリストを持つ基本的なページが作成されます。ただし、ユーザーリストは静的です。ページを更新して、データベースからユーザーを取得し、動的にしましょう。
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 bg-gray-50 flex flex-col items-center justify-center -mt-16">
<h1 className="text-4xl font-bold mb-8 font-[family-name:var(--font-geist-sans)] text-[#333333]">
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.1 データの更新(オプション)
データが更新されたときに何が起こるかを確認したい場合は、次のことができます。
- 任意の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 bg-gray-50 flex flex-col items-center justify-center -mt-16">
<h1 className="text-4xl font-bold mb-8 font-[family-name:var(--font-geist-sans)] text-[#333333]">
Posts
</h1>
<ul className="font-[family-name:var(--font-geist-sans)] max-w-2xl space-y-4">
<li>My first post</li>
</ul>
</div>
);
}
次に、`/posts` ルートにアクセスしたときに `posts/home.tsx` ページが表示されるように、`app/routes.ts` ファイルを更新します
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 bg-gray-50 flex flex-col items-center justify-center -mt-16">
<h1 className="text-4xl font-bold mb-8 font-[family-name:var(--font-geist-sans)] text-[#333333]">
Posts
</h1>
<ul className="font-[family-name:var(--font-geist-sans)] max-w-2xl space-y-4">
<li>My first post</li>
{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 bg-gray-50 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 text-[#333333]">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 bg-gray-50 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 text-[#333333]">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>
<h1 className="text-4xl font-bold mb-8 text-[#333333]">{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:3000/posts/1` および `localhost:3000/posts/2` に移動して試してみてください。`localhost:3000/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 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>
);
}
このフォームは見栄えが良いですが、まだ何もしていません。投稿をデータベースに保存するために `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;
await prisma.post.create({
data: {
title,
content,
authorId: 1,
},
});
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 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:3000/posts/new` に移動してフォームを送信して試してみてください。
7. 次のステップ
Prisma ORMを使用した動作するReact Routerアプリケーションができたので、アプリケーションを拡張および改善するためのいくつかの方法を以下に示します。
- ルートを保護するために認証を追加する
- 投稿を編集および削除する機能を追加する
- 投稿にコメントを追加する
- Prisma Studioをビジュアルデータベース管理に使用する
詳細および更新情報については
- Prisma ORMドキュメント
- Prisma Client APIリファレンス
- React Routerドキュメント
- Discordコミュニティに参加するDiscord community
- TwitterとYouTubeでフォローするTwitter and YouTube