Front-end gotchas of GraphQL

GraphQL on a computer screen

GraphQL syntax

If you have worked in the software industry for any significant amount of time, then you are probably well aware that this old adage rings true.

"The only constant is change."
— Heraclitus (500 B.C.)

It seems that every few months (if not every few days) something new comes along that promises to obviate everything we had come to rely on in the recent past.

Lately, much ink has been spilled on the topic of GraphQL. While I am a fan, as with any technological paradigm shift, GraphQL is not without its own unique set of drawbacks and potential pitfalls.

In this blog post, I will share my front-end perspective of the niceties and "gotchas" that I encountered when first working with GraphQL. Hopefully, it will be helpful to other developers.

Note: On a recent FinTech client project at Genpact, we used Apollo's flavor of GraphQL. So I will cover a few tips that are specific to that as well.


What is the problem?

First of all, for the uninitiated: What is GraphQL?

I like to think of it as a Star Trek universal translator. You know the drill. A federation starship encounters a new alien species. Despite not being fluent in each other's specific dialect, a simple tap of the trusty translator button enables humans and aliens to speak one universal language.

Bear with me, I have a point here.

Captain Kirk and Leonard McCoy

Universal translator

I am fond of saying to coworkers:

"Microservices [alien languages] are great, except when I am trying to build an app."

Meaning, if you are building an app and attempting to consume multiple, disparate API endpoints, you essentially need to be a subject matter expert in each of those API's quirks.

For instance, consider the concept of a "customer" object. In our mind's eye, we know this represents a real, living, breathing person. I will use myself as an example.

Though my personal info (name, email) might be associated with a customer object, depending on how a company's data is siloed, my bank accounts, car loan, and/or mortgage might be spread across several different microservices.

As a UI developer, I may not need to know or care about the intricacies of how various API teams decided to divvy up the work of data storage. I simply need to get the "Nathan" object, and all of his associated financial records, to display on a page.


How does GraphQL solve it?

This is where GraphQL comes in. It allows the flexibility to write a unified query for a specific customer, and retrieve all associated data, regardless of where the data originates.

Not only that, but with Apollo, it is possible to do so in a declarative way. Much like how React merges two UI aspects that are implicitly tied to one another (view logic and UI presentation), Apollo encourages us to move our data fetching into JSX as well.

This approach also allows a component to stay "dumb" (aka: presentational), strictly handling the display of data, without having to know about its source. For the sake of brevity, I will not go into great detail with a ton of code examples. Conceptually, that would look like this.

<Query>
<DumbComponent />
</Query>

If that component needed to make changes to the data (aka "mutation"), then it might have an additional declarative wrapper like this.

<Query>
<Mutation>
<DumbComponent />
</Mutation>
</Query>

For a more in-depth example, check out this gist.

// Dependencies.
import React from 'react';
import PropTypes from 'prop-types';
import { Mutation, Query } from 'react-apollo';

// Mutations and queries.
import MY_MUTATION from './MY_MUTATION.graphql';
import MY_QUERY from './MY_QUERY.graphql';

// UI components.
import DumbComponent from './DumbComponent';

// Define HOC.
const HigherOrderComponent = ({
// Props.
customerId = '',
}) => {
// Query info.
const queryInfo = {
query: MY_QUERY,
variables: { customerId },
};

// Mutation info.
const mutationInfo = {
mutation: MY_MUTATION,
};

// Expose query.
return (
<Query {...queryInfo}>
{({ loading, error, data }) => {
// Expose mutation.
return (
<Mutation {...mutationInfo}>
{(mutationMethod) => {
// Still loading?
if (loading) {
// Early exit.
return <p>Loading…</p>;

// Error exists?
} else if (error) {
// Early exit.
return <p>Error: {error.message}</p>;
}

// Peel apart data.
const { whatever } = data || {};

// Component info.
const componentInfo = {
customerId,
whatever,
handleSubmit: (variables) => {
mutationMethod({ variables });
},
};

// Expose UI.
return <DumbComponent {...componentInfo} />;
}}
</Mutation>
);
}}
</Query>
);
};

// Validation.
HigherOrderComponent.propTypes = {
customerId: PropTypes.string.isRequired,
};

