1.5: FraiseQL Compared to Other Approaches¶
Audience: Technical decision-makers, architects evaluating GraphQL solutions Prerequisite: Topics 1.1 (What is FraiseQL?), 1.2 (Core Concepts), 1.4 (Design Principles) Reading Time: 20-25 minutes
Overview¶
FraiseQL is one of several approaches to building GraphQL APIs. This topic compares FraiseQL with popular alternatives to help you understand where each approach excels and where it makes tradeoffs.
FraiseQL is a Python runtime GraphQL framework for PostgreSQL. You define types, queries, and mutations with decorators; at application startup the GraphQL schema is built in memory and served over FastAPI. Reads come from PostgreSQL views that return a data JSONB column, and writes are delegated to PostgreSQL functions. An optional Rust extension (fraiseql_rs) accelerates JSON transformation and field selection on the hot path.
Key Question: Which approach is right for your project?
The answer depends on:
- Your data source (a single PostgreSQL database vs. mixed sources)
- Your team's expertise (database, backend, frontend)
- Your performance requirements (predictability vs. flexibility)
- Your development speed priorities (time-to-market vs. long-term maintainability)
Comparison Matrix: At a Glance¶
| Aspect | FraiseQL | Apollo Server | Hasura | WunderGraph | Custom REST |
|---|---|---|---|---|---|
| Primary Data Source | PostgreSQL | Multiple sources | PostgreSQL (and others) | Multiple sources | Anything |
| Schema Definition | Python decorators | GraphQL schema language | PostgreSQL schema | Multiple languages | Not applicable |
| Execution Model | Runtime (Python over FastAPI), schema built at startup | Runtime | Runtime | Runtime | N/A |
| Resolver Code | Automatic (views + functions) | Manual custom code | Automatic rules | Automatic + manual | Manual code |
| Type Safety | Database + GraphQL | GraphQL only | PostgreSQL only | GraphQL + custom validation | Code-level only |
| Performance | Fast (JSONB + optional Rust pipeline, no N+1) | Variable (resolver dependent) | Variable (rule based) | Moderate (middleware overhead) | Variable |
| Flexibility | Limited to DB schema | Very high | Limited to DB + rules | High | Complete |
| Time to API | Fast (decorators → views) | Slow (code resolvers) | Fast (introspection) | Moderate | Slow |
| Learning Curve | Medium (GraphQL + SQL) | High (GraphQL + resolver patterns) | Low (just SQL) | Moderate | N/A |
| Best For | PostgreSQL-backed OLTP APIs | Complex multi-source APIs | Quick PostgreSQL APIs | Flexible API gateway | Simple services |
Detailed Comparisons¶
FraiseQL vs. Apollo Server¶
Apollo Server is the most popular GraphQL framework. It's flexible, well-documented, and the industry standard.
What Apollo Server Excels At¶
Flexibility
type Query {
user(id: Int!): User
trendingUsers: [User!]!
searchUsers(query: String!): [User!]!
recommendations(userId: Int!): [Recommendation!]!
}
Each field can resolve from a different source:
- Database query
- REST API call
- Cache lookup
- Computed value
- File system
Multi-Source Integration
// Apollo: Combine data from multiple sources
const resolvers = {
Query: {
user: async (_, { id }, context) => {
const user = await context.db.query('SELECT * FROM users WHERE id = ?', [id]);
const profile = await context.externalAPI.getProfile(id);
const recommendations = await context.ml.getRecommendations(id);
return { ...user, profile, recommendations };
}
}
};
Ecosystem & Plugins
- Apollo Server extensions (authentication, logging, monitoring)
- DataLoader (N+1 prevention)
- Apollo Federation (schema stitching)
- Hundreds of community plugins
Where Apollo Server Struggles¶
Resolver Complexity
// Apollo: Every field needs a resolver
const resolvers = {
Query: {
user: (_, { id }, context) => context.db.findUser(id),
},
User: {
id: (user) => user.id,
email: (user) => user.email,
orders: (user, _, context) => context.db.findOrders(user.id), // N+1 problem?
},
Order: {
id: (order) => order.id,
total: (order) => order.total,
items: (order, _, context) => context.db.findItems(order.id), // Another N+1?
}
};
Manual Optimization
// Apollo: You must implement optimization patterns
const dataLoaders = {
userLoader: new DataLoader(async (userIds) => {
return context.db.query('SELECT * FROM users WHERE id = ANY(?)', [userIds]);
}),
};
Performance Unpredictability
- Query performance depends on resolver implementation
- N+1 problems can hide until production
- No structural visibility into query costs
- Hard to debug performance issues
Synchronizing Schemas
TypeScript type definitions
↕ (must match)
GraphQL schema
↕ (must match)
Database schema
If you change the database, you must update two more places. With FraiseQL the read view's data JSONB and your @fraiseql.type are the single source of truth.
FraiseQL vs. Apollo: Decision¶
| Your Priority | Better Choice | Why |
|---|---|---|
| Single PostgreSQL-backed API | FraiseQL | Simpler, faster, fewer N+1 surprises |
| Multi-source data aggregation | Apollo Server | FraiseQL targets a single PostgreSQL database |
| Complex custom business logic in resolvers | Apollo Server | FraiseQL pushes write logic into PostgreSQL functions |
| Time-to-market wrapping existing REST APIs | Apollo Server | Easier to wrap external services |
| Performance predictability | FraiseQL | View-driven reads, no per-field resolver fan-out |
| Team has database expertise | FraiseQL | Database knowledge directly applies |
| Team has JavaScript expertise | Apollo Server | Lower learning curve |
FraiseQL vs. Hasura¶
Hasura automatically generates a GraphQL API by introspecting your PostgreSQL schema.
What Hasura Excels At¶
Fast Time to API
# Hasura: Point at database, get GraphQL API instantly
docker run hasura/graphql-engine:latest \
--database-url postgresql://user:pass@db:5432/mydb
Result: a complete GraphQL API with CRUD operations, relationships, and filtering—without writing a line of code.
Database-First Approach
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
total DECIMAL(10, 2)
);
Hasura immediately exposes:
type User {
id: Int!
email: String!
createdAt: DateTime!
orders: [Order!]!
}
type Order {
id: Int!
user: User!
total: Float!
}
Permission Rules
# Hasura: Row-level security via rules
Users:
select:
columns:
- id
- email
filter:
id: { _eq: X-Hasura-User-Id }
Simplicity for Standard CRUD
query {
users {
id
email
orders {
id
total
}
}
}
# Hasura handles the SQL automatically
Where Hasura Struggles¶
Fixed Query Patterns
# Hasura: No custom computed fields without Actions
query {
user(id: 1) {
id
email
orders {
id
total
items { # ✅ Can do this
name
}
}
orderCount # ❌ Requires custom Action (REST API call)
}
}
With FraiseQL, a computed field is just another key built into the view's data JSONB, so it is available without an external Action.
Action-Based Extensions
# Hasura: Must implement custom logic via Actions
actions:
- name: searchUsers
definition:
kind: query
arguments:
query: string!
output_type: SearchResult
handler: https://api.example.com/search
This converts back to the multi-source problem (like Apollo).
Runtime Permission Overhead
- Permission checks evaluated per request
- Permission rules are not statically analyzed
- Complex permissions can cause N+1 queries
Schema Coupling
PostgreSQL table/column names ←→ GraphQL schema (1:1 mapping)
Rename a table or column and the GraphQL API changes, which can break clients. FraiseQL decouples this: the GraphQL shape is defined by the view's data JSONB and your @fraiseql.type, independent of the underlying write tables.
FraiseQL vs. Hasura: Decision¶
| Your Priority | Better Choice | Why |
|---|---|---|
| Time to basic CRUD API | Hasura | Introspection is instant |
| Standard database queries | Hasura | Zero code needed |
| Custom computed fields | FraiseQL | Built into the read view's JSONB |
| Decoupling API shape from table layout | FraiseQL | Views isolate the public schema from write tables |
| Schema versioning/evolution | FraiseQL | Explicit types let you version deliberately |
| Team only knows SQL | Hasura | No Python needed |
| Write logic in the database | FraiseQL | Mutations call PostgreSQL functions |
| Rapid prototyping | Hasura | Get an API in minutes |
FraiseQL vs. WunderGraph¶
WunderGraph positions itself as a "serverless GraphQL federation platform." It supports multiple data sources and aims for developer productivity.
What WunderGraph Excels At¶
Configuration-First Development
# WunderGraph: Configure data sources and relationships
dataSources:
- name: database
kind: postgresql
database_url: ${DATABASE_URL}
- name: external_api
kind: graphql
url: https://api.example.com/graphql
Flexible Data Source Support
- Relational databases (PostgreSQL, MySQL, MongoDB)
- GraphQL APIs
- REST APIs
- Custom operations
Built-in Authentication
# WunderGraph: Auth integrated
authentication:
providers:
- github
- auth0
- custom_webhook
Federation Support
// WunderGraph: Compose multiple GraphQL APIs
import { introspectAndCompose } from '@wundergraph/sdk';
export default {
apis: [
introspectAndCompose({
apiNamespace: 'users',
url: 'http://users-service/graphql',
}),
introspectAndCompose({
apiNamespace: 'products',
url: 'http://products-service/graphql',
}),
],
};
Where WunderGraph Struggles¶
Still Manual for Complex Queries
// WunderGraph: You write resolvers for complex operations
export default async function GetUserRecommendations(
ctx: Context,
input: GetUserRecommendationsInput,
) {
const user = await ctx.user.findOne({ id: input.id });
const recommendations = await ctx.ml.getRecommendations(user.id);
// Still writing custom code
return recommendations;
}
Middle-Ground Positioning
- Not as fast to stand up as Hasura (requires more code)
- More moving parts than FraiseQL's single PostgreSQL path
- Not as flexible as Apollo (no custom middleware)
Optimization Still on You
// WunderGraph: You're still responsible for optimization
export default async function GetUserWithOrders(
ctx: Context,
input: GetUserWithOrdersInput,
) {
const user = await ctx.db.users.findOne({ id: input.id }); // 1 query
const orders = await ctx.db.orders.findMany({ userId: user.id }); // 1 query
// What if orders has 10,000 items? Pagination? Filtering?
// You have to handle this manually
return { user, orders };
}
FraiseQL vs. WunderGraph: Decision¶
| Your Priority | Better Choice | Why |
|---|---|---|
| Single PostgreSQL database | FraiseQL | Simpler, one data path |
| Multiple data sources | WunderGraph | Explicit multi-source support |
| Quick API for a single service | Hasura | Faster than both |
| Complex business logic | Apollo Server | More mature ecosystem |
| No N+1 by construction | FraiseQL | Nested data comes from view JSONB |
| Team learning curve | WunderGraph | Mid-point between options |
FraiseQL vs. Custom REST APIs¶
Before GraphQL was popular, teams built custom REST APIs. This is still the baseline to compare against.
What Custom REST Excels At¶
Simplicity for Simple Services
# REST: Simple to understand
@app.get("/users/{user_id}")
def get_user(user_id: int):
return db.query("SELECT * FROM users WHERE id = ?", [user_id])
Familiarity
- Every developer knows REST
- No GraphQL learning curve
- Mature tooling and libraries
Fine-Grained Control
# REST: You control exactly what goes into each endpoint
@app.get("/users/{user_id}/recommendations")
def get_recommendations(user_id: int, limit: int = 10):
# Your logic: exactly what you need, nothing more
return db.query(
"SELECT * FROM recommendations WHERE user_id = ? LIMIT ?",
[user_id, limit],
)
Where Custom REST Struggles¶
Versioning Chaos
/api/v1/users/{id}
/api/v2/users/{id}
/api/v3/users/{id}
Each API version requires separate endpoints and testing.
Over-fetching & Under-fetching
REST API returns:
GET /api/users/1
{
"id": 1,
"email": "user@example.com",
"name": "John",
"phone": "123-456-7890", // You don't need this
"address": { ... } // Or this
}
Returned 500 bytes, needed 200 bytes
Or:
You need user + orders + order items
3 separate requests: GET /users/1, GET /users/1/orders, GET /orders/123/items
No Standard Query Language
Custom filtering:
GET /api/users?filter=email:contains:@example.com&sort=-created_at&limit=10
Different service:
GET /api/products?q=coffee&sort=price&page=1&per_page=20
Inconsistent APIs everywhere
Documentation Burden
Each endpoint needs separate documentation:
- GET /users/{id}
- GET /users/{id}/orders
- GET /users/{id}/recommendations
- POST /users
- PUT /users/{id}
- DELETE /users/{id}
- GET /users?search=...&limit=...&offset=...
And that's just for users. Multiply by 20 resources = 100s of endpoints
FraiseQL vs. REST: Decision¶
| Your Priority | Better Choice | Why |
|---|---|---|
| Simple CRUD service | Custom REST | Less overhead |
| Mobile API with bandwidth concerns | FraiseQL | Query-specific fields only |
| Multi-use API (web + mobile + partners) | FraiseQL | Single flexible API |
| Team knows REST already | Custom REST | No GraphQL learning needed |
| Long-term API evolution | FraiseQL | Single versioning story |
| Fast development | REST or Hasura | Pre-built patterns |
FraiseQL's Unique Position¶
What FraiseQL Brings¶
1. PostgreSQL as the Source of Truth
Reads come from v_/tv_ views that build a data JSONB column; writes go through fn_ PostgreSQL functions. Your database team's work (indexes, views, function logic) directly improves API behavior, with no separate resolver layer to keep in sync.
2. No N+1 by Construction
Nested data is assembled inside the view's data JSONB, so a single GraphQL query maps to a single read. There are no per-field resolvers fanning out into extra queries.
3. Fast JSON Path
PostgreSQL JSONB feeds an optional Rust pipeline (fraiseql_rs) that handles field selection and JSON transformation efficiently. The framework runs in Python over FastAPI, with the schema assembled in memory at startup.
4. Minimal Application Code
Define types and operations with decorators (@fraiseql.type, @fraiseql.query, @fraiseql.mutation). No hand-written resolvers, no DataLoaders, no manual optimization patterns.
What FraiseQL Trades Off¶
Single Data Source
FraiseQL targets one PostgreSQL database. It is not designed to aggregate data from multiple external APIs in a single query—best for database-centric services.
Logic Lives in the Database
Complex write logic happens in PostgreSQL functions, and computed read fields are built into views. Teams that prefer to keep all logic in application code may find this constraining.
PostgreSQL Only
FraiseQL v1 deliberately supports PostgreSQL and leans into its strengths (JSONB, functions, views, indexes). This is a focused choice, not a missing feature—if you need a different database, FraiseQL is not the right tool.
Decision Framework: Choosing Your Approach¶
If You Answer "YES" to Most of These → Use FraiseQL¶
- Your primary data is in a PostgreSQL database
- You want fast, predictable query performance without N+1 surprises
- Your team has database expertise
- You are comfortable putting write logic in PostgreSQL functions
- Your data relationships are well-defined (not highly dynamic)
- You want minimal application code (no custom resolvers)
- You want the public API shape decoupled from your write tables
If You Answer "YES" to Most of These → Use Hasura¶
- You want to launch a GraphQL API as quickly as possible
- Your database schema is already well-designed
- Standard CRUD operations cover 80% of your use cases
- You're using PostgreSQL
- Simple permission rules are sufficient
If You Answer "YES" to Most of These → Use Apollo Server¶
- You need to aggregate data from multiple sources
- You need complex custom resolver logic
- Your team has strong JavaScript/TypeScript expertise
- Flexibility is more important than performance predictability
- You're building an API gateway or federation platform
If You Answer "YES" to Most of These → Use Custom REST¶
- This is a simple, single-purpose service
- You don't need a flexible query language
- Your team prefers REST familiarity
- Simplicity matters more than advanced features
Real-World Examples¶
Example 1: E-Commerce Platform¶
Requirements:
- Product catalog with search, filtering, recommendations
- Orders with items and order history
- User profiles and permissions
- Shopping cart state
Best Choice: FraiseQL
Why:
- Well-defined schema (products, orders, users, cart)
- Fast, predictable reads from view JSONB (catalog, recommendations)
- Performance is critical (search must be fast)
- Clear data relationships
- Database team can optimize indexes and views independently
API would include:
import fraiseql
from fraiseql.types import ID
@fraiseql.type(sql_source="v_product", jsonb_column="data")
class Product:
id: ID
name: str
price: float
rating: float
@fraiseql.query
async def product_search(info, query: str, limit: int = 10) -> list[Product]:
db = info.context["db"]
return await db.find("v_product")
@fraiseql.query
async def user_recommendations(info, user_id: ID) -> list[Product]:
db = info.context["db"]
return await db.find("v_product")
Example 2: Multi-Tenant SaaS Dashboard¶
Requirements:
- Multiple data sources (main DB, analytics DB, external services)
- Complex permission rules (tenant isolation, role-based)
- Custom computed fields (user's total spend, team metrics)
- Real-time updates via WebSocket
Best Choice: Apollo Server
Why:
- Multiple data sources (can't consolidate into one DB)
- Custom business logic needed (computations, complex auth)
- Flexibility more important than performance predictability
- Mature ecosystem for SaaS patterns
Example 3: Rapid Internal Tool¶
Requirements:
- Quick GraphQL API over an existing PostgreSQL database
- Standard CRUD operations
- Simple permission rules
- Time to launch: 1 week
Best Choice: Hasura
Why:
- Time to launch is critical
- Schema is already defined (existing database)
- Standard operations are sufficient
- Zero code = faster development
Example 4: Mobile App Backend¶
Requirements:
- Minimize bandwidth (mobile networks)
- Fetch exactly the fields needed
- Consistent schema across multiple client versions
- Performance matters (cellular networks)
Best Choice: FraiseQL or Apollo Server
Why:
- GraphQL eliminates over-fetching (good for mobile)
- FraiseQL for predictable PostgreSQL-backed performance
- Apollo Server for complex aggregation (if needed)
Summary¶
| Situation | Best Choice | Runner-Up |
|---|---|---|
| Single PostgreSQL DB, performance critical | FraiseQL | Hasura |
| Multiple data sources, complex logic | Apollo Server | WunderGraph |
| Rapid API for existing PostgreSQL | Hasura | FraiseQL |
| Flexible federation of services | WunderGraph | Apollo Server |
| Simple CRUD service | Custom REST | Hasura |
| Mobile app backend | FraiseQL | Apollo Server |
| Write logic in the database | FraiseQL | Hasura |
| Complex business logic, multi-source | Apollo Server | WunderGraph |
Related Topics¶
- Core Concepts: the building blocks of a FraiseQL API
- Database-Centric Architecture: how reads and writes map to views and functions
- Design Principles: why FraiseQL makes these tradeoffs
- Performance Characteristics: how FraiseQL stays fast
- Choosing FraiseQL: a deeper fit-for-purpose guide
- Quickstart: build your first FraiseQL API
Conclusion¶
FraiseQL is not the right tool for every job. It excels when:
- Your data is in PostgreSQL (the single source of truth)
- You want fast, predictable reads (view JSONB, no N+1)
- Your team values database expertise (views and functions carry the logic)
- You prefer simplicity over flexibility (minimal application code)
If your use case matches these criteria, FraiseQL gives you a fast, predictable GraphQL API with minimal code. If you need multi-source aggregation or extreme flexibility, other tools (Apollo Server, WunderGraph) may be better choices.
The key insight: Different tools for different jobs. Choose based on your actual constraints, not hype.