See all articles
GraphQL Part #3 - Creating a GraphQL Client with the Vue.js Framework

GraphQL Part #3 - Creating a GraphQL Client with the Vue.js Framework

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

For more knowledge, see our whole GraphQL Development Series:

  1. GraphQL Part #1 - Explaining GraphQL and How it Differs from REST and SOAP APIs
  2. GraphQL Part #2 - Creating an API with Node.js Using GraphQL
  3. GraphQL Part #3 - Creating a GraphQL Client with the Vue.js Framework [you are reading this article now]
  4. GraphQL Part #4 - Build GraphQL API with Ruby on Rails

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)">
          {{ project.name }}
        </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:

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">
      {{data.item.matchedSkills}} ({{data.item.matchedSkillsNo}})
    </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:

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 } }">
    {{ project.name }}
  </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>
      {{ selectedProject.skills }}
    </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:

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!

Read Similar Articles