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.).
Prisma generates a client library; FraiseQL serves a multi-transport API server. REST, GraphQL, and gRPC clients connect directly to FraiseQL — no client library needed on the server side.
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.querydef users(limit: int = 50) -> list[User]: return fraiseql.config(sql_source="v_user")
# 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, jsonb_build_object( 'id', u.id::text, 'name', u.name, 'email', u.email, 'posts', COALESCE( jsonb_agg(jsonb_build_object('id', p.id::text, 'title', p.title)) FILTER (WHERE p.pk_post IS NOT NULL), '[]'::jsonb ) ) AS dataFROM tb_user uLEFT JOIN tb_post p ON p.fk_user = u.pk_userGROUP BY u.pk_user, 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 enforces field-level access control at compile time via fraiseql.field():
from typing import Annotatedimport fraiseql
@fraiseql.inputclass CreateUserInput: email: str name: str age: int
# Protected field — requires admin scope role: Annotated[str, fraiseql.field( requires_scope="admin:write", on_deny="reject", description="User role — admin only", )]| Aspect | FraiseQL | Prisma |
|---|---|---|
| Field access control | Scope-based (compile-time) | None (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, jsonb_build_object( 'id', u.id::text, 'email', u.email, 'name', u.name, 'posts', COALESCE( jsonb_agg(jsonb_build_object('id', p.id::text, 'title', p.title)) FILTER (WHERE p.pk_post IS NOT NULL), '[]'::jsonb ) ) AS dataFROM tb_user uLEFT JOIN tb_post p ON p.fk_user = u.pk_userGROUP BY u.pk_user, 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, jsonb_build_object( 'id', u.id::text, 'name', u.name, 'posts', COALESCE( jsonb_agg(jsonb_build_object('id', p.id::text, 'title', p.title)) FILTER (WHERE p.pk_post IS NOT NULL), '[]'::jsonb ) ) AS dataFROM tb_user uLEFT JOIN tb_post p ON p.fk_user = u.pk_userGROUP BY u.pk_user, 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:
[observers]backend = "nats"nats_url = "nats://localhost:4222"
[[observers.rules]]entity = "Order"event = "UPDATE"condition = "status = 'pending'"
[[observers.rules.actions]]type = "webhook"url = "https://payments.internal/process"
[observers.rules.actions.body]order_id = "{id}"amount = "{total}"| 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.