How I built iOS Conf SG GraphQL API on Hasura

Photo by Growtika on Unsplash

How I built iOS Conf SG GraphQL API on Hasura

Β·

11 min read

It was 2018 and I was given a chance to rewrite an iOS app that facilitates an annual iOS conference based in Singapore: iOS Conf SG. Being an iOS dev, I have no prior knowledge of building a full backend application, but I understand what services needed to make the client app.

We already had the current app running with Firebase, but I had difficulties with NoSQL. I am also more familiar with relational database such as MySQL and PostgreSQL. GraphQL was gaining momentum and I was particularly attracted by GraphQL Subscriptions. That's when I stalked Hasura development and after attending one of Hasura workshop, I got the confidence to use it to improve the app!

GraphQL Subscriptions enable clients to immediately notified whenever there's a change. Having the ability to update the schedule in real time is immensely convenient and became the most desired feature. I want to be able to update any content and the app in attendees' hand will immediately gets updated πŸ’«!

This is how it looks like when the whole Hasura GraphQL + iOS App stacks are integrated. This article, however, is about the API and not the iOS app. I will guide you step-by-step on how I built the GraphQL API from scratch, including some of my thought processes.

A journey of a thousand miles begins with a single step

Let's follow along, one step at a time 🐾

Setup

Create a Hasura account, and create a new project. A free tier should be good for now. Click Launch Console button on the top right.

Screenshot 2022-03-26 at 12.26.00 PM.png

We don't have a database yet, so let's create it. Click Create Heroku Database tab and Create Database.

Screenshot 2022-03-26 at 12.31.11 PM.png

Now the default PostgreSQL database is created, we can start creating our data tables.

Screenshot 2022-03-26 at 12.32.44 PM.png

Tables

The way we design tables and relationships will impact the end result, which is the JSON API response.

Imagine these screens on the client side. The user should see the schedule containing list of talks. And when a talk is tapped, we'll show the talk detail.

screens.gif

The JSON response that works for me looks like this: schedule is a list of talks and each talk has speakers key which value can be a array of speakers.

{
  "data": {
    "schedule": [
      {
        "activity": "iosconfsg22.day2",
        "start_at": "2022-01-21T09:50:00+00:00",
        "end_at": "2022-01-21T10:25:00+00:00",
        "id": 43,
        "speaker_bio": "Paul is the author of Hacking with Swift, Pro Swift, Swift Design Patterns, Server-Side Swift, Hacking with macOS, Hacking with watchOS, Hacking with tvOS, Swift Coding Challenges, and more. Suffice it to say, he quite likes Swift. And coffee. (But mostly Swift.) (And coffee.)",
        "speaker_company": "Hacking with Swift",
        "speaker_image": "paul",
        "speaker_name": "Paul Hudson",
        "speaker_twitter": "twostraws",
        "talk_description": "SwiftUI is the fastest way to build apps using all the system-standard components we know and love, but in this talk we're going to stop for a moment and try to build something fun, beautiful, and unlike anything you've seen before.",
        "talk_type": "normalTalk",
        "title": "Special effects with SwiftUI"
      },
      ...
    ]
  }
}

I recommend that you should create tables that are not dependent to any other table yet. In this case, Speakers and Talks table are independent. Let's begin!

Speakers table

Click Create table and add columns such as name, company, short_bio, twitter.

Screenshot 2022-03-26 at 12.52.17 PM.png

Pay attention to the button + Frequently used columns as it's very helpful to speed up creation time. You can use it for id, created_at and updated_at. Ensure that id is selected as the primary key and create the table.

Screenshot 2022-03-26 at 12.58.20 PM.png

Now you can see a speakers table on the database schema tree on the left. Feel free to inspect the other tabs.

Screenshot 2022-03-26 at 1.00.56 PM.png

Now let's add some speakers data using Insert Row tab.

Screenshot 2022-03-26 at 1.07.08 PM.png

Once saved, go to Hasura's API page and you will see the Graphiql, click speakers and tick all the info. You should now see that there's one record from speakers!

Screenshot 2022-03-26 at 1.08.29 PM.png

Talks table

We have a speaker, now it's time to have a table to store talks.

Let me show you a more advanced way of creating a table through SQL statement. Click Hasura Data menu on top and then SQL, and paste the following:

CREATE TABLE public.talks (
    id integer NOT NULL,
    title text NOT NULL,
    start_at timestamp with time zone NOT NULL,
    end_at timestamp with time zone NOT NULL,
    talk_type text NOT NULL,
    talk_description text,
    activity text NOT NULL,
    updated_at timestamp with time zone DEFAULT now(),
    created_at timestamp with time zone DEFAULT now()
);

