GraphQL – Get More by Fetching Less

Since I started developing mobile apps, REST has seemed to be a standard for designing web APIs. However, I’d never imagined fetching data from the server could be easier than sending a request to a specific endpoint and fetching all the needed data.

I wasn’t always happy that, in response, I would get extra data that I didn’t exactly need. But the backend had to provide data to multiple applications, and creating different endpoints for each one of those applications wasn’t even an option.

One day I came across an article with REST and GraphQL comparison. That was a pro-GraphQL article, and I had to try it by myself. In this blog post, I will explain my first steps of learning GraphQL from the client-side – as a Flutter developer.

REST and GraphQL comparison

First, I will determine the two models that will be used in the following examples.

class Article {
    int id;
    int user_id;
    String title;
    String subtitle;
    String abstract;
    String body;
    String cover_url;
    DateTime created_at;
    DateTime updated_at;
    DateTime deleted_at;
}
class Comment {
    int id;
    int user_id;
    int article_id;
    String content;
    DateTime created_at;
    DateTime updated_at;
    DateTime deleted_at;
}

The core idea behind REST is working with resources. Each of those resources is represented with an endpoint. Fetching data from an endpoint is pretty simplified; the client only has to request a specific predefined endpoint. If there is a need to filter data or fetch more data, the next step would be to send specific request parameters.

For instance, if there is a need to fetch a list of articles and related comments using REST, the client would most likely send one of the next sets of requests with accompanying responses:

1. GET /articles or GET /articles?extra=comments

{
    "data":[
        {
            "id":1,
            "title":"title1",
            "subtitle":"subtitle",
            "abstract":"abstract",
            "body":"post body",
            "user_id":17,
            "comments":[
                {
                    "id":1,
                    "user_id":72,
                    "article_id":1,
                    "content":"comment-content",
                    "created_at":"2022-03-16T14:12:41.918743",
                    "updated_at":"2022-03-16T14:14:51.639264",
                    "deleted_at":null
                },
                {
                    "id":2,
                    "user_id":17,
                    "article_id":1,
                    "content":"comment-content",
                    "created_at":"2022-03-18T15:24:45.118149",
                    "updated_at":null,
                    "deleted_at":null
                }
            ],
            "created_at":"2022-03-16T12:32:44.719742",
            "updated_at":null,
            "deleted_at":null
        }
    ]
}

Both of these endpoints could provide the same response. The decision if there is a need to send extra parameters to get comments depends on backend implementation. However, this response has a lot of redundant data.

If it were made specifically for one application, that wouldn’t be the case, but, as said in the introduction, when the backend has to support more than one application, it will often provide more data than needed.

2. GET /articles

{
    "data":[
        {
             "id":1,
             "title":"title1",
             "subtitle":"subtitle",
             "abstract":"abstract",
             "body":"post body",
             "user_id": 17,
             "created_at":"2022-03-16T12:32:44.719742",
             "updated_at":null,
             "deleted_at": null
        }
    ]
}

3. GET /articles/1/comments

{
  "data":[
    {
      "id":1,
      "user_id":72,
      "article_id":1,
      "content":"comment-content",
      "created_at":"2022-03-16T14:12:41.918743",
      "updated_at":"2022-03-16T14:14:51.639264",
      "deleted_at":null
    },
    {
      "id":2,
      "user_id":17,
      "article_id":1,
      "content":"comment-content",
      "created_at":"2022-03-18T15:24:45.118149",
      "updated_at":null,
      "deleted_at":null
    }
  ]
}

These two code snippets show that endpoints for different objects are separated in some implementations. As one can notice, the same problem with redundant data also appears here. There is also another thing that slows down the process of fetching needed data, which is a need to make more than one API request, one to fetch the list of articles and others to fetch comments for each specific article.

In comparison, the main idea behind GraphQL is defining a structure of needed data in the request. This way, under-fetching, and over-fetching data are avoided, and everything needed is provided at one request. Therefore, if a client needs to fetch the title and body of each article and the accompanying comments content, the GraphQL request would be:

query {
  articles {
    title
    body
    comments {
      content
    }
  }
}

The response to that is pretty straightforward:

{
  "data":[
    {
      "title":"title1",
      "body":"post body",
      "comments":[
        {
          "content":"comment-content"
        },
        {
          "content":"comment-content"
        }
      ]
    }
  ]
}

There is no need for more than one request and no redundant data.

Based on this comparison, GraphQL is much easier to use from the client’s perspective because the client asks for specific data and gets the exact needed data, nothing more nor less.

Flutter + GraphQL

Is there a better way to learn new technology than trying it out? For this purpose, I decided to make a small mobile application that uses GraphQL for API requests.

