Skip to content

Migrating from REST API to GraphQL

This guide shows how to migrate from traditional REST APIs to GraphQL using FraiseQL.

ProblemRESTGraphQL
Over-fetchingGET /users returns all fieldsQuery only needed fields
Under-fetchingMust chain API callsGet all data in one request
Versioning/v1/, /v2/ endpointsSingle endpoint, evolves safely
DocumentationSeparate docsSelf-documenting via schema
Client bloatMultiple endpointsSingle endpoint
Real-timeWebSocket pollingNative subscriptions
Terminal window
# Request 1: Get user (gets all fields)
GET /api/users/123
# Response: 500 bytes with fields you don't need
# Request 2: Get user's posts
GET /api/users/123/posts
# Response: another round-trip
# Request 3: Get post comments
GET /api/posts/456/comments
# Total: 3 requests, lots of unused data
  1. Document REST Endpoints

    Map out every existing REST endpoint before you begin:

    Terminal window
    GET /api/users -> Get all users
    GET /api/users/:id -> Get user
    POST /api/users -> Create user
    PUT /api/users/:id -> Update user
    DELETE /api/users/:id -> Delete user
    GET /api/users/:id/posts -> Get user's posts
    GET /api/posts -> Get all posts
    GET /api/posts/:id -> Get post
    POST /api/posts -> Create post
  2. Map Endpoints to GraphQL Operations

    REST EndpointGraphQL Equivalent
    GET /usersquery { users { ... } }
    GET /users/:idquery { user(id: "...") { ... } }
    GET /users?status=activequery { users(where: { status: { _eq: "active" } }) { ... } }
    POST /usersmutation { createUser(input: { ... }) { ... } }
    PUT /users/:idmutation { updateUser(id: "...", input: { ... }) { ... } }
    DELETE /users/:idmutation { deleteUser(id: "...") }
    GET /users/:id/postsquery { user(id: "...") { posts { ... } } }
  3. Define FraiseQL Types and SQL Views

    Each REST resource becomes a FraiseQL type backed by a SQL view (v_*). The FraiseQL Rust binary reads data directly from these views — there are no Python resolvers at runtime.

    routes/users.py
    @app.get("/api/users")
    def get_users():
    return {"users": User.all()}
    @app.get("/api/users/{user_id}")
    def get_user(user_id: int):
    return {"user": User.get(user_id)}
    @app.post("/api/users")
    def create_user(data: UserInput):
    return {"user": User.create(data)}
  4. Build GraphQL API Alongside REST

    Run both systems in parallel during migration. FraiseQL can serve specific endpoints while your existing REST API handles the rest — you do not need to migrate everything at once.

    A typical routing setup during migration:

    /graphql → FraiseQL (migrated endpoints)
    /api/users → FraiseQL (migrated)
    /api/posts → FraiseQL (migrated)
    /api/reports → Legacy REST handler (not yet migrated)
    /api/webhooks → Legacy REST handler (not yet migrated)

    Route a percentage of traffic to the new GraphQL endpoint while keeping REST available as a fallback.

  5. Update Client Code

    // Before
    async function getUser(userId: number) {
    const userRes = await fetch(`/api/users/${userId}`);
    const user = await userRes.json();
    const postsRes = await fetch(`/api/users/${userId}/posts`);
    const posts = await postsRes.json();
    return { ...user, posts: posts.posts };
    }
  6. Route Traffic Gradually

    Week 1-2: Build GraphQL alongside REST
    Week 3-4: Route 50% of traffic to GraphQL
    Week 5-6: Route 100% to GraphQL
    Week 7+: Decommission REST
  7. Decommission REST Endpoints

    Once all clients are migrated and GraphQL is confirmed stable, remove the REST endpoints and clean up related dependencies.

Terminal window
# Request 1
GET /api/users/123
# Response includes unused fields: createdAt, updatedAt
# Request 2
GET /api/users/123/posts
# Response includes all post fields
# Request 3
GET /api/posts/456/comments
# Total: 3 requests + latency
Terminal window
GET /api/users?limit=10&offset=20
Terminal window
GET /api/posts?status=published&author_id=123&sort=-created_at
404 Not Found
400 Bad Request
500 Internal Server Error
401 Unauthorized

FraiseQL includes helpful error codes and context in all error responses.

REST has no good real-time support. FraiseQL provides native subscriptions via WebSocket and NATS:

subscription {
postCreated {
id
title
author {
name
}
}
}
MetricRESTGraphQL
Requests per view3+1
Response time200-500ms50-100ms
Unused data50-70%0%
Data joiningClient-sideServer-side (SQL views)

REST:

Multiple endpoints
/api/v1/users
/api/v1/posts
/api/v1/comments
/api/v2/users (new version!)
/api/v2/posts

GraphQL:

Single endpoint
/graphql
(Schema evolves safely; clients request only what they need)
  • Document all REST endpoints
  • Map endpoints to GraphQL queries/mutations
  • Write SQL views (v_*) for all read operations
  • Write PostgreSQL functions (fn_*) for all write operations
  • Define FraiseQL types and decorators
  • Build GraphQL API alongside REST
  • Update client code
  • Performance test
  • Route traffic gradually
  • Decommission REST endpoints
  1. Single Request - Get all data in one call
  2. Exact Data - No over/under-fetching
  3. Self-Documenting - Schema is documentation
  4. Subscriptions - Real-time updates
  5. Evolves Safely - Add fields without breaking clients
  6. Better Performance - Pre-compiled SQL views, automatic optimization

Prisma Migration

Migrating from Prisma ORM to FraiseQL.

Read guide

Apollo Migration

Migrating from Apollo Server to FraiseQL.

Read guide

Hasura Migration

Migrating from Hasura to FraiseQL.

Read guide

Getting Started

Learn FraiseQL fundamentals from scratch.

Read guide