🎉 New release for Pusher Chatkit - Webhooks! Extend your in-app chat functionality
Hide
Products
chatkit_full-logo

Extensible API for in-app chat

channels_full-logo

Build scalable realtime features

beams_full-logo

Programmatic push notifications

Developers

Docs

Read the docs to learn how to use our products

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Sign in
Sign up

Build an Instagram clone with Ionic: Part 2 - Connecting to GraphQL servers using Apollo client

  • Oreoluwa Ogundipe
June 27th, 2019
You will need Node 10+, Node Package Manager 6+, Cordova 8+ and Docker 18+ installed on your machine.

In the previous part of this series, we looked at how to set up the interface for our Instagram clone application. One thing though, was that we used static data to populate the application. In this part of the series, we will create the backend server for the application that will serve data to the application.

Pre-requisites

  • You should have followed through the first part of the series.
  • Basic knowledge of JavaScript.
  • Node installed on your machine (v10.13.0)
  • Node Package Manager installed on your machine (v 6.4.1)
  • Cordova installed on your machine (v 8.1.2)
  • Docker installed on your machine. (version 18.09.2) Download here.

Building the GraphQL server

The backend server will be responsible for serving the data that we will render in the application. By now, you have probably heard about GraphQL and wondered about how it all works, let’s go through a brief introduction and see what the buzz is all about

What is GraphQL?

GraphQL is a query language that helps speed up development by enabling developers to query exactly the data they need from the client without having to fetch other excess data. It was developed by Facebook and was open-sourced in 2015. Since then, it has been used in production by companies like Twitter and GitHub.

The query language is largely based on two major concepts, queries and mutations. Queries are used to fetch data from your data source and mutations are used to edit the existing data source.

As we progress through this series, we will take a deeper look at understanding what queries and mutations are and how to write them.

The only caveat though is that creating and managing a GraphQL server seems like a herculean task to people who are new to it. So, in this part of the series, we are going to see how to use Prisma to automatically turn your database into a GraphQL API thus enabling us to read and write to the application’s database using GraphQL queries and mutations. To read more about Prisma features, head over here.

To get started, install the Prisma CLI on your machine:

    #install using brew (if you have a mac)
    brew tap prisma/prisma
    brew install prisma

    # or install with npm
    npm install -g prisma

Next we need to create a Docker compose file in the server directory for your project that will configure the Prisma server and let it know what database to connect to. In your instagram-ionic project, create a folder server that will house the Prisma service:

    mkdir server
    cd server
    touch docker-compose.yml

Edit the docker-compose file to look like this:

    version: '3'
    services:
      prisma:
        image: prismagraphql/prisma:1.31
        restart: always
        ports:
        - "4466:4466"
        environment:
          PRISMA_CONFIG: |
            port: 4466
            databases:
              default:
                connector: mysql
                host: mysql
                port: 3306
                user: root
                password: prisma
                migrations: true
      mysql:
        image: mysql:5.7
        restart: always
        environment:
          MYSQL_ROOT_PASSWORD: prisma
        volumes:
          - mysql:/var/lib/mysql
    volumes:
      mysql:

Now, go ahead and start your Prisma server and the database by running the command:

    docker-compose up -d

You should get a prompt that looks like this:

Now, the Prisma server is up and running, let’s create a simple Prisma service in the server directory:

    cd server 
    prisma init --endpoint http://localhost:4466

http://localhost:4466 represents the port your local Prisma service is running on. To confirm the port, run the command docker ps . All the created containers will be listed for you to find the port your container will run on.

Initializing the Prisma service creates two files in the server directory:

  • prisma.yml defines some config for the Prisma service
  • datamodel.prisma specifies the data model our database will be based on.

Let’s edit the data model to meet what we need for our Instagram clone application. Update your datamodel.prisma to look like this:

    # server/datamodel.prisma
    type User  {
      id: ID! @unique @id
      username: String! @unique
      fullname: String!
      avatar: String!
      bio: String!
      followers: Int!
      following: Int!
      posts: [Post!]! @relation(name: "UserPosts")
      comments: [Comment!]! @relation(name: "UserComments")
    }

    type Comment{
      id: ID! @unique @id
      message: String
      postedAt: DateTime!
      user: User! @relation(name: "UserComments")
      post: Post! @relation(name: "PostComments")
    }

    type Post{
      id: ID! @unique @id
      image_url: String!
      description: String,
      likes: Int @default(value: 0)
      postedAt: DateTime!
      user: User! @relation(name: "UserPosts")
      comments: [Comment!]! @relation(name: "PostComments")
    }

