Prisma Migration
Also migrating from Prisma ORM.
This guide walks through migrating from manually-built Apollo Server GraphQL backends to FraiseQL’s database-first approach.
1. Write TypeScript interfaces2. Write GraphQL schema by hand3. Implement resolvers manually4. Connect to database5. Handle N+1 queries manually (DataLoader)6. Implement caching manually1. Define Python/TypeScript/Go schema types2. Schema derived from decorator types3. Resolvers mapped to hand-written SQL views (v_*)4. Mutations mapped to PostgreSQL functions (fn_*)5. Automatic N+1 batching6. Built-in caching7. Rust binary serves everything| Aspect | Apollo | FraiseQL |
|---|---|---|
| Schema Definition | GraphQL SDL (manual) | Python/TS/Go types with decorators |
| Resolvers | Hand-written TypeScript | Mapped to SQL views — no resolver code |
| Database | ORM or raw queries in resolvers | Hand-written SQL views (v_*) |
| Mutations | Resolver functions | PostgreSQL functions (fn_*) |
| Caching | redis-apollo-link | Built-in federation cache |
| Subscriptions | WebSocket (manual PubSub) | Native with NATS |
| Performance | Manual optimization + DataLoader | Automatic batching via SQL joins |
| Type Safety | apollo-codegen | Native SDK types |
| Runtime | Node.js | Rust binary |
| Complexity | High (resolver logic) | Low (decorators + SQL) |
Understand Your Current Apollo Setup
Inventory all type definitions, resolvers, and subscriptions before migrating.
import { ApolloServer, gql } from 'apollo-server-express';import { PrismaClient } from '@prisma/client';
const typeDefs = gql` type User { id: ID! email: String! name: String! posts: [Post!]! }
type Post { id: ID! title: String! content: String! author: User! }
type Query { user(id: ID!): User users(limit: Int): [User!]! post(id: ID!): Post posts(limit: Int): [Post!]! }
type Mutation { createUser(email: String!, name: String!): User! createPost(title: String!, content: String!, authorId: ID!): Post! }`;
const resolvers = { Query: { user: async (_, { id }) => prisma.user.findUnique({ where: { id } }), users: async (_, { limit }) => prisma.user.findMany({ take: limit }), post: async (_, { id }) => prisma.post.findUnique({ where: { id } }), posts: async (_, { limit }) => prisma.post.findMany({ take: limit }) },
User: { posts: async (user) => prisma.post.findMany({ where: { userId: user.id } }) // N+1 problem: runs once per user! },
Post: { author: async (post) => prisma.user.findUnique({ where: { id: post.userId } }) },
Mutation: { createUser: async (_, { email, name }) => prisma.user.create({ data: { email, name } }), createPost: async (_, { title, content, authorId }) => prisma.post.create({ data: { title, content, userId: authorId } }) }};Write SQL Views for Queries
For every Apollo Query resolver, create a PostgreSQL view (v_*). Relationships between types become JOIN clauses in the view — no DataLoader required.
-- v_user: replaces User query resolversCREATE VIEW v_user ASSELECT u.id, u.email, u.name, COALESCE( json_agg( json_build_object('id', p.id, 'title', p.title, 'content', p.content) ) 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;
-- v_post: replaces Post query resolversCREATE VIEW v_post ASSELECT p.id, p.title, p.content, u.id AS author_id, u.email AS author_email, u.name AS author_nameFROM tb_post pJOIN tb_user u ON u.id = p.user_id;Write PostgreSQL Functions for Mutations
For every Apollo Mutation resolver, create a PostgreSQL function (fn_*).
CREATE 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;
CREATE FUNCTION fn_create_post(p_title TEXT, p_content TEXT, p_author_id UUID)RETURNS SETOF v_post AS $$BEGIN INSERT INTO tb_post (title, content, user_id) VALUES (p_title, p_content, p_author_id); RETURN QUERY SELECT * FROM v_post WHERE user_id = p_author_id ORDER BY id DESC LIMIT 1;END;$$ LANGUAGE plpgsql;Convert Apollo Schema to FraiseQL Decorators
Map each GraphQL SDL type to a FraiseQL type decorator. The Rust binary reads these decorator definitions to generate the full GraphQL schema — no SDL authoring needed.
from fraiseql import FraiseQL, ID
fraiseql = FraiseQL()
@fraiseql.typeclass User: id: ID email: str name: str posts: list['Post']
@fraiseql.typeclass Post: id: ID title: str content: str author_id: ID author: User
# Queries map to SQL views@fraiseql.query(sql_source="v_user")def user(id: ID) -> User: """Get single user.""" pass
@fraiseql.query(sql_source="v_user")def users(limit: int = 50) -> list[User]: """Get multiple users.""" pass
@fraiseql.query(sql_source="v_post")def post(id: ID) -> Post: """Get single post.""" pass
@fraiseql.query(sql_source="v_post")def posts(limit: int = 50) -> list[Post]: """Get multiple posts.""" pass
# Mutations map to PostgreSQL functions@fraiseql.mutation(sql_source="fn_create_user")def create_user(email: str, name: str) -> User: """Create user.""" pass
@fraiseql.mutation(sql_source="fn_create_post")def create_post(title: str, content: str, author_id: ID) -> Post: """Create post.""" passUpdate Frontend Client Code
The GraphQL query syntax is identical — only the client initialization changes.
import { useQuery, gql } from '@apollo/client';
const GET_USERS = gql` query GetUsers { users(limit: 10) { id name } }`;
function UsersList() { const { data } = useQuery(GET_USERS); // ...}import { Client } from '@fraiseql/client';
const client = new Client({ url: 'https://api.example.com/graphql', apiKey: process.env.FRAISEQL_API_KEY});
async function getUsers() { return await client.query(` query GetUsers { users(limit: 10) { id name } } `);}Migrate Subscriptions
const pubsub = new PubSub();
const resolvers = { Subscription: { userCreated: { subscribe: () => pubsub.asyncIterator(['USER_CREATED']) } }, Mutation: { createUser: async (_, { email, name }) => { const user = await prisma.user.create({ data: { email, name } }); pubsub.publish('USER_CREATED', { userCreated: user }); return user; } }};@fraiseql.subscription(entity_type="User", topic="created")def user_created() -> User: """Subscribe to new users.""" pass
# Client code stays the same!Performance Test and Decommission
Run performance tests to validate the improvement. Apollo with DataLoader typically runs 10-20 queries per request; FraiseQL uses pre-compiled SQL views, reducing this to 1-2 queries.
Once results are confirmed, decommission the Apollo Server and remove its dependencies.
// Querying 10 users with posts = 11 queriesconst users = await prisma.user.findMany({ take: 10 }); // 1 query// Then for each user's posts field:// User.posts resolver called 10 times = 10 more queries// Total: 11 queriesEven with DataLoader, you must remember to wire it up manually for every relationship.
query GetUsers { users(limit: 10) { name posts { # Resolved by a single batched SQL JOIN in the view title } }}FraiseQL executes only 2 queries:
SELECT * FROM v_user LIMIT 10SELECT * FROM v_post WHERE user_id IN (list_of_user_ids)That’s 5x fewer queries — and you cannot accidentally create N+1 because the SQL is pre-written.
| Metric | Apollo Server | FraiseQL |
|---|---|---|
| Resolver Lines | 500+ | 0 (mapped to SQL views) |
| Queries/Request | 11 | 2 |
| Request Time | ~200ms | ~20ms |
| Development Time | High | Low |
| Caching | Manual | Automatic |
| N+1 Problems | Common | Architecturally impossible |
| Runtime | Node.js | Rust binary |
v_*) for all queriesfn_*) for all mutationsPrisma Migration
Also migrating from Prisma ORM.
Hasura Migration
Coming from Hasura’s database-first approach.
NATS Integration
Learn how FraiseQL subscriptions work with NATS.
Getting Started
Learn FraiseQL fundamentals from scratch.
Test API parity — Ensure both APIs return identical data:
# Apollo querycurl -X POST http://localhost:4000/graphql \ -H "Content-Type: application/json" \ -d '{"query": "{ users { id name posts { title } } }"}' \ > apollo_result.json
# FraiseQL querycurl -X POST http://localhost:8080/graphql \ -H "Content-Type: application/json" \ -d '{"query": "{ users { id name posts { title } } }"}' \ > fraiseql_result.json
# Compare (should be identical)diff apollo_result.json fraiseql_result.jsonLoad test comparison:
# Test Apollo performanceab -n 1000 -c 10 -p query.json -T application/json \ http://localhost:4000/graphql
# Test FraiseQL performanceab -n 1000 -c 10 -p query.json -T application/json \ http://localhost:8080/graphqlCheck N+1 elimination:
# Enable query logging in PostgreSQL# Run a query with nested data# FraiseQL: Should see 1 SQL query# Apollo (without DataLoader): Would see N+1 queriesVerify mutations work:
curl -X POST http://localhost:8080/graphql \ -H "Content-Type: application/json" \ -d '{ "query": "mutation { createUser(email: \"test@test.com\", name: \"Test User\") { id name } }" }'Check subscriptions (if used):
# WebSocket testwscat -c ws://localhost:8080/subscriptionsSymptoms:
Error: Cannot compile schema - missing type UserSolution:
'User'fraiseql validateSymptoms: GraphQL queries work but return different results than Apollo.
Solution:
Check SQL views match Apollo resolvers:
-- Compare Apollo resolver logicprisma.user.findMany({ include: { posts: true } })
-- With FraiseQL view\d+ v_userVerify column mappings:
-- Check view columns match GraphQL fieldsSELECT column_name FROM information_schema.columnsWHERE table_name = 'v_user';Symptoms:
{ "errors": [{"message": "Function fn_create_user does not exist"}]}Solution:
Create the PostgreSQL function:
CREATE OR REPLACE FUNCTION fn_create_user(...)Check function signature matches mutation:
SELECT proname, proargtypes FROM pg_proc WHERE proname LIKE 'fn_%';Symptoms: FraiseQL queries are slower than Apollo.
Solution:
Check for missing indexes:
SELECT * FROM pg_indexes WHERE tablename LIKE 'tb_%';Analyze view performance:
EXPLAIN ANALYZE SELECT data FROM v_user WHERE id = '123';Consider materialized views for heavy queries:
CREATE MATERIALIZED VIEW tv_user AS ...Symptoms: WebSocket connections fail.
Solution:
Verify NATS is configured:
[nats]enabled = trueurl = "nats://localhost:4222"Check subscription decorator:
@fraiseql.subscription(entity_type="User", topic="user_created")Test NATS connectivity:
nats pub test "hello"