// Export.
export default HigherOrderComponent;

Furthermore, if multiple components make use of the same (or similar) queries, Apollo will batch them up into a single query. That way, you are not forcing the browser to do multiple HTTP requests for what is essentially the same data.

Suppose one component needs this data:

  • First name
  • Last name
  • Social security number

And another component needs this data:

  • First name
  • Last name
  • Email address

Then only a single HTTP request would be sent, for an object that contains all four data points.

Think of it like a Venn diagram. Apollo detects that "overlap" based on a unique identifier, and is smart enough to realize that we are dealing with the same customer object. It tailors the actual request to envelop all the requisite data points for that customer.

For more on Apollo's query and mutation components, check out the following links:

https://apollographql.com/docs/react/data/queries

https://apollographql.com/docs/react/data/mutations


Gotchas: Waxing philosophical

According to the scientific principal of mass conservation:

"Mass can neither be created nor destroyed, although it may be rearranged…"

Concepts and workflows that are complex — those which get people arguing over the implementation of a solution — are inherently nuanced.

As such, there is rarely a "one true way" solution. Instead, we often talk in terms of compromises and trade-offs. When determining whether GraphQL is right for your project, it really comes down to a team consensus around this question:

"Where do we want to pay the complexity tax?"

Chiefly, you should make sure that a complexity tax actually needs to be paid.

If your API just so happens to encapsulate all the data that you might need, as a single endpoint, then adding GraphQL may be overkill. Everything may already be as unified as desired. If so, then a simple window.fetch is probably good enough.

When evaluating the possibility of using GraphQL, I would encourage you to audit your microservices as well. Assuming you have the wherewithal to make changes, some of the API pain you may be attempting to alleviate via GraphQL could possibly be addressed by better structuring your microservices first.

Rather than introduce another tier (GraphQL middleware), maybe fix the tiers you have.

aka: Do not reach for a screwdriver, if what you really need is to repair your hammer.


Gotchas: null vs. undefined

One big "duh!" moment for me came when using object destructuring with GraphQL. While this is really an issue with JavaScript itself, it is made more acute by how GraphQL handles nonexistent values.

For a typical Ajax request, if a response has a key/value that does not exist, you would simply expect to get undefined when peeling it apart. As such, by supplying a fallback when doing object destructuring, that would be used instead.

However, GraphQL returns null for anything you have requested that does not have a value. In which case, null is still used instead of your fallback. This can be maddening if you are not aware of what is happening.

Consider the following example.

// Render method.
render() {
// Set fallback.
const fallback = {
child: 'no data',
};

// Get whatever.
const { whatever = fallback } = this.props;

/*
Assuming `this.props.whatever` was
passed from a parent component, but
did not exist in the Ajax response:

- We would expect "no data" to be logged.

- GraphQL gives us `null` so this would
cause an error because `child` is not
a property that exists on `null`.
*/

const { child } = whatever;
console.log(child);
}

Note: This also applies to a component's defaultProps. If an incoming prop's value is explicitly set to null, then the default fallback will not be used. Again, this is simply how JavaScript (and React) work. Still, the insertion of otherwise unexpected null values can be tricky to track down, if you are oblivious about what GraphQL is doing for you.

It is a simple enough issue to work around, as long as you know that you need to solve for it. The following example would mitigate this problem.

The reason this works is that the || syntax applies to anything that evaluates to falsy, so it handles both null and undefined values.

// Render method.
render () {
// Set fallback.
const fallback = {
child: 'no data',
};

// Get whatever.
const { whatever } = this.props;

// Get child.
const { child } = (whatever || fallback);

// Reliably logs "no data".
console.log(child);
}

Explanation: truthy vs. falsy

As a rule of thumb, it is helpful to remember that React defaultProps and object destructuring fallbacks only work when a value is both undeclared and implicitly falsy.

Meaning, any values other than undefined will be used verbatim, and your fallback will be unused as a result.

Think of it as the various UI states of a toilet paper roll:

  1. Spool has paper
  2. Spool is empty
  3. Spool is missing
  4. Spool holder is missing

