Skip to content

Dart GraphQL Complete Guide — Query Data with GraphQL in Dart

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Dart GraphQL Complete Guide. We cover key concepts, practical examples, and best practices to help you master this topic.

Dart GraphQL integration using the graphql_flutter package enables type-safe queries and mutations against GraphQL APIs, replacing REST endpoints with a flexible, declarative data-fetching approach that reduces over-fetching and under-fetching.

What You Will Learn

  • What GraphQL is and how it differs from REST
  • Setting up the graphql_flutter package in a Flutter project
  • Writing queries with variables, fragments, and arguments
  • Performing mutations to create, update, and delete data
  • Managing local cache and optimistic updates
  • Handling errors and loading states in the UI

Why It Matters

GraphQL solves fundamental problems with REST APIs. In a RESTful system, you often either get too much data (over-fetching) or have to make multiple requests to get all the data you need (under-fetching). With GraphQL, the client specifies exactly which fields it needs in a single request, and the server responds with only those fields. This reduces bandwidth, simplifies client code, and enables faster iteration when APIs evolve. In Doda Browser, the bookmarks and history sync was migrated from a REST API to GraphQL, reducing payload sizes by 60 percent and eliminating three separate endpoint calls for the settings page.

Real-World Use

A social media feed app needs to display posts with the author name, avatar, post text, image URLs, like count, and the first two comments. With REST, this typically requires three requests: one for posts, one for users, and one for comments. With GraphQL, the client sends one query describing the exact shape of the response, and the server returns all the data in a single response.

Learning Path

flowchart LR
  A[WebSockets] --> B[GraphQL\nYou are here]
  B --> C[Project Todo App]
  style B fill:#f90,color:#fff

Setting Up graphql_flutter

Add the package to your Flutter project:

dependencies:
  graphql_flutter: ^5.2.0

Then configure the GraphQL client. The GraphQLClient manages the connection to your GraphQL server, handles Caching, and processes responses.

// lib/services/graphql_service.dart
import 'package:graphql_flutter/graphql_flutter.dart';

class GraphqlService {
  late final GraphQLClient _client;
  final String endpoint;

  GraphqlService(this.endpoint) {
    final httpLink = HttpLink(endpoint);
    final cache = GraphQLCache(
      store: InMemoryStore(),
      typePolicies: {
        TypePolicy(
          typeName: 'Query',
          fields: {
            'posts': FieldPolicy.keyArgs(['id']),
          },
        ),
      },
    );

    _client = GraphQLClient(
      link: httpLink,
      cache: cache,
      defaultPolicies: DefaultPolicies(
        query: Policies(fetch: FetchPolicy.networkFirst),
        mutate: Policies(fetch: FetchPolicy.noCache),
      ),
    );
  }

  GraphQLClient get client => _client;
}

The HttpLink connects to the server. The GraphQLCache with InMemoryStore stores query results locally. The typePolicies configure how the cache normalizes and merges data.

Writing Queries

GraphQL queries request specific fields from the server. Here is a query that fetches posts with related data:

// lib/models/post.dart
class Post {
  final String id;
  final String title;
  final String body;
  final String authorName;
  final int likeCount;

  Post({
    required this.id,
    required this.title,
    required this.body,
    required this.authorName,
    required this.likeCount,
  });

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'] as String,
      title: json['title'] as String,
      body: json['body'] as String,
      authorName: json['author']['name'] as String,
      likeCount: json['likeCount'] as int,
    );
  }
}
// lib/services/post_repository.dart
import 'package:graphql_flutter/graphql_flutter.dart';
import '../models/post.dart';

class PostRepository {
  final GraphQLClient client;

  PostRepository(this.client);

  static const String postsQuery = '''
    query GetPosts(\$limit: Int) {
      posts(limit: \$limit) {
        id
        title
        body
        author {
          name
        }
        likeCount
      }
    }
  ''';

  Future<List<Post>> fetchPosts({int limit = 10}) async {
    final result = await client.query(
      QueryOptions(
        document: gql(postsQuery),
        variables: {'limit': limit},
      ),
    );

    if (result.hasException) {
      throw result.exception!;
    }

    final List<dynamic> postsJson = result.data!['posts'] as List<dynamic>;
    return postsJson.map((json) => Post.fromJson(json as Map<String, dynamic>)).toList();
  }
}

