As software grows and evolves, the underlying data model evolves in unexpected ways. This seemingly obvious property of software development has caused some unexpected pain for my team recently. One major pain point has been non-nullable fields on our GraphQL API.
Since our project is full stack, I thought we would be able to aggressively make fields on our API non-nullable. For example, if we require a user in our system to have an email address, then it would make sense that an “email” field on our GraphQL’s “User” type should also be non-nullable. If we know all users will have email addresses, then why encode that information in the GraphQL schema and make that field non-nullable?
This reasoning works well for data models that are well known. However, applying these constraints to a developing data model leads to fragile software. Here are some of the practices my team has employed to handle uncertainty in a growing data model.
Backend Validation with Good Messages
One of the pain points for having a non-nullable field resolving a null value is the unhelpful error message. If a non-nullable field resolver receives a null value, the only feedback it can provide a developer is that it wasn’t expecting a null value. While this feedback is useful for a small query on a simple data type, the simple message loses all of its value as queries grow. Queries with lists of deeply nested data structures obfuscate the source of the problem. It’s quite difficult to work through a list of a hundred of items to determine which item is missing a particular field.
Because of this unclear message, I push most of my data validation before that data reaches the resolvers. During those validations, I’m able to control the feedback to the developer. At the very least, I’m able to let a developer know which record has a null value – and maybe even why I’m expecting the value not to be null.
Even better, if I control the feedback of a missing value, I can form an error message that the end user can read and send to the frontend. Instead of creating a schema with a non-nullable field, I tend to create a type that returns a nullable value and a reason why the value is missing.
Pages querying for non-nullable fields will not handle for unexpected null values. When something unexpected happens on the servers, the server response leaves a page with few options. At best, the page can communicate that something went wrong when communicating to the server. This typically leaves the user at a loss as to what happened and how they could proceed.
By having the nullable, the frontend implementation can determine what it can or cannot do. By returning nullable data, the GraphQL API gives the client whatever information it is able to. Even if a field is essential, frontend implementation — not the API schema — should determine if it can display something useful.
This forces the front-end implementation to be robust in the face of uncertainty. It’s likely that most of the page can render just fine with the data returned from the server. In my experience, placeholders can replace missing data, like a simple dash for a missing cell on a data table. If necessary, add a tooltip describing why the data is missing to help user resolve that issue.
Nullable fields reflect an important property of data backing software: uncertainty. As software grows and evolves, the API that wraps the underlying data model must allow the data to change in unexpected ways. By building APIs that express this uncertainty with nullable fields, the frontend implementation that uses those APIs can handle the less-than-ideal data.