Skip to main content
Prisma Documentation Docs

Search documentation

Type to search this documentation.

On this pageOverview

SolidStart

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.

  • Node.js 24 or later (the SolidStart 2 template requires it)
  • A PostgreSQL connection string, or nothing at all: npx create-db@latest can create a Prisma Postgres database for you

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

with
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.

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

bunx create-solid my-solid-prisma-app -s --v2 -t basic --ts
Bash
pnpm create solid my-solid-prisma-app -s --v2 -t basic --ts
Bash
yarn create solid my-solid-prisma-app -s --v2 -t basic --ts
Bash
npm init solid@latest my-solid-prisma-app -- -s --v2 -t basic --ts
no-copy
◇  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:

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

Install the dependencies:

◇  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:

title="bun"
bun install
pnpm
pnpm install
yarn
yarn install
npm
npm install

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

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
Bash
pnpm dlx prisma@latest orm init --yes --target postgres --authoring psl
Bash
yarn dlx prisma@latest orm init --yes --target postgres --authoring psl
Bash
npx prisma@latest orm init --yes --target postgres --authoring psl
no-copy
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.6.1
✔ 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.

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.6.1

✔ 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:

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

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

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

Create the tables the contract declares and sign the database:

bunx prisma db init
Bash
pnpm prisma db init
Bash
yarn prisma db init
Bash
npx prisma db init
no-copy
✔ 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.

✔ 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.

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:

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

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

title="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
Bash
pnpm run dev
Bash
yarn dev
Bash
npm run dev
no-copy
  VITE v8.3.1  ready in 5509 ms

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

Then request the route:

Bash
curl http://localhost:3000/api/users
no-copy
[
  {
    "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.

  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.

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:

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

  • 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.

Run npx prisma@latest init once to install the Prisma ORM skills for your coding agent and keep them matching your installed packages:

bunx --bun prisma@latest init
Bash
pnpm dlx prisma@latest init
Bash
yarn dlx prisma@latest init
Bash
npx prisma@latest init
no-copy
✔ 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.13  .claude/skills, .cursor/skills, .agents/skills, .devin/skills
prisma-composer-core-concepts  @prisma/composer      0.23.0       .claude/skills, .cursor/skills, .agents/skills, .devin/skills
prisma-platform-core-concepts  prisma                8.0.0-rc.17  .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 that map to 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."
✔ 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.13  .claude/skills, .cursor/skills, .agents/skills, .devin/skills

prisma-composer-core-concepts  @prisma/composer      0.23.0       .claude/skills, .cursor/skills, .agents/skills, .devin/skills

prisma-platform-core-concepts  prisma                8.0.0-rc.17  .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 that map to 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."
  • 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.
Suggest an edit

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

Export
Documentation menu