# 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 ## 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.6.2 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 contract emit ``` #### pnpm ```bash pnpm prisma contract emit ``` #### yarn ```bash yarn prisma contract emit ``` #### npm ```bash npx prisma 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 db init ``` #### pnpm ```bash pnpm prisma db init ``` #### yarn ```bash yarn prisma db init ``` #### npm ```bash npx prisma 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 contract emit` followed by `npx prisma 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 (

Superblog

    {users.map((user) => (
  1. {user.name} ({user.email})
  2. ))}
View posts
); } ``` 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 (

Posts

Create a post
); } ``` 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 (

{post.title}

by {post.author.name}

{post.content || "No content available."}
); } ``` `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 (

Create New Post

{actionData?.error &&

{actionData.error}

}