In JavaScript terms, using a destructuring fallback covers only scenario 4. Whereas the || approach covers all falsy scenarios: 2, 3, and 4.

toilet paper roll

UI state examples

What about Redux?

If you are using React, but not (yet?) using GraphQL, chances are that you are familiar with Redux "thunks" as a way of fetching data.

In fact, this was the approach we used for several enterprise React projects, before becoming enamored with Apollo's declarative approach.

If you are considering GraphQL for a new project, I would caution against attempting to use it alongside Redux. While not mutually exclusive, I feel like they solve slightly different use cases in overlappingly similar ways. So much so, that if you are using GraphQL, I would even go so far as to say you should not use Redux (and vice versa).

For an app we built recently, I found that we were able to accomplish everything we needed by using GraphQL for data queries/mutations, and managed UI changes using local state in higher order components.

One common misconception is that local state is verboten, and instead Redux should always be used for anything stateful. Coming from a "manage everything in Redux" approach, it was a refreshing change to have UI state logically collocated with its relevant component.

For more on this topic, read: How GraphQL Replaces Redux.


Gotchas: overhead

If you have read this far, then you might be thinking that GraphQL is something you would like to look into further. I would be remiss if I did not also mention the implicit overhead of adopting a technology with new syntax.

  1. Learning — Though it is a mental hurdle worth jumping over (onto a bandwagon?), GraphQL has a learning curve just like anything else. Once you have an "aha" moment, it is not too bad. Up until that point, it can feel a bit foreign.

  2. Tooling — You will need to ensure your text editor or IDE of choice supports syntax highlighting for GraphQL. Though not strictly necessary, it makes for a more pleasant development experience. Thankfully, there are a myriad of plugins available that add support for GraphQL string literals.

  3. Bundle size — Granted, this is a cost not often talked about since the benefits usually far outweigh the raw file size, but is a valid concern nonetheless. If you simply did regular HTTP requests, an extra parsing library would be unnecessary.

  4. HTTP verbs — GraphQL really speaks only GET and POST. Though not a huge concern from a purist standpoint, it may lead to confusion over which aspect of CRUD (create, read, update, delete) is being done if simply inspecting web traffic.

  5. Caching — Somewhat related to HTTP verbs, depending on how you are serving your API data, there is an opportunity for caching to be had. For example, if you have a GET endpoint that serves some data, and its params are constant, then a cache could possibly serve up an identical response if queried within a certain timeframe threshold. If you are doing lots of traffic over POST, you may not be able to reap these benefits.

  6. Performance — Tangential to caching, though a concern unto itself, is that of actual query performance. By this, I mean the way you choose to ping your microservices. GraphQL makes things inherently more performant on the front-end, but make sure that while you are deciding how to handle the middleware that you do so in a way that does not needlessly burden the RESTful API endpoints.

  7. Infrastructure — Perhaps this should go without saying, but you would be surprised how many mentions of GraphQL are initially met with enthusiasm, only to arrive at an impasse when reality sets in. You will need to run GraphQL (usually via Node.js) as a middle tier, between your apps and APIs in production.

For more reading on GraphQL HTTP and caching, check out the links below:

https://graphql.org/learn/serving-over-http

https://graphql.org/blog/rest-api-graphql-wrapper

https://apollographql.com/blog/announcement/platform/caching-graphql-results-in-your-cdn

Note: If for some reason the "infrastructure" concern is a non-starter, but you still want to use Apollo GraphQL (and the "Graphi-i-QL" UI) on the front-end, check out Apollo Bridge Link.

Personally, I believe that using Apollo Bridge Link is worth it, even if you are ultimately just hitting RESTful endpoints. It at least makes tinkering with APIs in the browser via GraphiQL possible. In my opinion, this is greatly preferable to using something like Swagger.


Conclusion

Okay, so you have reached the end. What now?

Should you definitely use GraphQL in your next project?

Honestly, as with every software design/dev question, the answer is: "It depends."

Only you and your team can determine that with any degree of certainty. There will always be trade-offs. Anything worthwhile usually has some level of difficulty attached to it.

However, I would encourage you to at least kick the tires and give it a try. You may find that it jives with your workflow too. If not, then at least you will have an informed opinion as to why.