Skip to content

Migrating from Prisma to FraiseQL

This guide walks through migrating a Prisma-based application to FraiseQL, including schema conversion, client migration, and performance optimization.

FeaturePrismaFraiseQL
API TypeORM (type-safe queries)GraphQL (API-first)
DatabasesAll major databasesPostgreSQL-first
Development SDKsPrisma Client (1)Python, TypeScript, Go SDKs
Real-timePollingNative subscriptions via WebSocket
Schema ManagementMigrations + Prisma SchemaSQL views/functions + FraiseQL decorators
FederationNoYes (multi-database queries)
Query LanguageProgrammatic (.findMany())GraphQL (industry standard)
PerformanceGood (cached queries)Excellent (pre-compiled SQL views, batching)
CachingManual or pluginsBuilt-in federation-aware caching
Multi-databaseVia relationships onlyNative federation support

FraiseQL asks you to write views. In exchange:

  • Your database schema is decoupled from your API. Prisma exposes models directly from your database tables — a model rename means changing client code. FraiseQL exposes views — a table rename is invisible to clients.
  • N+1 is structurally eliminated. Prisma’s include still produces one query per relation. FraiseQL builds JSONB nesting into the view, so every request is a single SQL query regardless of depth.
  • You control exactly what the API exposes. No leaking of internal columns, no default findMany/findUnique variants you didn’t ask for.
  • Complex filtering is SQL, not a chain of .where() calls.

The migration involves: creating v_* views for your Prisma models, writing fn_* functions for your mutations, and replacing Prisma Client calls with a standard GraphQL client.

Prisma users think in terms of models and relations. FraiseQL maps those same concepts to typed classes backed by SQL views. Here is the same User and Post model expressed in both:

model User {
id String @id @default(cuid())
name String
email String @unique
posts Post[]
}
model Post {
id String @id @default(cuid())
title String
content String
author User @relation(fields: [authorId], references: [id])
authorId String
}

The relationship (posts, author) is backed by a SQL JOIN inside your v_user and v_post views. FraiseQL never generates SQL at runtime — you write the view once, and all queries against it are pre-compiled into the Rust binary.

const users = await prisma.user.findMany({
where: { email: { endsWith: "@example.com" } },
include: { posts: true },
orderBy: { createdAt: "desc" },
take: 10
});
const user = await prisma.user.create({
data: {
email: "alice@example.com",
name: "Alice",
posts: {
create: [
{ title: "First Post", content: "..." }
]
}
},
include: { posts: true }
});
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String
userId Int
user User @relation(fields: [userId], references: [id])
}
  1. Audit Prisma Schema (Day 1)

    Review your current Prisma schema and plan the FraiseQL structure.

    Terminal window
    # Review your current Prisma schema
    cat prisma/schema.prisma
    # Export schema information
    npx prisma introspect # Generate schema from database

    Create a mapping document:

    Prisma Model FraiseQL Type
    User -> User (type)
    Post -> Post (type)
    Comment -> Comment (type)
    Prisma Query FraiseQL Query
    findMany -> list[Type] query
    findUnique -> single Type query
    findFirst -> first(n) with limit
  2. Set Up FraiseQL Project

    Initialize the FraiseQL binary and configure your database connection.

    Terminal window
    # Initialize FraiseQL
    fraiseql init fraiseql-api
    # Create fraiseql.toml
    cat > fraiseql.toml << 'EOF'
    [database]
    url = "${DATABASE_URL}"
    [server]
    port = 8080
    EOF
    # JWT authentication is configured via env var, not fraiseql.toml
    echo "JWT_SECRET=your-256-bit-secret" >> .env
  3. Convert Prisma Schema to FraiseQL Types (Days 2-3)

    Translate each Prisma model to a FraiseQL type using Python decorators. The FraiseQL Rust binary reads your SQL views (v_*) for queries — no Python resolvers needed.

    model User {
    id Int @id @default(autoincrement())
    email String @unique
    name String
    posts Post[]
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    }
    model Post {
    id Int @id @default(autoincrement())
    title String
    content String
    published Boolean @default(false)
    userId Int
    user User @relation(fields: [userId], references: [id], onDelete: Cascade)
    createdAt DateTime @default(now())
    }
    model Comment {
    id Int @id @default(autoincrement())
    text String
    postId Int
    userId Int
    post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
    user User @relation(fields: [userId], references: [id])
    }
  4. Define Queries backed by SQL Views

    FraiseQL queries map directly to PostgreSQL views (v_*). Write the SQL view, then declare the query with @fraiseql.query.

    const users = await prisma.user.findMany({
    where: { posts: { some: {} } }, // Has posts
    include: { posts: true },
    orderBy: { createdAt: 'desc' },
    take: 10
    });
  5. Define Mutations backed by PostgreSQL Functions

    FraiseQL mutations map to PostgreSQL functions (fn_*). The Rust binary calls these functions — no ORM layer involved.

    const user = await prisma.user.create({
    data: {
    email: "alice@example.com",
    name: "Alice",
    }
    });
  6. Update Client Code (Days 4-5)

    pages/users.tsx
    import { prisma } from "@/lib/prisma";
    export async function getServerSideProps() {
    const users = await prisma.user.findMany({
    include: { posts: true },
    take: 10
    });
    return { props: { users } };
    }
    export default function UsersPage({ users }) {
    return (
    <div>
    {users.map(user => (
    <div key={user.id}>
    <h2>{user.name}</h2>
    <p>{user.posts.length} posts</p>
    </div>
    ))}
    </div>
    );
    }
  7. Deploy FraiseQL Backend (Day 7)

    Terminal window
    # Build FraiseQL binary
    fraiseql compile
    # Deploy (example: Docker)
    docker build -t fraiseql-api .
    docker push your-registry/fraiseql-api
    # Deploy to Kubernetes/Cloud
    kubectl apply -f fraiseql-deployment.yaml
    Terminal window
    # .env.local — no database URL needed on the frontend
    FRAISEQL_URL=https://api.example.com/graphql
    FRAISEQL_API_KEY=sk_...
