Skip to main content
Prisma Documentation Docs

Search documentation

Type to search this documentation.

On this pageOverview

React Router 7

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 7 and handle migrations. You can find a deployment-ready example on GitHub.

From the directory where you want to create your project, run create-react-router to create a new React Router app that you will be using for this guide.

title="bun"
bunx create-react-router@latest react-router-7-prisma
pnpm
pnpm dlx create-react-router@latest react-router-7-prisma
yarn
yarn dlx create-react-router@latest react-router-7-prisma
npm
npx create-react-router@latest react-router-7-prisma

You'll be prompted to select the following, select Yes for both:

Now, navigate to the project directory:

cd react-router-7-prisma

To get started with Prisma, you'll need to install a few dependencies:

title="bun"
bun add prisma@prev tsx @types/pg --dev
pnpm
pnpm add prisma@prev tsx @types/pg --save-dev
yarn
yarn add prisma@prev tsx @types/pg --dev
npm
npm install prisma@prev tsx @types/pg --save-dev
bun add @prisma/client@7 @prisma/adapter-pg dotenv pg
Bash
pnpm add @prisma/client@7 @prisma/adapter-pg dotenv pg
Bash
yarn add @prisma/client@7 @prisma/adapter-pg dotenv pg
Bash
npm install @prisma/client@7 @prisma/adapter-pg dotenv pg

[!NOTE] If you are using a different database provider (MySQL, SQL Server, SQLite), install the corresponding driver adapter package instead of @prisma/adapter-pg. For more information, see Database drivers.

Once installed, initialize Prisma in your project:

Once installed, initialize Prisma in your project:

bunx --bun prisma init --output ../app/generated/prisma
Bash
pnpm prisma init --output ../app/generated/prisma
Bash
yarn prisma init --output ../app/generated/prisma
Bash
npx prisma init --output ../app/generated/prisma

[!NOTE] prisma init creates the Prisma scaffolding and a local DATABASE_URL. In the next step, you will create a Prisma Postgres database and replace that value with a direct postgres://... connection string.

This will create:

  • A prisma directory with a schema.prisma file.
  • A prisma.config.ts file for configuring Prisma
  • A .env file containing a local DATABASE_URL at the project root.
  • An output directory for the generated Prisma Client as app/generated/prisma.

Create a Prisma Postgres database and replace the generated DATABASE_URL in your .env file with the postgres://... connection string from the CLI output:

This will create:

  • A prisma directory with a schema.prisma file.
  • A prisma.config.ts file for configuring Prisma
  • A .env file containing a local DATABASE_URL at the project root.
  • An output directory for the generated Prisma Client as app/generated/prisma.

Create a Prisma Postgres database and replace the generated DATABASE_URL in your .env file with the postgres://... connection string from the CLI output:

title="bun"
bunx create-db
pnpm
pnpm dlx create-db
yarn
yarn dlx create-db
npm
npx create-db

In the prisma/schema.prisma file, add the following models and change the generator to use the prisma-client provider:

title="prisma/schema.prisma"
generator client {

  provider = "prisma-client"

  output   = "../app/generated/prisma"

}

datasource db {

  provider = "postgresql"

}

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 creates two models: User and Post, with a one-to-many relationship between them.

To get access to the variables in the .env file, they can either be loaded by your runtime, or by using dotenv. Include an import for dotenv at the top of the prisma.config.ts

import "dotenv/config"; 

import { defineConfig, env } from "prisma/config";

export default defineConfig({

  schema: "prisma/schema.prisma",

  migrations: {

    path: "prisma/migrations",

  },

  datasource: {

    url: env("DATABASE_URL"),

  },

});

Now, run the following command to create the database tables and generate the Prisma Client:

title="bun"
bunx prisma migrate dev --name init
pnpm
pnpm prisma migrate dev --name init
yarn
yarn prisma migrate dev --name init
npm
npx prisma migrate dev --name init
title="bun"
bunx prisma generate
pnpm
pnpm prisma generate
yarn
yarn prisma generate
npm
npx prisma generate

Add some seed data to populate the database with sample users and posts.

Create a new file called seed.ts in the prisma/ directory:

title="prisma/seed.ts"
import { PrismaClient, Prisma } from "../app/generated/prisma/client.js";

