Migrating to FraiseQL
From traditional GraphQL, Hasura, or REST. Gradual, incremental migration. Keep your database as-is.
Why Migrate?
Better Performance
Eliminate N+1 at the database level. 5-10x latency improvement.
Simpler Code
47 custom patterns → 4 core patterns. Easier to understand and maintain.
AI-Friendly
Consistent patterns mean LLMs can generate your API.
Type Safety
Compile-time validation. Catch errors before production.
Migration Paths
Path 1: From Hasura (Easiest)
Hasura already uses database views. FraiseQL builds on this foundation.
Keep Your Hasura Views
Your existing views in PostgreSQL are already FraiseQL-compatible.
Define Python/TypeScript Types
Create type classes that match your Hasura schema.
@fraiseql.type
class User:
id: ID
name: str
email: str Implement Resolvers
Convert Hasura GraphQL resolvers to FraiseQL query handlers.
# Hasura-like query
@fraiseql.query(sql_source="v_users")
def users(limit: int = 100) -> list[User]:
pass Test Incrementally
Migrate one endpoint at a time. Keep both systems running in parallel.
Switch Over
Update client code to hit FraiseQL endpoints. Sunset Hasura gradually.
Timeline: 1-2 weeks for small projects, depends on complexity
Path 2: From Traditional GraphQL
More rewarding, more impactful transformation.
Audit Resolver Patterns
Document your most common resolver patterns and N+1 hot spots.
# Identify patterns like:
# - @resolver that does db.find() + permission check
# - @resolver that loads relationships
# - @resolver with complex business logic Create FraiseQL Views
Translate your resolver logic into SQL views.
# Traditional GraphQL resolver
@resolver
async def user_with_posts(parent: User, info):
return {
...parent,
posts: db.find_posts(parent.id)
}
# Becomes FraiseQL view
CREATE VIEW v_user_with_posts AS
SELECT jsonb_build_object(
'id', u.id::text,
'name', u.name,
'posts', (
SELECT jsonb_agg(...) FROM tb_post
WHERE user_id = u.id
)
) AS data
FROM tb_user u; Implement FraiseQL Handlers
Replace resolvers with simple query handlers.
Run Side-by-Side
Use a query router to send requests to both systems initially for validation.
Timeline: 2-4 weeks depending on resolver complexity
Path 3: From REST API
Natural fit for REST → GraphQL migration.
Document Endpoints
List all REST endpoints and their response shapes.
Create FraiseQL Types
Each REST response becomes a FraiseQL type.
# REST: GET /api/users/:id
@fraiseql.query
async def user(id: ID) -> User:
...
# REST: GET /api/posts?user_id=:id
@fraiseql.query
async def posts(user_id: ID) -> list[Post]:
... Implement Queries
Each REST endpoint becomes a GraphQL query handler.
Route Queries
Add a GraphQL endpoint. Keep REST for backward compatibility.
Timeline: 3-5 days for small APIs, scales linearly with endpoint count
Migration Best Practices
1. Migrate by Feature, Not by Table
Don't migrate entire tables. Migrate user-facing features:
# Good approach:
# Week 1: User profile feature (types, views, queries)
# Week 2: Post listing feature
# Week 3: Comments feature
# Bad approach:
# Migrate all User table endpoints
# Then Post table
# Then Comment table 2. Use Feature Flags
Route traffic gradually to FraiseQL:
# Client routing logic
if feature_flag("use_fraiseql_posts"):
posts = fraiseql_client.posts()
else:
posts = legacy_client.posts()
# Gradually increase percentage:
# Day 1: 10% to FraiseQL
# Day 2: 25% to FraiseQL
# Day 3: 50% to FraiseQL
# Day 4: 100% to FraiseQL 3. Validate Responses
Compare outputs from both systems during migration:
# Parallel query execution
legacy_response = legacy_client.query(request)
fraiseql_response = fraiseql_client.query(request)
# Compare
if legacy_response != fraiseql_response:
log("MISMATCH", legacy_response, fraiseql_response)
# Still return legacy response to user
return legacy_response
else:
return fraiseql_response 4. Performance Testing
Validate performance improvements before full switch:
# Load test both endpoints
fraiseql_latency = load_test(fraiseql_endpoint, 1000_requests)
legacy_latency = load_test(legacy_endpoint, 1000_requests)
# Expect 5-10x improvement
assert fraiseql_latency < legacy_latency / 5
# Only migrate if performance is better
if fraiseql_latency < legacy_latency:
switch_to_fraiseql() 5. Database Indexes
Don't forget to create indexes for your new views:
-- Before deploying FraiseQL views, create indexes
CREATE INDEX idx_post_user_id ON tb_post(user_id);
CREATE INDEX idx_post_created_at ON tb_post(created_at DESC);
-- Deploy indexes with your FraiseQL code
-- EXPLAIN ANALYZE confirms they're used Rollback Strategy
Keep Legacy System Running
During migration, keep your old system available for quick rollback:
# API Router
def route_request(endpoint, request):
try:
response = fraiseql_client.query(endpoint, request)
# Log success
log("fraiseql_success", endpoint)
return response
except Exception as e:
# Log error, fall back to legacy
log("fraiseql_error", endpoint, e)
return legacy_client.query(endpoint, request)
# If FraiseQL causes issues, instant fallback Timeline: Keep legacy for 1-2 weeks post-migration. Then decommission.
Migration Resources
Ready to Migrate?
Start your migration to FraiseQL today. Gradual, safe, and proven.