# React Router (/docs/guides/frameworks/react-router-7)

> For the complete Prisma documentation index, see [llms.txt](https://www.prisma.io/docs/llms.txt). A markdown version of any docs page is available by appending `.md` to its URL.

Add Prisma ORM to a React Router app in framework mode with orm init, then build user and post pages with loaders and a form action.

Location: Guides > Frameworks > React Router

## Introduction [#introduction]

This guide shows you how to use Prisma ORM in a [React Router](https://reactrouter.com/) app running in framework mode. There is no `create-prisma` template for React Router, so you start from `create-react-router` and add Prisma ORM to the existing project with `orm init`. You then define a contract for users and posts, seed the database, read it from route loaders, and write to it from a form action.

`create-react-router@latest` scaffolds React Router 8. The route APIs this guide uses (`loader`, `action`, `routes.ts`, and the generated `Route` types) are the same in React Router 7, and the finished app was run on both majors.

Every command, page, and response below was run end to end against a local PostgreSQL database.

> [!NOTE]
> Using Prisma ORM 7?
> 
> Prisma ORM 8 is the current release. Prisma ORM 7 remains fully supported; the Prisma ORM 7 version of this guide is at [/guides/v7/frameworks/react-router-7](https://www.prisma.io/docs/guides/v7/frameworks/react-router-7).

## Prerequisites [#prerequisites]

* [Node.js](https://nodejs.org) 24 or later
* A PostgreSQL connection string, or nothing at all: `npx create-db@latest` can create a [Prisma Postgres](https://www.prisma.io/docs/postgres) database for you

## Use with your agent [#use-with-your-agent]

To delegate this guide to your coding agent, copy the prompt below and hand it over:

```text
Create a new React Router app in framework mode, add Prisma ORM to it, and build a small blog on top.

1. Scaffold: `npx create-react-router@latest my-app --yes`, then `cd my-app`. Run `npx prisma@latest orm init --yes --target postgres --authoring psl` so Prisma ORM is added to the existing project, then `npx prisma@latest init` so the Prisma agent skills are installed and stay current, and use them.
2. Get a database connection string: use the one I give you, or create a Prisma Postgres database with `npx create-db@latest` and show me the claim URL it prints. Write it to `.env` as `DATABASE_URL`; the scaffolded `src/prisma/db.ts` and `prisma.config.ts` both load `.env`.
3. Edit `src/prisma/contract.prisma` so `User` has `id`, `email`, `name`, `posts`, `createdAt`, `updatedAt` and `Post` has `id`, `title`, `content`, `published`, `author`, `authorId`, `createdAt`, `updatedAt`, following https://www.prisma.io/docs/guides/frameworks/react-router-7.md. Run `npx prisma@latest contract emit` and `npx prisma@latest db init`.
4. Add `allowImportingTsExtensions: true` to `tsconfig.json`, write `src/prisma/seed.ts` that creates two users with posts and closes the client, and run it with `node src/prisma/seed.ts`.
5. Create `app/lib/db.server.ts` that re-exports `db` from `src/prisma/db.ts`. Build these routes in `app/routes.ts`: `/` lists users, `/posts` lists posts with their author, `/posts/new` has a form whose action creates a post and redirects to it, `/posts/:postId` shows one post or a 404.
6. Start `npm run dev` in the background, wait until it reports ready, verify every route with curl including a POST to `/posts/new`, then stop the dev server. Run `npm run typecheck` and fix anything it reports.
```

## 1. Set up your project [#1-set-up-your-project]

Create a new React Router app in framework mode:

  

#### bun

```bash
bunx create-react-router@latest my-app
```

#### pnpm

```bash
pnpm dlx create-react-router@latest my-app
```

#### yarn

```bash
yarn dlx create-react-router@latest my-app
```

#### npm

```bash
npx create-react-router@latest my-app
```

Accept the defaults at the prompts: initialize a git repository and install dependencies with npm.

```text no-copy
         create-react-router v8.3.1

      ◼  Directory: Using my-app as project directory
      ◼  Using default template See https://github.com/remix-run/react-router-templates for more
      ✔  Template copied
      ✔  Dependencies installed
      ✔  Git initialized

  done   That's it!
```

Move into the project:

```bash
cd my-app
```

## 2. Add Prisma ORM to the project [#2-add-prisma-orm-to-the-project]

### 2.1. Run `orm init` [#21-run-orm-init]

`orm init` adds Prisma ORM to a project that already exists. Run it after the scaffold has installed dependencies so it picks up npm from the lockfile:

  

#### bun

```bash
bunx prisma@latest orm init --yes --target postgres --authoring psl
```

#### pnpm

```bash
pnpm dlx prisma@latest orm init --yes --target postgres --authoring psl
```

#### yarn

```bash
yarn dlx prisma@latest orm init --yes --target postgres --authoring psl
```

#### npm

```bash
npx prisma@latest orm init --yes --target postgres --authoring psl
```

`--target postgres` selects PostgreSQL, `--authoring psl` keeps the schema in Prisma Schema Language, and `--yes` accepts the default contract path. Run [`orm init`](https://www.prisma.io/docs/cli/orm-init) without flags for the guided version that asks the same questions.

```text no-copy
Updated tsconfig.json with required compiler options.
npm add @prisma/orm-postgres dotenv
npm add -D prisma@latest
npm add -D @prisma/cli-engine@0.3.0
Emit the contract

filesWritten: src/prisma/contract.prisma, prisma.config.ts, src/prisma/db.ts,
              prisma-8.md, .env.example, tsconfig.json, .gitignore,
              .gitattributes, package.json
contractEmitted: true
```

The command installs `@prisma/orm-postgres` (the Postgres runtime) and `dotenv`, adds the `prisma` CLI as a dev dependency, and writes:

* `prisma.config.ts`: the CLI configuration. It loads `.env` and points at your contract.
* `src/prisma/contract.prisma`: your schema, with starter `User` and `Post` models.
* `src/prisma/db.ts`: the Prisma ORM client your loaders import.
* `src/prisma/contract.json` and `src/prisma/contract.d.ts`: emitted from the contract; the runtime validates against the first and your queries are typed by the second.
* A `contract:emit` script in `package.json`, and `"module": "preserve"` in `tsconfig.json`.

There is no `prisma generate` step and no driver adapter package. The emitted contract is what `@prisma/client` and the adapter used to provide.

### 2.2. Set your database connection string [#22-set-your-database-connection-string]

Create a `.env` file at the project root. Use your own PostgreSQL connection string, or run `npx create-db@latest` to get a Prisma Postgres one:

```bash title=".env"
DATABASE_URL="postgres://user:password@localhost:5432/mydb"
```

Both `prisma.config.ts` and `src/prisma/db.ts` import `dotenv/config`, so the CLI, the seed script, and the dev server all read this file.

### 2.3. Define your contract [#23-define-your-contract]

Replace the starter models in `src/prisma/contract.prisma` with the blog schema this guide builds. `User` loses the `username` column and `Post` gains a `published` flag:

```prisma title="src/prisma/contract.prisma"
// use prisma-8

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt TimestamptzString @default(now())
  updatedAt temporal.updatedAtString()
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  createdAt TimestamptzString @default(now())
  updatedAt temporal.updatedAtString()
}
```

`TimestamptzString` and `temporal.updatedAtString()` are the Prisma ORM idioms for a `timestamptz` column that reaches your code as an ISO string, which is what a React Router loader can hand to the browser without extra serialization.

Emit the contract again so `contract.json` and `contract.d.ts` match:

  

#### bun

```bash
bunx prisma@latest contract emit
```

#### pnpm

```bash
pnpm dlx prisma@latest contract emit
```

#### yarn

```bash
yarn dlx prisma@latest contract emit
```

#### npm

```bash
npx prisma@latest contract emit
```

```text no-copy
"files": {
  "json": "src/prisma/contract.json",
  "dts": "src/prisma/contract.d.ts"
}
```

### 2.4. Initialize the database [#24-initialize-the-database]

`db init` creates the tables from the contract and signs the database, so later commands can tell whether it still matches:

  

#### bun

```bash
bunx prisma@latest db init
```

#### pnpm

```bash
pnpm dlx prisma@latest db init
```

#### yarn

```bash
yarn dlx prisma@latest db init
```

#### npm

```bash
npx prisma@latest db init
```

```text no-copy
"summary": "Applied 5 operation(s) across 1 space(s), database signed"
```

The five operations are the `user` and `post` tables, the unique constraint on `email`, the index on `authorId`, and the foreign key. If you change the contract later, run `npx prisma@latest contract emit` followed by `npx prisma@latest db update`, or plan a checked-in migration with [`migration plan`](https://www.prisma.io/docs/cli/migration-plan). There is no `prisma migrate dev`.

### 2.5. Seed the database [#25-seed-the-database]

The seed is a plain script that Node.js 24 runs directly, so it imports the client with its `.ts` extension. Tell TypeScript to allow that:

```json title="tsconfig.json"
{
  "compilerOptions": {
    "resolveJsonModule": true,
    "allowImportingTsExtensions": true, // [!code ++]
    "skipLibCheck": true,
    "strict": true
  }
}
```

Create `src/prisma/seed.ts`:

```typescript title="src/prisma/seed.ts"
import { db } from "./db.ts";

const users = [
  {
    email: "alice@prisma.io",
    name: "Alice",
    posts: [
      { title: "Join the Prisma Discord", content: "https://pris.ly/discord", published: true },
      { title: "Prisma on YouTube", content: "https://pris.ly/youtube" },
    ],
  },
  {
    email: "bob@prisma.io",
    name: "Bob",
    posts: [
      { title: "Follow Prisma on Twitter", content: "https://www.twitter.com/prisma", published: true },
    ],
  },
];

for (const { posts, ...data } of users) {
  const user = await db.orm.public.User.create(data);
  for (const post of posts) {
    await db.orm.public.Post.create({ ...post, authorId: user.id });
  }
  console.log(`Created ${user.name} with ${posts.length} post(s)`);
}

await db.close();
```

`create` returns the inserted row, so the user's generated `id` is available for the posts without a second query. The script closes the client at the end; without that call the connection pool keeps the process alive.

Run it:

```bash
node src/prisma/seed.ts
```

```text no-copy
Created Alice with 2 post(s)
Created Bob with 1 post(s)
```

## 3. Integrate Prisma into React Router [#3-integrate-prisma-into-react-router]

### 3.1. Create a server-only `db` helper [#31-create-a-server-only-db-helper]

React Router bundles route modules for the browser as well as the server. Loaders and actions are stripped from the browser build, but the safest way to keep the database client out of it is a module with a `.server` suffix, which React Router refuses to include in client code. Create `app/lib/db.server.ts` and re-export the scaffolded client from it:

```typescript title="app/lib/db.server.ts"
export { db } from "../../src/prisma/db";
```

Routes import it as `~/lib/db.server` through the `~` alias the template configures. The client is created once per process and shared by every request; never close it in a loader or action.

### 3.2. Query your database from a loader [#32-query-your-database-from-a-loader]

Replace `app/routes/home.tsx` so the home page lists users from the database:

```tsx title="app/routes/home.tsx"
import type { Route } from "./+types/home";
import { Link } from "react-router";
import { db } from "~/lib/db.server";

export function meta({}: Route.MetaArgs) {
  return [
    { title: "Superblog" },
    { name: "description", content: "A React Router app on Prisma 8" },
  ];
}

export async function loader() {
  const users = await db.orm.public.User.select("id", "name", "email").all();
  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">Superblog</h1>
      <ol className="list-decimal list-inside">
        {users.map((user) => (
          <li key={user.id} className="mb-2">
            {user.name} ({user.email})
          </li>
        ))}
      </ol>
      <Link to="/posts" className="mt-8 underline">
        View posts
      </Link>
    </div>
  );
}
```

Model access is namespace-qualified on PostgreSQL: `db.orm.public.User`. `select` narrows the columns and `all()` runs the query. The `Route.ComponentProps` type carries the loader's return type into the component, so `users` is typed without any annotation.

The template's `app/welcome` directory is no longer imported; delete it.

Start the dev server:

  

#### bun

```bash
bun run dev
```

#### pnpm

```bash
pnpm run dev
```

#### yarn

```bash
yarn dev
```

#### npm

```bash
npm run dev
```

```text no-copy
  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
```

Open [http://localhost:5173](http://localhost:5173). The page lists Alice and Bob.

> [!NOTE]
> If your editor reports an error on `import type { Route } from "./+types/home"`, run `npm run dev` or `npm run typecheck` once so React Router generates the route types.

## 4. Add a posts list page [#4-add-a-posts-list-page]

Create `app/routes/posts/home.tsx`. The loader includes each post's author so the page can show the name:

```tsx title="app/routes/posts/home.tsx"
import type { Route } from "./+types/home";
import { Link } from "react-router";
import { db } from "~/lib/db.server";

export async function loader() {
  const posts = await db.orm.public.Post.include("author").orderBy((p) => p.id.asc()).all();
  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">Posts</h1>
      <ul className="max-w-2xl space-y-4">
        {posts.map((post) => (
          <li key={post.id}>
            <Link to={`/posts/${post.id}`} className="font-semibold underline">
              {post.title}
            </Link>
            <span className="text-sm text-gray-600 ml-2">by {post.author.name}</span>
          </li>
        ))}
      </ul>
      <Link to="/posts/new" className="mt-8 underline">
        Create a post
      </Link>
    </div>
  );
}
```

Register the route in `app/routes.ts`:

```tsx title="app/routes.ts"
import { type RouteConfig, index, route } from "@react-router/dev/routes"; // [!code highlight]

export default [
  index("routes/home.tsx"),
  route("posts", "routes/posts/home.tsx"), // [!code ++]
] satisfies RouteConfig;
```

Open [http://localhost:5173/posts](http://localhost:5173/posts): three posts, each with its author. `include("author")` fetches the relation in the same query, and the result type gains an `author` object, so `post.author.name` type-checks.

## 5. Add a post detail page [#5-add-a-post-detail-page]

Create `app/routes/posts/post.tsx`. The loader reads the post by primary key and throws a 404 when there is none:

```tsx title="app/routes/posts/post.tsx"
import type { Route } from "./+types/post";
import { data } from "react-router";
import { db } from "~/lib/db.server";

export async function loader({ params }: Route.LoaderArgs) {
  const id = Number(params.postId);
  const post = Number.isInteger(id)
    ? await db.orm.public.Post.include("author").first({ id })
    : null;

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

`first({ id })` is the primary-key lookup; it returns the row or `null`. Route params are strings, so the loader converts `postId` and refuses anything that is not an integer before it queries.

Add the route:

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

Open [http://localhost:5173/posts/1](http://localhost:5173/posts/1) and [http://localhost:5173/posts/2](http://localhost:5173/posts/2). Then try [http://localhost:5173/posts/999](http://localhost:5173/posts/999): the template's root `ErrorBoundary` renders the 404.

## 6. Add a create page with an action [#6-add-a-create-page-with-an-action]

Create `app/routes/posts/new.tsx`. The `action` receives the form submission, inserts the post, and redirects to its detail page:

```tsx title="app/routes/posts/new.tsx"
import type { Route } from "./+types/new";
import { Form, redirect } from "react-router";
import { db } from "~/lib/db.server";

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const title = String(formData.get("title") ?? "").trim();
  const content = String(formData.get("content") ?? "").trim();

  if (!title) {
    return { error: "Title is required" };
  }

  const post = await db.orm.public.Post.create({
    title,
    content: content || null,
    authorId: 1,
  });

  return redirect(`/posts/${post.id}`);
}

export default function NewPost({ actionData }: Route.ComponentProps) {
  return (
    <div className="max-w-2xl mx-auto p-4">
      <h1 className="text-2xl font-bold mb-6">Create New Post</h1>
      {actionData?.error && <p className="text-red-600 mb-4">{actionData.error}</p>}
      <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>
  );
}
```

`create` returns the inserted row with its generated `id`, `published: false` default, and timestamps, so the redirect target is known without another query. The post is attributed to user `1` (Alice from the seed) to keep the example short; a real app takes the author from the session.

Register the route. React Router ranks static segments above dynamic ones, so `/posts/new` reaches this route and not `posts/:postId`, whichever order you list them in:

```tsx 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"), // [!code ++]
] satisfies RouteConfig;
```

Open [http://localhost:5173/posts/new](http://localhost:5173/posts/new) and submit the form, or post to it from the terminal:

```bash
curl -i -X POST http://localhost:5173/posts/new \
  --data-urlencode "title=Hello from curl" \
  --data-urlencode "content=Posted with a form action"
```

```text no-copy
HTTP/1.1 302
location: /posts/4
```

Follow the redirect and the new post renders with Alice as its author. Submitting without a title returns the page with the `Title is required` message instead.

## 7. Type-check and build [#7-type-check-and-build]

React Router's `typecheck` script generates the route types and runs `tsc` across the app, the seed script, and the Prisma files:

  

#### bun

```bash
bun run typecheck
```

#### pnpm

```bash
pnpm run typecheck
```

#### yarn

```bash
yarn typecheck
```

#### npm

```bash
npm run typecheck
```

A production build works the same way as before Prisma was added. `react-router build` bundles the server, and `react-router-serve` runs it on port 3000 (set `PORT` to change it):

  

#### bun

```bash
bun run build
bun run start
```

#### pnpm

```bash
pnpm run build
pnpm run start
```

#### yarn

```bash
yarn build
yarn run start
```

#### npm

```bash
npm run build
npm run start
```

```text no-copy
[react-router-serve] http://localhost:3000 (http://192.168.1.16:3000)
GET /posts 200 - - 7.514 ms
```

The server reads `DATABASE_URL` the same way the dev server does, through the `dotenv/config` import in `src/prisma/db.ts`, so a `.env` file next to the build is enough locally. On a host, set the variable in the environment instead.

## Common gotchas [#common-gotchas]

> [!WARNING]
> Keep the client out of the browser bundle. Import `db` from `~/lib/db.server` only in `loader`, `action`, or other server code. If a component or a shared module reaches it, the build stops with `Error: Server-only module referenced by client` from the `react-router:dot-server` plugin, naming the offending import. That is the `.server` suffix doing its job.

> [!WARNING]
> Do not call `db.close()` in a loader or action. The client in `src/prisma/db.ts` is a module-level singleton whose pool serves every request; closing it after one request breaks the next. Close it only in short scripts such as the seed.

> [!NOTE]
> `orm init` installs `prisma@latest` as the dev dependency, so `npm run contract:emit` runs the same CLI as the `npx prisma@latest` steps in this guide until a newer release ships. Pin the `prisma` dev dependency if you want the package script to stay on one version.

## Prompt your coding agent [#prompt-your-coding-agent]

Run [`npx prisma@latest init`](https://www.prisma.io/docs/cli/init) once to install the [Prisma ORM skills](https://www.prisma.io/docs/ai/tools/skills#available-skills-for-prisma-8) for your coding agent and keep them matching your installed packages. Prompts that map to this guide:

* "Using the prisma-8 skill, add an edit page at `/posts/:postId/edit` whose action updates the post with `db.orm.public.Post.where({ id }).update(...)`."
* "Add a delete button to the post page that removes the post in an action and redirects to `/posts`."
* "Only list published posts on `/posts`, using a `where` predicate on `published`."
* "Add a `Comment` model to `src/prisma/contract.prisma`, emit the contract, plan a migration, and render comments under each post."

## Next steps [#next-steps]

* Change the schema in `src/prisma/contract.prisma`, then run `npx prisma@latest contract emit` and `npx prisma@latest db update`, or plan a checked-in migration with [`migration plan`](https://www.prisma.io/docs/cli/migration-plan).
* [Learn the fundamentals](https://www.prisma.io/docs/orm/fundamentals/reading-data): filtering, sorting, pagination, and writes.
* [Relations and joins](https://www.prisma.io/docs/orm/fundamentals/relations-and-joins) covers `include` in depth.
* [Read the Prisma ORM overview](https://www.prisma.io/docs/orm) for the concepts behind contracts and typed queries.
* [React Router documentation](https://reactrouter.com/home) for loaders, actions, and routing.

## Related pages

- [`Astro`](https://www.prisma.io/docs/guides/frameworks/astro): Set up Prisma ORM in an Astro app with create-prisma, from scaffold to rendered data, and deploy it to Prisma Compute.
- [`Elysia`](https://www.prisma.io/docs/guides/frameworks/elysia): Build an Elysia API on Prisma ORM with the elysia template and deploy it to Prisma Compute.
- [`Hono`](https://www.prisma.io/docs/guides/frameworks/hono): Build a Hono API on Prisma ORM with the hono template, add your own routes, and deploy it to Prisma Compute.
- [`NestJS`](https://www.prisma.io/docs/guides/frameworks/nestjs): Set up Prisma ORM in a NestJS app with create-prisma, from scaffold to seeded API to a live deploy on Prisma Compute.
- [`Next.js`](https://www.prisma.io/docs/guides/frameworks/nextjs): Set up Prisma ORM in a Next.js app with create-prisma, from scaffold to rendered data, and deploy it to Prisma Compute.