How to stop using your GraphQL API as a REST API

Amanda Beiner

There have been few tools I’ve used in the last few years that have been more exciting to me than GraphQL.

GraphQL APIs have many benefits. They let us push more logic onto the server to let our front ends focus on user interaction. They let us minimize our payloads by only returning the information we ask for. They let us understand the shape of our data with an auto-generated schema and documentation. For all of these reasons, I’ve really enjoyed working with GraphQL in React and Elm projects.

A common pain point that I’ve seen in front ends consuming a GraphQL API is that GraphQL allows for so much flexibility that teams can struggle to figure out how they want to structure their projects. How do you plan for an infinite number of possible datasets?

In the middle of this structure churn, it’s common to see something like this:

const HomePage: FunctionComponent = () => {
  const { data } = useQuery(userQuery)

  return (
    <div>
      <h1>
        `Welcome back, ${data.user.firstName}!`
      </h1>
      <Settings user={user}>
    </div>
  )
}

const userQuery = gql`
  query userQuery {
    user {
      id
      firstName
      lastName
      email
      profile {
        verified
        avatarUrl
      }
      events {
        id
        name
        startTime
        endTime
      }
      ...
    }
  }
`

We have a component that only needs the user’s first name, but is querying for a lot of extra information, and even some associations. We’ve started using our GraphQL API as a REST API— we’re sending data that we don’t need right now over the wire because we might need it somewhere else.

This pain point is extremely common. Let’s take a look at some reasons we might be tempted to use some REST-ful patterns, and consider which more GraphQL-friendly patterns we can apply instead.

Something further down the tree needs this data

In the example above, the HomePage component directly renders a Settings component. If the Settings component requires the extra data that our userQuery is requesting, it makes sense that our HomePage component has to fetch it in order to pass it down to its child component. But that still leaves one problem: I can’t tell at a glance why the HomePage is requesting this data, or if it is used at all.

In these cases, I like using GraphQL fragments. Fragments let you construct a set of fields and include them in a query later. If I were to rewrite the query above, I would extract the data that the Settings component needs into a fragment:

const Settings: FunctionComponent<SettingsProps> = ({ user }) => {
  return (
  <div>
    <p>First name:</p>
    <p>{user.firstName}</p>
    <p>Last name:</p>
    <p>{user.lastName}</p>
    <p>Email:</p>
    <p>{user.email}</p>
  </div>
  )
}

export const settingsFragment = gql`
  fragment SettingsFragment on User {
    id
    firstName
    lastName
    email
  }
`

Then, you can import a component’s fragment alongside it where it is used. In this case, I would refactor the userQuery to use the SettingsFragment:

import { Settings, settingsFragment } from './Settings'

const userQuery = gql`
  query userQuery {
    user {
      id
      firstName
      ...SettingsFragment
    }
  }

  ${settingsFragment}
`

This approach has a few benefits that have been really helpful in development. By co-locating our fragment definition with the component that will consume the data, we are being explicit about the relationship between the data we’re requesting and how the component functions with it. If the Settings component no longer requires an email in the future, we’re more likely to see that it should be removed from the query than if we had to comb through all of the requested fields on a bloated HomePage query.

We’ve also cleaned up the userQuery as defined on the HomePage component to make it easier to understand what data it needs. We still get the information that the child component requires, but we can hide the implementation details that aren’t relevant to the parent.

We’ll also get more descriptive automated type annotations, but we’ll get to that point in a bit πŸ˜‰.

I want to make my queries reusable

This is one of the most common concerns that I hear when people write their first queries. Maybe you have a few different pages in the app that will need access to user data, and it seems easier to create a centrally located query that you can import and use in those cases. A file tree might look like:

.
β”œβ”€β”€ components
β”œβ”€β”€ queries
    β”œβ”€β”€ userQuery.ts
    β”œβ”€β”€ eventsQuery.ts
    └── todosQuery.ts

Just import the userQuery and bam! User achieved.

The problem with this approach is that there is no such thing as the user query in GraphQL. There are many user queries, each with a unique combination of fields to be returned. The query for a user’s profile page vs their account details page can be completely different. Even though they are both querying on the User, they may be asking for different fields. Creating one query that returns the data for both of these scenarios means we’re missing out on selectively querying for only the data we need.

Unlike REST APIs, where we assume we should use a pre-existing endpoint unless a special case warrants it, I like to assume that GraphQL queries will be unique by default and co-locate them in the files where they are executed. Occassionally there will actually be a need for the same exact query later, and even in those cases, I prefer to just repeat the query definition. This way, if the needs of the two components diverge in the future, the extra data isn’t automatically added to two pages.

I want type annotations for my query results

When working with a client written in a typed language such as TypeScript or Elm, writing types for the query responses can quickly become tricky. Just like there is no one user query, there’s no one user type. The type changes depending on the fields requested in the query, which can become cumbersome when you have to update the types manually.

Luckily, many GraphQL clients have built-in tooling to auto-generate types based on the GraphQL queries, mutations, and fragments you define in your app. Relay has type emission support for Flow and Typescript, while Apollo CLI additionally supports code generation for Swift and Scala. dillonkearns/elm-graphql generates Elm decoders for valid GraphQL queries.

In addition to automating the repetitive tasks of re-defining slightly different versions of a User, these libraries will also ensure that the queries and mutations you compose are valid according to your GraphQL schema, which can save you time debugging failed requests.

In the example of our Settings component, I can now import the auto-generated type to use in my props definition:

import { SettingsFragment } from './__generated__/SettingsFragment'

const Settings: FunctionComponent<SettingsFragment> = ({ lastName, email }) => {
  ...
}

Now, if I change the SettingsFragment to request different data, running the type generator will automatically update the type definition of SettingsFragment, and I will see type errors if I’m trying to use fields in my TypeScript code that I haven’t requested from the API.

I want to limit my network calls

Sometimes teams try to prevent extra round trips to the database by saving query responses to application state or context in order to prevent additional network requests on re-rendering. If you’re using Apollo or Relay, they can smartly handle this in-memory caching for you, returning records from the cache when they are queried multiple times, and ensuring that they are updated locally after data-altering mutations. These sophisticated caching strategies can simplify our application logic by removing the need for more complex state management libraries, and can save us time we might normally spend fussing over lifecycle renders or debugging inconsistencies in our DIY application state cache.

Apollo identifies records in its cache based on their __typename and unique identifier fields. The __typename field is added to responses by default unless you remove it, and the required fields validation from eslint-plugin-graphql can be a helpful nudge to make sure you request ids in queries and mutations. Apollo’s default cache-first policy will check if the data exists locally, and only query your GraphQL server if the data isn’t found. You can always opt for a different fetch policy if your use case requires a different behavior.

Taking full advantage

Moving away from a REST API and towards a GraphQL API is more than a shift in tooling; it’s also a shift to a new paradigm of how we think about the data and structure of our applications. When making decisions about how to structure our apps, it can be helpful to take a step back, and remember what we hoped to achieve by reaching for GraphQL in the first place.