Tutorial: Leveraging the Hasura platform, GraphQL and Apollo to build and deploy a fullstack react…

In this tutorial we are going to build a todo app from scratch. Our todo app will have the following features:

  • A simple UI to list, add, delete and toggle todos
  • Authentication
  • Connecting the react app to a backend using GraphQL APIs
  • Use the Apollo Client to make the GraphQL requests. 
    Note: We will be using the new Query and Mutation components that Apollo introduced in their 2.1.3 release of react-apollo.
  • Deployed on a publicly accessible URL

Table of contents

Pre-requisites

  • Ensure that you have node.js installed on your machine.
  • Install the hasura command line tool on your machine.
  • Login or register on the Hasura platform by running hasura login in your command shell.

Using Hasura to create our backend

The Hasura platform provides backend features like Authentication and a Database with GraphQL APIs. Moreover, you can also use it to deploy your apps to the cloud.

Let’s start with a basic Hasura project

$ hasura quickstart hasura/base todo-app

The above command does the following:

  • Clones a Hasura project into a directory called base
  • Creates a free Hasura cluster. In this case, the cluster is called illumination52. You can get information about your cluster by running the command hasura cluster status.
  • Initialises the base directory as a git repository

Let’s now deploy this project on to the cluster to start building our backend$ git add . && git commit -m "Initial Commit"
$ git push hasura master

$ cd todo-app

After you are done deploying this project. We can start building the backend for our todo app.

The API Console is basically a UI that helps you use and explore the backend features provided by the Hasura platform.

Open the API Console by running the following inside the project directory

$ hasura api-console

Creating tables

To create a new table, head to the Data tab and click on the Create Table button

We will be creating a table todo with the following columns

  • id of type Integer(auto-increment): This is the unique id associated with every todo.
  • task of type Text: This will be the content of the todo.
  • completed of type Boolean: This is a boolean field indicating whether the todo is completed.
  • user_id of type Integer: This column will store the id of the user who has created this todo.

Our primary key will be id

Click on create.

By default, every table created on the Hasura platform only has admin permissions set on it. This means that only admin users are allowed to fetch and modify data from tables by default.

Every user created using the Hasura platform’s authentication APIs are given a user role by default. Since we are going to be using Hasura to add authentication to our react app as well, we need to add permissions to the todo table. The permissions should allow users to fetch, add, update and delete their own todos. Fortunately, this also can be easily done on the API Console

Adding table permissions

Select the todo table from the panel on the left. Click on the Permissions tab.

Click on insert and add the permissions as shown below

The above permission means that for every row inserted into this table, the user_id should be the same as the id of the Hasura user who is making the insert request.

Similarly, set permissions for select, update and delete

Note: The columns selected for select permissions are the columns whose value a user should be allowed to fetch from the table. We allow select on all columns. Similarly, the selected columns for the update permission are the columns whose data can be modified once they have been inserted into the table. In this case, we only allow update on completed and task.

Our modelling for the todo app is now complete. Next, we need to figure out authentication for our app.

Authentication

The Hasura platform provides instant authentication APIs to be used in our front-end app. You can check these out in the API Explorer

Let’s take a look at a simple signup API to register a new user into our app with a username and password.

Click on SignUp under the Username Password

In the image above, I have signed up with the username jaison and password as secretpassword. The response of this signup looks like so:    

{
	"auth_token": "fdea373b0ec9065f207c3790f77bb18aea5345aa32ceb99",
	"username": "jaison",
	"hasura_id": 2,    
	"hasura_roles": [ "user" ]
}
  • auth_token: is the session token for this user. We will be sending this token in our request header while making requests to our todo table to identify as the user jaison
  • hasura_id: is the id of this user. This is the id we will store as user_id in the todo table. This will be unique for every user created.
  • hasura_roles: are the roles associated with this user. As mentioned above, every new user created using the authentication APIs are given a default role of user. You can add custom roles or add an admin role to this user. But that is beyond the scope of this tutorial.

Now that we have these APIs we can easily build a UI for our todo app. Having said that, we are actually NOT going to be building a UI for authentication and instead use the Auth UI Kit that Hasura provides for our app.

Auth UI Kit

The Auth UI kit is a ready made UI that handles authentication automatically for us. It runs on the url https://auth.<cluster-name>.hasura-app.io/ui. In this case, it is https://auth.illumination52.hasura-app.io/ui.

The Auth UI Kit does two major things for us

  • Redirects to a specified URL after successful authentication.
  • Sets the session of the user as a cookie.

So, all we have to do is show the Auth UI Kit with the url to our react app as the redirect URL and simply include the cookie in every request to the database.

