Sitemap

How to Build Bullet-Proof GraphQL Frontends in React in 2025

5 min readMar 28, 2025

--

GraphQL was invented in 2012 — The year that the whole world was in panic as they believed the world was going to an end.

But here we are in 2025, the world is still here, and so is GraphQL.

So what’s changed?

Quite a lot.

Especially the GraphQL ecosystem.

Today, we’re going to explore a tool that’s been released in the recent months that made my life as a developer 10x easier.

The tool is called “gql.tada”.

Github Repo: https://github.com/0no-co/gql.tada

To understand what is gql-tada, and why is it such a game-changer, we have to first understand a common problem that most frontend apps using GraphQL are facing.

The problem: Dealing with null and undefinedvalues

Let’s assume we have a simple GraphQL schema:

type User {
id: String
firstName: String
lastName: String
email: String
}

Query {
user($id: String!): User
}

Then, you make a GraphQL query that looks like this:

import { gql } from '@apollo/client';

export type User = {
id: string
firstName: string
lastName: string
email: string;
}

export const GET_USER_BY_ID = gql(`
query {
user(id: "test-id") {
id
firstName
}
}
`);

Then, you access the data via @apollo/client:

import { useQuery } from '@apollo/client';
import { GET_USER_BY_ID, User } from './LOCATION_OF_YOUR_QUERY';

const MyComponent = () => {
const { data, loading, error } = useQuery<User>(GET_USER_BY_ID);

if (data) {
return (
<div>
{data?.id}
{data?.firstName}
{data?.lastName}
{data?.email}
</div>
)
}
}

“I don’t see any issues, what’s wrong?”

You have to manually infer User as the response type for calling useQuery() hook. If you’re not doing the inference, your data will be un-typed. In addition, just because you’re inferring the right type definition here, doesn’t mean you’re not going to make any mistakes.

Okay, sure, but that’s not a big problem. I can always change the inferred type.

But here’s another issue — even if you inferred the correct return type

If you log out each property returned from data object, you’ll see that:
- data.id can be a string or null — you’ve asked for it
- data.firstName can be a string or null — you’ve asked for it
- data.lastName will be null — you DID NOT as for it
- data.email will be null— you didn’t ask for it either

This creates a problem where you’re accessing and rendering data that you didn’t even ask your GraphQL API to return.

When you do this, your UI will show you nothing, and you don’t even know if some of the data field is actually undefined or null in the API.

To make things worse, if you’re trying to access a deeply nested object (or arrays of them) of a field you didn’t ask for, you’d end up going into a rabbit hole of bugs trying to figure out which node on which edges is undefined or null . The consequences? You have a ton of bugs to fix — mostly type-errors, which ultimately surface themselves to users, and your UI will crash.

What’s the big deal? I can simply not access the fields I didn’t asked for 🤷

Yeah well… sure you can, but how would you make sure that other engineers working on the same codebase are not making the same mistakes? How do you make sure you don’t make the same mistake?

But trust myself and my team… We do code reviews… We’re strict on our processes…

Processes are created by people. They’re hard to foster and maintain, but super easy to stop following — as long as people become complacent.

But, today is your lucky day, because we can now automate the process of eliminating nullable-errors in GraphQL frontends by using gql.tada.

So umm… How does this “Gee-Cue-El ta da” thing help dealing with nullable-errors?

Think of gql.tada like the TS compiler, but for your GraphQL schema file (most commonly named schema.graphql .

For every GraphQL query you write, gql.tada figures out the exact shape of the return value for the fields you asked for. For example:

import { graphql } from 'gql.tada'

const GET_USER_BY_ID = graphql(`
query GET_USER_BY_ID ($id: String!) {
user(id: $id) {
id
firstName
}
}
`);

Now, `GET_USER_BY_ID` , is a fully-typed GraphQL query document, you can pass it to any GraphQL client, and get a fully-accurate response shape, for example:

import { useQuery } from '@apollo/client';
import { GET_USER_BY_ID } from './LOCATION_OF_YOUR_QUERY';

const MyComponent = () => {
const {
data, // ✅ fully-typed response data! No type-inference needed!
loading,
error,
} = useQuery(
// Insert fully-typed Graphql Query document
GET_USER_BY_ID,
// ✅ variable inputs are fully-typed as well!
variables: {
id: "test-id"
},
);

if (data) {
return (
<div>
{data?.id} // ✅ allowed
{data?.firstName} // ✅ allowed
{data?.lastName} // ❌ will throw type-errors
{data?.email} // ❌ will throw type-errors
</div>
)
}
}

Looks like magic! But still, I can get type-safe return values by using something like gql-codegen (by “the guild”). What makes gql.tada so special?

Press enter or click to view image in full size

Yes you can, it’s a great tool as well. I’ve been using it in the past and have nothing but respect for the team behind it.

The goal here isn’t to tell you to use X over Y, because software engineering is about trade-offs.

When you use gql.tada, you’re getting the benefits of writing GraphQL queries in TypeScript files that’s type-safe; you’re getting the convenience of not having to generate GraphQL query documents for each and every query you write or update; you’re getting the benefits of having fully-typed query inputs and response data. On the flip side, with gql.tada, you do have smaller community adoption, and a smaller team behind it. But, they’re rising in popularity, and there’s no doubt about it. At the time of writing this blog, their repo has +2700 stars, 22 contributors, and over 3800 engineers marked themselves as users of the package. If they’re not going to the moon, I don’t know where else they could be heading.

To finish off…

gql.tada solves two challenging problem in the GraphQL space: Type-safe query response and type-inference of query inputs and outputs. It helps bringing clarity and transparency throughout the entire GraphQL request cycle, and eliminated clutters from auto-generated code.

If you’d like to find out more about gql.tada and how to get started, please check out their repo and documentation website.

Till next time.

--

--

Mingyang Li
Mingyang Li

Written by Mingyang Li

Software Engineer | Node.js, TypeScript