The query uses a variable ($limit) to make it reusable. The server resolves the posts root field and returns only the fields requested: id, title, body, author.name, and likeCount.

Performing Mutations

Mutations change data on the server. Here is a mutation that creates a new post:

class PostRepository {
  // ... other methods

  static const String createPostMutation = '''
    mutation CreatePost(\$title: String!, \$body: String!) {
      createPost(title: \$title, body: \$body) {
        id
        title
        body
        author {
          name
        }
        likeCount
      }
    }
  ''';

  Future<Post> createPost({required String title, required String body}) async {
    final result = await client.mutate(
      MutationOptions(
        document: gql(createPostMutation),
        variables: {'title': title, 'body': body},
      ),
    );

    if (result.hasException) {
      throw result.exception!;
    }

    return Post.fromJson(result.data!['createPost'] as Map<String, dynamic>);
  }

  static const String deletePostMutation = '''
    mutation DeletePost(\$id: ID!) {
      deletePost(id: \$id) {
        id
      }
    }
  ''';

  Future<void> deletePost(String id) async {
    final result = await client.mutate(
      MutationOptions(
        document: gql(deletePostMutation),
        variables: {'id': id},
      ),
    );

    if (result.hasException) {
      throw result.exception!;
    }
  }
}

Mutations follow the same structure as queries but use the mutation keyword. The response typically includes the mutated object so you can update the local cache without another request.

Using GraphQL in Flutter Widgets

The graphql_flutter package provides widgets that handle loading and error states automatically:

// lib/screens/post_list_screen.dart
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import '../services/post_repository.dart';
import '../services/graphql_service.dart';

