How to use Prisma ORM with React Router 7
Introduction
This guide shows you how to use Prisma ORM with React Router 7, a multi-strategy router that can be as minimal as declarative routing or as full-featured as a fullstack framework.
You'll learn how to set up Prisma ORM and Prisma Postgres with React Router and handle migrations. You can find a deployment-ready example on GitHub.
Prerequisites
Before starting this guide, make sure you have:
- Node.js 20+ installed
- A account
1. Set up your project
From the directory where you want to create your project, run create-react-router
to create a new React Router app that we will be using for this guide.
npx create-react-router@latest my-app
You will be prompted if you want to initalize a new git repository and install dependencies with npm. Select yes for both.
Now, navigate to the project directory:
cd my-app
2. Set up Prisma ORM
2.1 Install Prisma ORM and create your first models
First, we need to install a few dependencies. At the root of your project in your terminal, run:
npm install prisma --save-dev
npm install tsx --save-dev
Then, run prisma init --db
to initialize Prisma ORM and a Prisma Postgres database for your project.
npx prisma init --db --output ../app/generated/prisma
You'll need to answer a few questions while setting up your Prisma Postgres database. Select the region closest to your location and a memorable name for your database like "My React Router Project"
This will create a new prisma
directory in your project, with a schema.prisma
file inside of it. The schema.prisma
file is where you will define your database models.
The prisma init
command also creates a .env
file in your project root, which is used to store your database connection string, and outputs the generated Prisma Client into the /app/generated/prisma
directory.
Next, let's add two models to your schema.prisma
file. A User
model and a Post
model.
generator client {
provider = "prisma-client-js"
}
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])
}
This represents a simple blog with users and posts. Each Post
can have a User
as an author and each User
can have many Post
s.
Now that we have a Prisma Schema and a model, let's migrate our Prisma Postgres database.
2.2 Update your database schema
If you're connecting to a database with existing data, use the prisma db pull
command and then skip to Set up Prisma Client.
We can apply your new schema to your database using the prisma migrate dev
command.
npx prisma migrate dev --name init
This creates an initial migration creating the User
and Post
tables and applies that migration to your database.
Now, let's add some initial data to our database.
2.3 Seed your database
Prisma ORM has built-in support for seeding your database with initial data. To do this, you can create a new file called seed.ts
in the prisma
directory.
import { PrismaClient, Prisma } from '@prisma/client'
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()
Now, add the prisma.seed
configuration to your package.json
file.
{
"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"
}
}
Finally, run prisma db seed
to seed your database with the initial data we defined in the seed.ts
file.
npx prisma db seed
We now have a database with some initial data! You can check out the data in your database by running prisma studio
.
npx prisma studio
2.4 Set up Prisma Client
Now that we have a database with some initial data, we can set up Prisma Client and connect it to our database.
Inside of your app
directory, create a new lib
directory and add a prisma.ts
file to it.
mkdir -p app/lib && touch app/lib/prisma.ts
Now, add the following code to your app/lib/prisma.ts
file:
import { PrismaClient } from "@prisma/client";
declare global {
// avoid multiple instances when hot-reloading
var prismaClient: PrismaClient;
}
globalThis.prismaClient ??= new PrismaClient();
const prisma = globalThis.prismaClient;
export default prisma;
This file creates a Prisma Client and attaches it to the global object so that only one instance of the client is created in your application. This helps resolve issues with hot reloading that can occur when using Prisma ORM in development.
We'll use this client in the next section to run your first queries.
3. Query your database with Prisma ORM
Now that we have an initialized Prisma Client, a connection to our database, and some initial data, we can start querying our data with Prisma ORM.
In our example, we'll be making the "home" page of our application display all of our users.
Open the app/routes/home.tsx
file and replace the existing code with the following:
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>
);
}
If you see an error on the first line, import type { Route } from "./+types/home";
, make sure you run npm run dev
so React Router generates needed types.
This gives us a basic page with a title and a list of users. However, the list of users is static. Let's update the page to fetch the users from our database and make it dynamic.
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>
);
}
We are now importing our client, using a React Router loader to query the User
model for all users, and then displaying them in a list.
Now your home page is dynamic and will display the users from your database.
3.1 Update your data (optional)
If you want to see what happens when data is updated, you could:
- update your
User
table via a SQL browser of your choice - change your
seed.ts
file to add more users - change the call to
prisma.user.findMany
to re-order the users, filter the users, or similar.
Just reload the page and you'll see the changes.
4. Add a new Posts list page
We have our home page working, but we should add a new page that displays all of our posts.
First, create a new posts
directory under the app/routes
directory and add a home.tsx
file:
mkdir -p app/routes/posts && touch app/routes/posts/home.tsx
Second, add the following code to the app/routes/posts/home.tsx
file:
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>
);
}
Second, update the app/routes.ts
file so when you visit the /posts
route, the posts/home.tsx
page is shown:
export default [
index("routes/home.tsx"),
route("posts", "routes/posts/home.tsx"),
] satisfies RouteConfig;
Now localhost:5173/posts
will load, but the content is static. Let's update it to be dynamic, similarly to the home page:
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>
);
}
This works similarly to the home page, but instead of displaying users, it displays posts. You can also see that we've used include
in our Prisma Client query to fetch the author of each post so we can display the author's name.
This "list view" is one of the most common patterns in web applications. We're going to add two more pages to our application which you'll also commonly need: a "detail view" and a "create view".
5. Add a new Posts detail page
To complement the Posts list page, we'll add a Posts detail page.
In the routes/posts
directory, create a new post.tsx
file.
touch app/routes/posts/post.tsx
This page will display a single post's title, content, and author. Just like our other pages, add the following code to the app/routes/posts/post.tsx
file:
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>
);
}
And then add a new route for this page:
export default [
index("routes/home.tsx"),
route("posts", "routes/posts/home.tsx"),
route("posts/:postId", "routes/posts/post.tsx"),
] satisfies RouteConfig;
As before, this page is static. Let's update it to be dynamic based on the params
passed to the page:
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>
);
}
There's a lot of changes here, so let's break it down:
- We're using Prisma Client to fetch the post by its
id
, which we get from theparams
object. - In case the post doesn't exist (maybe it was deleted or maybe you typed a wrong ID), we throw an error to display a 404 page.
- We then display the post's title, content, and author. If the post doesn't have content, we display a placeholder message.
It's not the prettiest page, but it's a good start. Try it out by navigating to localhost:3000/posts/1
and localhost:3000/posts/2
. You can also test the 404 page by navigating to localhost:3000/posts/999
.
6. Add a new Posts create page
To round out our application, we'll add a "create" page for posts. This will let you write your own posts and save them to the database.
As with the other pages, we'll start with a static page and then update it to be dynamic.
touch app/routes/posts/new.tsx
Now, add the following code to the app/routes/posts/new.tsx
file:
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>
);
}
This form looks good, but it doesn't do anything yet. Let's update the action
to save the post to the database:
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>
);
}
This page now has a functional form! When you submit the form, it will create a new post in the database and redirect you to the posts list page.
Try it out by navigating to localhost:3000/posts/new
and submitting the form.
7. Next steps
Now that you have a working React Router application with Prisma ORM, here are some ways you can expand and improve your application:
- Add authentication to protect your routes
- Add the ability to edit and delete posts
- Add comments to posts
- Use Prisma Studio for visual database management
For more information and updates:
- Prisma ORM documentation
- Prisma Client API reference
- React Router documentation
- Join our Discord community
- Follow us on Twitter and YouTube