We have created our database schema, figured out authentication and also set permissions on our database so that only authenticated users are allowed to get or modify data. Let’s take a look at how this works by inserting some data into the todo table as the user we created above (username: jaison, hasura_id: 2)

Exploring the GraphQL APIs

In the API Console click on the API Explorer tab and select GraphQL under the heading Data from the panel on the left.

You can now use the GraphiQL interface to explore the various GraphQL endpoints available.

GraphQL Mutations

Let’s start by inserting data into the todo table. The mutation to insert data will look like so  

mutation ($objects: [todo_input!]){
	insert_todo(objects: $objects) { 
		affected_rows 
		returning {      
			id
			task
			completed
			user_id    
		}  
	}
}

And the variables would be

{
 "objects": [
    {
      "task": "Task 1",
      "completed": false,
      "user_id": 2
    }
  ]
}

Trying this out in the API Explorer

The query fails! This is because we have not added the session token in the header and since anonymous users are not allowed to insert data, the query fails.

Try the query again after adding the following headers

  • Authorization: Bearer <session-token> (In this case Bearer fdea373b0ec9065f207c3790f77bb18aea5345aa32ceb99)
  • X-Hasura-Role: user

And it succeeds!

Similarly, update mutations would be  

mutation ($todoId: Int, $set: todo_input!){
	update_todo(where: { id: { _eq: $todoId } } _set: $set) {    
	affected_rows  
	}
}

Variables:  

{"todoId": 1,  "set": { "completed": true } }

Finally, delete mutations:  

mutation delete_todo ($todoId: Int) {
	delete_todo(where: {id: {_eq: $todoId}}) {      
		affected_rows    
	}
}

GraphQL Query

To get data from the table  

query ($userId: Int) {
	todo(where: { user_id: { _eq: $userId }}) {    
		id    
		completed    
		task  
	}
}

Variables  

{"userId": 2}

Response would be

{ "data": { "todo": [ { "id": 1, "completed": true, "task": "Task 1" }] }}

Ok, now that we have our backend figured out. Let’s move on to our react code and stitch all of this up.

Building the react app

We are going to use create-react-app as our react starter kit.

Install create-react-app by running

$ npm install -g create-react-app

Create a new project

$ create-react-app app

cd into the app directory and install dependencies$ npm install

$ cd app

Run the app

$ npm run start

Your app will now be running at http://localhost:3000

The current version of create-react-app comes with service workers enabled by default. Configuring and using service workers is a topic that is out of scope of this article. You can check out this blog post for insights into service workers.

Delete all the files inside thesrc directory. We are going to start from scratch.

Next, create new directories called components, graphQueries and styles inside the src directory.

Installing Dependencies

We are going to be using the Apollo Client to make the GraphQL APIs from our react app

$ npm install --save apollo-boost graphql graphql-tag react-apollo

constants.js

Let’s start by creating a constants.js file inside the src directory

const clusterName = 'illumination52';
const GRAPHQL_URL = 'https://data.' + clusterName + '.hasura-app.io/v1alpha1/graphql';
const AUTH_URL = 'https://auth.' + clusterName + '.hasura-app.io';


export {
  GRAPHQL_URL,
  AUTH_URL
};
  • clusterName is the name of your cluster. In this case it is illumination52.
  • AUTH_URL is the base url of the authentication APIs. It is always of the type https://auth.<cluster-name>.hasura-app.io
  • GRAPHQL_URL is the url for the GraphQL API. It is always of the type https://data.<cluster-name>.hasura-app.io/v1alpha1/graphql. We will be providing this url to the Apollo Client.

Index.js

Create a new index.js file inside the src directory with the following code

import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import ApolloClient from 'apollo-client';
import { createHttpLink } from 'apollo-link-http';
import { ApolloProvider } from 'react-apollo';
import { InMemoryCache } from 'apollo-cache-inmemory';  

import { GRAPHQL_URL } from './constants';

const client = new ApolloClient({
	link: createHttpLink({uri: GRAPHQL_URL, credentials: 'include'  }),  		cache: new InMemoryCache({    addTypename: false  })
});


ReactDOM.render(
	<ApolloProvider client={client}>    <App />  </ApolloProvider>,  			document.getElementById('root'))
;

Here we are initialising our App with Apollo.

  • credentials: 'include' is used to automatically add the cookies set by the Auth UI Kit to all the requests made by Apollo.
  • addTypename: false is added to prevent Apollo from automatically adding the addTypename to the queries to the GraphQL server.

App.js

This is the component that we are rendering from index.js.

Although, using the Auth UI Kit sets the session in the cookie. We still need to get the user’s hasura_id This is because we are going to be inserting rows in the todo table, which is going to require a user_id. We are going to use one of the authentication APIs to fetch the info of the logged in user. You can test this out in the API Explorer