class PostListScreen extends StatelessWidget {
  final repository = PostRepository(GraphqlService('https://api.example.com/graphql').client);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Posts')),
      body: Query(
        options: QueryOptions(
          document: gql(PostRepository.postsQuery),
          variables: {'limit': 20},
        ),
        builder: (QueryResult result, {VoidCallback? refetch, FetchMore? fetchMore}) {
          if (result.isLoading) {
            return Center(child: CircularProgressIndicator());
          }

          if (result.hasException) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Error: ${result.exception}', textAlign: TextAlign.center),
                  SizedBox(height: 16),
                  ElevatedButton(onPressed: refetch, child: Text('Retry')),
                ],
              ),
            );
          }

          final posts = (result.data!['posts'] as List)
              .map((json) => Post.fromJson(json as Map<String, dynamic>))
              .toList();

          return ListView.builder(
            itemCount: posts.length,
            itemBuilder: (context, index) {
              final post = posts[index];
              return Card(
                margin: EdgeInsets.all(8),
                child: ListTile(
                  title: Text(post.title),
                  subtitle: Text('by ${post.authorName} - ${post.likeCount} likes'),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

The Query widget automatically runs the query when the widget mounts, rebuilds when data arrives, and provides refetch and fetchMore callbacks.

Optimistic Updates

For a responsive UI, update the cache optimistically before the server confirms the mutation:

Future<Post> createPostOptimistic({required String title, required String body}) async {
  final optimisticPost = {
    'id': 'temp_${DateTime.now().millisecondsSinceEpoch}',
    'title': title,
    'body': body,
    'author': {'name': 'Current User'},
    'likeCount': 0,
  };

  final result = await client.mutate(
    MutationOptions(
      document: gql(createPostMutation),
      variables: {'title': title, 'body': body},
      optimisticResult: {'createPost': optimisticPost},
      update: (GraphQLProxyCache cache, QueryResult? result) {
        // Update the cache with the new post
        final existing = cache.readQuery(gql(postsQuery), variables: {'limit': 10});
        if (existing != null && result != null) {
          final posts = List<Map<String, dynamic>>.from(existing['posts'] as List);
          posts.insert(0, result.data!['createPost'] as Map<String, dynamic>);
          cache.writeQuery(gql(postsQuery), variables: {'limit': 10}, data: {'posts': posts});
        }
      },
    ),
  );

  if (result.hasException) throw result.exception!;
  return Post.fromJson(result.data!['createPost'] as Map<String, dynamic>);
}

The optimistic result shows immediately in the UI. When the server responds, the real data replaces the placeholder.

Fragments for Reusable Field Sets

GraphQL fragments let you define reusable field selections:

static const String postFields = '''
  fragment PostFields on Post {
    id
    title
    body
    author {
      name
    }
    likeCount
  }
''';

static const String postsWithFragments = '''
  query GetPostsWithFragments(\$limit: Int) {
    posts(limit: \$limit) {
      ...PostFields
    }
  }
  $postFields
''';

static const String singlePostWithFragments = '''
  query GetPost(\$id: ID!) {
    post(id: \$id) {
      ...PostFields
      comments {
        id
        text
        author {
          name
        }
      }
    }
  }
  $postFields
''';

Fragments ensure consistency: if the Post type gains a field, you update it in one place and all queries using the fragment automatically include it.

Common Mistakes

  1. Not handling partial errors: A GraphQL response can have both data and errors. Always check result.hasException even when result.data is non-null. The server may return partial data with error details for specific fields.

  2. Over-fetching in fragments: Including too many fields in a fragment causes over-fetching. Define fragments at the minimum granularity needed by each UI component.

  3. Forgetting to normalize cache: Without type policies, the cache stores query results by query text, which means the same object fetched from different queries is stored twice. Configure typePolicies with keyArgs to normalize by object ID.

  4. Ignoring pagination: Many GraphQL APIs use cursor-based pagination. Use the fetchMore method with the cursor from the response to load subsequent pages, rather than re-fetching the entire list.

  5. Hard-coding the endpoint URL: GraphQL endpoints differ between environments (development, staging, production). Use environment variables or build configuration to inject the correct URL.

  6. Not testing error states: Network failures, authentication errors, and validation errors all produce different GraphQL exceptions. Write tests that simulate each type and verify the UI response.

  7. Misusing variables in mutations: Mutation variables must match the exact names and types defined in the server schema. A typo in a variable name produces a GraphQL validation error before the mutation executes.

Practice Questions

  1. How does GraphQL prevent over-fetching compared to REST?
  2. What is the purpose of an optimistic update in a GraphQL mutation?
  3. How do GraphQL fragments improve query maintainability?
  4. What is the difference between a query and a mutation in GraphQL?
  5. Challenge: Build a GraphQL client that supports paginated comments. Use the fetchMore pattern to load 10 comments at a time with a "Load more" button. Handle the loading state and prevent duplicate fetches.

Mini Project

Build a movie database app using the TMDB GraphQL API (or use a mock GraphQL server built with package:graphql):

  • Configure a GraphQLClient with cache normalization
  • Define queries for popular movies, movie details, and search results
  • Implement a search screen with debounced input and paginated results
  • Add a detail screen showing movie info, cast, and reviews using fragments
  • Implement a "Watchlist" feature using local mutations that update the cache optimistically
  • Handle loading, error, and empty states for every screen
  • Write integration tests using MockClient from graphql_flutter

FAQ

What is the difference between GraphQL and REST?

REST uses multiple endpoints (e.g., /posts, /users) with fixed response shapes. GraphQL uses a single endpoint where the client specifies exactly which fields it needs, eliminating over-fetching and under-fetching.

Can I use GraphQL without Flutter?

Yes. The graphql package provides a pure Dart GraphQL client without Flutter dependencies. You can use it in server-side Dart, CLI apps, or any Dart project.

How do I handle authentication in GraphQL?

Pass the authentication token as an HTTP header. Create a custom AuthLink that adds the token to every request: final authLink = AuthLink(getToken: () => 'Bearer \$token'); final link = authLink.concat(httpLink);

What caching strategies should I use?

Use cache-first for data that rarely changes, network-first for frequently updated data, and no-cache for mutations. The cache policy is set per query in the Policies object.

How do I subscribe to real-time updates with GraphQL?

GraphQL subscriptions use WebSockets under the hood. Use the graphql_flutter package's Subscription widget with the WebSocketLink instead of HttpLink for the connection.

What is Next

Proceed to Project Todo App to build a complete Flutter application that combines networking, state management, and UI skills. Then check out Project Weather App for API integration patterns.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro