Full Stack Vue with GraphQL & AWS AppSync

Written by dabit3 | Published 2018/02/15
Tech Story Tags: javascript | vuejs | vue | graphql | apollo

TLDRvia the TL;DR App

Getting started with Vue & GraphQL using AWS AppSync.

To view the final project or to keep it as a reference, check out this repo.

This project will be using the vue-apollo and aws-appsync packages.

I am not a Vue developer. I’ve tried it when I’ve had the time to be learning something new, and I’ve really loved it so far, but really I specialize in React & React Native.

I was looking for some documentation about how to get up and running with AppSync & Vue & I realized there was none, so I decided to write a blog post to show others how it’s done!

If you haven’t already used it, AppSync is a service that allows you to quickly configure and deploy scalable serverless GraphQL APIs that also have features such as subscriptions (real time data) & multiple data sources among other things.

In this post, we’ll walk through wiring up a new AppSync API, attaching it to, and using it in a Vue application.

The application we will be building will need to demonstrate basic create, read, update, and delete operations.

Creating the API

The AppSync API will need to have the ability to create, read, update, and delete a list of items.

To create a new API, go to the AWS AppSync dashboard and create a new API.Give the API the following schema:

type Task {id: ID!name: String!completed: Boolean!}

type Query {fetchTask(id: ID): Task}

Click save, then click create resources.

Once you’ve created your resources around the above schema, download your AWS AppSync.json file, we will need it to configure the client. You can download it by scrolling to the bottom of the main page in your API and clicking on Download next to “2. Download the AWS AppSync.js config file”:

Save this file, we will need it once we have created our Vue app.

Creating the client

Next, we will use the Vue cli to create a new application using the vue router:

vue init webpack vue-graphql

Here are the exact options I chose:

Next, we need to change into the directory and install the dependencies we will need:

yarn add aws-appsync vue-apollo graphql-tag

Now, move the AppSync.js file you downloaded from the AppSync dashboard, and save it in the src directory of your project.

Next, we need to initialize the AppSync client. In src/main.js, update the entrypoint to the following:

// main.jsimport Vue from 'vue'import App from './App'import router from './router'

import AWSAppSyncClient from "aws-appsync"import VueApollo from 'vue-apollo'import appSyncConfig from './AppSync'

const client = new AWSAppSyncClient({url: appSyncConfig.graphqlEndpoint,region: appSyncConfig.region,auth: {type: appSyncConfig.authenticationType,apiKey: appSyncConfig.apiKey,}},{defaultOptions: {watchQuery: {fetchPolicy: 'cache-and-network',}}})

const appsyncProvider = new VueApollo({defaultClient: client})

Vue.config.productionTip = falseVue.use(VueApollo)

new Vue({el: '#app',router,components: { App },provide: appsyncProvider.provide(),template: '<App/>'})

Finally, we need to change our App.vue recognize that we do not want to render until the client has been rehydrated from local storage.

Because the AppSync client stores our data locally, making it available offline, we need to be sure the data is available to the application when it loads. To do this, we can wait for a promise called apollo.provider.defaultClient.hydrated, waiting for it to be fulfilled, before rendering our app. We provide a v-if statement that will show and hide the main entrypoint of our app depending on if the client is hydrated:

// App.js<template><div id="app" v-if="hydrated"><router-view/></div></template>

<script>export default {name: 'App',data: () => ({ hydrated: false }),async mounted() {await this.$apollo.provider.defaultClient.hydrated();this.hydrated = true;}}</script>

Once this is set up, run npm run dev and you should be ready to move to the next step!

Creating queries & mutations

Now that we have the base app up and running and our GraphQL backend ready to go, the next logical step may be to go ahead and begin writing the GraphQL queries that will correlate with our AppSync schema.

In the src directory, let’s go ahead and create a mutations and queries folder:

cd srcmkdir queries mutations

Next, let’s go ahead and create the following queries:

  1. ListTasks.js

// queries/ListTasks.js

import gql from 'graphql-tag'export default gql`query listTasks {listTasks {items {idnamecompleted}}}`

And the following mutations:

  1. AddTask.js

  2. UpdateTask.js

  3. DeleteTask.js

    // mutations/AddTask.js

import gql from 'graphql-tag'export default gql`mutation addTask($name: String!, $completed: Boolean!) {createTask(input: {name: $name, completed: $completed}) {idnamecompleted}}`

// mutations/UpdateTask.js

import gql from 'graphql-tag'

export default gql`mutation updateTask($id: ID!, $name: String!, $completed: Boolean!) {updateTask(input: {id: $idname: $namecompleted: $completed}) {idnamecompleted}}`

// mutations/DeleteTask.js

import gql from 'graphql-tag'

export default gql`mutation deleteTask($id: ID!) {deleteTask(input: {id: $id}) {id}}`

Now, go ahead and create a new file in the components folder called Tasks.vue.

Finally, we will update the router to use the new Tasks.vue file as the entrypoint.

Update index.js to the following:

import Vue from 'vue'import Router from 'vue-router'import Tasks from '@/components/Tasks'

Vue.use(Router)export default new Router({routes: [{path: '/',name: 'Tasks',component: Tasks}]})

