Hasura GraphQL Practical Guide
During this guide you will have a playground available to understand how to build and maintain a GraphQL API using Hasura, it’s recommended that you are familiar with what is GraphQL and its advantages over REST since this is a practical guide, you can learn the basics about GraphQL in this tutorial.
The code is available in this Github repository.
Technology Stack
Before starting this tutorial, make sure you have installed and understand the basics of:
- git: Code version management
- docker: Tool to build and manage containers
- docker-compose: Tool to manage several containers
- NodeJS and npm: Node is used only to set up Hasura actions and triggers.
Note: To fully explore Hasura, some examples use a basic NodeJS server with express however this is just for demonstrating purposes and understanding NodeJS is NOT required.
Quick Start
For this guide, I have prepared a playground repository with everything you need to fully test the tool and build your own GraphQL API from scratch. To avoid the overhead to set up a relational database with data and relationships, we will use an existing one called Pagila a Postgres Database Sample taken from this great article with a list of example databases, this database is about a DVD rental store.
I recommend you to use a database explorer tool like DbBeaver
or DataGrip
so you can take a better look at the database structure, however, it’s not important to understand everything, you just need to get familiar with it since we will query some data and change it.
Note: I wrote this tutorial so you can set up everything step by step, however, if for some reason you just want to run and test the final results, this document explains how to do it
Steps to set up the project
- Clone the repository:
git clone https://github.com/jsilversun/hasura_tutorial
- Install the dependencies:
npm install
- Clone the Pagila SQL scripts to the
database
folder
npm run database:clone
- Run
npx hasura init hasura-tutorial --admin-secret secret
afterward, you will see a new folder namedhasura-tutorial
this will contain all the settings you need for your particular Hasura instance like metadata, actions, and migrations - Run
docker-compose up -d
to start the docker containers specified in thedocker-compose.yml
file
Note: By running
docker-compose down
you can delete your containers and then rundocker-compose up -d
to start from scratch (This will reset both the API and the database).
Then you will see the following file structure:
Note: If you are interested in understanding the project’s file structure, you can read this doc.
And you can see your running containers with the docker ps
command, which confirms we have a GraphQL API running in the 8080
port and the database is using the 5432
.
Now we can use the Hasura Console
which is a graphical tool to explore or change settings in our new GraphQL API.
Run npx hasura console --project=hasura-tutorial
Then you will be redirected to http://localhost:9695/ and you will see how the Hasura Console
looks like:
Now we are ready to start!
Set up the GraphQL API
- Go to the
Data
tab - Click on
Track all tables
- Click on
Track all relations
Now you have your own GraphQL API ready to go!
Basics
In this section we will learn the three main operations you can do over a GraphQL API. Go to the GraphiQL
Tab.
Query
Remember to check Hasura queries documentation so you can learn more about the API, in this tutorial we will explore different examples but you can try your own examples from whatever you want to try in the database.
Queries are a simple way to get data from a backend, in this case, we can get information from the database by using Hasura.
Get all available actors
query getActors {
actor {
actor_id
first_name
last_name
}
}
Get an actor by id
query getActorById {
actor_by_pk(actor_id: 1) {
actor_id
first_name
last_name
}
}
Get an actor by id with his films
query getActors {
actor_by_pk(actor_id: 1) {
actor_id
first_name
last_name
film_actors {
film {
film_id
description
}
}
}
}
Get films with a replacement cost lower than 10
query getFilmsFrom {
film(where: {replacement_cost: {_lt: 10}}) {
film_id
description
replacement_cost
}
}
Aggregation queries
Get the average rental rate for films
query {
film_aggregate {
aggregate {
avg {
rental_rate
}
}
}
}
Subscription
Hasura subscriptions documentation
“A GraphQL subscription is essentially a query where the client receives an update whenever the value of any field changes upstream.
Subscriptions are supported for all kinds of queries. All the concepts of queries hold true for subscriptions as well.”
Subscriptions are an incredible feature for automatic updates in the client, let’s see it in action.
Run the following GraphQL subscription:
subscription getActorById {
actor_by_pk(actor_id: 1) {
actor_id
first_name
last_name
}
}
You will see it's still running since it has a pause button, now open another Hasura console in a different browser tab, go to the Data
section, look for the actor table click the edit
button and change the name of the actor from PENELOPE
to PENELOPE!
Go back to your original tab and you will see how the result changed automatically
This amazing feature will allow the frontend clients to get automatic updates so your system will be refreshed in real-time, subscriptions apply to any Hasura query, test this feature further so you can see its capabilities.
Mutation
Let’s roll back the name of the actor we changed earlier in the subscription example from PENELOPE!
to PENELOPE
mutation updateActorById {
update_actor_by_pk(
pk_columns: {
actor_id: 1
},
_set: {
first_name: "PENELOPE"
}
) {
actor_id
first_name
last_name
}
}
Then if you refresh the actor
table in the Data
tab and you will see the record was updated successfully.
Authentication
For authentication, we will explain how to use firebase to get a JSON Web Tokens (JWT) and then perform queries to the API, since I like practical examples I prepared two endpoints that you can use to get some JWT’s to use with Hasura.
Note: If you want to learn how these endpoints work and how you can run them locally please read this doc.
For more information in this link you can read more about what JWT are, in this guide we will focus on how to actually use them and see their content so we can build our secure API.
Authentication
- To get a JWT for the staff role visit the URL https://hasura-guide.herokuapp.com/staff and copy the token
- To get a JWT for the customer role visit the URL https://hasura-guide.herokuapp.com/customer and copy the token
- With each one of the tokens, go to the Hasura console
- Add an
Authorization
header and paste the token like this:Bearer <pasted_token>
- Press the Decode JWT button shown in the screenshot below
In the FULLY PAYLOAD DATA
section you will see the entire JWT, in few words the entire token you copied from the page is nothing more than a JSON encrypted as a string and signed by firebase.
Authorization using JWT is just sending this token in the headers of any HTTP request, then Hasura will decode the JWT and get the user identifiers like the user id and email, which can be used to see if the user is authorized or not to do the requested operation, but first, how does Hasura know that he can trust the JWT received?
Well, it’s important to notice, Hasura receives an environment variable HASURA_GRAPHQL_JWT_SECRET
this is another JSON used to set up the JWT authentication feature.
HASURA_GRAPHQL_JWT_SECRET is passed to the GraphQL Engine
If we visit the URL provided in the jwk_url
property we will see the following:
I know this looks a bit confusing but this is just a JSON Web Key, it’s a public key that can be used to verify if a JWT has been signed using it. Hasura will perform an HTTP request to the jwk_url
specified to get the public key(JWK) and then it will proceed to verify each JWT received to see if it has been signed with it, if the verification succeeds, then it means Hasura can trust the JWT content and then use the values to perform validations and see if the user is authorized or not.
Hasura documentation about this subject is great, it has all the information you need to know, in this guide I just want to give you the easiest example so you can test it on your own.
Note: Please notice in the
jwk_url
we are specifying the url where firebase publish their JWK used to sign the tokens, however you should specify theaudience
in theHASURA_GRAPHQL_JWT_SECRET
to guarantee that only signed tokens by firebase for your specific application are considered valid otherwise any other JWT signed by firebase for other projects will be marked as valid for hasura and this is not safe, for the simplicity of the example this setting is omitted, make sure you read hasura documentation to understand this better.
Authorization
Now we have two sample JWT that Hasura recognizes as valid, however, if we attempt to perform any query with them we will get an error because none of those roles are recognized by Hasura.
Note: You need to disable the
x-hasura-admin-secret
header so hasura will use the JWT to perform the authentication and authorization.
Let’s allow customers to query their own information.
Go to the Data
tab, look for the customer
table and go to Permissions
tab, enter the role customer
in the “Enter new role” input and click on the pencil in the select
section.
Now click the With custom check
radio button and you will see a form that will help you build a JSON used to specify the permissions, however, you can just write the JSON on your own and then paste it in the small input above, paste the following JSON into the mentioned input.
{
"_and": [
{
"active": {
"_eq": 1
}
},
{
"email": {
"_eq": "X-Hasura-User-Email"
}
}
]
}
This JSON is saying: A user can see the record(s) in the customer
table where the record’semail
is the same as the one provided in the JWT and the record is active.
Also, we can specify which fields can be queried through Hasura, this way we can keep hidden internal fields like the active
and activebool
fields since it’s not useful to have them visible for the customers.
After you finish selecting the fields that will be visible, you can click on Save permissions
.
Pretty simple but powerful. Now let’s test this, first, we need to change the email of one of the customers to customer@example.com
since the Pagila database had different email examples.
Let’s change the email of the first customer from MARY.SMITH@sakilacustomer.org
to customer@example.com
Then we go to the GraphiQL tab and perform the same query we did before:
Notice how we are not using any filter operators and yet we only get one customer record because it matches the email from the JWT which we can trust since it was signed by firebase. If the customer table had the user id from firebase we would have been able to use it as well.
Now, let’s allow the customer to query their rental records. Go to the Data
> Rental
> Permissions
then paste the following JSON in the same way we did before, select all the fields and afterward save the permissions.
{
"customer": {
"email": {
"_eq": "X-Hasura-User-Email"
}
}
}
Now we can perform a query like this one:
{
customer {
customer_id
email
first_name
last_name
rentals {
rental_date
}
}
}
And also you can query the rentals directly without having to query the customer first, the permissions will work anyway.
{
rental {
customer_id
rental_id
rental_date
return_date
}
}
You can even allow the user to perform aggregation queries by checking a field in the permissions tab.
Then we can perform aggregation queries like counting the total rentals for the authenticated user
Migrations
Migrations are SQL instructions to indicate how the database structure and data can change over time without having to wipe the entire database to do so.
For example, I realized the activebool
field in the table customer is not useful since all the records have a true
value and there is an existing active
field with values of 1 or 0 to indicate if the customer is active or not, to remove the unused field we can use the Hasura UI to remove the field.
Then we will get this message:
So, a cool Hasura feature, is that you can use the Hasura console (Web UI) to modify the database structure and automatically generate migrations so the system will be modified automatically.
Let’s take a look at the migrations folder and we can see what was generated:
We have our first migration!
Each migration is a folder with the timestamp as a prefix, this is used to guarantee migrations run in the order shown in the folder so they can mutate the database and the data with it.
The up.sql
file just contains a SQL sentence to remove the column.
And the down.sql
has the following SQL sentences to restore the field.
So what can we do if we realize we need to roll back our system since it was a mistake to remove the activebool
?
First, we can use the hasura cli
to ask the migrations status by running at the hasura-tutorial
folder:
npx hasura migrate status
And get something like the following:
Now we can ask Hasura to roll back that migration by its version (The number in the migration folder we saw earlier).
hasura migrate apply --version 1611884791540 --type down
If we go back to the Data
tab we will notice the activebool
field is back and since we had existing data, the value was set to NULL
.
We can modify the down migration so it can set the activebool
field to true
that way we can “travel in time” in the database structure, this example is just meant to teach you that you can modify the generated SQL’s as you please since it’s normal that you might want to perform more complex operations to the database that cannot be generated by using the Hasura UI automatically.
We will simplify the SQL sentence of the down migration like this:
ALTER TABLE "public"."customer" ADD COLUMN "activebool" bool DEFAULT true NOT NULL;
Then we can run the following sentences to remove the field again and to rollback one more time
If we refresh the Hasura console we will see the activebool
field is back with true
as the default value for all the records as it was initially.
As a final tool about database migrations, what happens if you want to do any complex operation like adding a trigger or several updates to the database?
Well you can go to Data
> Migrations
section and write some SQL change the default value of the activebool
column to false
This will generate a new migration.
However, we can see it doesn’t have a down.sql
file, it’s important we add one so it’s possible to roll back database changes whenever possible.
Do the same process mentioned for the first migration to roll back and apply again the migrations to verify everything is working as expected.
Metadata
Hasura metadata is a data structure used to specify the GraphQL API settings like permissions per role or fields, Hasura defines it as:
“All the actions performed on the console, like tracking tables/views/functions, creating relationships, configuring permissions, creating event triggers and remote schemas, etc. can be exported as a JSON/yaml metadata file which can be version controlled. The metadata file can be later imported to another Hasura instance to get the same configuration. You can also manually edit the metadata file to add more objects to it and then use it to update the instance.”
This data structure is critical and understanding it will help you debug any problems when your API is unable to run due to inconsistent metadata.
As mentioned earlier, all settings are stored in the Hasura folder we created named hasura-tutorial
in here we have a folder called metadata which looks like this:
Hasura has many features, more than the ones we are covering in this tutorial, however as you go testing features I recommend you to get familiar with each structure so you can debug problems in case they occur. The main one is the tables.yaml
:
It’s actually a simple data structure once you get familiar with it. It defines all the tables available with their name, fields, relationships, and even permissions as we can see below.
In this section we can see how Hasura stores the information we saved in their UI in the first part of this tutorial, this specifically describes the select permissions over the customer table for the customer role.
Besides the data structure is simple and you can understand it by being familiar with yaml
files, inconsistent metadata is a scenario that can happen when for example a field is removed from the database by using a migration but the field is still present in the metadata, if you restart your GraphQL container you will end up with your API not being able to run due to inconsistent metadata.
The first step is to always look for the logs to understand what might be causing the issue, and that if it’s a situation like I described earlier about some field that is no longer present, then you can update the metadata manually to fix the issue and then restart the container.
Tip
You might use the command
docker logs <container_name>
to read its logs but I recommend using the dashboard for docker
There are other tools that can improve the development experience and help you debug any problems with metadata like portainer
, choose the best for your use case.
As a final recommendation, make sure you do a code review over the metadata changes on your project, don’t treat these changes like something generated since this can break your GraphQL API.
Actions
Hasura is an amazing tool to generate an API that allows CRUD operations over a database, but what if we need some custom logic like using a payment gateway for the movie rental?
For these scenarios we can use actions to extend our existing Graphql API, in few words we can tell Hasura to register custom mutations and then act as a middleware between a REST endpoint and the final client. Let’s see this with an example.
Go to the Actions
section
And create a new one like this:
Click on Create
Go back to the project folder and run npm run server:start
this will start a basic NodeJS server with an endpoint to return a string like “Greetings, <name>! Have a nice day”
.
This is the sample code for both the Hasura trigger and Hasura action.
Triggers
To finish this tutorial we will use Hasura triggers, which is a sure feature where a REST
endpoint can be called when a change occurs in the database either an INSERT
, UPDATE
or DELETE
.
Go to the Events
tab and click on the Create
button.
Now, we will go and update the activebool
field in the customer table
Now you can go to Events
> Processed Events
tab and you can see the event was processed successfully.
You can see both the request and the response of the trigger.
This feature is useful to automate any action that must happen when something changes in the database, maybe like sending an email or notification, start background processing tasks, etc.
Conclusion
I hope this tutorial has been useful to you to learn how to quickly build a GraphQL API from an existing database and how to extend it with custom logic or background processing tasks, Hasura has many features that are not covered in this guide, and the best way you find out about them is for you to read the documentation and keep testing everything on your own.