Build a CRUD Application with Hasura and Vue-Apollo

Build a CRUD Application with Hasura and Vue-Apollo


In this tutorial, we would be building a simple Vue application with Hasura and Vue-Apollo that allows you to add, delete, and get all books you have read.

Prerequisites

  • Familiarity with HTML, CSS and JavaScript
  • Basic Knowledge of Vuejs
  • Basic Knowledge of Graphql
  • GraphQL Playground app. Download here
  • VS Code or any other code editor

App Requirements

  • Hasura: It’s an open-source graphql service that gives you instant realtime graphql APIs with PostGre database.
  • Heroku: We would be using heroku for hosting the API.
  • Vue-Apollo: This integrates ApolloClient in our Vue app with declarative components. We would use vue-apollo to consume our graphQL API

GraphQL is a query language for your APIs and runtime for consuming those queries with your existing data. Simply, it gives you the luxury of asking for a specific kind of data from your server and getting expected results.

GraphQL has two parts:

  • The Server: GraphQL server simply exposes your API via a single endpoint. APIs here are written as a series of type definitions and resolvers. Graphql gives you a variety of server libraries to expose your APIs.
  • The Client: GraphQL client accepts queries on the frontend, connects to the server endpoint, and ask for the data you want to receive. They are also a variety of client libraries and JavaScript has a handful including popular libraries such as AWS Amplify and ApolloClient.

Let’s get started with coding already

Building the API and setting up DB with Hasura

Let’s get our API and database ready. Follow these steps:

  • Go to hasura.io and sign up. Then click the Start for free button.
  • Create my first project and then choose heroku (sign up if you don’t have an account) for Database Setup.
  • Give it some seconds, and it’ll generate a postgre uri, copy it and store somewhere. Next page would have your console details, you can screenshot them too. You should also get your graphql API url. Something like this: this
  • Now we are here in the hasura graphql console, let’s create our database tables and relationships.

Setting up database Navigate to the Data tab. We would just be working with a single table called books. Click the add table on the left sidebar of the console. Follow these steps:

  • Give the table a name, i’m calling mine, “books” but you can decide to call yours anything else.
  • Create a column, name it “id”, set column_type to integer(auto-increment) and select the Unique checkbox.
  • Create another column, name it “title” and set column_type to text. Create 2 other columns, name them description and written_by and set their column_type to text.
  • Create a column, name it is_completed, set the column_type to date and default to now().
  • On the Primary Key field, select the id. Then click on Add table, this should successfully create your books table.

At the end, you should have something like this:

At the moment, our table has no rows. Let’s create some. Click on the Insert Row tab and add some content to the empty fields (text, description and written_by). You should have something like this when you browse rows:

Queries and Mutations

GraphQL has three operation types:

  • Queries: This is a read-only operation requested to the graphql server. Similar to REST’s GET method. We would be querying the db we just created.
  • Mutation: This is a read-write operation. It’s similar to POST, PUT, PATCH and DELETE methods in REST. We would be writing mutations to create and update our DB rows.
  • Subscriptions: Subscriptions are used for creating websocket connections which allows servers to push live data.

Let’s query our database. Go to the Graphql playground (click on the GRAPHIQL tab). Copy this and hit the play button.

query MyBooks {
  books {
    id
    description
    title
    written_by
    created_at
  }
}

and you should be getting something like this:

{
  "data": {
    "books": [
      {
        "description": "book for new javasvript engineers",
        "title": "JS guide",
        "written_by": "Umoren Samuel",
        "created_at": "2020-08-22"
      },
      {
        "description": "Eminem aka Slim Shady is back to end us all",
        "title": "Slim Shady ",
        "written_by": "The Hip pop baddie",
        "created_at": "2020-08-22"
      },
      {
        "description": "For the people who love creating good things",
        "title": "Creationist",
        "written_by": "Samuel the creator",
        "created_at": "2020-08-22"
      }
    ]
  }
}

Let’s take a close look at the query

query MyBooks {
  books {
    id
    description
    title
    written_by
    created_at
  }
}

The query is a keyword and it’s required. The name MyBooks is optional but i am used to naming my queries for debugging purposes. books is the requested field, in graphql it’s called the object type and id, description, title, written_by and created_at are called scalar types. Scalar types can be a primitive type such as string, int, boolean, float, id or custom type.

Currently, we are querying for all users, let’s now query for a single user. Start your GraphQL Playground app on your PC, choose URL Endpoint option and paste your endpoint url; in my case it’s . You should have something like this:

Graphql playground auto-generates your API docs for you and it looks very beautiful, thanks to hasura graphql engine. You have your queries, mutations and subscriptions already all in one endpoint!!! (that’s pretty cool).

Let’s query a single book, copy and run this on your playground.

query getBookById($id:Int, $name:String) {
  books(where:{
    id: {_eq: $id},
    written_by:{_eq: $name}
  }) {
    id
    description
    title
    written_by
    created_at
  }
}

You should find a Query Variables tab below, write this:

{
  "id":  1,
  "name": "Umoren Samuel"
}

And when you run, you should get this:

  {
    "data": {
      "books": [
        {
          "id": 1,
          "description": "book for new javasvript engineers",
          "title": "JS guide",
          "written_by": "Umoren Samuel",
          "created_at": "2020-08-22"
        }
      ]
    }
  }

Let’s now try creating new books by writing mutations.

 mutation ($title: String $description: String $author: String) {
  insert_books(objects: {
    title: $title,
    description: $description
    written_by: $author
  }) {
    affected_rows
    returning {
      id
      title
      description
      created_at
    }
  }
}

Just like when you were querying, add these variables:

  {
    "title": "Bad ass books",
    "description": "For bad ass writers",
    "author": "Sammy Bijman"
  }

Your results should look like this:

  {
    "data": {
      "insert_books": {
        "affected_rows": 1,
        "returning": [
          {
            "id": 8,
            "title": "Bad ass books",
            "description": "For bad ass writers",
            "created_at": "2020-08-22"
          }
        ]
      }
    }
  }

Let’s now try deleting a row

    mutation delete_books($id: Int!){
      delete_books(where: {id: {_eq: $id}}){
        affected_rows
      }
    }

Add the variable

    {
      "id": 7
    }

You should get something like this

    {
      "data": {
        "delete_books": {
          "affected_rows": 1
        }
      }
    }

Creating the Vue app and consuming the API

Let’s create our vue project vue create graphqlbookstore and when that’s finished, install these packages: yarn add apollo-boost vue-apollo graphql-tag vue-cli-plugin-apollo bootstrap.

Apollo-boost is the easiest way to do stuff with ApolloClient, vue-apollo brings ApolloClient to our vue app with declarative components, graphql-tag or gpl a JavaScript template literal tag that parses GraphQL query strings into the standard GraphQL AST

Let’s hook up our client by passing it to ApolloClient exported from vue-apollo and pass our graphql endpoint to uri. Paste this in your main.js

    import Vue from 'vue';
    import App from './layout/App.vue';
    import router from './router';
    import 'bootstrap';
    import 'bootstrap/dist/css/bootstrap.min.css';
    import './registerServiceWorker';
    import VueApollo from "vue-apollo";
    import ApolloClient from 'apollo-boost';
    import gql from 'graphql-tag';
    
    // create a provider
    const apolloProvider = new VueApollo({
      defaultClient : new ApolloClient({
        uri: 'https://tender-meerkat-56.hasura.app/v1/graphql'
      })
    })
    Vue.use(VueApollo, gql);
    Vue.config.productionTip = false
    new Vue({
      router,
      apolloProvider,
      render: h => h(App)
    }).$mount('#app')

In the src folder create these new folders: grapql, pages and layout. Your directory tree should look like this:

In the graphql folder, create two new folders, name them mutation and queries. In queries, create a allBooks.gql file and paste this:

  query allBooks{
    books{
      id
      title
      description
      is_completed
      author{
        id
        name
      }
    }
  }

In mutation, create 2 new files, call them insert.gql and delete.gql. Paste this in insert.gql

  // src/graphql/mutation/insert.gql
  const ADD_BOOKS = gql `
    mutation ($book_string: String!) {
    insert_books(objects: {title: $book_string}) {
      affected_rows
      returning {
        id
        title
        description
        created_at
      }
    }
  }
  `;

now paste this in delete.gql

  // src/graphql/delete.gql
  mutation deleteTodo ($id: Int!){
    delete_books(where; {id: {_eq: $id}}){
      affected_rows
    }
  } 

Let’s write some UI. Go to layout and create App.vue file, then paste this

  
    <div class="bg-dark app">
      <section class="row overlay">
        <div class="container-fluid">
          <div class="header text-center mb-3">
            <h2 class="text-uppercase"> My Books Store </h2>
            <h4> Record of all the books i've read</h4>
          </div>
                   
      <div>
            
            
          </div>
        </div>
      </section>
    </div>
  
  
  
     @import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,wght@0,300;1,400&amp;display=swap');
  
      .app{
        font-family: 'Nunito Sans', sans-serif;
        
        background: #121212;
        min-height: 100vh;
        overflow-x: hidden;
        margin: 0 auto;
        padding: 0 auto;
      }
      .overlay{
        margin: 30px;
        background: rgb(20, 20, 20);
        /* width: 100%; */
        padding: 1em;
        border-radius: 10px;
        color: rgba(248, 248, 248, 0.8);
        text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
      }
  

Let’s create 2 pages, one for creating new books and the other for viewing all books. Go to pages and create a file AddBooks.vue and paste this

  // src/pages/AddBooks.vue
  
      <div>
          
      </div>
  
  import AddBooks from '../components/addbooks';
  export default {
      components : {
          AddBooks
      }
  }
  

Create `Home.vue`, add this:

  
    <div class="home ">
  
    
    </div>
 
  import AllBooks from "../components/allbooks";
  export default {
    components : {
      AllBooks
    }
  }
  

Let’s now write our router. It’s quite simple, we just have 2 pages. Add this to index.js in src/router/index.js

  import Vue from 'vue'
  import VueRouter from 'vue-router'
  import Home from '../pages/Home.vue'
  Vue.use(VueRouter)
    const routes = [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/addbook',
      name: 'AddBook',
      component: () =&gt; import('../pages/AddBooks.vue')
    }
  ]
  const router = new VueRouter({
    routes
  })
  export default router

Let’s now write the addbooks and allbooks components. Go to components and create 2 files;addbooks.vue and allbooks.vue.

In allbooks.vue, paste this:

  
      <div class="allBooks">
          <div class="text-center">
               Add New Book  
          </div>
           gql`
              query allBooks{
                  books{
                  id
                  title
                  description
                  created_at
                  written_by
                  
                  }
              }
              `"
              :variables="{
                  limit: $options.pageSize
              }"
              &gt;          
              
                  <div class="loading apollo"> Loading ...</div>
                  <div> Oh Yikes :) </div>               
                  <div>
                  
                      <div class="row"> 
                          <div class="col-md-4 col-sm-12 col-lg-4 col-xl-4 "> 
                              <div class="card shadow-lg  bg-danger mb-3">
                                  <div class="card-body ">
                                      
                                      <h5 class="card-title">{{book.title}}</h5>
                                      <div class=" justify-content-between">
                                          <p class="small "> written by: {{book.written_by}}</p>
                                          <p class="small "> {{book.created_at}}</p>
                                      </div>
                                          <p class="card-text"> {{book.description }} </p>
                                  
                                      <div class="text-right"> 
                                          <button class="btn btn-sm btn-dark"> delete book</button>
                                  </div>
                              </div>
                        </div>
                          </div>
                      </div>
                      
                  </div>    
                  <div class="no-result apollo text-center"> <h2>Nothing dey here </h2></div>   
              
          
      </div>
  
  
  import gql from 'graphql-tag';
  const REMOVE_BOOK = gql `
    mutation delete_books($id: Int!){
    delete_books(where: {id: {_eq: $id}}){
      affected_rows
    }
  }
    
  `;
  export const ALL_BOOKS = gql `
      query allBooks{
          books {
              id
              title
              description
              is_completed
              author{
              id
              name
              }
          }
  }`;
  
  export default {
    name: 'AllBooks',
    mounted(){
        this.forceRerender();
    },
    data(){
        return{
            componentKey: 0
        }
    },
    methods: {
   
     async removeBook(book){
      await this.$apollo.mutate({
          mutation: REMOVE_BOOK,
          variables: {
            id: book.id
          },
          refetchQueries: [
            {
              query: ALL_BOOKS
            }       
            
          ]
        })
      },
      forceRerender() {
        this.componentKey += 1;  
      }
    }
   
  }
  
  
      .allBooks{
          margin-top: 2em;
      }
  