It is basically a Get request to https://auth.illumination52.hasura-app.io/v1/user/info with the session token sent in the header. The response is the same as the response from the login/signup endpoint.

So our App.js file will look like so

The flow of the code is such, hit the /v1/user/info endpoint to get user information when the component mounts

  • If this request responds with a non 200 response, redirect the user to https://auth.<project-name>.hasura-app.io/ui?redirect_url=http://localhost:3000 (In this case https://auth.illumination52.hasura-app.io/ui?redirect_url=http://localhost:3000)
  • If the request succeeds, then save the response as a global object.
  • We are using the constants.js file we created earlier to hold this information. Add the following to your constants.js file.
const REDIRECT_URL = 'http://localhost:3000'

var userInfo = null;

function saveUserInfo(info) {
}

function getUserInfo() {
}

export { REDIRECT_URL };

Run the app:

Now we have a basic react app with authentication.

Next, let’s create the todo components

GraphQL APIs

Let’s list down all of our GraphQL APIs in a file at graphQueries/todoQueries.js so that we are defining these APIs at once place.

import gql from 'graphql-tag';

const QUERY_TODO = gql`query fetch_todos {    todo {      id      task      completed    }  }`;


const MUTATION_TODO_ADD = gql`mutation insert_todo ($objects: [todo_input!]){    insert_todo(objects: $objects) {      affected_rows      returning {        id        task        completed      }    }  }`;


const MUTATION_TODO_UPDATE = gql`mutation update_todo ($todoId: Int, $set: todo_input!) {    update_todo(where: {id: {_eq: $todoId}} _set: $set) {      affected_rows    }  }`; 


const MUTATION_TODO_DELETE = gql`mutation delete_todo ($todoId: Int) {    delete_todo(where: {id: {_eq: $todoId}}) {      affected_rows    }  }`; 


export {
QUERY_TODO,  MUTATION_TODO_ADD,  MUTATION_TODO_UPDATE,  MUTATION_TODO_DELETE
};

Next we move on to our TodoComponents

TodoComponent.js

This component consists of two children components TodoInput and TodoList.

import React from 'react';
import '../../styles/Todo.css';
import TodoInput from './TodoInput';
import TodoList from './TodoList';

export default class TodoComponent extends React.Component {

	render() {
		return (
            <div className="parentContainer">
                <h1 className="header">Todos</h1>        
                <TodoInput userId={this.props.userInfo.hasura_id}/>        		
                <TodoList />      
            </div>    
		)  
	}
}

To keep our code clean, let’s put all of our todo related components inside components/todo.

TodoInput.js

We maintain the value of the input box in local state and then directly make the query to insert the query into the Hasura database and then update the local Apollo cache.

TodoList.js

Here, we are fetching the various todos for the particular user and then render each todo with the help of a Todo component.

Todo.js

This component handles the updation(toggle) and deletion of todos.

Run the app

And there we have it! A todo app built on react with authentication and a database.

Deploying the app on the Hasura platform

Now that we are done building our app, let’s deploy this app on the Hasura platform.

The platform has a docker based deployment. We need to write a Dockerfile to run our app. But, to make our lives easier, the Hasura platform also provides a react quickstart with a microservice running a react app. We are going to clone that microservice and replace the apps code with ours.

To clone a microservice from another hub project

$ hasura microservice clone ui --from hasura/hello-react

To expose this microservice on a route

$ hasura conf generate-route ui >> conf/routes.yaml

To enable the ability to git push to deploy this app

$ hasura conf generate-remote ui >> conf/ci.yaml

Your file structure will now look like this

The source code to the react app lives inside the microservices/ui/app directory. Let’s go ahead and replace the contents inside app with the contents of our react app.

If you face problems running the app after moving it to another directory, delete your node_modules and install it again by running npm install

After replacing the contents of app with our react code, the file structure would be

Deploying on the Hasura platform is literally pushing to a remote git repo$ git push hasura master

$ git add . && git commit -m "A good commit message"

This will push the react app and make is accessible on the ui subdomain.

To open the microservice on your web browser, run

$ hasura microservice open ui

To learn more about the Hasura platform check out the react boiler plate here.

You can find the code to this project at the following repo:

jaisontj/react-todo-app
react-todo-app - A fully functional todo app built using React and Hasura. It uses Hasura's GraphQL APIs with the help…github.com


Hasura is an open-source engine that gives you realtime GraphQL APIs on new or existing Postgres databases, with built-in support for stitching custom GraphQL APIs and triggering webhooks on database changes.


Hasura

Hasura

The Hasura GraphQL Engine gives you realtime, high performance GraphQL on any Postgres app. Now supports event triggers for use with serverless.

Read More