What building an internal application revealed about Apollo

For almost a year, the Gusto Development Experience (or DEx) team has been working on an internal application called Flightdeck. Engineers visit Flightdeck for regular engineering tasks, like running database migrations. Our long-term goal is for Flightdeck to be a tool with all phases of Gusto software development and maintenance.

pasted-image-0
Flightdeck: like this, but for your web applications.

Learnings and warnings

Most of the engineers on our DEx team have product engineering experience. We have worked on our customer-facing app as well as applications used by internal operations teams. These applications are mostly React.js built atop RESTful APIs.

When starting development on Flightdeck, we knew that we loved React’s declarative and productive paradigm. However, we also were jaded from the front-end and back-end boilerplate needed to integrate it with a RESTful API. Having heard about GraphQL from React Conf, GitHub's API, and elsewhere, we decided to try it. We chose Apollo because of its good documentation and community involvement.

We’ve passed the first months with Apollo on our young application. Here are the biggest things we learned about the framework.

Container components are hard to unit test

For the uninitiated, “container” components are the components returned by the graphql higher-order component (HOC). In order to test them, you must mock the GraphQL layer; this involves recreating the type definitions in your GraphQL backend, in addition to any mutation signatures.

For a young application in which mutations are likely used in only one place, recreating their signatures feels unpleasantly tautological. To make matters worse, the test utils' documentation isn't great.

In Flightdeck, container components are only used in one place each. Given that, and that we're encouraging Capybara integration tests for discrete new features, we've skipped unit testing the container components for now. (We’re still unit testing all of our other components.)

Because Gusto engineers have strong reflexes to maximize test coverage, this has resulted in integration tests for all of our recent features. We recognize that integration tests may become more difficult to write if our app grows much larger, and we’re hoping for improvements in apollo-test-utils’s documentation.

Colocating components and their data requires some boilerplate, but it’s worth it

We’ve arrived at a simple pattern for components at different levels of nesting to define their data requirements. This is inspired by Relay's "fragment containers".

Each “view” (non-container) component defines a static fragments attribute. This attribute is an object whose keys are GraphQL type names, and whose values are GraphQL fragments. This attribute has an entry for any type whose data is required by the component or one of its children. If a child depends on a type's data as well, the child fragment is included in the parent’s corresponding fragment.

The name of each fragment must be unique across the set of all fragments used in any given GraphQL document. We’ve found the naming convention ${ComponentName}_${TypeName} as in TodoItemList_Todo, to be the best combination of readability and uniqueness guarantees.

class TeamShow extends Component {
  static fragments = {
    team: gql`
      fragment TeamShow_Team on Team {
        name
        mission {
          name
        }
        failedBackgroundJobs {
          ...JobTabs_FailedBackgroundJob
          application {
            name
          }
          environment {
            id
          }
        }
        runtimeExceptions {
          ...ExceptionTable_RuntimeException
          project {
            id
          }
        }
      }
      ${JobTabs.fragments.failedBackgroundJob}
      ${ExceptionTable.fragments.runtimeException}
    `
  }

  render = () => (
    <div>
      {
        this.props.failedBackgroundJobs.map(job => (
          <JobTabs job={job} key={job.id} />
        ))
      }
      {
        this.props.runtimeExceptions.map(exception => (
          <ExceptionTable exception={exception} key={exception.id} />
        ))
      }
    </div>
  )
}

Polling is a nice option

Apollo polls your backend with the same query if the pollInterval prop (Apollo 2+) or option (Apollo 1) is supplied. For pages of your application that have well-isolated data requirements, it’s a much simpler option than writing mutation cache updates. However, beware of errors: if you hit any, polling will log tens of errors to your browser devtools.

unnamed
Polling pains

Be careful with required fields

Think very carefully about what fields are required in your GraphQL type definitions, i.e. those that use the ! operator. We've already run into multiple Apollo-layer exceptions from overzealous usage of this operator. If you’re certain that the field will always be present, as in field that directly maps to a NOT NULL database column populated by your backend, then it’s the right choice.

For fields that have presence validations at the backend validator/model layer, you may want to make them nullable in the GraphQL mutation signature. If a null value is passed to a mutation whose definition includes a not-null argument, the mismatch will be surfaced as the unergonomic error “cannot return null for nullable field…”.

If your backend model layer is the highest layer at which you validate presence, you will be able to surface more semantically meaningful error messages. Performing this validation at the view layer (before the data reaches GraphQL) could also help.

Mutation resolvers that return static values are okay

Most GraphQL examples illustrate querying for fields on the return value of a mutation. We’ve found that it often makes sense to call a mutation without any such querying. For example, we have one mutation resolver that simply enqueues a background job, and always returns a JSON response { “success”: true }. We wrote a simple “Status” type, with a single required boolean success field, to make this easier.

Expose datetime fields as strings

We chose to expose datetime fields as strings rather than integers (which would represent seconds since the Epoch). This lets us use a single backend service to handle date formatting, and lets us skip a front end dependency to handle time zones.

Beware __typename__

The __typename__ field is present by default in GraphQL query results. This could cause problems as an extra field when feeding a query result into a mutation. To make this more seamless, the addTypename configuration field for ApolloClient can be leveraged:

new ApolloClient({
    link: new HttpLink({ uri: 'http://localhost:4000' }),
    cache: new InMemoryCache({
        addTypename: false
    }),
});

This is a little different for react-apollo 1.x:

new ApolloClient({
  addTypename: false,
  networkInterface: createNetworkInterface({ uri: '/graphql' })
});

Future Improvements to our Flightplan

We’ve made a lot of improvements by reflecting on these points, but we’re not done yet. Here are some of our ideas for future improvements to our Apollo stack:

Be more consistent when handling error fields

Most of Flightdeck’s interactions with its backend consist of CRUD actions with corresponding validations. For this reason, it’s common for our GraphQL type definitions to have an errors field that contains the details of validation failures for an action, if any. These fields are currently defined on an ad-hoc basis for each type. We’d like to try leveraging interface types so that we can define this field only once.

More consistent handling of dates

I mentioned earlier that we’re defining date fields on GraphQL types as strings, to prevent having to handle formatting and time zones on both the front and back end of Flightdeck. However, we’re still invoking a date formatting class for each of our many date field definitions.

A cleaner approach may be to define a custom scalar type that knows how to coerce inputs and results. Here’s what that might look like in graphql-ruby:

Types::Scalar::DateTimeType = GraphQL::ScalarType.define do
  name 'DateTime'
  description 'A date and time in ISO 8601 format: "2018-01-01T23:59:59.123Z"'
  coerce_input(
    -> (value, _ctx) do
      begin
        DateTime.parse(value)
      rescue ArgumentError
        raise GraphQL::CoercionError, "cannot coerce `#{value.inspect}` to DateTime"
      end
    end,
  )
  coerce_result -> (value, _ctx) { value.nil? ? nil : value.to_time.utc.iso8601(3) }
end

Installing eslint-plugin-graphql

This eslint plugin enables checking your GraphQL documents against your GraphQL schema for a host of problems, including missing operation names and type name formatting. Installing the plugin is simple, but generating a schema for it to consume requires a bit more thought.

Conclusion

We’ve had a good time with Apollo so far. It’s freed us of a lot of boilerplate, and allowed us to focus more time on thinking about and writing features. We’d definitely recommend you give it a spin, but just remember our suggestions above when things get bumpy.

Bio

Kurt has worked as a software engineer at Gusto for 3 years. He's passionate about developer tooling and front-end technologies. He’s a telenovela connoisseur.