The data model above specifies that our application has the main models with relationships with one another. The data model is written in GraphQL Schema Definition Language which is largely based on two concepts of types and fields. Head over here to learn more about writing in the GraphQL SDL.

The prisma.yml file looks like this:

    endpoint: http://localhost:4466
    datamodel: datamodel.prisma

Now that we have specified the data model for the application, we then deploy the Prisma service by running the command below in the server directory:

    prisma deploy

You get a prompt that looks like this:

Now that we have deployed our Prisma service, let’s go ahead to the playground to see how fetching data using the GraphQL API will look like. Navigate to http://localhost:4466 and you get a view that looks like this:

Now with Prisma, all the possible queries and mutations possible on the data model are created automatically after we deployed the service. This means that as we update our data model, the possible queries and mutations on our data are also updated accordingly.

Creating a new user

Creating a new user from the playground will look like this:

    mutation(
    $username: String!, $fullname: String!, $avatar: String!, $bio: String!,
    $followers: Int!, $following: Int!){
     createUser(data: {username: $username, fullname: $fullname, avatar: $avatar,
    bio: $bio, followers: $followers, following: $following}){
              username
              fullname
              bio
            } 
    }

Add the query variables in the bottom left section:

    {
      "username": "oreog",
      "fullname": "Ore Og!",
      "avatar": "https://api.adorable.io/avatars/100/big_dawg@oreog.png",
      "bio": "Software Engineer",
      "followers": 1000,
      "following": 534
    }

When you run the mutation, you will have a view that looks like this. With the created user returned on the right side of the view.

Fetching the list of users

Now, to view the available users, create a query that looks like this:

    query{
      users{
        id
        username
        followers
        following
      }
    }

When the query is run, you get the list of users with the requested information.

Creating a new post

To create a new post, the mutation will look like this:

    mutation(
      $image_url: String!, $description: String, $likes: Int, $postedAt: DateTime!,
      $user: UserCreateOneWithoutPostsInput!
    ){
      createPost(data: {image_url: $image_url, description: $description,
      likes: $likes, postedAt: $postedAt, user: $user}){
        id
        image_url
        description
        user{
          id
          username
        }
      }
    }

Add the query variables to the bottom left of the console. This will specify the content of the post you’re about to create:

    {
      "image_url": "https://pbs.twimg.com/media/D4hTNmQWsAADzpo?format=jpg&name=medium",
      "description": "Hi there",
      "likes": 1104,
      "postedAt": "2019-04-21T12:19:05.568Z",
            "user": {"connect": {"id": "USER_ID_FETCHED_FROM_GRAPHQL_SERVER"}}
    }

Pick an id of your choice from the previous query above

Rendering data in our application

Now that we have seen how to create a GraphQL server using Prisma, let’s go ahead to enable our current Ionic application to fetch data dynamically using GraphQL. To do this, we going to make use of Apollo Client. Apollo Client gives developers the ability to bind GraphQL data to their user interface with ease.

We are going to assume that our database has already been populated with some sample data we are going to fetch

Installing the Apollo client

Let’s see how to use this with our application. First install the necessary packages in your ionic-insta-clone project, because Ionic applications are built with Angular, we are going to install packages that allow us to use the Apollo Client in Angular applications:

    cd instagram-ionic

    npm install apollo-angular@1.1.2 apollo-angular-link-http@1.1.1 apollo-client@2.3.8 apollo-cache-inmemory@1.2.7 graphql-tag@2.9.2 graphql@0.13.2 pluralize --save

    npm install apollo-utilities@1.0.22 --no-save
    npm install typescript@3.0.0 --save-dev

