(This article originally appeared on Bearer.sh)
At Bearer, we use GraphQL to allow our Dashboard application to communicate with our database. Recently we gave this GraphQL API a re-design. Here are a couple of lessons we learned during the process.
Lesson one: Serve the user, not your database
"[GraphQL] is a query language for your API".
This bears repeating: GraphQL is a query language for your API. When consumers want to talk to your API, they’ll be doing so in GraphQL.
The primary focus of your GraphQL API is the needs of your API consumers. Unlike other query languages such as SQL, GraphQL does not care that much about your database structure. This may sound trivial in theory, but it is surprisingly tempting in practice to take a database-first approach when designing a GraphQL API.
To give a simplistic example, let’s imagine we are building a profile website for dogs. Our UI contains the dog's name, date of birth, and favorite toy.
Our database looks something like this:
dogs
name: String
birthdate: Date
toys
dog_id: Int
description: String
preference: Int
A dog has a name, a birthday, and can have any number of toys. Each toy has a preference, meaning that each dog with one or more toys has a “favorite toy”.
In Rails, this would look something like:
class dog < ApplicationRecord
has_many :toys
def favourite_toy
toys.order(preference: :desc).first
end
end
class toy < ApplicationRecord
belongs_to :dog
end
Modeling our GraphQL API directly against this database would give us a Dog type that looks something like this:
type Dog {
id: ID!
name: String!
birthDate: DateTime!
toys: [Toy!]!
}
type Toy {
preference: Int!
description: String!
}
And a query endpoint that looks something like this:
query {
dogs {
name
birthDate
toys {
preference
description
}
}
}
This works fine for our API consumer, but it asks them to understand how our toy preference metric works and to calculate each dog’s favorite toy using this metric.
If, however, we could shift our focus away from our existing database structure and towards the needs of our API consumer (in this case, building that profile website), we would arrive at a Dog type more like this:
type Dog {
id: ID!
name: String!
birthDate: DateTime!
favoriteToy: Toy!
}
type Toy {
description: String!
}
And a query endpoint more like this:
query {
dogs {
name
birthDate
favoriteToy {
description
}
}
}
Here, our GraphQL API gives its consumers exactly what they want: the name and favorite toy for each dog, without requiring any calculations or any knowledge about the underlying data structure.
Focusing on the consumer in this way can have hidden benefits. For example, we are free to switch up our current “favorite toy” calculation, or to restructure our existing database, all without bothering the consumers of our GraphQL API.
This is how we approached the re-design of our internal GraphQL API. We started with the consumer, our re-designed Dashboard application, and worked backwards to our database. In this way, we arrived at a GraphQL API that is consumer-focused, pleasant to use, and largely shielded from behind-the-scenes changes and updates.
Lesson two: Say no to null! Embrace interfaces
Let’s imagine we want to show additional information on our dog profile:
For rescue dogs, we want to show their adoption date and adoption shelter.
For show dogs, we want to show their full, registered name.
We can expand our GraphQL API to accommodate this. Since the new data does not apply to all dogs, we add the additional fields as nullable fields:
type Dog {
id: ID!
name: String!
birthDate: DateTime!
favoriteToy: Toy!
adoptionDate: DateTime
adoptionShelter: String
registeredName: String
}
These nullable fields allow us to represent different kinds of dogs within a single Dog type. A schema like this keeps our API simple, but this simplicity can come at a cost. Because we are representing all dogs under one Dog type, information about the different kinds of dogs becomes implicit.
Our API, for example, does not tell us about the two subsets of dogs available, rescue and show, nor does it tell us that only show dogs would have registered names, and that rescue dogs have adoption dates and shelters. It expects its consumers to know what kinds of dogs (rescue dogs, show dogs) are represented and what fields are associated with which kinds of dogs.
Moreover, we cannot express which fields are mandatory for which kinds of dogs. Can we expect that every rescue dog has an adoption date and an adoption shelter, or do only some rescue dogs have their shelter name recorded?
Lastly, in its current form, our schema makes no guarantee against returning nonsensical data like the following:
{
"data": {
"dogs": [
{
"id":"RG9nLTE=",
"name": "Fido",
"birthDate": "2017-08-30",
“favouriteToy”: {
“description”: “Yellow Tennis Ball”
},
“adoptionDate”: null,
“adoptionShelter”: “Second Chance”,
“fullName”: “Patrice Fidelius Wonderful III”
},
...
]
}
}
Is Fido a rescue dog, or is he a show dog? Even if we grant that Fido is a rescue show dog, he has no adoption date, which we may or may not be expecting for a rescue dog.
Instead, we could use a GraphQL interface to express shared fields between related types. Interfaces in GraphQL operate as abstract classes that define which fields must be present on any type which implements the interface. Following our example, we would have a Dog interface like so
interface Dog {
id: ID!
name: String!
birthDate: DateTime!
favoriteToy: Toy!
}
Common dogs, rescue dogs, and show dogs (and, if we so fancy, rescue show dogs) would then be represented as different types implementing this Dog interface
type CommonDog implements Dog {
id: ID!
name: String!
birthDate: DateTime!
favoriteToy: Toy!
}
type RescueDog implements Dog {
id: ID!
name: String!
birthDate: DateTime!
favoriteToy: Toy!
adoptionDate: DateTime!
adoptionShelter: String!
}
type ShowDog implements Dog {
id: ID!
name: String!
birthDate: DateTime!
favoriteToy: Toy!
registeredName: String!
}
By using an interface, we can clearly show the relationship between the different kinds of dogs, while also removing the problems that nullable fields bring.
Querying interface types looks something like this, and fully embraces the GraphQL philosophy of “Ask for what you need, get exactly that”.
query {
dogs {
name
birthDate
favoriteToy {
description
}
... on ShowDog {
registeredName
}
... on RescueDog {
adoptionDate
adoptionShelter
}
}
}
Conclusion
These are only a couple of insights we gained from re-designing our GraphQL API, and some things to keep in mind when building your own.
We’d love to hear your thoughts on GraphQL, and any lessons you’ve got to share from your own experience building and working with GraphQL APIs.
Top comments (4)
Just giving up on allowing a dog to have more than one toy?
I think you might be joking here but of course you can still allow the poor little doggie [Toy] along side favourite toy.
The point of the article I believe is to highlight the benefit of understanding what your consumers want and giving them it directly.
The part of this that doesn't make sense to me is that it seems like this article is suggesting that the API be built not only to support one specific consumer, but one specific user interface for one specific consumer. I'm not sure that I ever see situations where it would be a good idea to let one particular user interface dictate the design of an API so directly.
Everyone has different use cases that they are supporting with their work. I'm sure there are cases where this level of direct couplings between the needs of the UI and the API layer would be reasonable, but I wouldn't think it would be the best direction in most cases where an API it's built to expose a business domain for multiple different consumers.
Not that this is an argument for having the database model define the API design either. I'd think the goal should be to allow an understanding of the business domain to guide the API design.
Didn't know GraphQL has interfaces.
Nice article. Need to dive a bit on GraphQL in 2020 ;)