DEV Community

Simon Horlick
Simon Horlick

Posted on

Thoughts on our first production hasura deployment

Hasura is a tool to generate an api directly from your database. The workflow boils down to:

  1. define your database tables and relationships (using normal SQL DDL statements)
  2. spin up hasura
  3. configure permissions rules
  4. hook it into something like auth0 for user management
  5. implement a ui using whatever's hot at the moment

My first thought about hasura is just how boring it is. Everything just.. works? Like, you find yourself needing to add a new feature to your app and with very little effort you find yourself finished with loads of time to spare.

The UI side is also pretty great - I'm using @graphql-codegen/typescript-react-apollo to generate client code for react. You write a graphQL query (this is the hardest part), run the codegen, then it gives you a hook you can use in your component.

Here's an example that draws a table with a bunch of data, including pagination, ordering and a search field that filters by event names. It's what we're using in production:

const EVENTS_QUERY = gql`
  query Events(
    $limit: Int = 10
    $offset: Int = 0
    $order_by: [events_order_by!] = []
    $where: events_bool_exp = {}
  ) {
    events(limit: $limit, offset: $offset, order_by: $order_by, where: $where) {
      date
      eventnumber
      name
      seriesevent {
        id
        seriesid
        series {
          seriesname
        }
      }
    }
    events_aggregate(where: $where) {
      aggregate {
        count
      }
    }
  }
`;

export const DEFAULT_PAGE_SIZE = 10;

export const FieldContainsComparison = (s: string): String_Comparison_Exp => {
  return { _ilike: `%${s}%` };
};

export function EventListContainer(props: { searchText: string }) {
  const [offset, setOffset] = useState(0);
  const [orderBy, setOrderBy] = useState<Events_Order_By>({
    date: Order_By.Desc,
  });

  let filter: Events_Bool_Exp | undefined = undefined;
  if (props.searchText !== "") {
    filter = { name: FieldContainsComparison(props.searchText) };
  }

  const { loading, error, data, previousData, refetch } = useEventsQuery({
    variables: {
      limit: DEFAULT_PAGE_SIZE,
      offset: offset,
      where: filter,
      order_by: orderBy,
    },
  });

  const latest = data ?? previousData;

  if (error) return <div>Error: {error.message}</div>;

  /* Don't attempt to draw the table until after the first set of data has been loaded. */
  if (loading && !latest) return <Loading loading={loading} />;

  return (
    <>
      <Loading loading={loading} />

      <table>
        <thead>
          <tr>
            <td>
              <div>
                Event Number
                <OrderByControls
                  setAsc={() => setOrderBy({ eventnumber: Order_By.Asc })}
                  setDesc={() => setOrderBy({ eventnumber: Order_By.Desc })}
                />
              </div>
            </td>
            <td>
              <div>
                Event Name
                <OrderByControls
                  setAsc={() => setOrderBy({ name: Order_By.Asc })}
                  setDesc={() => setOrderBy({ name: Order_By.Desc })}
                />
              </div>
            </td>
            <td>
              <div>
                Date
                <OrderByControls
                  setAsc={() => setOrderBy({ date: Order_By.Asc })}
                  setDesc={() => setOrderBy({ date: Order_By.Desc })}
                />
              </div>
            </td>
            <td>
              <div>
                Series
                <OrderByControls
                  setAsc={() =>
                    setOrderBy({ seriesevent: { seriesid: Order_By.Asc } })
                  }
                  setDesc={() =>
                    setOrderBy({ seriesevent: { seriesid: Order_By.Desc } })
                  }
                />
              </div>
            </td>
            <td>Action</td>
          </tr>
        </thead>
        <tbody>
          {latest?.events.map((event) => (
            <tr key={event.eventnumber}>
              <td>{event.eventnumber}</td>
              <td>{event.name}</td>
              <td>{event.date}</td>
              <td>{event.seriesevent?.series?.seriesname ?? ""}</td>
              <td>
                <Link to={`/dashboard/events/${event.eventnumber}`}>
                  <img width="20" height="20" src="/edit.svg" />
                </Link>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      <Pagination
        pageSize={DEFAULT_PAGE_SIZE}
        total={latest?.events_aggregate.aggregate?.count}
        offset={offset}
        setOffset={setOffset}
      />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

We went with Auth0 for user management. Figuring out how to create JWTs with the right payloads was definitely not straight forward, but it didn't take that long either. Getting hasura to accept those JWTs was very easy - just copy and paste the JWT secret into an env variable and you're good to go.

One of the screens in our app displays data from a third party REST API. We set up a hasura action to expose the REST endpoint as a graphql query and it pops up in the graphql api definitions like everything else. Pretty neat!

So, what could be improved? I'd say the experience of manipulating data needs some work. If you try to insert a record and something's not right, you'll either get a constraint violation or a permissions error. There's just not enough information to build a proper error message for end users. Another key feature I think is missing is the ability to mark fields as optional or required. Right now the graphql definitions that come out have every field as optional, even though I know many of them will cause an error if omitted. I hope given Hasura's crazy fundraising they'll be able to address these points, but so far I'm really happy with the product.

Discussion (0)