Skip to content

FraiseQL vs Apollo

Apollo is the most popular GraphQL framework. Here’s how FraiseQL differs — and when each is the right choice.

Apollo requires you to write resolver functions for every field and relationship.

FraiseQL eliminates resolvers entirely by mapping your schema to optimized database views. It runs as a compiled Rust binary with no resolver code needed.

AspectFraiseQLApollo Server
ArchitectureCompiled Rust binary, database-firstRuntime, resolver-based
Resolver codeNoneRequired for every field
N+1 handlingEliminated by designDataLoader (manual setup)
ConfigurationTOMLJavaScript/TypeScript
Schema definitionCode (Python, TS, Go…)SDL or code-first
PerformancePredictable, sub-msDepends on resolvers
SubscriptionsNative (WebSocket + NATS)Requires Apollo Server + PubSub setup
Schema federationBuilt-in multi-databaseApollo Federation (separate package)
Learning curveLowerHigher
FlexibilityDatabase-centricUnlimited
// Apollo Server
const resolvers = {
Query: {
users: async (_, args, context) => {
return context.db.query('SELECT * FROM users');
},
user: async (_, { id }, context) => {
return context.db.query('SELECT * FROM users WHERE id = $1', [id]);
},
},
User: {
posts: async (user, _, context) => {
// N+1 problem without DataLoader!
return context.db.query(
'SELECT * FROM posts WHERE author_id = $1',
[user.id]
);
},
},
Post: {
author: async (post, _, context) => {
return context.db.query(
'SELECT * FROM users WHERE id = $1',
[post.author_id]
);
},
comments: async (post, _, context) => {
return context.db.query(
'SELECT * FROM comments WHERE post_id = $1',
[post.id]
);
},
},
Comment: {
author: async (comment, _, context) => {
return context.db.query(
'SELECT * FROM users WHERE id = $1',
[comment.author_id]
);
},
},
};

Lines of resolver code: 40+ Potential N+1 queries: 4 DataLoaders needed: 3+

# FraiseQL
@fraiseql.type
class User:
id: str
name: str
posts: list['Post']
@fraiseql.type
class Post:
id: str
title: str
author: User
comments: list['Comment']
@fraiseql.type
class Comment:
id: str
content: str
author: User

Lines of code: 15 (Python) / 24 (TypeScript) / 21 (Go) Potential N+1 queries: 0 DataLoaders needed: 0

// You must manually implement DataLoaders
const userLoader = new DataLoader(async (ids) => {
const users = await db.query(
'SELECT * FROM users WHERE id = ANY($1)',
[ids]
);
return ids.map(id => users.find(u => u.id === id));
});
// And wire them up in context
const context = {
loaders: {
user: userLoader,
post: postLoader,
comment: commentLoader,
}
};
// And use them in every resolver
User: {
posts: (user, _, { loaders }) => loaders.post.loadMany(user.postIds)
}
-- Composed views: tb_ tables, v_ views, child .data embedded in parent
CREATE VIEW v_comment AS
SELECT c.id, c.fk_post,
jsonb_build_object('id', c.id, 'content', c.content) AS data
FROM tb_comment c;
CREATE VIEW v_post AS
SELECT p.id, p.fk_user,
jsonb_build_object(
'id', p.id, 'title', p.title,
'comments', COALESCE(jsonb_agg(vc.data), '[]'::jsonb)
) AS data
FROM tb_post p
LEFT JOIN v_comment vc ON vc.fk_post = p.id
GROUP BY p.id, p.fk_user;
CREATE VIEW v_user AS
SELECT u.id,
jsonb_build_object(
'id', u.id, 'name', u.name,
'posts', COALESCE(jsonb_agg(vp.data), '[]'::jsonb)
) AS data
FROM tb_user u
LEFT JOIN v_post vp ON vp.fk_user = u.id
GROUP BY u.id;

One query. Zero N+1. No DataLoaders.

schema.graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
author: User!
}
type Query {
users: [User!]!
user(id: ID!): User
}

Plus all the resolver code shown above.

@fraiseql.type
class User:
id: str
name: str
email: str
posts: list['Post']
@fraiseql.type
class Post:
id: str
title: str
author: User

Queries and mutations are derived from your SQL views and functions.