We then need to import the modules in our app.module.ts file:

    // src/app/app.module.ts
    import { NgModule, ErrorHandler } from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';
    import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
    import { MyApp } from './app.component';

    // import modules for apollo client
    import {HttpClientModule} from '@angular/common/http';
    import {ApolloModule, Apollo} from 'apollo-angular';
    import {HttpLinkModule, HttpLink} from 'apollo-angular-link-http';
    import {InMemoryCache} from 'apollo-cache-inmemory';

    // import other pages
    [...]

    @NgModule({
      declarations: [...],
      imports: [
        HttpClientModule,
        ApolloModule,
        HttpLinkModule,
        BrowserModule,
        IonicModule.forRoot(MyApp)
      ],
      // other app specifications
    })
    export class AppModule {}

Configuring the Apollo client

In the app.module.ts file, we then go ahead to inject Apollo into our application like this:

    // src/app/app.module.ts
    import {HttpClientModule} from '@angular/common/http';
    import {ApolloModule, Apollo} from 'apollo-angular';
    import {HttpLinkModule, HttpLink} from 'apollo-angular-link-http';
    import {InMemoryCache} from 'apollo-cache-inmemory';
    // other application imports
    [...]

    export class AppModule {
      constructor(apollo: Apollo, httpLink: HttpLink){
        apollo.create({
          link: httpLink.create({uri: 'http://localhost:4466'}), 
          // uri specifies the endpoint for our graphql server
          cache: new InMemoryCache()
        })    
      }
    }

To avoid an error when your application is being compiled, add the following to your tsconfig.json:

    // tsconfig.json
    {
      "compilerOptions": {
        // other options
        "lib": [
          "esnext.asynciterable"
        ]
        // other options
      }
      // other options
    }

Now that we have the client fully configured, let’s get to using it to fetch and render data to the user.

Fetching and rendering posts on home page

Let’s head over to the homepage to see how we can achieve this. In your home.ts file, import the Apollo client and then create a query to fetch the post as follows:

    // src/pages/home/home.ts
    import { Component, OnInit } from '@angular/core';
    import { NavController, NavParams } from 'ionic-angular';
    import { ProfilePage } from '../profile/profile';
    import { CommentPage } from '../comment/comment';
    import gql from 'graphql-tag';
    import { Apollo } from 'apollo-angular';

    @Component({
      selector: 'page-home',
      templateUrl: 'home.html',
      entryComponents: [ProfilePage, CommentPage]
    })

    export class HomePage implements OnInit {
      posts: any;

      constructor(public navCtrl: NavController, private apollo: Apollo) { 
      }

      ngOnInit(){
        this.fetchPosts();
      }

      fetchPosts() {
        this.apollo
          .query({
            query: gql`
              {
                posts {
                  image_url
                  description
                  likes
                  user {
                    id
                    username
                    avatar
                  }
                  comments {
                    id
                  }
                }
              }
            `
          })
          .subscribe(({ data }) => {
            let inner_posts: any = data;
            this.posts = inner_posts.posts;
          });
      }

      [...]
    }

Afterward, we then go ahead to the home.html and then render the posts on the homepage as follows:

    <!-- src/pages/home/home.html -->
    <ion-header>
      <ion-navbar>
        <ion-title>Instagram Clone</ion-title>
      </ion-navbar>
    </ion-header>

    <ion-content>
      <!-- this is where the posts will be -->
      <div *ngFor="let post of posts">
        <ion-card class="single-post-home">
          <ion-item (click)="toProfilePage(post.user.id)">
            <ion-avatar item-start>
              <img [src]="post.user.avatar">
            </ion-avatar>
            <h2>{{post.user.username}}</h2>
          </ion-item>

          <img [src]="post.image_url">

          <ion-card-content>
            <p><strong>{{post.user.username}}</strong> &nbsp;&nbsp;&nbsp; {{post.description}}</p>
          </ion-card-content>

          <ion-row>
            <ion-col>
              <button ion-button icon-start clear small (click)="likePost()">
                <ion-icon name="heart"></ion-icon>
                <div>{{post.likes}} likes</div>
              </button>
            </ion-col>
            <ion-col>
              <button ion-button icon-start clear small (click)="toCommentSection()">
                <ion-icon name="text"></ion-icon>
                <div>{{post.comments.length}} Comments</div>
              </button>
            </ion-col>
          </ion-row>

        </ion-card>
      </div>

      <ion-fab bottom right>
        <button ion-fab mini><ion-icon name="add"></ion-icon></button>
      </ion-fab>
    </ion-content>

