Building GraphQL APIs with Vue.js and Apollo Client

Introduction

GraphQL is a graph-oriented query language written by Facebook. In contrast to REST APIs, GraphQL introduces features that make API development more efficient and in tune with database models.

GraphQL Features

  • Unlike REST, there is only one endpoint where all requests are going to be sent to. So instead of querying /users to get a list of users, or /user/:id to get a particular user, the endpoint will look like /graphql for all the requests.
  • In GraphQL, the data coming back from a response is set by the query library stated and it can be set to only send a few data properties, therefore, queries in GraphQL have better performance.
  • No need to set method verbs in GraphQL. Keywords such as Query or Mutation will decide what the request will perform.
  • REST API routes usually are handled by one route handler. In GraphQL you can have a single query trigger multiple mutations and get a compound response from multiple sources.

Queries

A query is a GraphQL method that allows us to GET data from our API. Even though it may receive parameters to filter, order, or simply search for a particular document a query can not mutate this data.

Mutations

Mutations are everything that is not what would refer to a GET verb in regular APIs. Updating, creating, or deleting data from our API is done via mutations

Subscriptions

With the use of web sockets, a subscription refers to a connection between the client and the server.

The server is constantly watching for mutations or queries that are attached to a particular subscription, and communicate any changes to the client in real time. Subscriptions are mostly used for real-time widgets/apps.

Types and Inputs

To make sure our queries and mutations can process the data to query a database, types work much like a model ORM for databases. By setting types up we can define the type of variable our resolvers will return.

Similarly, we need to set input types for our resolvers to receive.

For example, we will define a couple types and inputs:

type User {
  id: ID
  name: String!
  age: Int!
  address: Address
  followers: [ID]
}

type Address {
  street: String
  city: String
  country: String
}

input UserInput {
  name: String!
  age: Int!
}

type Query {
  getAllUsers: [User]
}

type Mutation {
  createUser(user: UserInput!): ID
}

Properties can have a custom type as its type apart from the primitive ones, such as:

  • String
  • Int
  • Float
  • Boolean
  • ID

And they can also be an array of a certain type determined by the brackets, which is shown in the example above.

Furthermore, the mandatory status of a property can be set with the !, meaning that the property needs to be present.

Resolvers

These are the actions that are performed when calling queries and mutations.

getAllUsers and createUser are going to be connected to a resolver that will perform the actual calculations and database queries.

Creating our Project

For this tutorial, we will be creating a Vue.js project using the Vue CLI 3.0, which will bootstrap a project with a folder structure that looks like this:

If you need help setting up the project, you can look at this tutorial for the command line interface.

We can start serving our application with the command:

$ npm run serve

Apollo Client

Apollo Client brings a tool to front-end development to make GraphQL queries/mutations easier. It acts as an HTTP client that connects to a GraphQL API and provides caching, error handling, and even state management capabilities.

For this tutorial, Vue-Apollo will be used, which is the Apollo integration specially designed for Vue.js.

Apollo Configuration

To start our Apollo configuration a few packages will need to be installed:

$ npm install apollo-client apollo-link-http apollo-cache-inmemory vue-apollo graphql graphql-tag

Inside a /graphql folder in our project, we will create apollo.js:

// apollo.js

import Vue from 'vue'
import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import VueApollo from 'vue-apollo'

const httpLink = new HttpLink({
    uri: process.env.VUE_APP_GRAPHQL_ENDPOINT
})

// Create the apollo client
export const apolloClient = new ApolloClient({
    link: httpLink,
    cache: new InMemoryCache(),
    connectToDevTools: true
})

// Install the Vue plugin

Vue.use(VueApollo)

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

HttpLink is an object that requires a uri property, which refers to the GraphQL endpoint from the API being used. Ex: localhost:8081/graphql

Then, a new ApolloClient instance needs to be created, where the link, cache instance, and further options can be set.

Finally, we wrap our ApolloClient inside a VueApollo instance so we can use its hooks inside our Vue components.

Global Error Handling

There is a way of handling errors globally inside the configuration file. For that we need to install an npm package called apollo-link-error, which inspects and manages errors from the network:

// apollo.js

import Vue from 'vue'
import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { onError } from "apollo-link-error"
import { InMemoryCache } from 'apollo-cache-inmemory'
import VueApollo from 'vue-apollo'

const httpLink = new HttpLink({
    uri: process.env.VUE_APP_GRAPHQL_ENDPOINT
})