// Apollo Server subscriptions require a PubSub implementation
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const resolvers = {
Subscription: {
userCreated: {
subscribe: () => pubsub.asyncIterator(['USER_CREATED']),
},
},
Mutation: {
createUser: async (_, { input }) => {
const user = await db.createUser(input);
// Must manually publish to subscribers
pubsub.publish('USER_CREATED', { userCreated: user });
return user;
},
},
};

For production, graphql-subscriptions must be replaced with a scalable PubSub backend (Redis, MQTT, etc.) and managed separately.

@fraiseql.subscription(entity_type="User", topic="user_created")
def user_created() -> User:
"""Subscribe to new users via NATS."""
pass

FraiseQL subscriptions are backed by NATS messaging. When a PostgreSQL function (fn_*) completes a write, FraiseQL automatically publishes the event — no manual pubsub.publish() calls needed.

Apollo Federation: Composing Multiple GraphQL Services

Section titled “Apollo Federation: Composing Multiple GraphQL Services”

Apollo Federation is designed to compose multiple independent GraphQL subgraph services into a unified supergraph. Each subgraph is a separate process with its own schema, and the Apollo Router stitches them together at the gateway layer.

users-subgraph/schema.graphql
type User @key(fields: "id") {
id: ID!
name: String!
}
# posts-subgraph/schema.graphql
type Post {
id: ID!
title: String!
author: User @provides(fields: "name")
}
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post!]!
}

Each subgraph is a separate deployed service. You run an Apollo Router instance in front of them. Federation resolves cross-service references at the gateway level with additional network hops.

FraiseQL Federation: Connecting Multiple Databases in One Process

Section titled “FraiseQL Federation: Connecting Multiple Databases in One Process”

FraiseQL federation connects multiple databases (PostgreSQL, MySQL, SQLite, SQL Server) inside a single compiled Rust binary. There is no gateway layer, no inter-service network calls, and no subgraph protocol.

fraiseql.toml
[[databases]]
name = "users_db"
url = "${USERS_DB_URL}"
[[databases]]
name = "analytics_db"
url = "${ANALYTICS_DB_URL}"
@fraiseql.type
class User:
id: str
name: str
# analytics field resolved from analytics_db at SQL level
page_views: int

FraiseQL joins across databases at the SQL view layer — the result is a single query executed by the Rust binary, with no inter-process communication.

Apollo FederationFraiseQL Federation
Unit of federationGraphQL subgraph servicesDatabases
ArchitectureDistributed (multiple processes + gateway)Single process
Cross-unit joinNetwork hop through RouterSQL JOIN in view
DeploymentRouter + N subgraph servicesSingle Rust binary
Use caseIndependent teams, polyglot servicesMultiple databases, monolithic backend

Use Apollo Federation when you need to compose independently deployed services owned by separate teams. Use FraiseQL federation when your data lives in multiple databases but you want a single cohesive backend.

Apollo relies on decorators and custom validation logic:

// Apollo Server with @apollo/server and custom validators
import { GraphQLInputObjectType } from 'graphql';
const userInputValidator = new GraphQLInputObjectType({
name: 'UserInput',
fields: {
email: {
type: GraphQLString,
validate: (value) => {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
throw new Error('Invalid email');
}
}
}
}
});
  • Runtime validation — Happens during query execution
  • Decorator-based — Use @constraint directives
  • Limited scope — Basic rules only (~5-8 validators)
  • Manual implementation — Custom validators for complex logic
  • Database risk — Invalid data can reach the database

FraiseQL enforces validation during schema compilation:

[fraiseql.validation]
email = { pattern = "^[^@]+@[^@]+\\.[^@]+$" }
age = { range = { min = 0, max = 150 } }
status = { enum = ["active", "inactive"] }
  • 13 built-in validators across 4 categories
    • Standard: required, pattern, length, range, enum, checksum
    • Cross-field: comparison operators, conditionals
    • Mutual exclusivity: OneOf, AnyOf, ConditionalRequired, RequiredIfAbsent
  • Compile-time enforcement — Invalid schemas impossible at runtime
  • Zero overhead — No per-request validation overhead
  • Database protection — Invalid data never reaches the database