CREATE SEQUENCE public.talks_id_seq
    AS integer
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;
ALTER SEQUENCE public.talks_id_seq OWNED BY public.talks.id;
ALTER TABLE ONLY public.talks ALTER COLUMN id SET DEFAULT nextval('public.talks_id_seq'::regclass);
ALTER TABLE ONLY public.talks
    ADD CONSTRAINT talks_pkey PRIMARY KEY (id);
CREATE TRIGGER set_public_talks_updated_at BEFORE UPDATE ON public.talks FOR EACH ROW EXECUTE PROCEDURE public.set_current_timestamp_updated_at();
COMMENT ON TRIGGER set_public_talks_updated_at ON public.talks IS 'trigger to set value of column "updated_at" to current timestamp on row update';

Ensure the Track this checkbox is ticked! Click Run! button and wait until you see the message SQL executed on the top right toast notification. Now you should see a new talks table on the left! isn't it faster πŸ˜† by SQL statement?

Screenshot 2022-03-26 at 1.23.50 PM.png

You might ask, how do I get the SQL statement correctly? I'm glad you asked, it will take another long post but the short story is that I got it from pgdump, which is the process to backup a PostgreSQL database. You can find more info on PG dump API here.

Let me also show you the way to insert a talk using GraphQL mutation, which I think, it is safer and faster once you get the gist of it.

Let's go to the Graphiql and paste this and click the Play button to run it.

mutation insertTalks {
  insert_talks(objects: [
    {
         activity: "iosconfsg22.day2",
      start_at: "2022-01-21T09:50:00+00:00",
      end_at: "2022-01-21T10:25:00+00:00",
      talk_description: "SwiftUI is the fastest way to build apps using all the system-standard components we know and love, but in this talk we're going to stop for a moment and try to build something fun, beautiful, and unlike anything you've seen before.",
      talk_type: "normalTalk",
      title: "Special effects with SwiftUI"
    }
  ]) {
    affected_rows
  }
}

If successful, you should see there's 1 affected row!

Screenshot 2022-03-26 at 2.04.25 PM.png

Now if you query talks table, you should also get 1 record.

Screenshot 2022-03-26 at 2.03.14 PM.png

Congrats! Now you've learned 2 different ways of creating a table and 2 different ways of inserting a record. There's actually 1 more way to insert a record using SQL insert statement however, in my opinion it's more error prone because you need to either match the order of data with order of column, or specify the column sequence in the insert statement itself (some of you might not understand this for now but my point is ... trust me, go with the easier one πŸ˜‰).

Relationship table

Alright now we've come to the part where we need to link speakers and talks table by defining the relationship.

Remember the JSON shape that we want? each talk can have 1 or more speakers. There's a need to define a bridging table where it looks like this.

talk_idspeaker_id
11

And if 1 talk has 2 speakers

talk_idspeaker_id
22
23

This is called a "normalisation" table, I believe ... so let's move on and create this using SQL statement. Go to the Raw SQL again and paste this.

CREATE TABLE public.speakers_talks (
    speaker_id integer NOT NULL,
    talk_id integer NOT NULL
);
ALTER TABLE ONLY public.speakers_talks
    ADD CONSTRAINT speakers_talks_pkey PRIMARY KEY (talk_id, speaker_id);
ALTER TABLE ONLY public.speakers_talks
    ADD CONSTRAINT speakers_talks_speaker_id_fkey FOREIGN KEY (speaker_id) REFERENCES public.speakers(id);
ALTER TABLE ONLY public.speakers_talks
    ADD CONSTRAINT speakers_talks_talk_id_fkey FOREIGN KEY (talk_id) REFERENCES public.talks(id);

What we're going to do is:

  1. Create a table called speakers_talks with only 2 columns: speaker_id and talk_id
  2. The primary key of this table should be the combination of talk_id and speaker_id this ensure that there must be no duplicated pair added to the record
  3. The speaker_id of this table should have a reference from speakers table id column
  4. Likewise, the talk_id of this table should have a reference from talks table id column

Screenshot 2022-03-26 at 2.24.00 PM.png

Once Run, you will see something is updated in each table's Relationship tab. This is for the speakers_talks table. Click Add and Save for each suggested relationship.

Screenshot 2022-03-27 at 10.26.20 AM.png

This is for speakers table, add and save.

Screenshot 2022-03-26 at 2.26.37 PM.png

And this is for talks table, add and save.

Screenshot 2022-03-26 at 2.26.47 PM.png

We also want to insert a row that says speaker_id 1 is linked to talk_id 1, and save it.

Screenshot 2022-03-27 at 10.08.18 AM.png

To confirm that our relationship table works, you should now be able to query this from the Graphiql!

query MyQuery {
  talks {
    title
    id
    activity
    speakers_talks {
      speaker_id
      speaker {
        id
        name
        short_bio
        twitter
      }
    }
  }
}

Screenshot 2022-03-27 at 10.31.54 AM.png

That looks great but you might wonder, the API response that we want is without speakers_talks and that's where we will also flatten the query through a view!

Bridging Views

A database view is a way to present data from multiple table query. We can use this method to combine the data from both standalone talks and speakers tables!

Head over to the Raw SQL and paste this snippet to create the bridging view.

CREATE OR REPLACE VIEW talk_speakers_view AS
SELECT talk_id, speakers.*
FROM speakers_talks LEFT JOIN speakers
ON speakers_talks.speaker_id = speakers.id;

Notice that this view has talk_id from talks column and the rest of columns are from the speakers table.

Screenshot 2022-03-27 at 12.06.40 PM.png

Now we need to create the array relationship to this view. Go to talks table, Relationship tab, and let's configure a new relationship manually.

Screenshot 2022-03-27 at 12.18.46 PM.png

After that, your talks table should have this additional array relationship named speakers

Screenshot 2022-03-27 at 12.19.03 PM.png

To confirm that this relationship works, you should now be able to query this from the Graphiql! Just what we wanted!

Screenshot 2022-03-27 at 12.32.30 PM.png

At this point, it is normal if you feel lost. I learned this flattening technique from one of Hasura doc page. Take it slow and perhaps a sip of πŸ₯€!

Onto the next last few steps, we will improve how our talks data are queried.

Schedule View

Head over to Raw SQL and let's create a new view.

CREATE OR REPLACE VIEW "public"."schedule" AS 
 SELECT talks.id,
    talks.title,
    talks.start_at,
    talks.end_at,
    talks.talk_type,
    talks.talk_description,
    talks.activity
 FROM talks
ORDER BY talks.start_at;

We also need to define a relationship with talk_speakers_view view. Go to the relationship tab of schedule and add a new relationship manually.

Screenshot 2022-03-27 at 1.38.24 PM.png

Now our query looks so much neater and readable in Graphiql, don't you think 😎?

Screenshot 2022-03-27 at 1.43.33 PM.png

This is the query you can use.

query Schedule {
  schedule {
    title
    id
    talk_type
    talk_description
    start_at
    end_at
  }
  speakers {
    id
    name
    short_bio
    twitter
  }
}

Setup unauthorized role name

Alright we're on the last mile! What we've worked so far is accessible for us as the Hasura admin. Now we want to enable some kind of publicly allowed consumption of our schedule so that our client, web or mobile application can fetch the schedule of the conference without authorization.

We can achieve this by adding a public read access to schedule. Go to the Permissions tab for schedule view and add a new role public with this configuration.

Screenshot 2022-03-27 at 1.58.49 PM.png

We also need to allow permission for talk_speakers_view in a slightly different setup. Anyone can read the speaker info but we don't need to expose created_at and updated_at columns.

Screenshot 2022-03-27 at 2.20.43 PM.png

Next, head back to Hasura Cloud administration page and click New Env Var button inside Env Vars section.

Screenshot 2022-03-27 at 2.05.56 PM.png

Find HASURA_GRAPHQL_UNAUTHORIZED_ROLE and enter public. This public must be the same name that you used in your view's permission.

Screenshot 2022-03-27 at 2.03.04 PM.png

Save and wait until the refresh is done. You will know when there is a green tick next to the app name. And that means the finish line 🏁 is right in front of us!

Screenshot 2022-03-27 at 2.09.17 PM.png

Finally, the Subscriptions!

From Hasura Cloud admin page, click Explore GraphQL API as an unauthenticated user and you should be able to access the Public GraphQL API page.

Screenshot 2022-03-27 at 2.12.30 PM.png

Let's query the schedule, but replace query with subscriptions. You can paste this query.

subscription Schedule {
  schedule {
    id
    title
    talk_type
    talk_description
    start_at
    end_at  
    speakers {
      id
      name
      short_bio
      twitter
    }
  }
}

I've added 1 more speaker and a talk but haven't linked them. Now see how the GraphQL Subscription on the left instantly updated after I linked it in the normalisation table.

gif

Closing

Congratulations πŸ™ŒπŸΌ you've made it here! Did you get your own API working perfectly? I hope you learn something new or even able to apply those GraphQL techniques on your own GraphQL API! I am very happy with the improvement and consistency that Hasura team has worked on until now.

If you didn't get it worked at the first try, don't give up and keep trying! Some advanced techniques I wrote didn't happen over night but discoveries since 2018 until now, I hope that this article would accelerate someone's learning :)

If you have any thoughts or questions, feel free to ask in comments or tweet me Vina Melody.

If you'd like to see the iOS app code, it's open sourced at iOS Conf SG Github.

And thank you Hashnode and Hasura for organising this hackathon and motivated me to write!

Β