Lesson 52 of 77 intermediate

GraphQL vs REST: When to Choose Which

Understanding query languages, pagination strategies, and caching tradeoffs for senior interviews

Open interactive version (quiz + challenge)

Real-world analogy

REST is like a restaurant with a fixed menu — you order dish #3 and get exactly what's listed. GraphQL is like ordering à la carte — you tell the kitchen exactly which ingredients you want on your plate and nothing more, but the kitchen needs more sophisticated infrastructure to handle custom orders.

What is it?

GraphQL is a query language and runtime for APIs that lets clients specify exactly the data they need, while REST is an architectural style using HTTP methods and resource-based URLs. Each has distinct caching, tooling, and performance characteristics.

Real-world relevance

A fintech claims app uses REST for its payment processing endpoints (leveraging HTTP caching and simple audit logs) but adds a GraphQL layer for its mobile client's dashboard — allowing the app to fetch claim status, related documents, and user profile in a single query instead of three separate REST calls.

Key points

Code example

// graphql_flutter — typed query with ferry
// schema.graphql excerpt:
// type Claim { id: ID!, status: ClaimStatus!, amount: Float!, documents: [Document!]! }

// Generated with ferry + build_runner:
// flutter pub run build_runner build

import 'package:ferry/ferry.dart';
import 'package:ferry_flutter/ferry_flutter.dart';
import 'claims_query.req.gql.dart';
import 'claims_query.data.gql.dart';

class ClaimsDashboard extends StatelessWidget {
  const ClaimsDashboard({super.key, required this.userId});
  final String userId;

  @override
  Widget build(BuildContext context) {
    final client = context.read<Client>();
    final request = GClaimsQueryReq((b) => b..vars.userId = userId);

    return Operation(
      client: client,
      operationRequest: request,
      builder: (context, response, error) {
        if (response?.loading ?? true) return const CircularProgressIndicator();
        if (error != null) return ErrorView(error: error);

        final claims = response!.data!.claims;
        return ListView.builder(
          itemCount: claims.length,
          itemBuilder: (_, i) => ClaimTile(claim: claims[i]),
        );
      },
    );
  }
}

// Cursor pagination query
const claimsQuery = r'''
  query ClaimsQuery($userId: ID!, $after: String, $first: Int = 20) {
    claims(userId: $userId, after: $after, first: $first) {
      edges { node { id status amount } cursor }
      pageInfo { hasNextPage endCursor }
    }
  }
''';

Line-by-line walkthrough

  1. 1. ferry generates type-safe request/response classes from your SDL schema at build time — GClaimsQueryReq and GClaimsQueryData are Dart classes, not raw maps.
  2. 2. GClaimsQueryReq builder sets typed variables (vars.userId) — compile-time safety means typos in field names are caught before runtime.
  3. 3. Operation widget (from ferry_flutter) subscribes to the client's response stream and rebuilds automatically when data, loading state, or errors change.
  4. 4. response?.loading ?? true handles the initial state where response is null before the first server reply arrives.
  5. 5. The query string uses cursor-based pagination — $after accepts the endCursor from the previous page's pageInfo.
  6. 6. edges { node { ... } cursor } is the Relay-style pagination pattern — each edge carries the item and its cursor position.
  7. 7. pageInfo.hasNextPage tells the UI whether to show a 'load more' button or trigger the next fetch on scroll.
  8. 8. The client is read from context via Provider — ferry's Client is typically provided at the app root with configured link chain (auth, error, http).

Spot the bug

// Attempting a GraphQL subscription for new messages
final subscriptionRequest = GNewMessageSubReq(
  (b) => b..vars.channelId = channelId,
);

Operation(
  client: client,
  operationRequest: subscriptionRequest,
  builder: (context, response, error) {
    if (response?.data == null) return const SizedBox();
    final msg = response!.data!.newMessage;
    return MessageBubble(message: msg);
  },
);
Need a hint?
The subscription connects but only ever shows one message — new messages don't appear. What's the architectural mistake?
Show answer
Bug: the Operation widget re-renders on each subscription event but replaces the single displayed message rather than accumulating messages into a list. Subscriptions emit individual events, not full lists. Fix: use a StreamBuilder or listen to the client's responseStream imperatively, accumulating messages into a local state list. With flutter_bloc or Riverpod: use an AsyncNotifier that listens to the subscription stream and adds each new message to a List<Message> state, then display the full list. The Operation widget pattern works for queries/mutations but is awkward for accumulating subscription events.

Explain like I'm 5

Imagine you ask a waiter for a burger. With REST, the waiter always brings the burger with fries, salad, and a drink — even if you only wanted the burger. With GraphQL, you tell the waiter exactly what you want: 'just the burger patty and the bun, no extras.' You get exactly what you asked for, nothing more.

Fun fact

GitHub migrated its public API from REST to GraphQL (v4) because mobile clients were making 300+ REST calls to render a single pull request page. The GraphQL API reduced that to a single query.

Hands-on challenge

Design a GraphQL schema for a multi-workspace chat app: define types for Workspace, Channel, Message, and User. Write a query that fetches a channel's last 20 messages with cursor pagination, and a subscription for new messages. Implement a basic Operation widget in Flutter using graphql_flutter that handles loading, error, and data states.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter Interview Mastery