Prisma Migration Guide
Step-by-step guide to migrate from Prisma to FraiseQL.
Prisma is a popular ORM. FraiseQL is a GraphQL framework backed by a Rust binary. Here’s why you might choose one over the other.
Prisma is an ORM that generates a type-safe database client. You still need to build your API layer (Express, Fastify, Apollo, etc.).
FraiseQL generates the entire GraphQL API, including the database access layer — backed by hand-written PostgreSQL views (v_*) for reads and PostgreSQL functions (fn_*) for writes.
With Prisma: Your Code -> Prisma Client -> Database (You build the API layer)
With FraiseQL: Client -> GraphQL -> FraiseQL Rust Binary -> PostgreSQL Views/Functions (API layer is built-in)| Aspect | FraiseQL | Prisma |
|---|---|---|
| What it is | GraphQL framework (Rust binary) | ORM / Database client |
| Output | Complete GraphQL API | Database client library |
| API layer | Included | Build yourself |
| Schema source | Python, TypeScript, Go decorators | Prisma Schema Language |
| Reads | Pre-compiled PostgreSQL views (v_*) | Generated SQL via ORM |
| Writes | PostgreSQL functions (fn_*) | Generated SQL via ORM |
| N+1 handling | Eliminated by design (SQL JOINs in views) | include option (manual) |
| Query language | GraphQL | Prisma Client API |
| Configuration | TOML | .prisma files |
| Database support | PostgreSQL-first | PostgreSQL, MySQL, SQLite, SQL Server, MongoDB |
| Runtime | Rust binary | Node.js |
// 1. Define Prisma schema// schema.prismamodel User { id String @id @default(uuid()) name String email String @unique posts Post[]}
model Post { id String @id @default(uuid()) title String author User @relation(fields: [authorId], references: [id]) authorId String}
// 2. Generate client// $ prisma generate
// 3. Build your API layer (Express, Fastify, Apollo, etc.)app.get('/users', async (req, res) => { const users = await prisma.user.findMany({ include: { posts: true } }); res.json(users);});
app.get('/users/:id', async (req, res) => { const user = await prisma.user.findUnique({ where: { id: req.params.id }, include: { posts: true } }); res.json(user);});
app.post('/users', async (req, res) => { const user = await prisma.user.create({ data: req.body }); res.json(user);});
// ... 50+ more endpoints# 1. Define schema with decorators@fraiseql.typeclass User: id: str name: str email: str posts: list['Post']
@fraiseql.typeclass Post: id: str title: str author: User
# 2. Declare queries (backed by PostgreSQL views)@fraiseql.query(sql_source="v_user")def users(limit: int = 50) -> list[User]: pass
# 3. Build and serve# $ fraiseql run
# Done. Full GraphQL API is ready.-- The backing SQL view (you write this)CREATE VIEW v_user ASSELECT u.id, u.name, u.email, COALESCE(json_agg(p.*) FILTER (WHERE p.id IS NOT NULL), '[]') AS postsFROM tb_user uLEFT JOIN tb_post p ON p.author_id = u.idGROUP BY u.id;include// Without include: N+1 problemconst users = await prisma.user.findMany();for (const user of users) { user.posts = await prisma.post.findMany({ where: { authorId: user.id } }); // N queries!}
// With include: You must remember to add itconst users = await prisma.user.findMany({ include: { posts: true }});If you forget include, you get N+1. If you include too much, you fetch unnecessary data.
# This query:query { users { name posts { title } }}Because the v_user view already JOINs tb_post, this resolves in a single SQL execution. You cannot accidentally create N+1 queries — the SQL is pre-compiled into the view.
model User { id String @id @default(uuid()) email String @unique name String? posts Post[] createdAt DateTime @default(now())
@@index([email]) @@map("users")}
model Post { id String @id @default(uuid()) title String content String? published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId String
@@map("posts")}@fraiseql.typeclass User: id: str email: str name: str | None posts: list['Post'] created_at: datetime
@fraiseql.typeclass Post: id: str title: str content: str | None published: bool = False author: UserPrisma doesn’t include validation — you must use external libraries:
// Prisma + Zod for validationimport { z } from 'zod';
const createUserSchema = z.object({ email: z.string().email(), name: z.string().min(1), age: z.number().min(0).max(150)});
app.post('/users', async (req, res) => { const validated = createUserSchema.parse(req.body); // Runtime validation const user = await prisma.user.create({ data: validated }); res.json(user);});FraiseQL includes 13 validators enforced at compile-time:
[fraiseql.validation]email = { pattern = "^[^@]+@[^@]+\\.[^@]+$" }age = { range = { min = 0, max = 150 } }name = { length = { min = 1 } }| Aspect | FraiseQL | Prisma |
|---|---|---|
| Built-in validators | 13 rules | 0 (requires external libs) |
| External dependencies | None | Zod, Joi, Yup, etc. |
| Compile-time enforcement | Yes | No |
| Runtime validation overhead | None | Per-request |
| Learning curve | Low | Medium (varies by library) |
| Database protection | Guaranteed | Depends on implementation |
Prisma is a better choice when:
FraiseQL is a better choice when:
Create a Prisma project:
npm init -ynpm install @prisma/client prismanpx prisma initEdit schema.prisma:
generator client { provider = "prisma-client-js"}
datasource db { provider = "postgresql" url = env("DATABASE_URL")}
model User { id String @id @default(uuid()) email String @unique name String posts Post[]}
model Post { id String @id @default(uuid()) title String author User @relation(fields: [authorId], references: [id]) authorId String}npx prisma migrate dev --name initnpx prisma generateBuild the API layer manually:
// server.js - You must build this yourselfconst { PrismaClient } = require('@prisma/client');const express = require('express');
const prisma = new PrismaClient();const app = express();
app.get('/users', async (req, res) => { const users = await prisma.user.findMany({ include: { posts: true } // Remember this or get N+1! }); res.json(users);});
app.get('/users/:id', async (req, res) => { const user = await prisma.user.findUnique({ where: { id: req.params.id }, include: { posts: true } }); res.json(user);});
app.listen(3000);Test: curl http://localhost:3000/users
Now try the same with FraiseQL:
import fraiseql
@fraiseql.typeclass User: id: str email: str name: str posts: list['Post']
@fraiseql.typeclass Post: id: str title: str author: User-- Create the SQL view (write once)CREATE VIEW v_user ASSELECT u.id, u.email, u.name, COALESCE(json_agg(p.*) FILTER (WHERE p.id IS NOT NULL), '[]') AS postsFROM tb_user uLEFT JOIN tb_post p ON p.user_id = u.idGROUP BY u.id;fraiseql run
# GraphQL API is ready - no REST endpoints to writecurl -X POST http://localhost:8080/graphql \ -H "Content-Type: application/json" \ -d '{"query": "{ users { name posts { title } } }"}'Compare N+1 handling:
With Prisma, you must remember include:
// Without include: N+1 queries!const users = await prisma.user.findMany();for (const user of users) { user.posts = await prisma.post.findMany({ where: { authorId: user.id } });}With FraiseQL, N+1 is impossible — the view already JOINs the data.
Compare deployment complexity:
Prisma deployment:
FraiseQL deployment:
You can use Prisma’s migration tooling alongside FraiseQL’s API server:
# Use Prisma Migrate for database schema managementprisma migrate dev
# Use FraiseQL to serve the GraphQL APIfraiseql runOr use FraiseQL for your customer-facing GraphQL API and Prisma for admin/internal tools.
Convert Schema
model User { id String @id @default(uuid()) name String posts Post[]}@fraiseql.typeclass User: id: str name: str posts: list['Post']Write SQL Views to Replace the API Layer
Your Express/Fastify routes are replaced by the GraphQL API. Each query maps to a PostgreSQL view that you write and control.
CREATE VIEW v_user ASSELECT u.id, u.name, COALESCE(json_agg(p.*) FILTER (WHERE p.id IS NOT NULL), '[]') AS postsFROM tb_user uLEFT JOIN tb_post p ON p.user_id = u.idGROUP BY u.id;Move Business Logic to PostgreSQL Functions
Complex route handlers become PostgreSQL functions (fn_*), or use FraiseQL’s observer pattern to react to data changes:
@observer( entity="Order", event="UPDATE", condition="status = 'pending'", actions=[ webhook("https://payments.internal/process", body={"order_id": "{id}", "amount": "{total}"}), ],)def on_order_pending(): """Process payment when order becomes pending.""" pass| Choose | When |
|---|---|
| Prisma | You want an ORM, building REST, need MongoDB, want full control over application code |
| FraiseQL | You want GraphQL, less application code, guaranteed N+1 prevention, Rust-level performance |
Prisma is a database client. FraiseQL is a complete GraphQL API server.
Prisma Migration Guide
Step-by-step guide to migrate from Prisma to FraiseQL.
Getting Started
Learn FraiseQL fundamentals from scratch.
Schema Concepts
Understand SQL views, types, and decorators in depth.
All Comparisons
See how FraiseQL compares to other tools.