Thursday 22 March 2018

GraphQL pitfalls

GraphQL has received much fanfare of late, has a nice website, lots of cool cats are blogging about it and, of course, it was developed by Facebook.  Must be amazing right?  We decided to replace our REST interface in a complex application.  Here's what we found.

Initial impressions were good, we used graphql-java on our backend defining the schema in SDL (rather than programmatically which is ridiculously verbose).  Querying worked really well, being able to specify the exact fields returned was especially nice (and useful to us).  A minor annoyance with interfaces is that all sub-types must repeat all interface fields.

To take an example from the GraphQL website:

interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!


type Human implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
}


type Droid implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  primaryFunction: String
}


Why do the id, name, friends and appearsIn fields have to be repeated in Human and Droid?  No doubt there are reasons for doing this, but as a user rather than a developer of GraphQL, I don't really care, I don't want to be repeating myself.  That aside, GraphQL querying seems great.

Unfortunately things go rapidly downhill when tackling updates (mutations in GraphQL parlance).  Firstly you can't use any of the types you've just defined, because you need to use input types for mutation parameters.  So you end up cut-and-pasting a large chunk of your SDL and then search-and-replace type->input.  This is nuts, again, I'm sure there are reasons for doing this, but as a user you don't care, you just don't want the duplication and worse the maintenance burden (yeah, I know you're thinking it's not much bother, but in years of programming I have seen people forget to update both places time and again.  And the cost comes not from having to quickly add a few more lines of code, but from having to build a new release, do an emergency release and deal with a pissed off customer).

It gets worse... input types themselves can't use normal types so you need to go through each input type changing type fields.  This is getting frustrating, but then you discover you can't use interfaces or unions in input types so you have to create brand new input types as substitutes or possibly breakout a type field into multiple input type fields.  Oh and now you've got to update all your DTOs too.  Guess what? The object mappers server side no longer work so you've got to start configuring them (and it's not simple field name mapping, it could be multi-field to single field mapping stuff, so now you've got to read the manual page of the object mapper).  It's strangely satisfying getting it all working, but you're starting to burn a lot of time.

Not being able to use interfaces on input types is a major problem.  If you have a limited amount of sub-types you can break out individually:

input HumanCharacterInput {
  id: ID!
  name: String!
  humanFriends: [HumanCharacterInput]
  droidFriends: [DroidCharacterInput]
  appearsIn: [EpisodeInput]!
  starships: [StarshipInput]
  totalCredits: Int
}

input DroidCharacterInput {
  id: ID!
  name: String!
  humanFriends: [HumanCharacterInput]
  droidFriends: [DroidCharacterInput]
  appearsIn: [EpisodeInput]!
  primaryFunction: String
}

input Movie {
  humanCharacters: [HumanCharacterInput]
  droidCharacters: [DroidCharacterInput]
}


Notice how messy it is getting.  Not a great solution. More than 2 or 3 sub-types it rapidly gets out of hand.  An alternative solution may be to have a single Character input type which contains every possible sub-type field, but this is a kludge and starts to erode the benefits of using a type system. We are not the only people struggling with this issue.

That was enough for us to discontinue use of GraphQL.  There were also concerns about validation support, the schema seems limited to type checking and not null.  I guess the onus is on the backend implementations to provide an easy validation hooks.  Graphql-java  appears to provide basic support under instrumentation.

As with many projects that appear attractive and work well in simple demos, it's only when you dig deeper on real world projects that you discover limitations.  I'd argue GraphQL is not yet ready for complex production systems that require mutations.  But if you're just using it for querying (and to be fair it is called a "query language") then it's worth a go.