In part 3 of our GraphQL series, its time to put our API into action - this tutorial will guide you through creating a simple GraphQL client web app, all through the use of the Vue.js framework. Contains all the relevant code you need for the project!

Recently, we’ve been introducing you to GraphQL and how it fits into the new wave style of designing APIs. It’s a clever alternative to REST architectures, and works very well for particular projects and use cases. Part one of our series was a run down of GraphQL, how it differs from other technologies, and where to use it, and part two outlined creating an API in Node.js using GraphQL.

In this third part of our series we will focus on building a simple web application using GraphQL for communication - with the API we built in part two.

We will use vue-apollo (powered by apollo-client internally) for integrating the GraphQL library into our Vue.js application.

Our interface will allow us to list projects requiring specific tech skills and show the best candidates for each project based on their knowledge and experience.

This tutorial assumes you already have Node.js and NPM installed and that the API that we built in part two is up and running on port 3200.

Here is a link to our repo with finished project - https://github.com/iRonins/GraphQL-client-with-Vue.js

Creating a new project

We will use the vue-cli tool to bootstrap our project. vue-cli allows us to easily create a structure and the necessary configuration for the project using one of the available templates.

npm install -g vue-cli
vue init webpack-simple talent-matcher
cd talent-matcher
npm install

To keep this tutorial simple, we will use the webpack-simple template. For real-world applications we would recommend that you use the full-featured webpack template.

The above commands will create an initial file structure and Webpack configuration for our app.

Before we start writing our app, we need to install vue-apollo and apollo-client (for connecting to the GraphQL API), bootstrap-vue (for some nice UI), and some css loaders (for importing Bootstrap styles):

npm install -D vue-apollo bootstrap-vue apollo-client@1.0.1 style-loader css-loader

We also recommend that you install Apollo DevTools to improve your debugging and development experience. It will provide you information about the available queries and mutations within your API, execute them, and preview the current state of the application’s store.

Writing the actual app

We need to start by making our app aware of all the add-ons that we just installed:

src/main.js

// src/main.js
import Vue from 'vue'
import { ApolloClient, createNetworkInterface } from 'apollo-client'
import BootstrapVue from 'bootstrap-vue'
import VueApollo from 'vue-apollo'

import 'bootstrap-vue/dist/bootstrap-vue.css'
import 'bootstrap/dist/css/bootstrap.css'
import App from './App.vue'

const apolloClient = new ApolloClient({
  networkInterface: createNetworkInterface({
    uri: 'http://localhost:3200/graphql'
  }),
  connectToDevTools: true
})

const apolloProvider = new VueApollo({
  defaultClient: apolloClient
})

Vue.use(VueApollo)

// Fix for compatibility issue with Vue 2.5.1
// https://github.com/bootstrap-vue/bootstrap-vue/issues/1201
let originalVueComponent = Vue.component
Vue.component = function(name, definition) {
  if (name === 'bFormCheckboxGroup' || name === 'bCheckboxGroup' ||
      name === 'bCheckGroup' || name === 'bFormRadioGroup') {
    definition.components = {bFormCheckbox: definition.components[0]}
  }
  originalVueComponent.apply(this, [name, definition])
}
Vue.use(BootstrapVue)
Vue.component = originalVueComponent

new Vue({
  el: '#app',
  apolloProvider,
  render: h => h(App)
})

We basically import vue-apollo, apollo-client, bootstrap-vue and some Bootstrap styles. Unfortunately, we also need to override the Vue.component function while we are importing BoostrapVue, because there is an incompatibly bug that causes an error (Invalid value for option "components": expected an Object, but got Array) on the 2.5.x version of Vue.

We also need to import style-loader and css-loader modules to our Webpack configuration so we can import the Bootstrap styles in our main.js script:

// webpack.conf.js

...

module.exports = {
  ...
  module: {
    rules: [
      ...,
      {
        test: /\.css$/,
        use: [
          { loader: "style-loader" },
          { loader: "css-loader" }
        ]
      }
    ]
  }
}

