Prisma Migration
Migrating from Prisma ORM to FraiseQL.
This guide shows how to migrate from traditional REST APIs to GraphQL using FraiseQL.
| Problem | REST | GraphQL |
|---|---|---|
| Over-fetching | GET /users returns all fields | Query only needed fields |
| Under-fetching | Must chain API calls | Get all data in one request |
| Versioning | /v1/, /v2/ endpoints | Single endpoint, evolves safely |
| Documentation | Separate docs | Self-documenting via schema |
| Client bloat | Multiple endpoints | Single endpoint |
| Real-time | WebSocket polling | Native subscriptions |
# 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 postsGET /api/users/123/posts# Response: another round-trip
# Request 3: Get post commentsGET /api/posts/456/comments# Total: 3 requests, lots of unused dataquery { user(id: 123) { name email posts { title comments { text } } }}# Single request, exactly what you needDocument REST Endpoints
Map out every existing REST endpoint before you begin:
GET /api/users -> Get all usersGET /api/users/:id -> Get userPOST /api/users -> Create userPUT /api/users/:id -> Update userDELETE /api/users/:id -> Delete userGET /api/users/:id/posts -> Get user's postsGET /api/posts -> Get all postsGET /api/posts/:id -> Get postPOST /api/posts -> Create postMap Endpoints to GraphQL Operations
| REST Endpoint | GraphQL Equivalent |
|---|---|
GET /users | query { users { ... } } |
GET /users/:id | query { user(id: "...") { ... } } |
GET /users?status=active | query { users(where: { status: { _eq: "active" } }) { ... } } |
POST /users | mutation { createUser(input: { ... }) { ... } } |
PUT /users/:id | mutation { updateUser(id: "...", input: { ... }) { ... } } |
DELETE /users/:id | mutation { deleteUser(id: "...") } |
GET /users/:id/posts | query { user(id: "...") { posts { ... } } } |
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.
@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)}@fraiseql.typeclass User: id: ID name: str email: str posts: list['Post']
# Reads from SQL view v_user@fraiseql.query(sql_source="v_user")def users(limit: int = 50) -> list[User]: """GET /users -> query { users { ... } }""" pass
@fraiseql.query(sql_source="v_user")def user(id: ID) -> User: """GET /users/:id -> query { user(id: ...) { ... } }""" pass
# Calls database function fn_create_user@fraiseql.mutation(sql_source="fn_create_user")def create_user(name: str, email: str) -> User: """POST /users -> mutation { createUser(...) { ... } }""" passimport { type, query, mutation, ID } from 'fraiseql';
@type()class User { id!: ID; name!: string; email!: string; posts!: Post[];}
// Reads from SQL view v_user@query({ sqlSource: 'v_user' })function users(limit: number = 50): Promise<User[]> { return Promise.resolve([]);}
@query({ sqlSource: 'v_user' })function user(id: ID): Promise<User> { return Promise.resolve({} as User);}
// Calls database function fn_create_user@mutation({ sqlSource: 'fn_create_user' })function createUser(name: string, email: string): Promise<User> { return Promise.resolve({} as User);}import "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"`}
func init() { // Reads from SQL view v_user fraiseql.Query("users", fraiseql.QueryConfig{ SQLSource: "v_user", }, func(args fraiseql.Args) ([]User, error) { return nil, nil })
fraiseql.Query("user", fraiseql.QueryConfig{ SQLSource: "v_user", }, func(args fraiseql.Args) (User, error) { return User{}, nil })
// Calls database function fn_create_user fraiseql.Mutation("createUser", fraiseql.MutationConfig{ SQLSource: "fn_create_user", }, func(args fraiseql.Args) (User, error) { return User{}, nil })}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.
Update Client Code
// Beforeasync 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 };}// Afterasync function getUser(userId: number) { const response = await client.query(` query { user(id: ${userId}) { name email posts { id title } } } `);
return response.data.user;}Route Traffic Gradually
Week 1-2: Build GraphQL alongside RESTWeek 3-4: Route 50% of traffic to GraphQLWeek 5-6: Route 100% to GraphQLWeek 7+: Decommission RESTDecommission REST Endpoints
Once all clients are migrated and GraphQL is confirmed stable, remove the REST endpoints and clean up related dependencies.
# Request 1GET /api/users/123# Response includes unused fields: createdAt, updatedAt
# Request 2GET /api/users/123/posts# Response includes all post fields
# Request 3GET /api/posts/456/comments
# Total: 3 requests + latencyPOST /graphqlContent-Type: application/json
{ "query": "query { user(id: 123) { name email posts { title comments { text } } } }"}
# Response{ "data": { "user": { "name": "Alice", "email": "alice@example.com", "posts": [ { "title": "First Post", "comments": [ { "text": "Nice!" }, { "text": "Great article!" } ] } ] } }}
# Total: 1 request, exact data neededGET /api/users?limit=10&offset=20query { users(limit: 10, offset: 20) { id name }}GET /api/posts?status=published&author_id=123&sort=-created_atquery { posts( status_eq: "published" author_id_eq: 123 orderBy: "created_at DESC" ) { id title }}404 Not Found400 Bad Request500 Internal Server Error401 Unauthorized{ "errors": [ { "message": "User not found", "extensions": { "code": "NOT_FOUND", "userId": "123" } } ]}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 } }}| Metric | REST | GraphQL |
|---|---|---|
| Requests per view | 3+ | 1 |
| Response time | 200-500ms | 50-100ms |
| Unused data | 50-70% | 0% |
| Data joining | Client-side | Server-side (SQL views) |
REST:
Multiple endpoints/api/v1/users/api/v1/posts/api/v1/comments/api/v2/users (new version!)/api/v2/postsGraphQL:
Single endpoint/graphql (Schema evolves safely; clients request only what they need)v_*) for all read operationsfn_*) for all write operationsPrisma Migration
Migrating from Prisma ORM to FraiseQL.
Apollo Migration
Migrating from Apollo Server to FraiseQL.
Hasura Migration
Migrating from Hasura to FraiseQL.
Getting Started
Learn FraiseQL fundamentals from scratch.