Let’s go through the code and get to understand the moving parts.

Starting with ApolloQuery: Earlier, i mentioned that VueApollo gives you all these cool declarative clients for integrating ApolloClient to your app. ApolloQuery is one of those queries, you can write your queries directly in your template. Inside the component, we have a slot where you can access various slot data about the query, like the result object which has default states such as loading, error and data.

Next is the removeBook(book) method: You can either use “ component or this.$apollo.mutate in methods. In there, i’m passing mutation (which gets the graphql mutation), variables and refetchQueries.

Add this to addbooks.vue

  
      <div class="form_input mx-auto col-md-6">
          
              <div class="mb-3">
                  <label for="title"> Book Title</label>
                  
              </div>
              <div class="mb-3">
                  <label for="title"> Author: </label>
                  
              </div>
         
              <div>
                  <label for="description"> Book Description</label>
                  <textarea class="form-control" />
                 
              </div>
              <button class="btn btn-primary mt-2"> Add Book</button>
          
      </div>
  
  
      import gql from "graphql-tag";
      import { ALL_BOOKS } from './allbooks.vue';
       const ADD_BOOKS = gql`
          mutation ($title: String!, $description: String!, $written_by: String!) {
          insert_books(objects: {title: $title, description: $description, written_by: $written_by}) {
              affected_rows
              returning {
                  id
                  title
                  description
                  created_at
                  written_by
              }
          }
      }`;
     
      export default {
      
          data() {
              return {
                  newTitle: '',
                  newDesc: '',
                  newAuthor: ''
              }
          },
          methods:{
              async addBook() {
                  // const { newTitle, newDesc } = this.$data;
                  const title = this.newTitle &amp;&amp; this.newTitle.trim();
                  const description= this.newDesc &amp;&amp; this.newDesc.trim() ;
                  const written_by = this.newAuthor &amp;&amp; this.newAuthor.trim() ;
                  await this.$apollo.mutate({
                      mutation: ADD_BOOKS,
                      variables: {    
                          description,
                          title,
                          written_by
                      },
                      update: (cache, {
                           data: { insert_books}}) =&gt; {
                          // Read data from cache for this query
                          try {
                              const insertedBook = insert_books.returning;
                              console.log(insertedBook)
                              cache.writeQuery({
                                  query: ALL_BOOKS
                              })
                          }
                          catch (err) {
                              console.error(err)
                          }
                      }
                  }).then(() =&gt; {
                      this.$router.push({path: '/'})
                  }).catch(err =&gt; console.log(err));
                  this.newTitle = '';
                  this.newDesc = '';
                  this.newAuthor = '';
              },
          
          }
      }
  
  
      .form-group{
      
        margin: 10px;
        background: rgb(35, 35, 35);
        
        padding: 1em;
        border-radius: 10px;
        color: rgba(248, 248, 248, 0.8);
        text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
      
      }
  

Now in your terminal, run your server yarn serve and you should have something like this as your homepage.

Try adding a book too.

Challenge

  • Add Edit book functionality
  • Add some kind of alert when a book is deleted or added. You could check Sweetalert.

Summary

GraphQL is really cool when you’re coming from a REST background, and what’s even more exciting is that there’s a lot of open source work going in that direction. Hasura GraphQL engine is one of those really awesome GraphQL services.

The full Source Code is on Github and the demo here.


Share on social media

//