WebInspiration

REST vs GraphQL from a frontend angle – part 2/3

In part 2, we will go through many code snippets from the two apps, showing REST and GraphQL approaches one below the other. They will not contain information that is in my opinion obsolete for this article like initializing Google Maps, styles and some template parts.

In part 1 of GraphQL vs REST, I went through some basic concepts and differences between the two approaches.

Reminder of what we are building

Main configuration

In Rest, I’ve decided to use Vuex.

In GraphQL, main library to keep state is VueApollo.

REST

// main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import '@/config/axios.config';
Vue.config.productionTip = false

new Vue({
 router,
 store,
 render: h => h(App)
}).$mount('#app')


GraphQL

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import VueApollo from 'vue-apollo';
import { apolloClient } from './config/apollo.config';
Vue.config.productionTip = false

Vue.use(VueApollo)

const apolloProvider = new VueApollo({
 defaultClient: apolloClient,
 defaultOptions: {
   $query: {
     loadingKey: 'loading',
     fetchPolicy: 'cache-and-network'
   }
 }
})

new Vue({
 router,
 apolloProvider,
 render: h => h(App)
}).$mount('#app')

Other configuration

In REST, let’s use Axios for making API requests. We will use axios later in Vuex store. For now, let’s only initialize default state.

In GraphQL, Apollo provides a lot of boilerplate, but I also import InMemoryCache and typeDefs. To have some default data in the beginning I write data to cache. During creating new Apollo Client, it’s important to remember that even without mutations in the project, empty resolvers are needed.


REST

// config/axios.config.js

import axios from 'axios';

axios.defaults.baseURL = process.env.VUE_APP_API_BASE_URL;
axios.defaults.headers.common['Authorization'] = `Bearer ${process.env.VUE_APP_YELP_API_KEY}`;

// part of store/index.js to show where empty data is initialized

import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios';

Vue.use(Vuex)

