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@latestcan create a Prisma Postgres database for you
To delegate this guide to your coding agent, copy the prompt below and hand it over:
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 --tspnpm create solid my-solid-prisma-app -s --v2 -t basic --tsyarn create solid my-solid-prisma-app -s --v2 -t basic --tsnpm init solid@latest 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.yamlInstall 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.yamlInstall the dependencies:
bun installpnpm installyarn installnpm installThis 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 pslpnpm dlx prisma@latest orm init --yes --target postgres --authoring pslyarn dlx prisma@latest orm init --yes --target postgres --authoring pslnpx prisma@latest orm init --yes --target postgres --authoring pslUpdated 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 withUserandPostmodels and a one-to-many relation between themsrc/prisma/db.ts: the Prisma ORM client, constructed once and imported by your routesprisma.config.ts: tells the CLI where the contract lives and readsDATABASE_URLfrom.env
The starter contract is the same shape the Prisma ORM 7 guide asked you to write by hand:
// 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:
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:
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 withUserandPostmodels and a one-to-many relation between themsrc/prisma/db.ts: the Prisma ORM client, constructed once and imported by your routesprisma.config.ts: tells the CLI where the contract lives and readsDATABASE_URLfrom.env
The starter contract is the same shape the Prisma ORM 7 guide asked you to write by hand:
// 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:
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:
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 initpnpm prisma db inityarn prisma db initnpx 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 91e7f9f035806fa2789a4d726ef7724cad434fd6b00014d47ebf12d6e6bb784edb 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 91e7f9f035806fa2789a4d726ef7724cad434fd6b00014d47ebf12d6e6bb784edb 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:
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.tsSeeded 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:
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 devpnpm run devyarn devnpm run dev VITE v8.3.1 ready in 5509 ms
➜ Local: http://localhost:3000/
➜ Network: use --host to exposeThen 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.
VITE v8.3.1 ready in 5509 ms
➜ Local: http://localhost:3000/
➜ Network: use --host to exposeThen 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:
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.nameanduser.postsautocomplete without importing any generated types. TheUserandPosttype imports from the Prisma ORM 7 guide are gone becausecreateAsyncinfers the shape from the query. <Suspense>renders the loading state while the query runs and<ErrorBoundary>renders the error state if it throws. Thecatchblock logs the real Prisma error on the server and throws a plainErrorfor 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 initinstalls with pnpm. The SolidStart template ships apnpm-lock.yaml, andorm initpicks its package manager from the lockfile it finds. Delete that file beforenpm 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 throwsTypeError: 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
Errorfrom 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 throwingnew Error("Could not load users")keeps the fallback working and keeps database details out of the browser. - The first request after
npm run devcan return a 503. While Vite is still creating its server environment, a request may answerVite 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 initpnpm dlx prisma@latest inityarn dlx prisma@latest initnpx 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.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/:idthat returns one user with posts or a 404." - "Add a
POST /api/usersroute that creates a user from the request body withdb.orm.public.User.create." - "Turn the user list into a form that creates a post through a SolidStart
actionand revalidates theusersquery."
✔ 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/:idthat returns one user with posts or a 404." - "Add a
POST /api/usersroute that creates a user from the request body withdb.orm.public.User.create." - "Turn the user list into a form that creates a post through a SolidStart
actionand revalidates theusersquery."
- Change the schema in
src/prisma/contract.prisma, then runnpx prisma contract emitandnpx prisma db update. SolidStart builds on Vite, so the Vite plugin can runcontract emitfor 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.