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.

1

Keep Your Hasura Views

Your existing views in PostgreSQL are already FraiseQL-compatible.

2

Define Python/TypeScript Types

Create type classes that match your Hasura schema.

@fraiseql.type
class User:
  id: ID
  name: str
  email: str
3

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
4

Test Incrementally

Migrate one endpoint at a time. Keep both systems running in parallel.

5

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.

1

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
2

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;
3

Implement FraiseQL Handlers

Replace resolvers with simple query handlers.

4

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.

1

Document Endpoints

List all REST endpoints and their response shapes.

2

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]:
  ...
3

Implement Queries

Each REST endpoint becomes a GraphQL query handler.

4

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

📘 Getting Started

FraiseQL quickstart guide to understand the basics.

Read →

💻 For Developers

Deep dive into FraiseQL development patterns.

Read →

⚡ Performance

Understand performance improvements you'll see.

Read →

🔄 N+1 Elimination

The biggest win from migrating to FraiseQL.

Read →

Ready to Migrate?

Start your migration to FraiseQL today. Gradual, safe, and proven.