Now let’s modify our application’s template:

// src/App.vue
<template>
  <div id="app">
    <header class="header">
      <b-navbar toggleable="md" type="light" variant="light">
        <b-container>
          <b-navbar-brand href="#">Talent Matcher</b-navbar-brand>
        </b-container>
      </b-navbar>
    </header>

    <main>
      <b-container>
        <projects />
      </b-container>
    </main>
  </div>
</template>

<script>
import projects from './components/Projects.vue'

export default {
  name: 'app',
  components: {
    projects
  }
}
</script>

<style>
.header {
  margin-bottom: 2rem;
}
</style>

This basically creates a “shell” of our app (navbar, container, etc.). Our projects will be displayed by the Projects component, which will be responsible for 2 things:

  • listing all projects
  • showing the best candidates for the currently selected project

Let’s start by building the skeleton of our Projects component:

// src/components/Projects.vue
<template>
  <b-row>
    <b-col cols="4">
      <b-list-group>
        <b-list-group-item v-for="project in projects"
          :key="project.id"
          href="#"
          :active="selectedProjectId == project.id" @click="selectProject(project)">
          
        </b-list-group-item>
      </b-list-group>
    </b-col>
    <b-col>
      <p v-if="!selectedProjectId">Select project on the sidebar to see its best candidates</p>
    </b-col>
  </b-row>
</template>

<script>
import gql from 'graphql-tag'

const query = gql`
  query projects {
    projects {
      id,
      name
    }
  }
`

export default {
  apollo: {
    projects: query
  },
  data() {
    return {
      projects: [],
      selectedProjectId: null
    }
  },
  methods: {
    selectProject(project) {
      this.selectedProjectId = project.id
    }
  }
}
</script>

As you can see, we are importing gql from graphql-tag in order to create the projects query responsible for fetching our projects with their id and name attributes. Then we provide that query to the apollo property. The component will automatically send the query to the GraphQL API and place the results inside its projects data so we can iterate over them using v-for directive and display each of them using the list-group-item component.

We have also added some nice tweaks from vue-bootstrap like highlighting the currently selected projects on the list, etc. to make our app more visually appealing.

When you run the app it should look similar to the screenshot below:

images/blog/2018-02-23-creating-a-graphql-client-with-the-vuejs/talent-matcher1.png

Let’s add to it so that we can display the candidates for selected project. To do that we will need to create a new Candidates component which we will use in the Projects component.

// src/components/Candidates.vue
<template>
  <b-table bordered hover :items="candidates" :fields="fields">
    <template slot="skills" slot-scope="data">
       ()
    </template>
  </b-table>
</template>

<script>
import gql from 'graphql-tag'

const query = gql`
  query projectCandidates($projectId: Int!) {
    candidates(projectId: $projectId) {
      id,
      fullName,
      matchedSkillsNo,
      matchedSkills,
      experience
    }
  }
`

export default {
  props: ['projectId'],
  apollo: {
    candidates: {
      query,
      variables() {
        return {
          projectId: this.projectId
        }
      }
    }
  },
  data() {
    return {
      candidates: [],
      fields: [
        {
          key: 'id',
          label: 'Id',
          sortable: false
        },
        {
          key: 'fullName',
          label: 'Full name'
        },
        {
          key: 'skills',
          label: 'Matched skills'
        },
        {
          key: 'experience',
          label: 'Matched skills experience',
          sortable: true
        }
      ]
    }
  }
}
</script>

As you can see above, we are defining our query to fetch candidates who match the project requirements based on their skills and experience. Our query is a little bit more complicated than the previous one because the projectId parameter must be bound to the component’s property.

The bootstrap-vue library also provides a nice table component which allows us to customize the columns and add simple sorting based on the candidate’s experience.

You can check all available options in the documentation for bootstrap-vue.

We also need to include our nearly created component in the Projects.vue component:

// src/components/Projects.vue
<template>
  ...
  <p v-if="!selectedProjectId">Select project on the sidebar to see its best candidates</p>
  <candidates v-else :projectId="selectedProjectId" />
  ...
</template>

<script>
import candidates from './Candidates.vue'

...

export default {
  components: {
    candidates
  }

  ...
}
</script>

When you run the app it should look similar to the screenshot below:

images/blog/2018-02-23-creating-a-graphql-client-with-the-vuejs/talent-matcher2.png

So far so good but when we refresh the page we are losing the previously selected project - it basically always goes back to its initial state.

Let’s fix that by adding proper routing. We will use vue-router for that, the official routing library for Vue.

Let’s install the dependency first:

npm install -D vue-router

and make our application aware of it:

// src/main.js
import VueRouter from 'vue-router'

...

import Projects from './components/Projects.vue'

...

const router = new VueRouter({
  routes: [
    {
      name: 'projects',
      path: '/:projectId?',
      component: Projects
    }
  ]
});

Vue.use(VueRouter)

...

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

We also created our single projects route which supports the optional projectId parameter to persist our application’s state between page refreshes.

Now we need to modify our Projects component to take the projectId parameter from the $route and assign it to the selectedProjectId property which will highlight the correct link on the sidebar and pass the correct selectedProjectId to the Candidates component which will fetch the correct data.

// src/components/Projects.vue
<template>
  ...
  <b-list-group-item v-for="project in projects"
    :key="project.id"
    :to="{ name: 'projects', params: { projectId: project.id } }">
    
  </b-list-group-item>
  ...
</template>

<script>
...
export default {
  components: {
    candidates
  },
  apollo: {
    projects: query
  },
  data() {
    return {
      projects: [],
    }
  },
  computed: {
    selectedProjectId() {
      return this.$route.params.projectId
    }
  }
}
</script>

We added a computed property that returns selectedProjectId based on the projectId param in the $route.

We also bind selectedProjectId to projectId when loading, so when a user refreshes the page, our application fetches candidates correctly after refresh. Also the back / forward buttons now work thanks to vue-router.

Let’s also connect the navbar logo (actually navbar-brand) with our main page trough the router:

// src/App.vue
<template>
  <div id="app">
    <header class="header">
      <b-navbar toggleable="md" type="light" variant="light">
        <b-container>
          <b-navbar-brand :to="{ name: 'projects' }">Talent Matcher</b-navbar-brand>
        </b-container>
      </b-navbar>
    </header>
...
</template>

We are linking b-navbar-brand to our main route (projects) - but without the optional projectId parameter - this way we can “reset” the application’s state.

We are almost done but we forgot about one missing bit - we do not yet display the skills required by the selected project. Let’s display them above the candidates table.

// src/components/Projects.vue
<template>
  ...
  <p v-if="!selectedProjectId">Select project on the sidebar to see its best candidates</p>
  <div v-else>
    <p>
      <strong>Required skills:</strong>
      
    </p>
    <candidates :projectId="selectedProjectId" />
  </div>
</template>

<script>
...

const query = gql`
  query projects {
    projects {
      id,
      name,
      skills
    }
  }
`

export default {
  ...
  computed: {
    selectedProject() {
      return this.projects.find(({ id }) => id === this.selectedProjectId) || {}
    },
    ...
  },
  ...
}
</script>

We’ve added selectedProject as computed property which will return project data or an empty object, based on the selectedProjectId value.

We have also modified our query to ask the API to include the skills attribute for each project.

Now our complete app should look like on the screenshot below:

images/blog/2018-02-23-creating-a-graphql-client-with-the-vuejs/talent-matcher3.png

Here is the link to the repository containing full code of the application.

GraphQL is just one of the tools that we have at our disposal to help build web apps that are easier to comprehend, more flexible, and are fully featured. If you are interested in learning how this new technology could benefit your next web application, or are interested in changing over your current infrastructure, then email us at iRonin to have a chat!