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

Prisma ORMをReact Router 7で使用する方法

10分

はじめに

このガイドでは、宣言的なルーティングからフルスタックフレームワークのような全機能まで、様々な戦略に対応するルーターである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を使い始めるには、いくつかの依存関係をインストールする必要があります

npm install prisma tsx --save-dev
npm install @prisma/extension-accelerate @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プロバイダーを使用するように変更します

prisma/schema.prisma
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])
}

これにより、UserPostの2つのモデルが作成され、それらの間には1対多のリレーションシップが設定されます。

2.3. Prisma Clientジェネレーターの設定

次に、データベーステーブルを作成し、Prisma Clientを生成するために以下のコマンドを実行します

npx prisma migrate dev --name init

2.4. データベースのシード

サンプルユーザーと投稿でデータベースを埋めるためのシードデータを追加します。

prisma/ディレクトリにseed.tsという新しいファイルを作成します

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にこのスクリプトを実行する方法を伝えます

package.json
{
"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クライアントを次のように設定します

app/lib/prisma.ts
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
注意

データベース接続を効率的に管理するために、コネクションプーラー(Prisma Accelerateなど)を使用することをお勧めします。

コネクションプーラーを使用しない場合は、長期間稼働する環境でPrismaClientをグローバルにインスタンス化することは避けてください。代わりに、データベース接続を使い果たさないように、リクエストごとにクライアントを作成して破棄してください。

次のセクションでこのクライアントを使用して最初のクエリを実行します。

3.2. Prismaでデータベースをクエリする

Prisma Clientが初期化され、データベースへの接続があり、初期データが用意できたので、Prisma ORMでデータのクエリを開始できます。

この例では、アプリケーションの「ホーム」ページにすべてのユーザーが表示されるようにします。

app/routes/home.tsxファイルを開き、既存のコードを以下に置き換えます

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を実行してください。

これにより、タイトルとユーザーリストを含む基本的なページが得られます。しかし、ユーザーリストは静的です。データベースからユーザーを取得して、ページを動的に更新してください。

app/routes/home.tsx
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ファイルに以下のコードを追加します

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ページが表示されるようにします

app/routes.ts
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がロードされますが、コンテンツは静的です。ホームぺージと同様に動的に更新します

app/routes/posts/home.tsx
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ファイルに追加します

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

次に、このページに新しいルートを追加します

app/routes.ts
export default [
index("routes/home.tsx"),
route("posts", "routes/posts/home.tsx"),
route("posts/:postId", "routes/posts/post.tsx"),
] satisfies RouteConfig;

以前と同様に、このページは静的です。ページに渡されるparamsに基づいて動的に更新します

app/routes/posts/post.tsx
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ファイルに追加します

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に追加する必要があります

app/routes.ts
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を更新して、投稿をデータベースに保存するようにします

app/routes/posts/new.tsx
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を使用する

詳細情報と更新情報

© . All rights reserved.