Apollo Server Migration
Migrate from another popular GraphQL option.
This guide walks through migrating a Prisma-based application to FraiseQL, including schema conversion, client migration, and performance optimization.
| Feature | Prisma | FraiseQL |
|---|---|---|
| API Type | ORM (type-safe queries) | GraphQL (API-first) |
| Databases | All major databases | PostgreSQL-first |
| Development SDKs | Prisma Client (1) | Python, TypeScript, Go SDKs |
| Real-time | Polling | Native subscriptions via WebSocket |
| Schema Management | Migrations + Prisma Schema | SQL views/functions + FraiseQL decorators |
| Federation | No | Yes (multi-database queries) |
| Query Language | Programmatic (.findMany()) | GraphQL (industry standard) |
| Performance | Good (cached queries) | Excellent (pre-compiled SQL views, batching) |
| Caching | Manual or plugins | Built-in federation-aware caching |
| Multi-database | Via relationships only | Native federation support |
FraiseQL asks you to write views. In exchange:
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.findMany/findUnique variants you didn’t ask for..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}import { type, field, ID } from 'fraiseql';
@type()class User { @field() id!: string; @field() name!: string; @field() email!: string; @field() posts!: Post[];}
@type()class Post { @field() id!: string; @field() title!: string; @field() content!: string; @field() author!: User;}@fraiseql.typeclass User: id: ID name: str email: str posts: list['Post']
@fraiseql.typeclass Post: id: ID title: str content: str author: Userimport "github.com/fraiseql/fraiseql-go"
type User struct { ID fraiseql.ID `fraiseql:"id"` Name string `fraiseql:"name"` Email string `fraiseql:"email"` Posts []Post `fraiseql:"posts"`}
type Post struct { ID fraiseql.ID `fraiseql:"id"` Title string `fraiseql:"title"` Content string `fraiseql:"content"` Author User `fraiseql:"author"`}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});query GetUsers { users( email_endsWith: "@example.com" limit: 10 orderBy: "createdAt DESC" ) { id email posts { id title } }}const user = await prisma.user.create({ data: { email: "alice@example.com", name: "Alice", posts: { create: [ { title: "First Post", content: "..." } ] } }, include: { posts: true }});mutation CreateUser { createUser(input: { email: "alice@example.com" name: "Alice" posts: [ { title: "First Post", content: "..." } ] }) { id email posts { id title } }}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])}@fraiseql.typeclass User: id: ID email: str posts: list['Post']
@fraiseql.typeclass Post: id: ID title: str content: str user_id: ID user: UserAudit Prisma Schema (Day 1)
Review your current Prisma schema and plan the FraiseQL structure.
# Review your current Prisma schemacat prisma/schema.prisma
# Export schema informationnpx prisma introspect # Generate schema from databaseCreate a mapping document:
Prisma Model FraiseQL TypeUser -> User (type)Post -> Post (type)Comment -> Comment (type)
Prisma Query FraiseQL QueryfindMany -> list[Type] queryfindUnique -> single Type queryfindFirst -> first(n) with limitSet Up FraiseQL Project
Initialize the FraiseQL binary and configure your database connection.
# Initialize FraiseQLfraiseql init fraiseql-api
# Create fraiseql.tomlcat > fraiseql.toml << 'EOF'[database]url = "${DATABASE_URL}"
[server]port = 8080EOF
# JWT authentication is configured via env var, not fraiseql.tomlecho "JWT_SECRET=your-256-bit-secret" >> .envConvert 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])}from fraiseql import FraiseQL, IDfrom datetime import datetime
fraiseql = FraiseQL()
@fraiseql.typeclass User: id: ID email: str # Unique enforced by database name: str posts: list['Post'] created_at: datetime updated_at: datetime
@fraiseql.typeclass Post: id: ID title: str content: str published: bool user_id: ID user: User comments: list['Comment'] created_at: datetime
@fraiseql.typeclass Comment: id: ID text: str post_id: ID user_id: ID post: Post user: Userimport { type, ID } from 'fraiseql';
@type()class User { id!: ID; email!: string; name!: string; posts!: Post[]; createdAt!: Date; updatedAt!: Date;}
@type()class Post { id!: ID; title!: string; content!: string; published!: boolean; userId!: ID; user!: User; comments!: Comment[]; createdAt!: Date;}
@type()class Comment { id!: ID; text!: string; postId!: ID; userId!: ID; post!: Post; user!: User;}import "github.com/fraiseql/fraiseql-go"
type User struct { ID fraiseql.ID `fraiseql:"id"` Email string `fraiseql:"email"` Name string `fraiseql:"name"` Posts []Post `fraiseql:"posts"` CreatedAt time.Time `fraiseql:"created_at"` UpdatedAt time.Time `fraiseql:"updated_at"`}
type Post struct { ID fraiseql.ID `fraiseql:"id"` Title string `fraiseql:"title"` Content string `fraiseql:"content"` Published bool `fraiseql:"published"` UserID fraiseql.ID `fraiseql:"user_id"` User User `fraiseql:"user"` Comments []Comment `fraiseql:"comments"` CreatedAt time.Time `fraiseql:"created_at"`}
type Comment struct { ID fraiseql.ID `fraiseql:"id"` Text string `fraiseql:"text"` PostID fraiseql.ID `fraiseql:"post_id"` UserID fraiseql.ID `fraiseql:"user_id"` Post Post `fraiseql:"post"` User User `fraiseql:"user"`}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});@fraiseql.query(sql_source="v_users_with_posts")def users( limit: int = 10, offset: int = 0, has_posts: bool = False) -> list[User]: """Get users, optionally with posts.""" pass-- The backing SQL viewCREATE VIEW v_users_with_posts ASSELECT u.* FROM tb_user uWHERE (SELECT COUNT(*) FROM tb_post p WHERE p.user_id = u.id) > 0;import { query } from 'fraiseql';
@query({ sqlSource: 'v_users_with_posts' })function users(limit: number = 10, offset: number = 0, hasPosts: boolean = false): Promise<User[]> { return Promise.resolve([]);}-- The backing SQL viewCREATE VIEW v_users_with_posts ASSELECT u.* FROM tb_user uWHERE (SELECT COUNT(*) FROM tb_post p WHERE p.user_id = u.id) > 0;import "github.com/fraiseql/fraiseql-go"
func init() { fraiseql.Query("users", fraiseql.QueryConfig{ SQLSource: "v_users_with_posts", }, func(args fraiseql.Args) ([]User, error) { return nil, nil })}-- The backing SQL viewCREATE VIEW v_users_with_posts ASSELECT u.* FROM tb_user uWHERE (SELECT COUNT(*) FROM tb_post p WHERE p.user_id = u.id) > 0;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", }});@fraiseql.inputclass CreateUserInput: email: str name: str
@fraiseql.mutation(sql_source="fn_create_user")def create_user(input: CreateUserInput) -> User: """Create user via PostgreSQL function.""" pass-- The backing PostgreSQL functionCREATE FUNCTION fn_create_user(p_email TEXT, p_name TEXT)RETURNS SETOF v_user AS $$BEGIN INSERT INTO tb_user (email, name) VALUES (p_email, p_name); RETURN QUERY SELECT * FROM v_user WHERE email = p_email;END;$$ LANGUAGE plpgsql;import { input, mutation } from 'fraiseql';
@input()class CreateUserInput { email!: string; name!: string;}
@mutation({ sqlSource: 'fn_create_user' })function createUser(input: CreateUserInput): Promise<User> { return Promise.resolve({} as User);}-- The backing PostgreSQL functionCREATE FUNCTION fn_create_user(p_email TEXT, p_name TEXT)RETURNS SETOF v_user AS $$BEGIN INSERT INTO tb_user (email, name) VALUES (p_email, p_name); RETURN QUERY SELECT * FROM v_user WHERE email = p_email;END;$$ LANGUAGE plpgsql;import "github.com/fraiseql/fraiseql-go"
type CreateUserInput struct { Email string `fraiseql:"email"` Name string `fraiseql:"name"`}
func init() { fraiseql.Mutation("createUser", fraiseql.MutationConfig{ SQLSource: "fn_create_user", }, func(input CreateUserInput) (User, error) { return User{}, nil })}-- The backing PostgreSQL functionCREATE FUNCTION fn_create_user(p_email TEXT, p_name TEXT)RETURNS SETOF v_user AS $$BEGIN INSERT INTO tb_user (email, name) VALUES (p_email, p_name); RETURN QUERY SELECT * FROM v_user WHERE email = p_email;END;$$ LANGUAGE plpgsql;Update Client Code (Days 4-5)
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> );}import { Client } from "@fraiseql/client";
const client = new Client({ url: "https://api.example.com/graphql", apiKey: process.env.FRAISEQL_API_KEY});
export async function getServerSideProps() { const response = await client.query(` query GetUsers { users(limit: 10) { id name posts { id title } } } `);
return { props: { users: response.data.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> );}Deploy FraiseQL Backend (Day 7)
# Build FraiseQL binaryfraiseql compile
# Deploy (example: Docker)docker build -t fraiseql-api .docker push your-registry/fraiseql-api
# Deploy to Kubernetes/Cloudkubectl apply -f fraiseql-deployment.yaml# .env.local — no database URL needed on the frontendFRAISEQL_URL=https://api.example.com/graphqlFRAISEQL_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:
const users = await prisma.user.findMany({ skip: 10, take: 10});query { users(offset: 10, limit: 10) { id name }}const posts = await prisma.post.findMany({ where: { AND: [ { published: true }, { author: { email: { contains: "@example.com" } } } ] }});query { posts( published_eq: true author_email_contains: "@example.com" ) { id title }}const result = await prisma.post.aggregate({ where: { published: true }, _count: true, _avg: { rating: true }});query { postsAggregate(published_eq: true) { count avgRating }}// N+1 problemconst users = await prisma.user.findMany();for (const user of users) { const posts = await prisma.post.findMany({ where: { userId: user.id } }); // N queries!}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.middlewareasync 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)import { middleware } from 'fraiseql';
middleware(async (request, next) => { if (shouldUseFraiseQL(request.userId)) { return fraiseqlHandler(request); } return prismaHandler(request);});import "github.com/fraiseql/fraiseql-go"
fraiseql.Use(func(req fraiseql.Request, next fraiseql.Handler) fraiseql.Response { if shouldUseFraiseQL(req.UserID) { return fraiseqlHandler(req) } return prismaHandler(req)})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:
┌─────────────────────────────────┐ │ PostgreSQL Database │ └──────────────┬──────────────────┘ │ ┌───────────────────┴───────────────────┐ │ │ ┌───────────▼──────────┐ ┌──────────────▼────────┐ │ Prisma Client │ │ FraiseQL Rust Binary │ │ (Admin / Internal) │ │ (Public GraphQL API) │ └───────────────────────┘ └────────────────────────┘Setup:
# prisma/schema.prisma — unchanged, used by admin tooling# fraiseql.toml — points to the same DATABASE_URL// admin/seed.ts — Prisma for admin operationsimport { PrismaClient } from '@prisma/client';const prisma = new PrismaClient();await prisma.user.createMany({ data: seedData });# Public API served by FraiseQLquery { users(limit: 20, orderBy: "created_at DESC") { id name posts { title } }}v_*) for all queriesfn_*) for all mutationsv_* viewsfn_* functionsApollo Server Migration
Migrate from another popular GraphQL option.
Hasura Migration
Coming from Hasura’s database-first GraphQL.
Getting Started
Learn FraiseQL fundamentals from scratch.
Schema Concepts
Understand SQL views, types, and decorators.