import { PrismaPg } from "@prisma/adapter-pg";

const adapter = new PrismaPg({

  connectionString: process.env.DATABASE_URL!,

});

const prisma = new PrismaClient({

  adapter,

});

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, tell Prisma how to run this script by updating your prisma.config.ts:

title="prisma.config.ts"
import "dotenv/config";

import { defineConfig, env } from "prisma/config";

export default defineConfig({

  schema: "prisma/schema.prisma",

  migrations: {

    path: "prisma/migrations",

    seed: `tsx prisma/seed.ts`, 

  },

  datasource: {

    url: env("DATABASE_URL"),

  },

});

Run the seed script:

title="bun"
bunx prisma db seed
pnpm
pnpm prisma db seed
yarn
yarn prisma db seed
npm
npx prisma db seed

And open Prisma Studio to inspect your data:

title="bun"
bunx prisma studio
pnpm
pnpm prisma studio
yarn
yarn prisma studio
npm
npx prisma studio

Inside of your app directory, create a new lib directory and add a prisma.ts file to it. This file will be used to create and export your Prisma Client instance.

Set up the Prisma client like this:

title="app/lib/prisma.ts"
import { PrismaClient } from "../generated/prisma/client.js";

import { PrismaPg } from "@prisma/adapter-pg";

const adapter = new PrismaPg({

  connectionString: process.env.DATABASE_URL!,

});

const globalForPrisma = global as unknown as {

  prisma: PrismaClient;

};

const prisma =

  globalForPrisma.prisma ||

  new PrismaClient({

    adapter,

  });

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

export default prisma;

You'll use this client in the next section to run your first queries.

Now that you have an initialized Prisma Client, a connection to your database, and some initial data, you can start querying your data with Prisma ORM.

In this example, you'll be making the "home" page of your application display all of your users.

Open the app/routes/home.tsx file and replace the existing code with the following:

title="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>

  );

}

This gives you a basic page with a title and a list of users. However, the list of users is static. Update the page to fetch the users from your database and make it dynamic.

title="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>

  );

}

You are now importing your 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.

If you want to see what happens when data is updated, you could:

  • update your User table via an 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.

Reload the page to see the changes.

You have your home page working, but you should add a new page that displays all of your 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:

title="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>

  );

}

Second, update the app/routes.ts file so when you visit the /posts route, the posts/home.tsx page is shown:

title="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;

Now localhost:5173/posts will load, but the content is static. Update it to be dynamic, similarly to the home page:

title="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">                {" "}                // [!code ++] 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 you've used include in your Prisma Client query to fetch the author of each post so you can display the author's name.

This "list view" is one of the most common patterns in web applications. You're going to add two more pages to your application which you'll also commonly need: a "detail view" and a "create view".

To complement the Posts list page, you'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 your other pages, add the following code to the app/routes/posts/post.tsx file:

title="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>

  );

}

And then add a new route for this page:

title="app/routes.ts"
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. Update it to be dynamic based on the params passed to the page:

title="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>  );}

There's a lot of changes here, so break it down:

  • You're using Prisma Client to fetch the post by its id, which you get from the params object.
  • In case the post doesn't exist (maybe it was deleted or maybe you typed a wrong ID), you throw an error to display a 404 page.
  • You then display the post's title, content, and author. If the post doesn't have content, you display a placeholder message.

It's not the prettiest page, but it's a good start. Try it out by navigating to localhost:5173/posts/1 and localhost:5173/posts/2. You can also test the 404 page by navigating to localhost:5173/posts/999.

To round out your application, you'll add a "create" page for posts. This will allow you to write your own posts and save them to the database.

As with the other pages, you'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:

title="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>

  );

}

You can't open the posts/new page in your app yet. To do that, you need to add it to routes.tsx again:

title="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;

Now you can view the form at the new URL. It looks good, but it doesn't do anything yet. Update the action to save the post to the database:

title="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>

  );

}

When you submit the form, the action creates a new post in the database and redirects you to the posts list page.

Try it out by navigating to localhost:5173/posts/new and submitting the form.

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:

Suggest an edit

Propose a replacement for this page. The site team reviews it before applying any changes.

Export
Documentation menu