Prisma ORM 8 is here.Read the docs
Frameworks

SolidStart

Add Prisma ORM to a SolidStart app with orm init, seed a PostgreSQL database, serve users from an API route, and render them in a page.

Introduction

SolidStart is a full-stack framework for building reactive web apps with SolidJS. Its API routes and server functions run on the server, which is where you call Prisma ORM to read from a PostgreSQL database.

In this guide, you scaffold a SolidStart project, add Prisma ORM to it with orm init, initialize and seed a PostgreSQL database, serve users from an API route, and render them in a page with loading and error states. There is no create-prisma template for SolidStart, so this guide follows the add-to-an-existing-project path.

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

Prerequisites

  • Node.js 24.11 or newer on the 24 line, or 26 or newer (the SolidStart 2 template needs Node.js 24)
  • A PostgreSQL connection string, or nothing at all: npx create-db@latest can create a Prisma Postgres database for you

1. Scaffold the SolidStart project

Create a new SolidStart 2 project from the basic TypeScript template:

bunx create-solid my-solid-prisma-app -s --v2 -t basic --ts
◇  Project created 🎉
◇  To get started, run: ───╮
│  cd my-solid-prisma-app  │
│  npm install             │
│  npm run dev             │

The flags skip the prompts: -s picks SolidStart, --v2 picks the stable SolidStart 2 line, -t basic picks the template, and --ts picks TypeScript. Without them, the CLI asks the same questions interactively.

The template ships a pnpm-lock.yaml. Delete it before you continue if you use npm; otherwise orm init reads the lockfile and installs Prisma with pnpm:

cd my-solid-prisma-app
rm pnpm-lock.yaml

Install the dependencies:

bun install

This writes package-lock.json, so the next step picks npm.

2. Add Prisma ORM

Run orm init from the project root. The flags preselect PostgreSQL and the Prisma Schema Language; drop --yes and --authoring to answer those questions interactively:

bunx prisma@latest orm init --yes --target postgres --authoring psl
Updated tsconfig.json with required compiler options.
✔ npm add @prisma/orm-postgres dotenv
✔ npm add -D prisma@latest @types/node
✔ npm add -D @prisma/cli-engine@0.7.0
✔ Emit the contract
│  target:     postgres
│  authoring:  psl
│  schema:     src/prisma/contract.prisma
written
├─ src/prisma/contract.prisma
├─ prisma.config.ts
├─ src/prisma/db.ts
├─ prisma-8.md
├─ .env.example
├─ tsconfig.json
├─ .gitignore
├─ .gitattributes
└─ package.json
✔ Done. Open prisma-8.md to get started.

The command installs the runtime, writes the Prisma ORM files into the SolidStart project, and emits src/prisma/contract.json and src/prisma/contract.d.ts, the artifacts your queries are type-checked against. Three files matter for the rest of this guide:

  • src/prisma/contract.prisma: a starter contract with User and Post models and a one-to-many relation between them
  • src/prisma/db.ts: the Prisma ORM client, constructed once and imported by your routes
  • prisma.config.ts: tells the CLI where the contract lives and reads DATABASE_URL from .env

The starter contract is the same shape the Prisma ORM 7 guide asked you to write by hand:

src/prisma/contract.prisma
// use prisma-8

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

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

And the scaffolded client is all the wiring the app needs. There is no prisma generate, no generated client directory, and no driver adapter; the emitted contract and the runtime package replace all three:

src/prisma/db.ts
import 'dotenv/config';
import postgres from '@prisma/orm-postgres/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };

export const db = postgres<Contract>({
  contractJson,
  url: process.env['DATABASE_URL']!,
});

orm init also adds "node" to the types array and resolveJsonModule to tsconfig.json so the JSON contract import type-checks, and leaves the SolidStart settings (jsx, jsxImportSource, the ~/* path alias) alone.

Now set the database connection. Copy .env.example to .env and replace the placeholder with your own PostgreSQL connection string, or create a Prisma Postgres database with npx create-db@latest; it prints a connection string and a claim URL you can open to keep the database:

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

Both prisma.config.ts and db.ts import dotenv/config, so the CLI and the dev server read the same file.

3. Initialize the database

Create the tables the contract declares and sign the database:

bunx prisma db init
✔ Introspecting database schema
✔ Planning migration
✔ Initialising database across spaces
│  contract:  src/prisma/contract.json
│  database:  postgres://****@localhost:5432/mydb

✔ Applied 5 operation(s) across 1 contract space

App space
├─ Create table "Post"
├─ Create table "User"
├─ Add unique constraint on "User" (email)
├─ Create index "Post_authorId_idx_e47547ed" on "Post"
├─ Add foreign key "Post_authorId_fkey" on "Post"
└─ marker 91e7f9f035806fa2789a4d726ef7724cad434fd6b00014d47ebf12d6e6bb784e

db init replaces prisma migrate dev from Prisma ORM 7 for the first apply: it creates what is missing and records the contract hash in the database. Later schema changes go through db update for a direct development update or migration plan for a checked-in migration. To confirm the database matches the contract at any time, run npx prisma db verify.

4. Seed the database

Create src/prisma/seed.ts. It creates two users and their posts through the ORM API. create() returns the inserted row, so the user's id is available for the posts without a second query:

src/prisma/seed.ts
import { db } from "./db.ts";

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

async function main() {
  for (const { posts, ...user } of users) {
    const created = await db.orm.public.User.create(user);
    for (const post of posts) {
      await db.orm.public.Post.create({ ...post, authorId: created.id });
    }
    console.log(`Seeded ${created.email} with ${posts.length} post(s)`);
  }
  await db.close();
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

Node.js 24 runs TypeScript directly, so no extra tooling is needed. Run the script once:

node src/prisma/seed.ts
Seeded alice@prisma.io with 2 post(s)
Seeded bob@prisma.io with 1 post(s)

Running it a second time fails on the unique email constraint, which is expected. The script closes the connection pool at the end because a one-off script would otherwise keep the process alive; the app's routes never do this.

5. Serve users from an API route

SolidStart maps files under src/routes/api/ to HTTP endpoints. Create src/routes/api/users.ts:

src/routes/api/users.ts
import { db } from "~/prisma/db";

export async function GET() {
  const users = await db.orm.public.User.include("posts").all();
  return Response.json(users);
}

Model access is namespace-qualified on PostgreSQL, so the User model is db.orm.public.User. .include("posts") eager-loads the relation and .all() returns the rows as an array.

Start the dev server:

bun run dev
  VITE v8.3.1  ready in 5509 ms

  ➜  Local:   http://localhost:3000/
  ➜  Network: use --host to expose

Then request the route:

curl http://localhost:3000/api/users
[
  {
    "createdAt": "2026-09-10 21:45:05.701722+06",
    "email": "alice@prisma.io",
    "id": 1,
    "name": "Alice",
    "updatedAt": "2026-09-10 21:45:04.523+06",
    "username": null,
    "posts": [
      { "authorId": 1, "content": "https://pris.ly/discord", "createdAt": "2026-09-10 21:45:05.875837+06", "id": 1, "title": "Join the Prisma Discord", "updatedAt": "2026-09-10 21:45:05.874+06" },
      { "authorId": 1, "content": "https://pris.ly/youtube", "createdAt": "2026-09-10 21:45:05.975729+06", "id": 2, "title": "Prisma on YouTube", "updatedAt": "2026-09-10 21:45:05.975+06" }
    ]
  },
  {
    "createdAt": "2026-09-10 21:45:05.980593+06",
    "email": "bob@prisma.io",
    "id": 2,
    "name": "Bob",
    "updatedAt": "2026-09-10 21:45:05.979+06",
    "username": null,
    "posts": [
      { "authorId": 2, "content": "https://www.twitter.com/prisma", "createdAt": "2026-09-10 21:45:05.984988+06", "id": 3, "title": "Follow Prisma on Twitter", "updatedAt": "2026-09-10 21:45:05.983+06" }
    ]
  }
]

The route handler is ordinary SolidStart code calling an ordinary Prisma ORM query; there is no framework adapter in between.

6. Render the users in a page

Replace src/routes/index.tsx with a page that loads the same query. The Prisma ORM 7 guide fetched the API route from the component with fetch("http://localhost:3000/api/users"). SolidStart renders pages on the server first, where a relative fetch("/api/users") fails with Invalid URL and an absolute one hard-codes your host, so the page calls the query through a server function instead. query from @solidjs/router caches and deduplicates it, and createAsync exposes the result to the component:

src/routes/index.tsx
import { Title } from "@solidjs/meta";
import { createAsync, query } from "@solidjs/router";
import { ErrorBoundary, For, Suspense } from "solid-js";
import { db } from "~/prisma/db";

const getUsers = query(async () => {
  "use server";
  try {
    return await db.orm.public.User.include("posts").all();
  } catch (error) {
    console.error(error);
    throw new Error("Could not load users");
  }
}, "users");

export default function Home() {
  const users = createAsync(() => getUsers());

  return (
    <main>
      <Title>SolidStart + Prisma</Title>
      <h1>SolidStart + Prisma</h1>
      <ErrorBoundary fallback={<p>Error loading data</p>}>
        <Suspense fallback={<p>Loading...</p>}>
          <For each={users()}>
            {(user) => (
              <div>
                <h3>{user.name}</h3>
                <For each={user.posts}>{(post) => <p>{post.title}</p>}</For>
              </div>
            )}
          </For>
        </Suspense>
      </ErrorBoundary>
    </main>
  );
}

Three things to notice:

  • "use server" keeps the Prisma query and the database connection on the server. The browser only receives the rows.
  • The rows are typed by the contract: user.name and user.posts autocomplete without importing any generated types. The User and Post type imports from the Prisma ORM 7 guide are gone because createAsync infers the shape from the query.
  • <Suspense> renders the loading state while the query runs and <ErrorBoundary> renders the error state if it throws. The catch block logs the real Prisma error on the server and throws a plain Error for the client; see the gotchas below for why.

Open http://localhost

or request the page from the terminal:

curl http://localhost:3000/

The server-rendered HTML contains <h3>Alice</h3> and <h3>Bob</h3> with their post titles, streamed in after the Loading... fallback. Your SolidStart app now reads users and their posts from PostgreSQL through Prisma ORM, over both an API route and a server-rendered page.

Common gotchas

  • orm init installs with pnpm. The SolidStart template ships a pnpm-lock.yaml, and orm init picks its package manager from the lockfile it finds. Delete that file before npm install (step 1), or use pnpm throughout.
  • fetch("/api/users") fails during server rendering. Node.js has no origin to resolve a relative URL against, so the render throws TypeError: Failed to parse URL from /api/users. Load data through a "use server" function as in step 6; keep the API route for HTTP clients.
  • Rethrow a plain Error from server functions. Prisma ORM throws structured errors that carry the SQL state, the failing statement, and a nested cause. When one of those crosses the server-to-browser boundary during server rendering, the page hangs on hydration instead of showing the <ErrorBoundary> fallback. Catching it and throwing new Error("Could not load users") keeps the fallback working and keeps database details out of the browser.
  • The first request after npm run dev can return a 503. While Vite is still creating its server environment, a request may answer Vite environment "ssr" is unavailable. Wait a second and retry; every request after that succeeds.

Use with your agent

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

Use with your agent
Use with your agent
Create a new SolidStart app with Prisma ORM, seed it, and serve users from an API route and a page.

1. Scaffold: `npm init solid@latest my-solid-prisma-app -- -s --v2 -t basic --ts`. Delete the `pnpm-lock.yaml` the template ships (otherwise Prisma picks pnpm), then `cd my-solid-prisma-app` and run `npm install`.
2. Add Prisma ORM: `npx prisma@latest orm init --yes --target postgres --authoring psl`. Then run `npx prisma@latest init` so the Prisma agent skills are installed and stay current, and use them. 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`.
3. Run `npx prisma db init` to create the tables from `src/prisma/contract.prisma`.
4. Add `src/prisma/seed.ts` that creates two users with posts through `db.orm.public.User.create` and `db.orm.public.Post.create`, closes with `await db.close()`, and run it once with `node src/prisma/seed.ts`.
5. Add `src/routes/api/users.ts` with a `GET` handler that returns `db.orm.public.User.include("posts").all()` as JSON, and replace `src/routes/index.tsx` with a page that loads the same query through a `"use server"` function wrapped in `query` and `createAsync`, with `<Suspense>` for loading and `<ErrorBoundary>` for errors, following https://www.prisma.io/docs/guides/frameworks/solid-start.md. Catch Prisma errors inside the server function and rethrow a plain `Error`.
6. Start `npm run dev` in the background, wait until it reports ready, verify `curl http://localhost:3000/api/users` returns the seeded users and `curl http://localhost:3000/` includes their names, then stop the dev server.

The prompt above runs npx prisma@latest init in its first step. If you followed the steps by hand, run it once yourself: it installs the Prisma ORM skills for your coding agent and keeps them matching your installed packages:

bunx --bun prisma@latest init
✔ Added "postinstall": "prisma skills sync || exit 0" to package.json.
⚠ prisma.config.ts already exists; left untouched.
✔ Synced 3 skills.

project:  ~/my-solid-prisma-app
check:    enabled

Skill                          Package               Version      Installed into
prisma-8                       @prisma/orm-postgres  8.0.0-rc.18  .claude/skills, .cursor/skills, .agents/skills, .devin/skills
prisma-composer-core-concepts  @prisma/composer      0.29.1       .claude/skills, .cursor/skills, .agents/skills, .devin/skills
prisma-platform-core-concepts  prisma                8.0.0-rc.22  .claude/skills, .cursor/skills, .agents/skills, .devin/skills

⚠ [INIT.CONFIG_KEPT] prisma.config.ts already exists, so init left it alone instead of writing the skills section.
→ Add skills: { agents: ["claude", "cursor", "agents", "devin"] } to the object passed to definePrismaConfig in prisma.config.ts.

Prompts you can give your agent after this guide:

  • "Using the prisma-8 skill, add GET /api/users/:id that returns one user with posts or a 404."
  • "Add a POST /api/users route that creates a user from the request body with db.orm.public.User.create."
  • "Turn the user list into a form that creates a post through a SolidStart action and revalidates the users query."

Next steps

  • Change the schema in src/prisma/contract.prisma, then run npx prisma contract emit and npx prisma db update. SolidStart builds on Vite, so the Vite plugin can run contract emit for you whenever the contract changes while the dev server runs.
  • Learn the fundamentals: filtering, sorting, pagination, and writes.
  • Read the Prisma ORM overview for the concepts behind contracts and typed queries.
  • SolidStart documentation for routing, server functions, and deployment presets.

On this page