// Error Handling
const errorLink = onError(({ graphQLErrors, networkError }) => {
    if (graphQLErrors)
        graphQLErrors.map(({ message, locations, path }) =>
            console.log(
                `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
            )
        )
    if (networkError) console.log(`[Network error]: ${networkError}`)
})

// Create the apollo client
export const apolloClient = new ApolloClient({
    link: errorLink.concat(httpLink),
    cache: new InMemoryCache(),
    connectToDevTools: true
})

// Install the Vue plugin
Vue.use(VueApollo)

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

After importing the onError function from the package, we can implement it as a sort of middleware of Apollo Client. It'll catch any network or GraphQL errors, giving us the chance to manage them globally.

The callback gets called with an object with some properties whenever an error has happened:

  • operation: The operation that triggered the callback because an error was found.
  • response: The result of the operation.
  • graphQLErrors: An array of errors from the GraphQL endpoint
  • networkError: Any error during the execution of the operation or server error.
  • forward: The next link referenced in the chain.

Managing State with Apollo Client

A different alternative to using Vuex with Vue projects, and when using the Apollo Client is to use a package called apollo-link-state.

It works as a local data management tool that works as if you were querying a server, but it does it locally.

Also, it is a great way of managing the cache for our application, thus making Apollo Client an HTTP client and state/cache management tool.

For more information, you can check the official documentation for Apollo-link-state.

Creating Queries

For creating queries we need to set up a string-type tag with the package graphql-tag. For keeping a tidy and structured project, we will create a folder called queries inside the graphql folder.

Assuming the server receiving the query is set up properly to interpret this query, for example, we can trigger a resolver called getAllUsers:

import gql from 'graphql-tag'

export const GET_ALL_USERS_QUERY = gql`
  query getAllUsers {
    getAllUsers {
      // Fields to retrieve
      name
      age
    }
  }
`

The default operation in GraphQL is query, so the query keyword is optional.

If a retrieved field has subfields, then at least one of them should be fetched for the query to succeed.

Using Mutations

Much like queries, we can also use mutations by creating a gql-string.

import gql from 'graphql-tag'

export const CREATE_USER_MUTATION = gql`
  mutation createUser($user: UserInput!) {
    createUser(user: $user)
  }
`

Our createUser mutation expects a UserInput input, and, to be able to use parameters passed by Apollo. We will first define a variable with the $ called user. Then, the outside wrapper will pass the variable to the createUser mutation, as expected by the server.

Fragments

In order to keep our gql-type strings tidy and readable, we can use fragments to reuse query logic.

fragment UserFragment on User {
  name: String!
  age: Int!
}

query getAllUsers {
  getAllUsers {
    ...UserFragment
  }
}

Using GraphQL in Vue components

Inside the main.js file, to configure the Apollo Client, we need to import and attach the client to our instance.

// main.js
import Vue from 'vue'
import { apolloProvider } from './graphql/apollo'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
    el: '#app',
    apolloProvider,
    render: h => h(App)
})

Since we have added our ApolloProvider to the Vue instance, we can access the client through the $apollo keyword:

// GraphQLTest.vue
<template>
    <div class="graphql-test">
        <h3 v-if="loading">Loading...</h3>
        <h4 v-if="!loading">{{ getAllUsers }}</h4>
    </div>
</template>

<script>
import { GET_ALL_USERS_QUERY } from '../graphl/queries/userQueries'
export default {
    name: 'GraphQLTest',
    data () {
        return {
            users: []
        }
    },
    async mounted () {
        this.loading = true
        this.users = await this.$apollo.query({ query: GET_ALL_USERS_QUERY })
        this.loading = false
    }
}
</script>

If we want to create a user, we can use a mutation:

// GraphQLTest.vue
<template>
    <div class="graphql-test">
        <input v-model="user.name" type="text" placeholder="Name" />
        <input v-model="user.age" placeholder="Age" />
        <button @click="createUser">Create User</button>
    </div>
</template>

<script>
import { CREATE_USER_MUTATION } from '../graphl/queries/userQueries'
export default {
    name: 'GraphQLTest',
    data() {
        return {
            user: {
                name: null,
                age: null
            }
        }
    },
    methods: {
        async createUser () {
            const userCreated = await this.$apollo.mutate({
                mutation: CREATE_USER_MUTATION,
                variables: {
                    user: this.user // this should be the same name as the one the server is expecting
                }
            })
            // We log the created user ID
            console.log(userCreated.data.createUser)
        }
    }
}
</script>

Using this approach lets us micro-manage when and where our mutations and queries will execute. Now we will see some other ways of handling these methods that Vue Apollo gives us.

The Apollo Object

Inside our Vue components, we get access to the Apollo object, which can be used to easily manage our queries and subscriptions:

<template>
    <div class="graphql-test">
        {{ getAllUsers }}
    </div>
</template>

<script>
import { GET_ALL_USERS_QUERY } from '../graphl/queries/userQueries'
export default {
    name: 'GraphQL-Test',
    apollo: {
        getAllUsers: {
            query: GET_ALL_USERS_QUERY
        }
    }
}
</script>
Refetching Queries

When defining a query inside the Apollo object, it is possible to refetch this query when calling a mutation or another query with the refetch method or the refetchQueries property:

<template>
    <div class="graphql-test">
        {{ getAllUsers }}
    </div>
</template>

<script>
import { GET_ALL_USERS_QUERY, CREATE_USER_MUTATION } from '../graphl/queries/userQueries'
export default {
    name: 'GraphQL-Test',
    apollo: {
        getAllUsers: {
            query: GET_ALL_USERS_QUERY
        }
    },
    methods: {
        refetch () {
            this.$apollo.queries.getAllUsers.refetch()
        },
        queryUsers () {
            const user = { name: Lucas, age: 26 }
            this.$apollo.mutate({
                mutation: CREATE_USER_MUTATION,
                variables: {
                    user
                }
                refetchQueries: [
                    { query: GET_ALL_USERS_QUERY }
                ]
            })
        }
    }
}
</script>

By using the Apollo object, provided to us by Vue-Apollo, we no longer need to actively use the Apollo client way of triggering queries/subscriptions and some useful properties and options become available to us.

Apollo Object Properties
  • query: This is the gql type string referring to the query that wants to get triggered.
  • variables: An object that accepts the parameters being passed to a given query.
  • fetchPolicy: A property that sets the way the query will interact with the cache. The options are cache-and-network, network-only, cache-only, no-cache, standby and the default is cache-first.
  • pollInterval: Time in milliseconds that determines how often a query will automatically trigger.
Special Options
  • $error to catch errors in a set handler.
  • $deep watches deeply for changes in a query.
  • $skip: disables all queries and subscriptions in a given component.
  • $skipAllQueries: disables all queries from a component.
  • $skipAllSubscriptions: to disables all subscriptions in a component.

Apollo Components

Inspired by the way the Apollo Client is implemented for React (React-Apollo), Vue-Apollo provides us with a few components that we can use out of the box to manage the UI and state of our queries and mutations with a Vue component inside the template.

ApolloQuery

Simpler way of managing our queries in a more intuitive manner:

<ApolloQuery
  :query="GET_ALL_USERS_QUERY"
>
    <template slot-scope="{ result: { loading, error, data } }">
        <!-- Loading -->
        <div v-if="loading">Query is loading.</div>

        <!-- Error -->
        <div v-else-if="error">We got an error!</div>

        <!-- Result -->
        <div v-else-if="data">{{ data.getAllUsers }}</div>

        <!-- No result (if the query succeed but there's no data) -->
        <div v-else>No result from the server</div>
    </template>
</ApolloQuery>
ApolloMutation

Very similar to the example above, but we must trigger the mutation with the mutate function call:

<ApolloMutation
  :mutation="CREATE_USER_MUTATION"
  :variables="{
    name,
    age
  }"
  @done="mutationFinished"
>
    <template slot-scope="{ mutate, loading, error }">
        <!-- Loading -->
        <h4 v-if="loading">The mutation is loading!</h4>

        <!-- Mutation Trigger -->
        <button @click="mutate()">Create User</button>

        <!-- Error -->
        <p v-if="error">An error has occurred!</p>
    </template>
</ApolloMutation>

Conclusion

GraphQL brings a lot of flexibility to API development, from performance, ease-of-use, and an overall different perspective of what an API should look and behave like. Furthermore, ApolloClient and Vue Apollo delivers a set of tools for better management of our UI, state and operations, even error handling and cache!

For more information about GraphQL and Apollo Client you can visit the following:

Author image
About Lucas Otero
Buenos Aires, Argentina Website