Now that we have our application configured with AppSync, our queries and mutations created, and our folder structure configured, the only thing we need to do is create our Task component and wire everything together.

There will be a lot going on in the Tasks.vue component, and if you understand how Vue works, then the Template will make a lot of sense. If you are new to Vue, it is actually extremely straight forward for newcomers, as this is the first Vue app I’ve ever created and the first time I’ve ever used Vue!

Here is the final Tasks.vue component. (also embedded below)

We will be breaking apart the functionality in this component and going into each area to describe what is going on.

Template

<template><div class="tasks"><h1>Task Manager</h1><input v-model="taskname" placeholder="Task Name" class="input"><button@click="createTask()"class="taskButton">Create Task</button><ul><liclass="task"v-for="(task, index) in tasks" :key="index"><p class="text">{{ task.name }}</p><p@click="toggleComplete(task)"class="text button">{{ task.completed ? 'completed' : 'not completed' }}</p><p@click="deleteTask(task)"class="text button delete">Delete task</p></li></ul></div></template>

Our template is pretty basic. We are looping through an array of tasks, showing the name, whether the task is completed or not, and a delete button.

We also have a text input with a button that will submit to the API to create a new task.

Imports

At the top of our script declaration, we import the queries & mutations that we will be needing to interact with our AppSync API.

import ListTasks from '../queries/ListTasks'import CreateTask from '../mutations/CreateTask'import DeleteTask from '../mutations/DeleteTask'import UpdateTask from '../mutations/UpdateTask'

Initial state

Let’s now take a look at our initial data:

data () {return {taskname: '',tasks: []}}

We set an initial value of tasks to an empty array, and taskname to an empty string. tasks will hold the task list array after it is fetched from AppSync, and taskname will hold the value of our text input.

Apollo configuration

Next, we will look at our apollo configuration for the tasks array:

apollo: {tasks: {query: () => ListTasks,update: data => return data.listTasks.items}}

We have set an initial query of ListTasks, and an update function that returns just the array of items from the data and sets it as the value for tasks.

CreateTask mutation

Now, let’s see how we can perform a mutation to add a new task to our API:

createTask() {const taskname = this.tasknameif ((taskname) === '') {alert('please create a task')return}this.taskname = ''const task = {name: taskname,completed: false}

this.$apollo.mutate({mutation: CreateTask,variables: task,update: (store, { data: { createTask } }) => {const data = store.readQuery({ query: ListTasks })data.listTasks.items.push(createTask)store.writeQuery({ query: ListTasks, data })},optimisticResponse: {__typename: 'Mutation',createTask: {__typename: 'Task',...task}},}).then(data => console.log(data)).catch(error => console.error("error!!!: ", error))

We take the taskName and create a new object called task, setting completed to false. We then call $this.apollo.mutate, setting the variables as the task object we just created.

We also supply both an optimisticResponse & update function to implement a snappy optimistic UI. This will basically assume the API call was successful and update our cache and UI, and if not it will then revert back to the state before the mutation was called.

DeleteTask mutation

deleteTask(task) {this.$apollo.mutate({mutation: DeleteTask,variables: {id: task.id},update: (store, { data: { deleteTask } }) => {const data = store.readQuery({ query: ListTasks })data.listTasks.items = data.listTasks.items.filter(task => task.id !== deleteTask.id)store.writeQuery({ query: ListTasks, data })},optimisticResponse: {__typename: 'Mutation',deleteTask: {__typename: 'Task',...task}},}).then(data => console.log(data)).catch(error => console.error(error))}

This is very similar to the CreateTask mutation in the sense that we are not only updating our API, but also providing an optimistic response as well as update function for optimistic UI.

UpdateTask mutation

toggleComplete(task) {const updatedTask = {...task,completed: !task.completed}this.$apollo.mutate({mutation: UpdateTask,variables: updatedTask,update: (store, { data: { updateTask } }) => {const data = store.readQuery({ query: ListTasks })const index = data.listTasks.items.findIndex(item => item.id === updateTask.id)data.listTasks.items[index] = updateTaskstore.writeQuery({ query: ListTasks, data })},optimisticResponse: {__typename: 'Mutation',updateTask: {__typename: 'Task',...updatedTask}},}).then(data => console.log(data)).catch(error => console.error(error))}

We use the UpdateTask mutation to toggle whether or not the task was completed. To do that, we take in the task as an argument, toggle the completed value to its opposite, then resubmit the task object to our API.

The final Tasks.vue should look like the following:

Conclusion

To view the final project, go here.

With this being my first time to use Vue, I was very surprised to see how easy it was to get up and running with everything.

The API for Vue is very intuitive and I did not run into anything that wasn’t just a search away either on Google or within their documentation.

I think for Vue developers looking to get up and running with GraphQL, this is a powerful and easy way to do so without having to deal with creating and maintaining your own backend & API.

My Name is Nader Dabit . I am a Developer Advocate at AWS Mobile working with projects like AppSync and Amplify, and the founder of React Native Training.

If you like React and React Native, check out out our podcast — React Native Radio on Devchat.tv.

Also, check out my book, React Native in Action now available from Manning Publications.

If you enjoyed this article, please recommend and share it! Thanks for your time.


Published by HackerNoon on 2018/02/15