export default new Vuex.Store({
 state: {
   results: [],
   activeCategory: {},
   loading: false,
   activeMarker: null,
   categories: []
 },
...


GraphQL

// config/apollo.config.js

import { ApolloClient } from 'apollo-client'
import { createHttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { setContext } from 'apollo-link-context';
import { typeDefs } from '@/graphql/typeDefs';

// HTTP connection to the API
const httpLink = createHttpLink({
 // You should use an absolute URL here
 uri: process.env.VUE_APP_API_BASE_URL
})

const authLink = setContext((_, { headers }) => {
 // get the authentication token from local storage if it exists
 // return the headers to the context so httpLink can read them
 return {
   headers: {
     ...headers,
     authorization: `Bearer ${process.env.VUE_APP_YELP_API_KEY}`,
     'Accept-Language': `en_US`
   }
 }
});

// Cache implementation
const cache = new InMemoryCache()

cache.writeData({
 data: {
   search: {
     business: [],
     __typename: 'Search'
   },
   activeCategory: {
     __typename: 'ActiveCategory'
   },
   loading: false,
   activeMarker: null,
   center: {
     lat: 52.237022,
     lng: 21.050440,
     __typename: 'MapCenter'
   },
   zoom: 12,
   categories: []
 },
});

// Create the apollo client
export const apolloClient = new ApolloClient({
 link: authLink.concat(httpLink),
 cache,
 typeDefs,
 resolvers: {}
})

Fetching initial results

In REST, I do it by dispatching an action on mounted in App.vue

In GraphQL, there are two ways of doing it. Here, I went for the ApolloQuery component with a v-slot approach. This gave me easy access to result data, loading state and query to refetch data later. Take note of notifyOnNetworkStatusChange set to true.

Another way to do it is to use apollo key on the same level as mounted and specify the query there.


REST

<script>
// part of App.vue
export default {
 ...
 mounted() {
   this.$store.dispatch('getInitialResults');
   this.$store.dispatch('getCategories');
 }
}
</script>


GraphQL

// part of App.vue

<template>
...
 <ApolloQuery
  :query="GET_RESULTS"
  :notifyOnNetworkStatusChange="true"
 >
  <template v-slot="{ result: { data, loading }, query }">
    <AppNav @search="handleSearch($event, query)" />
    <div class="content"></div>
  </template>
 </ApolloQuery>
...
</template>

Making requests

In REST, the requests to API can go either from components or store, like in my example. Since actions are asynchronous in Vuex, this is the best place to fetch data.

In GraphQL, I’ve created three files that are responsible for specifying queries, one for all type definitions, but the requests for e.g. results go from App.vue through ApolloQuery component.


REST

// store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios';

Vue.use(Vuex)

export default new Vuex.Store({
 state: {
   results: [],
   activeCategory: {},
   loading: false,
   activeMarker: null,
   categories: []
 },
 mutations: {...},
 actions: {
   async getCategoryResults({commit}, payload) {
     commit('setCategoryResultsMutation', []);
     commit('setActiveCategoryMutation', payload);

     commit('setLoading', true);
     const {data: { businesses }} = await axios.get(`businesses/search?categories=${payload.name}&latitude=52.237022&longitude=21.050440&limit=50&radius=15000&sort_by=best_match`);
     commit('setCategoryResultsMutation', businesses);
     commit('setLoading', false);
   },
   async getCategories({commit}) {
     commit('setLoading', true);

     const {data: { categories }} = await axios.get(`categories`);
     commit('setCategoriesListMutation', categories);

     commit('setLoading', false);
   },
   async getInitialResults({commit}) {
     commit('setLoading', true);

     const {data: { businesses }} = await axios.get(`businesses/search?latitude=52.237022&longitude=21.050440&limit=50&radius=15000&sort_by=best_match`);
   }
 }


GraphQL

// graphql/categories.query.js
export const GET_CATEGORIES = gql`
 query AllCategoriesQuery {
   categories {
     total
     category {
       title
       alias
     }
   }
 }
`

// graphql/results.query.js
export const GET_RESULTS = gql`
 query GetResults($term: String, $category: String) {
   search (
     term: $term,
     categories: $category,
     limit: 50
     latitude: 52.237022,
     longitude: 21.050440,
     radius: 15000
   ) @connection(key: "search") {
     total
     business {
       name
       distance
       location {
         address1
         city
       }
       url
       rating
       review_count
       price
       photos
       coordinates {
         latitude
         longitude
       }
     }
   }
 }
`

// graphql/local.query.js
export const GET_ZOOM = gql`
 {
   zoom @client
 }
`
export const GET_CENTER = gql`
 {
   center @client {
     lat
     lng
   }
 }
`

// graphql/typeDefs.js
export const typeDefs = gql`
 type MapCenter {
   lat: String!
   lng: String!
 }
 type Category {
   title: String!
   alias: String!
 }
 type Categories {
   total: Int!
   category: Category!
 }
 type BusinessLocation {
   address1: String!
   city: String!
 }
 type Business {
   name: String!
   distance: String!
   url: String!
   rating: String
   review_count: String
   price: String
   photos: [String]
   location: BusinessLocation
 }
 type Search {
   total: Int!
   business: Business!
 }
`;

Handling loading

In REST, I use a global Vuex loading flag that I change during API calls and I directly access it through $store object. Since the app is small, one such key is enough.

In GraphQL it proved to be more difficult. When I tried to use the apollo key in script and refetch of data, my $apollo.queries.search.loading was always false during 2nd and later fetches. Because of that, I changed the approach to ApolloQuery component and used loading in App.vue, which is later passed to Results component as a prop.


REST

// part of components/Results.vue

<div
 class="results__wrapper"
 v-else-if="$store.state.loading"
>
 <h2 class="results__wrapper-loading">
   Loading...
 </h2>
</div>


// store/index.js

actions: {
 async getQueryResults({commit}, {input, category}) {
   commit('setCategoryResultsMutation', []);
   commit('setLoading', true);
   const {data: { businesses }} = await axios.get(`businesses/search?term=${input}&categories=${category}&latitude=52.237022&longitude=21.050440&limit=50&radius=15000&sort_by=best_match`);
   commit('setCategoryResultsMutation', businesses);
   commit('setLoading', false);
 }
}


GraphQL

// part of components/Results.vue

<div
 class="results__wrapper"
 v-else-if="loading"
>
 <h2 class="results__wrapper-loading">
   Loading...
 </h2>
</div>

// part of App.vue

<ApolloQuery
  :query="GET_RESULTS"
  :notifyOnNetworkStatusChange="true"
>
  <template v-slot="{ result: { data, loading }, isLoading, query }">
   <AppNav @search="handleSearch($event, query)" />
   <div class="content">
     ...
         <Results
           :results="data && data.search && data.search.business"
           :loading="loading"
         />
   </div>
  </template>
</ApolloQuery>

<script>
 methods: {
  handleSearch({ term, category }, query) {
    query.refetch({term, category});
  }
 }
</script>

Summary

These are the most important parts of the app and main differences between the two apps. To see the whole project and compare, please visit my GitHub repositories. You can clone the project, register API keys on Yelp and Google Maps and see it working:

Github repo for REST approach

Github repo for GraphQL approach

Stay tuned for part 3. It will be available on August 6th 2020.

Subscribe to our newsletter

Want to receive a fortnightly round up of the latest tech updates? Subscribe to our free newsletter. No spam, just insightful content covering design, development, AI and much more.

Most Popular

5 ½ Greatest Things About Remote Work

5 technology trends likely to continue booming in a ...

Thinking of building an app in React Native? Learn f...

You may also like these posts

Start a project with 10Clouds

Hire us