Use GraphQL without writing GraphQL

👍 Follow me on Twitter @andycoupedev

In this walkthrough, we are going to create a full stack application with full type safety using GraphQL without writing any actual GraphQL with the star of the show being GenQL. Below is a list of tools we will be using.

  • TypeScript - typed JavaScript from the future.
  • Hasura - instant GraphQL and REST APIs on new or existing data sources.
  • React Query - manage fetching, caching and server state easily.
  • GenQL - Generate a type safe GraphQL client for our GraphQL API.
  • NextJS - Arguably the best React framework.

Create our frontend

To generate our frontend let's create our NextJS TypeScript project with the following command from a directory of your choice.

npx create-next-app@latest your-app-name --ts

Create our GraphQL API

For our GraphQL API, let's head over to Hasura and create a project - you'll need to create an account. Once you've done that, select the create project option and select all of the free tier options. Click "Launch Console" and you should be presented with the Hasura console.

We have rapidly generated the frontend and API layers of our application, leaving just the DB layer left. Thankfully, Hasura has our back.

Click on the "Data" tab from the Hasura console and you should see a button to connect a database. From here, there should be a "Create Heroku database" option. Follow these steps (you may have to sign up to Heroku if you're not already signed up) and we'll have a Postgres database managed by Heroku, connected to our GraphQL API.

Create our database

Now, let's create a table. For this application I'm going with a football (soccer) theme so let's name our table teams.

The frequently used columns button is useful and lets us quickly add columns id, created_at and updated_at. Add a column of type Text named name to store our team names as well.

Click "Add table" to create the table.

After creating the table, the insert row tab will allow us to manually create a row in the table, let's do that and hit "Save".

Head over to the "API" tab and you will now be able to query the data from our database using Hasura's playground 😎.

Back to the frontend

We have our backend setup. To interact with our GraphQL API from our frontend we are going to generate a GraphQL client using GenQL so we need to install some dependencies in our NextJS application.

npm i -D @genql/cli # cli to generate the client code
npm i @genql/runtime graphql # runtime dependencies

@genql/cli is a dev dependency because it is only required to generate the client, @genql/runtime instead is a direct dependency of the generated code.

To generate our client we can use the following command.

genql --endpoint <your graphql endpoint from hasura console> --output ./genql-generated -H 'x-hasura-admin-secret: <your admin secret from hasura console>'

The generated files expose a function createclient. This creates a client you can use to send requests.

Let's create a file at the root of our project named genql-client.ts with the following contents to create our client.

import { createClient } from "./genql-generated";

const client = createClient({
    url: <your graphql endpoint from the hasura console>,
    headers: {
        'x-hasura-admin-secret': <your hasura admin secret from hasura console>,
    },
})

With our GraphQL client in our holster, we're ready to start firing requests.

We are going to use React Query to manage fetching and server state.

npm i react-query

For the purpose of this walkthrough we will just make the request in the default index page provided by NextJS. So head to pages/index.tsx and import our client below the rest of the existing imports.

I like to work inside the src directory so your imports may be a level higher than mine. NextJS supports moving the pages directory into a src directory out of the box.

// ...existing imports
import { client } from '../../genql-client'

Let's create a function to fetch the teams in our database. Don't just copy and past the code below. Type it out and appreciate the autocompletion using CMD or CTRL + SPACE depending on your OS 😎

const fetchTeams = () => {
  return client.query({
    teams: [{}, { id: true, name: true, created_at: true }],
  });
};

Consult the GenQL docs on the syntax but you can get the general idea of how to build a query. Once again, autocompletion will guide you like a good friend.

Our generated files also export an object called everything which allows us to query all fields in a type instead of providing a boolean to every type, like so.

// ...existing imports
import { everything } from "../../genql-generated";

const fetchTeams = () => {
  return client.query({ teams: [{}, everything] });
};

Now let's bring in useQuery from React Query and wire it up to our fetchTeams function.

// ...existing imports
import { client } from "../../genql-client";
import { everything } from "../../genql-generated";
import { useQuery } from "react-query";

Invoke the useQuery hook inside the Home page component and provide it with your query key and query function as the second and third arguments respectively.

const { data } = useQuery("teams", fetchTeams);

Almost there! We need to wrap our app in a <QueryClientProvider /> component provided to us by React Query. This will have to be added further up the tree in the _app.tsx file. Update _app.tsx with the following code.

import type { AppProps } from "next/app";
import { QueryClientProvider, QueryClient } from "react-query";

const queryClient = new QueryClient();

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <QueryClientProvider client={queryClient}>
      <Component {...pageProps} />
    </QueryClientProvider>
  );
}

export default MyApp;

Let's update our index.tsx page to look like the following and we should be seeing our team rendering on the page.

import type { NextPage } from "next";
import styles from "../styles/Home.module.css";

import { useQuery } from "react-query";
import { client } from "../../genql-client";
import { everything } from "../../genql-generated";

const fetchTeams = () => {
  return client.query({ teams: [{}, everything] });
};

const Home: NextPage = () => {
  const { data, isLoading } = useQuery("teams", fetchTeams);

  return (
    <div className={styles.container}>
      <h1>Teams</h1>
      {isLoading && <p>Loading...</p>}
      {data && data.teams.map((team) => <p key={team.id}>{team.name}</p>)}
    </div>
  );
};

export default Home;

There are certain best practices to follow when using React Query with SSR/NextJS that are beyond the scope of this walkthrough which can be found here.

I may do a follow up post using mutations and GenQL to create a CRUD application but hopefully this has shown you the power of GenQL

28