GraphQL basics and practical examples with Vue
GraphQL is a query language for your API, and a server side runtime for executing queries. A single endpoint can return data about multiple resources, which makes it a great fit for Vue.js single page applications.
This article will go over how to build a GraphQL API from scratch, as well as define and implement queries and mutations with custom types. I will use Node.js for the GraphQL server, and make requests and display the results using a Vue.js single page application.
The source code for this article is here.
Introduction
A GraphQL service is created by defining types and fields, then providing functions for each field on each type. The canonical example from the GraphQL documentation is:
type Query { // define the query
me: User // define the fields
}type User { // define the type
id: ID
name: String
}function Query_me(request) { // define the function
return request.auth.user
}
The above is how we implement a query, custom type, and endpoint using GraphQL. The matching client side query looks like this:
{
me {
name
}
}
which returns:
{
“me”: {
“name”: “username”
}
}
The official GraphQL documentation is excellent, but I found it lacking in practical examples of how to query the data using a standard HTTP client, and integrate it into my Vue.js app, so I’ll do the following in this article. We will use the new vue-cli to scaffold a Vue project to go with it.
Getting Started
Install the vue-cli by running:
npm install -g @vue/cli@latest
Create a new project by running:
vue create graphql-example
and go with the default by choosing ❯ default (babel, eslint)
. A ton of nod e modules will be installed. We also need to create a folder for the API server, so run the following after cd
into the project (cd graphql-example
)
mkdir server
npm install express express-graphql graphql --save
We added graphql
, as well as express
and express-graphql
, which is a thin layer that implements some best practices and guidelines for serving queries over HTTP.
Basic Query
Let’s setup up a simple query to make sure everything is working, and see what a GraphQL server looks like. Inside of server/index.js
, require some modules:
const express = require('express')
const { graphql, buildSchema } = require('graphql')
const graphqlHTTP = require('express-graphql')
const cors = require('cors')
- express and express-graphql will let us response to HTTP requests
- buildSchema is used to define the types (more soon)
- cors will let us make requests from our Vue app, which will run on port
8080
, to the server running on port4000
The next thing to do is define the schema — what types of queries and types the server will use. Our first schema is basically the “hello world” of GraphQL:
const schema = buildSchema(`
type Query {
language: String
}
`)
We define a Query type called language
. It returns a String
. GraphQL is statically typed — fields have types, and if something doesn’t match up, and error is thrown.
Unlike REST APIs, Graph APIs have just one endpoint, which responds to all requests. This is called a resolver. I’ll call mine rootValue
, and include the implementation for the language
query:
const rootValue = {
language: () => 'GraphQL'
}
language
just returns a String
. If we returned a different type, for example 1
or {}
, an error would be thrown, since when we declared language
in the schema, we specified a String
would be returned.
The last step is to create the express app, and mount the resolver, rootValue
, and schema
.
const app = express()
app.use(cors())app.use('/graphql', graphqlHTTP({
rootValue, schema, graphiql: true
}))app.listen(4000, () => console.log('Listening on 4000'))
Let’s now implement the client side Vue app, that will make the request.
Making a request
Head over to src/App.vue
, and delete the boilerplate. It should now looks like this:
<template>
<div id="app">
</div>
</template><script>
import axios from 'axios'export default {
name: 'app'
}
</script>
We also import axios
, which we will use to make the HTTP requests.
By default, graphqlHTTP
listens for POST
requests. According to the recommendations in serving over HTTP, we should include the query
and variables
in the body of the request. This will lead us to the following request:
axios.post('http://localhost:4000/graphql', {
query: '{ language }'
})
The query should be inside curly braces. Adding a button to trigger the request, and a variable to save the result, we end up with:
<template>
<div id="app">
<h3>Example 1</h3>
<div>
Data: {{ example1 }}
</div>
<button @click="getLanguage">Get Language</button>
<hr>
</div>
</template><script>
import axios from 'axios'export default {
name: 'app', data () {
return {
example1: ''
}
}, methods: {
async getLanguage () {
try {
const res = await axios.post(
'http://localhost:4000/graphql', {
query: '{ language }'
})
this.example1 = res.data.data.language
} catch (e) {
console.log('err', e)
}
}
}
}
</script>
Let’s run this. In one terminal, start the GraphQL server with node server
. In another, run the Vue app using npm run serve
. Visit http://localhost:8080
. If everything went well, you will see:
Clicking “Get Language” should return and render the result.
Ok, great. So far we:
- defined a schema
- created the resolver,
rootValue
- make a request using
axios
, which included the query
What else can we do with GraphQL?
Custom Types with Models
GraphQL lets us define custom types, and objects to represent them in the target language — in this case, JavaScript, but there are GraphQL clients for most server side languages. I will define a Champion
type in the schema, and matching ES6 class to store any properties and methods.
Firstly, update the schema:
const schema = buildSchema(`
type Query {
language: String
} type Champion {
name: String
attackDamage: Float
}
`)
Nothing too exciting other than a new type, Float
. Next we can define an ES6 class to represent this type, and store any instance methods or additional data. I’ll define this in a new file, server/champion.js
.
class Champion {
constructor(name, attackDamage) {
this.name = name
this.attackDamage = attackDamage
}
}module.exports = Champion
Nothing special, just a ES6 class. Note we have name
and attackDamage
— the same fields defined in the schema for Champion
.
Now, let’s create another query that uses the Champion
type. The updated schema
is as follows:
const schema = buildSchema(`
type Query {
language: String
getChampions: [Champion]
} type Champion {
name: String
attackDamage: Float
}
`)
getChampions
returns an array of Champion
. Great! To finish this example off, some mock data and another endpoint:
const champions = [
new Champion('Ashe', 100),
new Champion('Vayne', 200)
]const rootValue = {
language: () => 'GraphQL', getChampions: () => champions
}
Restart the server by pressing ctrl+c
in the terminal running the server, and run node server
again. Let’s verify if this is working, by sending a query from the client.
Querying for specific fields
Querying getChampions
is a little more interesting than language
. This time the result will contain the user defined Champion
type — and whatever fields we ask for. GraphQL requires us to be explicit in which fields we want. For example the following query:
{
getChampions
}
will not work. At least one field should be specified. The updated query:
{
getChampions {
name
}
}
Returns:
{
"data": {
"getChampions": [
{
"name": "Ashe"
},
{
"name": "Vayne"
}
]
}
}
Notice only the name is returned! If we included attackDamage
, we would get that too. The query:
{
getChampions {
name
attackDamage
}
}
and response:
{
"data": {
"getChampions": [
{
"name": "Ashe"
"attackDamage": 100
},
{
"name": "Vayne"
"attackDamage": 200
}
]
}
}
Implementing this in the Vue app is equally straightforward:
<template>
<div id="app">
<!-- ... --> <h3>Example 2</h3>
<div>
Data:
<div v-for="champion in champions">
{{ champion }}
</div>
</div>
<button @click="getChampions">Get Champions</button>
</div>
</template>export default {
name: 'app', data () {
return {
/* ... */,
champions: []
}
}, methods: {
/* ... */
async getChampions () {
const res = await axios.post(
'http://localhost:4000/graphql', {
query: `{
getChampions {
name
}
}`
})
this.champions = res.data.data
}
}
}
Make sure you restart the server with node server
, if you didn’t already. No need to restart the Vue app, since webpack’s hot reload will automatically update when you save any changes.
Clicking “Get Champions” yields:
Passing Arguments
getChampions
return all champions. GraphQL also supports passing arguments, to return a subset of data. This requires:
- an additional
variables
object in the POST body - telling the client side query the type of arguments you will parse to the query from
variables
.
Let’s implement a getChampionByName
query. As usual, start with the query definition:
const schema = buildSchema(`
type Query {
language: String
getChampions: [Champion]
getChampionByName(name: String!): Champion
} type Champion {
name: String
attackDamage: Float
}
`)
Notice we declare the argument name
, and the type String!
. The !
means the argument is required.
Next, the implementation:
const rootValue = {
language: () => 'GraphQL', getChampions: () => champions, getChampionByName: ({ name }) => {
return champions.find(x => x.name === name)
}
}
Nothing too exciting — we just use find
to get the corresponding champion. An improvement would be to add some error handling, and compare name
disregarding case.
Now, the client side implementation. This is where things get a bit more interesting. When passing arguments, we should name the query, and declare the arguments with the corresponding type:
async getChampionByName () {
const res = await axios.post('http://localhost:4000/graphql', {
query: `
query GetChampionByName($championName: String!) {
getChampionByName(name: $championName) {
name
attackDamage
}
}`,
variables: {
championName: 'Ashe'
}
})
this.champion = res.data.data.getChampionByName
}
Line by line:
query GetChampionByName
is the name we are giving to the query. This can be anything, but should be descriptive of what the query is doing. In this case, since we are only callinggetChampionByName
, I used a convention when the name is the same as the query on the server side, but capitalized the first letter. In real application, a single API call might include many different operations. Naming the query can make the code more easily understood.($championName: String!)
means thatvariables
should contain achampionName
, and it is not optional.getChampionByName(name: $championName)
is the query to execute on the server side. The first argument,name
, should use thechampionName
value in thevariables
object.- We are requesting
name
andattackDamage
in the response.
Some extra markup will let us display the result in the Vue app (don’t forget to restart the GraphQL server):
<template>
<div> <!-- ... --> <h3>Example 4</h3>
Name: <input v-model="name">
<div>
Data:
{{ champion }}
</div>
<button @click="getChampionByName">Get Champion</button> </div>
</template><script>
import axios from 'axios'export default {
data () {
return {
/* ... */
champion: {}
}
},methods: { /* ... */ async getChampionByName () {
const res = await axios.post(
'http://localhost:4000/graphql', {
query: `
query GetChampionByName($championName: String!) {
getChampionByName(name: $championName) {
name
attackDamage
}
}`,
variables: {
championName: 'Ashe'
}
})
this.champion = res.data.data.getChampionByName
}
}
}
Updating Records
So far, we have just been fetching data. You will often want to update data, too, which is why GraphQL also provides mutations. The syntax and implementation isn’t too far from what we covered so far. Let’s start with by defining the mutation:
const schema = buildSchema(`
type Query {
language: String
getChampions: [Champion]
getChampionByName(name: String!): Champion
} type Mutation {
updateAttackDamage(name: String!, attackDamage: Float): Champion
} type Champion {
name: String
attackDamage: Float
}
`)
Mutations goes in a Mutation
type. The rest of the syntax should be familiar by this point. We are returning the updated record, a Champion
type. The implementation is equally straightforward:
const rootValue = {
language: () => 'GraphQL', getChampions: () => champions, getChampionByName: ({ name }) => {
return champions.find(x => x.name === name)
}, updateAttackDamage: ({ name, attackDamage = 150 }) => {
const champion = champions.find(x => x.name === name)
champion.attackDamage = attackDamage return champion
}
}
In a more realistically example, you might execcute an SQL query to update a record in a database, or do some validation. We have to return a Champion
type, since we specified so in the mutation declaration. GraphQL will automatically select the correct fields to return, based on the request — we will ask for the name
and updated attackDamage
, so shown below:
methods: {
/* ... */ async updateAttackDamage () {
const res = await axios.post('http://localhost:4000/graphql', {
query: `
mutation UpdateAttackDamage(
$championName: String!, $attackDamage: Float) {
updateAttackDamage(name: $championName, attackDamage: $attackDamage) {
name
attackDamage
}
}`,
variables: {
championName: this.name,
attackDamage: this.attack
}
})
this.updatedChampion = res.data.data.updateAttackDamage
}
}
The only real difference here is we declared the operation name to be a mutation
type instead of a query
type.
The fully updated example is as follow:
<template>
<div>
<!-- ... -->
<h3>Example 4</h3>
Name: <input v-model="name">
Attack Damage: <input v-model.number="attack">
<div>
Data:
{{ updatedChampion }}
</div>
<button @click="updateAttackDamage">Update Champion</button> </div>
</template><script>
import axios from 'axios'export default { data () {
return {
/* ... */
updatedChampion: {},
attack: 5.5
}
}, methods: {
/* ... */
async updateAttackDamage () {
const res = await axios.post('http://localhost:4000/graphql', {
query: `
mutation UpdateAttackDamage($championName: String!, $attackDamage: Float) {
updateAttackDamage(name: $championName, attackDamage: $attackDamage) {
name
attackDamage
}
}`,
variables: {
championName: this.name,
attackDamage: this.attack
}
})
this.updatedChampion = res.data.data.updateAttackDamage
}
}
}
As usual, restart the GraphQL server. The result is as follows:
You can click “Get Champion”, and see if the data was saved correctly (it should return the newly updated attack damage):
Testing
I did not go over testing. However, testing the server side endpoints is esay, since it’s just plain JavaScript — just export the rootValue
object, and tests the functions like you normally would. I’ll explore testing a GraplQL API in a future post.
Conclusion
There is a ton of other things GraphQL can do. Read more on the official site. I hope to explore more in future posts. It is a refreshing alternative to REST, and a great fit for Vue.js based single page applications.
The source code for this article is here.