Build an Instagram clone with Ionic: Part 2 - Connecting to GraphQL servers using Apollo client
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 commanddocker 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 servicedatamodel.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> {{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> {{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> {{ 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.
27 June 2019
by Oreoluwa Ogundipe