AspectFraiseQLApollo Server
Built-in validators13 rules~5-8 (via decorators)
Compile-time enforcement✅ Yes❌ No
Runtime validation overheadNonePer-request
Mutual exclusivityOneOf, AnyOf, ConditionalRequired, RequiredIfAbsent@oneOf only (via GraphQL spec)
Cross-field validation✅ Yes❌ Custom code required
Learning curveLowHigh

Apollo is a better choice when:

  • You need maximum flexibility — Custom data sources, REST APIs, microservices
  • Your data doesn’t come from a database — Third-party APIs, computed fields
  • You want the ecosystem — Apollo Client, Apollo Studio, Apollo Router
  • You need GraphQL Federation at scale — Apollo Federation is mature
  • Your team knows the resolver pattern — Familiar mental model

FraiseQL is a better choice when:

  • Your data is in a database — PostgreSQL, MySQL, SQLite, SQL Server
  • You want zero resolver code — Define types, get API
  • You need guaranteed performance — No N+1, ever
  • You prefer less code — 15 lines vs 40+ lines
  • You want simple deployment — Single Rust binary vs Node.js + DataLoaders
  1. Create a simple Apollo Server:

    Terminal window
    npm init -y
    npm install apollo-server graphql

    Create index.js:

    const { ApolloServer, gql } = require('apollo-server');
    const typeDefs = gql`
    type User {
    id: ID!
    name: String!
    posts: [Post!]!
    }
    type Post {
    id: ID!
    title: String!
    author: User!
    }
    type Query {
    users: [User!]!
    }
    `;
    const resolvers = {
    Query: {
    users: () => [{ id: "1", name: "Alice", posts: [] }]
    },
    User: {
    posts: (user) => [{ id: "p1", title: "Hello", author: user }]
    },
    Post: {
    author: (post) => ({ id: "1", name: "Alice", posts: [] })
    }
    };
    const server = new ApolloServer({ typeDefs, resolvers });
    server.listen().then(({ url }) => {
    console.log(`Server ready at ${url}`);
    });
  2. Test the Apollo API:

    Terminal window
    node index.js &
    curl -X POST http://localhost:4000 \
    -H "Content-Type: application/json" \
    -d '{"query": "{ users { name posts { title } } }"}'

    Works, but requires 30+ lines of resolver code for simple types.

  3. Now try the same with FraiseQL:

    # schema.py - just 10 lines
    import fraiseql
    @fraiseql.type
    class User:
    id: str
    name: str
    posts: list['Post']
    @fraiseql.type
    class Post:
    id: str
    title: str
    author: User
    Terminal window
    fraiseql run
    curl -X POST http://localhost:8080/graphql \
    -H "Content-Type: application/json" \
    -d '{"query": "{ users { name posts { title } } }"}'

    Same result, zero resolver code, automatic database integration.

  4. Verify N+1 handling:

    In Apollo, you’d need DataLoader to avoid N+1:

    // Apollo requires manual DataLoader setup
    const DataLoader = require('dataloader');
    const userLoader = new DataLoader(async (ids) => {
    // Batch load users
    });

    In FraiseQL, N+1 is impossible by design — the SQL view already JOINs the data.

Extract types from your SDL:

type User {
id: ID!
name: String!
posts: [Post!]!
}
@fraiseql.type
class User:
id: str
name: str
posts: list['Post']

Apollo resolver:

Mutation: {
createUser: async (_, { input }, context) => {
const user = await context.db.insert('users', input);
await sendWelcomeEmail(user);
return user;
}
}

FraiseQL observer (the framework handles CRUD, you react to events):

@observer(
entity="User",
event="INSERT",
actions=[
email(
to="{email}",
subject="Welcome to our platform!",
body="Hi {name}, thanks for signing up.",
),
],
)
def on_user_created():
"""Send welcome email when user is created."""
pass

They’re not needed. Delete them.

Migrating from Apollo? See the step-by-step migration guide.

ChooseWhen
ApolloMaximum flexibility, non-database sources, existing expertise
FraiseQLDatabase-first, zero resolvers, guaranteed performance

Apollo gives you more control. FraiseQL gives you less code.


Performance Benchmarks

See how FraiseQL performs compared to Apollo with real numbers. View Benchmarks

Migrate from Apollo

Step-by-step guide to moving your existing Apollo API. Migration Guide

How FraiseQL Works

Understand the compiled, database-first architecture. Core Concepts