Firstly, I needed to set up a simple API from which I could fetch data for my application. Since this blog post isn’t about the backend implementation of GraphQL, I decided to make it as simple as possible.

For that, I used the Hasura GraphQL engine and created a small database there, entered some initial data and the “backend” was ready to make my first GraphQL queries.

For GraphQL integration into the Flutter application, I used graphql_flutter package. Installation and usage is pretty simple, one just needs to follow steps from their instructions page.

As it is written in the official graphql_flutter documentation, for connecting to GraphQLServer I needed to create a GraphQLClient. Hakura provided me with an API link and an authorization token.

final HttpLink httpLink = HttpLink(
  'https://relevant-monarch-52.hasura.app/v1/graphql',
  defaultHeaders: {
    'X-hasura-admin-secret': '**********************',
  },
);

Nothing more than that was needed for connecting to the API, and after writing only a few lines of code, I was able to query and mutate data! The next step was wrapping my MaterialApp with a GraphQLProvider widget so I could use Query and Mutation widgets everywhere in the application.

The homepage of my application shows a list of Q blog articles with accompanying titles, subtitles, and cover images. I’ve wrapped a ListView with a Query widget and created a simple multiline query for fetching needed data. The query string is parsed to the standard GraphQL abstract syntax tree using the gql() method from the graphql_flutter package.

static String getArticles = """
  query Articles {
    articles {
      id
      title
      subtitle
      cover_url
    }
  }
""";
…
…    
child: Query(
  options: QueryOptions(
    document: gql(getArticles),
  ),
  builder: (QueryResult result, VoidCallback refetch, FetchMore fetchMore}) {
    // check is result successfully loaded
    List articles = result.data['articles'];
    return ListView.builder(
      itemCount: articles.length,
      itemBuilder: (context, index) {
        return BlogListItem(
          article: articles[index],
        );
      },
    );
  },
),
…

I wanted to show more data related to a specific article on the second screen. For that need, I had to fetch an article by ID with its title, content, cover image, and related comments.

The query that fetches needed data is:

static String getArticleById = """
  query GetArticleById(\$id: Int!) {
    articles(where: {id: {_eq: \$id}}) {
      body
      title
      cover_url
      comments(order_by: {created_at: desc}) {
        content
        created_at
        id
      }
    }
  }
""";

The biggest difference from the previous request, where I fetched the list of articles, is that in this example, in QueryOptions, I had to send an ID parameter for filtering results.

Query(
    options: QueryOptions(
        document: gql(
            getArticleById,
        ),
        variables: {
            'id': blogId,
        },
    ),
...
),

In GraphQL, any request that causes a change in the database is called a mutation. For that purpose, the graphql_flutter package provides a Mutation widget. In my app, I added functionality to add, edit and delete comments. The following three code snippets show a Mutation request for each of those functionalities.

static String addComment = """
  mutation AddComment(\$content: String!, \$articleId: Int!, \$userId: Int!) {
    insert_comments_one(
      object: {content: \$content, article_id: \$articleId, user_id: \$userId}
    ) {
      content
    }
  }
""";
static String editComment = """
  mutation EditComment(\$commentId: Int!, \$editedComment: String!) {
    update_comments_by_pk(pk_columns: {id: \$commentId}, _set: {content: \$editedComment}) {
      content
    }
  }
""";
static String deleteComment = """
  mutation DeleteComment(\$commentId: Int!) {
    delete_comments_by_pk(id: \$commentId) {
      id
    }
  }
""";
The following code is an example of using the Mutation widget for deleting a comment item.
…
return Mutation(
    options: MutationOptions(
        document: gql(deleteComment),
        // you can update the cache based on results
        update: (GraphQLDataProxy cache, QueryResult result) {
            return cache;
        },
        // or do something with the result.data on completion
        onCompleted: (dynamic resultData) {
            print(resultData);
        },
    ),
    builder: (
        RunMutation runMutation,
        QueryResult result,
    ) {
        return IconButton(
            onPressed: () => runMutation({'commentId': commentId}),
            icon: Icon(Icons.delete),
        );
    },
);
…

Conclusion

In this blog, I have described my path of learning GraphQL from the client’s side and building my first Flutter app that uses GraphQL. Since I used the Hasura GraphQL engine to create the API, I developed a simple app with CRUD operations without needing to write one line of code on the backend.

The thing I like most about GraphQL is the simplicity fetching and modifying needed data. I haven’t noticed a difference in response time because small requests I made are still countable in milliseconds.

In the case of this particular app, I can say that Hasura and the graphql_flutter package played a huge role in making it so easy to learn and develop. From a Flutter developer’s side, I would like to try using GraphQL even for more complex projects!


Leave a Reply

Your email address will not be published. Required fields are marked *