Instagram Clone with GraphQL through Hasura

Hashnode Hasura Hackathon

Β·

9 min read

Picked Up Flutter & Dart 🎯

I started to learn Flutter in Dec'2021. Mostly to get acquainted with it, as my team was going to be using it in a big way.

I absolutely loved it from the get-go and used it to build progressively more complex apps over a few weeks. One of those was a partial clone for Expedia, the popular travel app. You can check it out here, if you'd like. The APIs were hosted on mockAPI.

Instagram Clone: REST for a bit, then GraphQL πŸ™‚

Subsequently, I thought I'll develop a clone of Instagram, as it will allow me to solidify my Flutter knowledge and build on it further. For this app though, I decided to hack together a REST API in Java and run it locally (and host on AWS later).

Early into it, I realized that a GraphQL API would be more useful for this app. A few reasons:

  • The various screens in the app needed similar but not exactly the same data. Having the ability to flexibly ask for exactly what was needed would help.
  • While I was presently building for the mobile form factor, a web interface (which I'm looking to do next) would look different and hence will have different data needs, across the various screens.
  • If the entire data set was available through a graph, new ideas could be thought of and implemented rather quickly.

GraphQL.com shares something similar. It says, GraphQL lets you:

Iterate quickly on apps without waiting for new backend endpoints. Simplify data fetching and management code by getting the data in the shape you need. Make your apps more responsive than ever before by only loading the data you're actually using, and reduce the number of roundtrips to fetch all of the resources for a particular view.

Enter Hasura 😈

As I was starting to move the back-end from Springboot to GraphQL, I came across Hasura and decided to give it a whirl (as against doing the GraphQL implementation by myself). And a bit later on, found out about the hackathon through their LinkedIn page. So, I thought I'll work towards building this by the 31st of March.

So, this is what I currently have (not a complete clone yet but getting close) πŸ™‚

Following is how Hasura works at a high level. You point it to your DB (in my case, a POSTGRES running on Heroku) and within minutes, it provides you a production grade GraphQL API on that data. The picture below is from Hasura's website:

hasura.png

Alright, on to the functionality now. There is a lot of functionality I added but here are some major ones:

  • Display Home Feed for the logged in user.
  • Display User feed for a user when their profile is clicked.
  • Ability to Add/Remove a Like on a Post.
  • Ability to Add a Comment on a Post.
  • Ability to Add/Remove Bookmark on a Post.
  • See Network Activity for the logged in user.

How the Home Feed Works? Rough outline!

I'll do a walkthrough of how I build the Home Feed. Below is the outline of a crude algorithm I follow:

  1. Find all the users that the logged-in user follows.
  2. For every user being followed, get the username and the url of their profile picture.
  3. For every user being followed, get all their posts ( it’s a small number for now, pre-populated in the database).
  4. For every post, get the total Likes.
  5. For every post, get details of the likes, to see if this current user has liked the post or not. This helps show the appropriate Like icon, empty or filled, on the UI (see following image).
  6. For every post, get the total number of Comments. No details on comments needed until a post is clicked.
  7. For every post, get details of the bookmarks, to see if this current user has bookmarked the post or not. Again, this helps show the appropriate Bookmark icon on the UI.

LikeBookmark.png

How the Home Feed Works? The GraphQL stuff!

I'm assuming some familiarity with GraphQL for the following section.

The following is what my Home Feed query looked like:

query getHomeFeed(\$username: String) {
  follow(where: {follower: {_eq: \$username}}) {
    followingUserDetails {
      username
      profileImageSource
      posts {
        postId
        caption
        imageUrl
        likestotal {
          totalLikes
        }
        likesdetails(where: {username: {_eq: \$username}}) {
            username
        }
        bookmarks(where: {username: {_eq: \$username}}) {
          username
        }
        commentstotal {
          totalComments
        }
      }
    }
  }
}

I'll explain this a bit now.

I start things off with a 'follow' table which has the following structure (ignore the 'roughTime' column for now):

FollowTableStructureDetails.png

The Primary Key is the combination of the 'follower' username and the 'following' username. There is an Object Relationship with the 'users' table for both the follower and the user being followed.

Based on the Foreign Key constraints defined at create-table time, Hasura makes it super easy to build the relationships. It supports 2 types of relationships: More on relationships here. Pretty intuitive.

Below are the relationships for the 'follow' table:

FollowRelationships.png

With the help of these relationships, we grab the following info for every user that the current logged-in user follows:

  1. username
  2. profileImageSource (url for profile image)

The 'users' table further has a relationship with the 'posts' table. This is an Array Relationship, because 1 user will have many posts in the system (your classic 1:Many relationship). Here is how the relationship looks:

UserAndPostsRelationship.png

The 'posts' table has 1 unique row per post in the system. Below is the structure of the Posts table for reference:

PostsTableStructureAndForeignKey.png

There are a few more relationships that emanate from the 'posts' table (as seen below):

PostsAllRelationships.png

These relationships help us get info on the total likes on a post, whether the current user has liked or bookmarked the post, and the total number of comments on the post.

In the current implementation, the home feed algorithm doesn’t do anything much smarter beyond this. It simply renders all these posts. After doing a bit of shuffle. For fun πŸ™‚

var usersFollowed = result.data!['follow'];
List<FeedItem> all = [];
.....
all.shuffle();
return buildFeedListView(all);

This renders the Home Feed as shown at the start of the video above. Obviously, rendering Home Feeds in a full blown social media app is much much more involved. Ranking, Push, Pull, Hybrid, the works πŸ™‚

Now, one could also view the feed from a single user (through their profile). Below is the query that gets run for that:

query getUserFeed(\$username: String) {
  users(where: {username: {_eq: \$username}}) {
    profileImageSource
    username
    posts { 
      postId
      caption
      imageUrl
      likestotal {
        totalLikes
      }
      likesdetails(where: {username: {_eq: \$username}}) {
        username
      }
      bookmarks(where: {username: {_eq: \$username}}) {
        username
      }
      commentstotal {
        totalComments
      }
    }
  }
}

Almost the same as the home feed query, except the first part (finding the users being followed).

Enough of Queries, let's Mutate Something!

Querying is an interesting aspect of working with servers. But unless you can make changes too, things aren’t much fun! So, we’ll now look at a mutation.

All operations that cause a write on the server side are done through Mutations in GraphQL.

For instance, we click on a post and then add a comment.

1011.png

The comment "adds" new stuff to the server. Hence done through a mutation. Below is what the GraphQL Mutation for this looks like:

mutation addComment(\$postId: Int!, \$username: String, \$contents: String, \$roughTime: String, \$newTotalComments: Int) {
  insert_commentdetails (
    objects: {
      postId: \$postId,
      username: \$username,
      contents: \$contents,
      roughTime: \$roughTime,
    }
  )
  {
  affected_rows
  }
  update_commentstotal_by_pk (
    pk_columns: {postId: \$postId}
    _set: { totalComments: \$newTotalComments }
  ) 
  {
      totalComments
  }
}

You’ll see 2 interesting operations in there.

  1. insert_commentdetails
  2. update_commentstotal_by_pk

What are these?

Well, there is a slew of mutations that Hasura puts together for you as it creates the GraphQL API:

Mutations.png

These provide simple and robust ways (right out of the box) to modify your data. I used a subset of these to:

  1. Add a comment and also update the total number of comments.
  2. Add a like or Remove a like. Also update the total number of likes accordingly.
  3. Add or Remove a bookmark.

A quick look at the table structures for 'commentsdetails' and 'commentstotal' should help you understand the mutation above. It's very straightforward and the mutation names help understand their intent easily:

Comments Details: CommentDetailsTablee.png

Comments Total: CommentTotalTable.png

I should mention that any new comment updates the total number of comments appropriately, but the initial total numbers that I've put in the 'commentstotal' table are random (just to make the UI feel more real without going through the task of adding 20, 30 or 50 comments). The same goes for the total number of likes.

Next Steps

With a combination of some queries and mutations, I was able to implement a whole bunch of stuff in the app.

Only 2 flows are still on the older REST API:

  1. Saved Collections by a User.
  2. Uploading a new post.

My next endeavor (beyond 31st March) will be to move these to the GraphQL API as well. 3 new things to add too:

  1. Ability to Sign-Up and Sign-In User (this would have been my first priority if I was building for the Hackathon right from the get-go).
  2. Ability to Add a Story.
  3. Possibly the ability to Add Reels.

On the technical front, I want to add some Widget tests too (which I did in my previous Flutter projects), as I think they add immense value.

Hasura Love πŸ’š

Working with Hasura was such a breeze!

It helped me very very quickly move the application from having 0% of the back-end in GraphQL to having 80-85% of the back-end in GraphQL. And it worked beautifully πŸ™‚

I especially loved the highly intuitive user interface for Hasura. It has been a while that I saw such a well laid-out and intuitive user interface for a developer-focussed product. Everything was so easy to figure out.

The integration with Heroku worked without a hitch too.

Lastly, the documentation from Hasura was really comprehensive and helped me get unstuck quickly on more than a couple of occasions.

Bonus: I like it when you shimmer! ✨✨

Both Hasura and Heroku deployed my apps in the US region:

Regions.png

This meant that all my queries took slightly longer to come back (as compared to when I was running my API locally or a preferred local region in AWS). As a result, I had to start using the Shimmer Dart package to show pleasing progress indicators while my various screens loaded. Took a bit to understand how to make it work well, but I was happy with the results in the end πŸ™‚

Shimmer123.png

Shimmer467.png

The user experience with this was so much better as compared to showing a circular progress bar right dab in the middle of the screen!

That's a wrap!

It was super fun working with Hasura while also continuing on my Flutter journey. Would love to build the remaining flows through the GraphQL API soon. And share an update.

Here is the link to the Github repository if you'd like to check out the code in detail. Some parts still need to be cleaned-up and made better post the initial deadline.

Credits

Some absolutely brilliant and stunning images from Unsplash. They've got a great collection of images that I've used for a lot of my content. Youtube videos and blogs. Check them out!

Β