Best Practices for Building Scalable and Secure GraphQL APIs on Hasura

This comprehensive article by Muhammad Hassan Bilal, Co-founder & CFO of Antematter.io, serves as a detailed guide for developers working with Hasura, Firebase, and Next.js. It delves into best practices for developing scalable, secure GraphQL APIs. Key topics include managing metadata and migrations through the Hasura console, implementing robust authentication and authorization with Firebase, and enhancing query performance through caching. Aimed at ensuring code reliability in production, the article is a must-read for developers looking to streamline their GraphQL API development and deployment process.

đŸ’¡ Articles
6 December 2023
Article Image

Hasura is the go-to platform to develop, build and deploy graphQL APIs. Pair up Hasura with firebase for authentication and Next.js for frontend and you have a whole architechture ready to develop your application. In this article we will discuss some general developmental practices with Hasura where we will deep dive into how we can make our software more scalable, secure.

Metadata and Migrations:

In order to keep track and do version control on the changes you are making into your graphQL schema the best way to go forward is to do every thing from hasura console. Follow these steps to set it up:

  1. Install hasura-cli in your local machine:
npm install -g hasura-cli

  1. Initialize your hasura project in your project directory:
hasura init my-hasura-project

  1. In the /my-hasura-project directory, there will be a config.yaml file. Fill in the endpoint name etc. It should look something like this:
version: 3
endpoint: <https://my-hasura-project.hasura.app>
metadata_directory: metadata
actions:
  kind: synchronous
  handler_webhook_baseurl: <http://localhost:3000>

  1. Create a .env file in the /my-hasura-project and enter the admin secret of your project there like this:
HASURA_GRAPHQL_ADMIN_SECRET=secret

  1. Now run the following file in /my-hasura-project directory. Your browser should open a localhost hasura console in your default browser. The changes you make through this console is reflected on the hasura cloud as well:
hasura console

In your /my-hasura-project folder there will be two directories named ‘metadata’ and ‘migrations’. These directories will keep track of the changes made in the schema and you can version control it using git. Any changes in these directories will be reflected in your commit.

Authentication and Authorization

Almost any kind of web application requires signup and login functionality. This can be easily achieved by using firebase with hasura. Firebase provide JWT based authentication via which you can create multiple hasura user-roles and give read/write/update/delete permissions on any table properties inside hasura. Here’s how your header should look like when calling a graphQL API endpoint that requires authenticatication:

{
  "content-type": "application/json",
  Authorization: `Bearer ${<JWT>}`,
}

The JWT is returned by your firebase login function. It has an expiry of 1hour by default and can be refreshed by using firebase. You can use react hook state to globally store your JWT in the application which can also be deleted when the logout function is called. You can setup multiple user-roles using firebase custom claims during user signup like this:

import {
  getAuth,
  createUserWithEmailAndPassword
  } from "firebase/auth";
import firebaseAdmin from "firebase-admin";

if (!firebaseAdmin.apps.length) {
  firebaseAdmin.initializeApp({
    credential: firebaseAdmin.credential.cert(
      firebaseCreds as firebaseAdmin.ServiceAccount
    ),
  });
}

const auth = getAuth();

await createUserWithEmailAndPassword(auth, email, password!!)
const user = auth.currentUser;
const customClaim = {
      "<https://hasura.io/jwt/claims>": {
        "x-hasura-default-role":
          "admin"
        "x-hasura-allowed-roles": ["admin", "teacher", "student"]
        "x-hasura-user-id": user.uid,

      },
    };

await firebaseAdmin.auth().setCustomUserClaims(user.uid, customClaim);

Caching and Query Performance

If your project requirement is as such that you frequently need to fetch data from the same table, you should implement caching in order to minimize the response time, improving your query performance. Otherwise the large number of concurrent API calls can increase your response time, making your overall application slow.

In hasura you can implement simple query response caching by adding ‘@cached’ directive in your graphQL queries. Here’s an example:

query userDataCached @cached {
  users {
    id
    name
    date_of_birth
    email
  }
}

By default the cache the query response is cached for 60seconds. You can read the cache-control value here to check:

You can customize the cache time like this. The maximum time hasura supports is 300 seconds and the query can be written like this to implement that:

query userDataCached @cached(ttl: 300) {
  users {
    id
    name
    date_of_birth
    email
  }
}

In conclusion, Hasura is one of the best tools to deal with graphQL APIs with the lowest learning curve in order to swiftly deliver production ready products. These are one of the few key must-implement key points that a developer should keep in mind when initiating the project. Regular standups and code reviews will make sure these SOPs are being followed. Implementing these practices will ensure that your code never breaks in production and are easily scalable and most importantly, secure.

This article is written by Muhammad Hassan Bilal , Co-founder & CFO at Antematter.io.