Plug-and-play GraphQL type safety using gql.tada

Much of the appeal of GraphQL compared to e.g. REST is that it enables retrieval of data in potentially complex structures in a uniform manner. Both data from the requested resource, as well as desired data from any relationships, can be easily extracted in a single request. At the same time, it is the client itself that defines what data will actually be returned, which can reduce the amount of network traffic between client and server.

In addition, all data made available through the GraphQL API is automatically described using types. This is a major advantage over other API technologies, and is a useful safety net to weed out potential bugs that occur when the server or client receives data it does not expect. Ola has previously written about the use of Protobuf to enable type safety in heterogeneous networks. But what if you already have an existing app that uses a GraphQL API, and you don't want to spend a lot of time and effort rewriting the architecture? Can one still take advantage of the benefits types of gears with minimal effort?

Do you have a client written in TypeScript the answer is “yes!” , using a useful framework called gql.tada. In this article, I'm going to show how we applied this to an existing web application, and what usefulness it gave us.

Setup

Unfortunately, even if the actual API of GraphQL is typed, there is no automatism in that data retrieved on the client is also so.

There have long been tools such as GraphQL-CodeGen which automatically generates types based on an existing GraphQL API. My experience is that such tools have much required setup with packages that need to be installed and scripts that need to be added, and are twisted to get to work exactly as one wants.

The framework gql.tada works in a completely different way, and is written as a TypeScript plugin (new in version 5) that automatically retrieves the types from the GraphQL API when TypeScript is compiled. It allows you as a developer to have a “set it and forget it” experience, and personally I almost forgot that gql.tada is installed because it just works.

In addition, you choose which queries and mutations to type automatically or not, making it easier to apply to an existing project without having to fix thousands of bugs first.

We implemented the tool on the frontend of Mist, a web application written in React that uses @apollo /client to communicate with a GraphQL API exposed from a backend. Here is a (simplified) example of the code of an existing query to extract a paginated list of users and display them in a table:


import { gql, useQuery } from '@apollo/client'
import { AllUsersQuery, AllUsersQueryInput } from 'modules/users/types'

const ALL_USERS_QUERY = gql`
  query AllUsers(
    $q: String
    $orderBy: String
  ) {
    allUsers(
      q: $q
      orderBy: $orderBy
    ) {
      edges {
        node {
          id
          username
          firstName
          lastName
          phone
        }
      }
    }
  }
`

const { data } = useQuery(
    ALL_USERS_QUERY,
    {
      variables: {
        q: debouncedSearch,
        orderBy: sort
      },
    }
)
  
const tableData = data.allUsers.edges.map(({ node }) => node).map(user => {
  return {
    id: user.id,
    data: {
      username: user.username,
      email: user.email,
      firstName: user.firstName,
      lastName: user.lastName,
      phone: parseInt(user.phone)
    },
  }
})

return (
	
)

This is code that probably seems familiar to most people who have worked with GraphQL in React before. However, changes in Utilizerresource from the API not automatically be reflected in the code. It creates challenges if you forget to maintain the types AllUsersQuery and AllUsersQueryInput when there is a change.

For example, if you remove FirstName and LastName to introduce a new field nome Instead, this code will not give any error messages on it, but all GraphQL queries will start to fail because the requested fields no longer exist in the schema. And if Telefon suddenly being returned as a Int, will parseIntfunction fail, because it expects an argument of type Strings.

Results

Let's install gql.tada and apply it to automatically follow the types from the API. We followed start guide on the official website, which at the time of writing implies:

  1. Install the packages gql.tada and @0no -co/graphqlsp (TypeScript plugin)
  2. Add the TypeScript plugin below CompileOptions.Plugins in your existing tsconfig.json, and point it towards your GraphQL API address.
  3. There is no step 3 😉

Then we can rewrite the code above to completely get rid of the static types:


import { useQuery } from '@apollo/client'
import { graphql } from 'gql.tada';

const ALL_USERS_QUERY = graphql(`
  query AllUsers {
    ...
  }
`)

const { data } = useQuery(
    ALL_USERS_QUERY,
    { ... }
)
  
const tableData = data.allUsers.edges.map(({ node }) => node).map(user => {
  return {
    id: user.id,
    data: {
      username: user.username,
      email: user.email,
      firstName: user.firstName,   // TS2339: Property firstName does not exist on type
      lastName: user.lastName,     // TS2339: Property lastName does not exist on type
      phone: parseInt(user.phone)  // TS2345: Argument of type `number` is not assignable to parameter of type `string`
    },
  }
})

return (
	
)

Now both problems mentioned above are picked up by the TypeScript compiler before the code is even built. If FirstName and LastName has been removed, we will bring up an error that User.firstName and User.lastName no longer exists. And correspondingly with Telefon: Now we will get an error message that the function expects a Strings, but that type is Numero.

In other files that made use of the static types, we can instead replace them with the built-in tool types ResultOf <typeof ALL_USERS_QUERY> and VariablesOf <typeof ALL_USERS_QUERY>. To even more encourage reuse, one can extract the requested fields to a fragment, and then use this to define how Utilizertype looks.

After turning on typing on more and more queries in the existing MIST code, errors started popping up that had unfortunately been overlooked in the past. This was mostly incorrect about data potentially being returned as null or indefinido, but in one case we discovered a different implementation of an enum on the frontend and backend -- this probably wouldn't have been discovered organically by us developers, so there gql.tada saved us from a potential (angry) customer email!

Pitfalls

Finally, I want to mention some less positive experiences I've made myself after using gql.tada for a while.

My biggest challenge is that type safety only works on the retrieved data, and not while writing the actual queries or mutations. That is, all code inside graphql ()the wrapper needs to be written as plain text, and all the potential flaws that entails. Subject to the fact that I have configured somewhat incorrectly, one could argue that this goes a bit outside of the tool's original mission. In any case, there are own GraphQL plugins for the biggest IDEs that take care of this task, not to mention GraphiQL or other GraphQL clients. It would be nice to have it all in one place!

If you use pipelines to build and roll out code, it should be mentioned that you have to check in and include the autogenerated env.d.ts the type file for the code to be built on server. Whether you are using an external API, or not developing the backend yourself, it is common for changes to this file that have nothing to do with the code you are writing in right now, which can mess up pull requests. In addition, the file size is relatively large — in our case it is over 1 MB.

It is also worth reading the documentation, especially if you use fragments. These are not automatically typed, and one has to call the function readFragment () for the types from the fragment to be adopted. And if you have defined your own scalars in the API, e.g. a ID or Datum, you have to override the default configuration and import graphql ()wrapper from there for these to be properly typed.

Conclusion

No tool is perfect, but all in all, I think the utility gql.tada delivers makes it worth trying out, especially when the stakes are so low. It is an exciting tool that has already helped us in our development and that we will continue to use on Mist and other projects in the future.

Skrevet av
Christian De Frène

Andre artikler