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 todate
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&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: () => 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
}"
>
<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 && this.newTitle.trim();
const description= this.newDesc && this.newDesc.trim() ;
const written_by = this.newAuthor && this.newAuthor.trim() ;
await this.$apollo.mutate({
mutation: ADD_BOOKS,
variables: {
description,
title,
written_by
},
update: (cache, {
data: { insert_books}}) => {
// 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(() => {
this.$router.push({path: '/'})
}).catch(err => 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.