Frontend -> Prisma Client -> Database
(Direct database access)
Frontend -> HTTP/GraphQL -> FraiseQL Rust Binary -> PostgreSQL Views/Functions
(Backend API layer, no ORM)

Benefits of FraiseQL approach:

  • Decoupled frontend from database
  • Can scale backend independently
  • Security: database credentials never exposed to frontend
  • Business logic in SQL functions, not application code
  • Built-in authentication/authorization
const users = await prisma.user.findMany({
skip: 10,
take: 10
});
const posts = await prisma.post.findMany({
where: {
AND: [
{ published: true },
{ author: { email: { contains: "@example.com" } } }
]
}
});
const result = await prisma.post.aggregate({
where: { published: true },
_count: true,
_avg: { rating: true }
});
// N+1 problem
const users = await prisma.user.findMany();
for (const user of users) {
const posts = await prisma.post.findMany({ where: { userId: user.id } });
// N queries!
}

After (FraiseQL — pre-compiled SQL views)

Section titled “After (FraiseQL — pre-compiled SQL views)”
query {
users {
name
posts { # Backed by a SQL JOIN in the view, not a separate query
title
}
}
}
# 2 queries total: users + posts (batched by user_id)

Use a feature flag middleware to route traffic between Prisma and FraiseQL during a gradual rollout:

@fraiseql.middleware
async def feature_gate_fraiseql(request, next):
"""Route to FraiseQL based on feature flag."""
if should_use_fraiseql(request.user_id):
return await fraiseql_handler(request)
else:
return await prisma_handler(request)

Using Both: Prisma for Admin, FraiseQL for Public API

Section titled “Using Both: Prisma for Admin, FraiseQL for Public API”

You do not have to choose one or the other. A common pattern during migration — and even long-term — is to use Prisma for your admin panel or internal tooling while FraiseQL serves the public-facing GraphQL API.

Why this works well:

  • Prisma excels at ad-hoc admin queries, seed scripts, and backoffice tooling where developer ergonomics matter more than query throughput.
  • FraiseQL excels at the high-traffic public API where predictable performance and zero N+1 are non-negotiable.
  • Both connect to the same PostgreSQL database — there is no data duplication.
┌─────────────────────────────────┐
│ PostgreSQL Database │
└──────────────┬──────────────────┘
┌───────────────────┴───────────────────┐
│ │
┌───────────▼──────────┐ ┌──────────────▼────────┐
│ Prisma Client │ │ FraiseQL Rust Binary │
│ (Admin / Internal) │ │ (Public GraphQL API) │
└───────────────────────┘ └────────────────────────┘

Setup:

Terminal window
# prisma/schema.prisma — unchanged, used by admin tooling
# fraiseql.toml — points to the same DATABASE_URL
// admin/seed.ts — Prisma for admin operations
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
await prisma.user.createMany({ data: seedData });
# Public API served by FraiseQL
query {
users(limit: 20, orderBy: "created_at DESC") {
id
name
posts { title }
}
}
  • Audit Prisma schema and relationships
  • Design FraiseQL types and queries
  • Create FraiseQL decorators for all Prisma models
  • Write SQL views (v_*) for all queries
  • Write PostgreSQL functions (fn_*) for all mutations
  • Update client code to use GraphQL queries
  • Write API tests with equivalent coverage
  • Load test FraiseQL vs Prisma
  • Set up monitoring and alerting
  • Train team on GraphQL queries
  • Plan phased rollout with feature flags
  • Monitor for 2 weeks and iterate
  1. No ORM - FraiseQL is a Rust binary serving a GraphQL API, not an ORM
  2. API-first - Clients call HTTP API, not database directly
  3. SQL views for reads - All queries map to PostgreSQL v_* views
  4. SQL functions for writes - All mutations map to PostgreSQL fn_* functions
  5. Subscriptions - Real-time via WebSocket, not polling
  6. Federation - Native multi-database support

Apollo Server Migration

Migrate from another popular GraphQL option.

Read guide

Hasura Migration

Coming from Hasura’s database-first GraphQL.

Read guide

Getting Started

Learn FraiseQL fundamentals from scratch.

Read guide

Schema Concepts

Understand SQL views, types, and decorators.

Read guide