Now, we also need to update the toProfilePage() function, in our home.ts file to take us to the profile page.

    // src/pages/home/home.ts

    [...]

    export class HomePage implements OnInit {
      [...]

      public toProfilePage(user_id: string) {
        let nav_params = new NavParams({ id: user_id });
        this.navCtrl.push(ProfilePage, nav_params);
      }

      [...]
    }

We created a navigation parameter object with the user_id passed to the next page. Now, when we run the application:

    ionic serve

we have the following view:

Fetching and rendering data on the profile page

When the username or avatar is clicked, we want to navigate to the user’s profile page. Now, the profile.ts page is also updated to fetch the users information from the GraphQL server and display it. Update the file as follows:

    // src/pages/profile/profile.ts

    import { Component, OnInit } from '@angular/core';
    import { IonicPage, NavController, NavParams } from 'ionic-angular';
    import { Apollo } from 'apollo-angular';
    import  gql from 'graphql-tag';
    import pluralize from 'pluralize';

    @IonicPage()
    @Component({
      selector: 'page-profile',
      templateUrl: 'profile.html',
    })

    export class ProfilePage implements OnInit {
      user: any;

      constructor(public navCtrl: NavController, public navParams: NavParams,  private apollo: Apollo) {
      }

      ngOnInit(){
        this.fetchProfile( this.navParams.get('id'));
      }


      fetchProfile(user_id: string){
        this.apollo
          .query({
            query: gql`
            {
              user(where: {id: "${user_id}"}){
                id
                username
                fullname
                avatar
                bio
                followers
                following
                posts{
                  image_url
                }
              }
            }
            `,
          })
          .subscribe(({ data }) => {
            let result:any = data;
            this.user = result.user;
          });
      }

      plural(word, number){
        return pluralize(word, number);
      }
    }

After the page is created, the Apollo Client makes a query to fetch the user profile using the user_id and then assigns the results to the user property of the Profile page class.

Next, update the profile.html to render the user’s data:

    <!-- src/pages/profile/profile.html -->
    <ion-header>

      <ion-navbar>
        <ion-title>{{user?.username}}</ion-title>
      </ion-navbar>

    </ion-header>


    <ion-content>
      <!-- first set should be a row -->
      <ion-grid class="profile-intro">
        <ion-row>
          <ion-col col-4>
            <img class="profile-photo" [src]="user?.avatar">
          </ion-col>
          <ion-col col-8>
            <div class="profile-info">
              <div class="post-count info-square">
                <p>
                  <strong>{{ user?.posts.length }}</strong><br>
                  <em>{{ this.plural('post', user?.posts.length) }}</em>
                </p>
              </div>
              <div class="follower-count info-square">
                <p>
                  <strong>{{ user?.followers }}</strong><br>
                  <em>{{ this.plural('follower', user?.followers) }}</em>
                </p>
              </div>
              <div class="following-count info-square">
                <p>
                  <strong>{{ user?.following }}</strong><br>
                  <em>following</em>
                </p>
              </div>
            </div>
            <div>
              <button ion-button class="follow-button">Follow</button>
            </div>
          </ion-col>
        </ion-row>
      </ion-grid>

      <div class="more-details">
        <p class="user-name"><strong>{{ user?.fullname }}</strong></p>
        <p class="user-bio">{{ user?.bio }}</p>
      </div>

      <ion-segment color="primary">
        <ion-segment-button value="posts" selected>
          <ion-icon name="grid"></ion-icon>    
        </ion-segment-button>
        <ion-segment-button value="tagged">
            <ion-icon name="contacts"></ion-icon>
          </ion-segment-button>
        <ion-segment-button value="bookmark">
          <ion-icon name="bookmark"></ion-icon>
        </ion-segment-button>
      </ion-segment>

      <ion-grid class="image-grid">
        <ion-row class="single-row">
            <ion-col *ngFor = "let post of user?.posts" col-4 class="single-image">
              <img width="100%" height="100%" [src]="post.image_url">
            </ion-col>
        </ion-row>
      </ion-grid>
    </ion-content>

Now, make sure your server is running and then visit the browser at http://locahost:8100 - where ionic is serving your application at. You should get a view that looks like this:

Fetching and rendering and creating comments on the comments page

Finally, let’s consider how we handle comments in our application dynamically. In the home.html let’s update the comment button to send the user to view the post comments:

    <!-- src/pages/home/home.html -->

    [...]
      <ion-col>
        <button ion-button icon-start clear small (click)="toCommentSection(post)">
          <ion-icon name="text"></ion-icon>
          <div>{{post.comments.length}} Comments</div>
        </button>
      </ion-col>
    [...]

And then update the toCommentSection() function in the home.ts to pass the post as a parameter to the Comments page:

    // src/pages/home/home.ts

    [...]
      public toCommentSection(post_data: any) {
        let nav_params = new NavParams({ post: post_data });
        this.navCtrl.push(CommentPage, nav_params);
      }
    [...]

Now, in the comment.ts, we import the Apollo client that was configured earlier and fetch the comments for the selected post:

    // src/pages/comment/comment.ts

    import { Component } from '@angular/core';
    import { IonicPage, NavController, NavParams } from 'ionic-angular';
    import { Apollo } from 'apollo-angular';
    import gql from 'graphql-tag';

    @IonicPage()
    @Component({
      selector: 'page-comment',
      templateUrl: 'comment.html'
    })

    export class CommentPage {
      comments: any;
      username: string;
      post_desc: string;
      user_avatar: string;

      constructor(
        public navCtrl: NavController,
        public navParams: NavParams,
        private apollo: Apollo
      ) {
        this.username = this.navParams.get('username');
        this.user_avatar = this.navParams.get('avatar');
        this.post_desc = this.navParams.get('post_desc');
        this.loadComments(this.navParams.get('post_id'));
      }

      loadComments(post_id: string) {
        this.apollo
          .query({
            query: gql`
              {
                comments(where: { post: { id: "${post_id}" } }) {
                  id
                  message
                  user {
                    avatar
                    username
                  }
                }
              }
            `
          })
          .subscribe(({ data }) => {
            let result: any = data;
            this.comments = result.comments;
          });
      }
    }

Afterwards, we update the comment.html to show the users, comments as follows:

    <!-- src/pages/comment/comment.html -->
    <ion-header>
      <ion-navbar>
        <ion-title>Comments</ion-title>
      </ion-navbar>
    </ion-header>

    <ion-content>
      <ion-grid>
        <ion-row class="post-content">
          <ion-col col-2>
            <ion-avatar item-start>
              <img class="icon-photo" [src]="user_avatar">
            </ion-avatar>
          </ion-col>
          <ion-col col-10>
            <div>
              <p>
                <strong>{{username}}</strong>&nbsp;&nbsp;&nbsp; {{post_desc}}
              </p>
            </div>
          </ion-col>
        </ion-row>

        <ion-row *ngFor="let comment of comments" class="user-comments">
          <ion-col col-2>
            <ion-avatar item-start>
              <img class="icon-photo" [src]="comment.user.avatar">
            </ion-avatar>
          </ion-col>
          <ion-col col-10>
            <div>
              <p>
                <strong>{{comment.user.username}}</strong>&nbsp;&nbsp;&nbsp;{{ comment.message }}
              </p>
            </div>
          </ion-col>
        </ion-row>
      </ion-grid>
    </ion-content>

    <ion-footer>
      <ion-grid>
        <ion-row class="comment-area">
          <ion-col col-9>
            <ion-textarea placeholder="Enter your comment..."></ion-textarea>
          </ion-col>
          <ion-col col-3>
            <button ion-button class="comment-button">
              <ion-icon name="paper-plane"></ion-icon>
            </button>
          </ion-col>
        </ion-row>
      </ion-grid>
    </ion-footer>

Conclusion

In this part of this series, we examined how to connect our application with some dynamic data using Prisma to generate a GraphQL API and Apollo Client to interact with our GraphQL API seamlessly only requesting data that we need to render. In the next part, we will examine how to add this data from the interface and integrate realtime functionality to the application. Here’s a link to the full GitHub repository for more reference.

Clone the project repository
  • CSS
  • Cordova
  • GraphQL
  • HTML
  • JavaScript
  • Node.js
  • TypeScript
  • Channels

Products

  • Channels
  • Chatkit
  • Beams

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.