Designing GraphQL APIs - Best Practices & Learnings from REST API Design
An API Layer As A Contract
Paraphrasing Eve Porcello: "a GraphQL API is an agreement between backend and frontend devs."
An API represents the data backend engineers need to provide and the data that frontend developers need to consume. Therefore, in a REST API, the endpoints represent the intersection of these needs.
This can get messy, say, if a backend engineer who is writing the ORM or API provides all the data on a customer to an endpoint and the frontend dev only needs a customer's name for a certain page. This can lead to either data pollution, in which case the frontend developer needs to write unnecessary business logic, or endpoint pollution, in which case the number of endpoints multiplies to the extent that both engineers lose track of the purpose of each endpoint.
GraphQL entered the scene as a way to avoid this type of pollution: ask for what you need, and get it.
And yet, there remains a factor that can multiply complexity: GraphQL queries. Used correctly, they minimize pain, but used incorrectly, they can create confusion and stifle an API layer.
What makes for a messy GraphQL API layer?
For one, naming convention should remain consistent within the API. If your API returns information on 3 different pet breeds but your naming is inconsistent, it's just not easy to read:
catBreedQuery
dog_breed_query
getInfoOnHamsterBreeds
Consistency is key for easy reading:
cat_breed_query
dog_breed_query
hamster_breed_query
Next, meaningless nesting can make for an inconvenient developer experience as well.
For example, wrapping your users
data unnecessarily in a data
element:
"data" : {
"users" { :
[
{
"username": "Jane Doe"
"id": 1
"joined_at": 12-12-2002
},
{... etc}
]
Why not just have the following?
"users" : {
[
{
"username": "Jane Doe"
"id": 1
"joined_at": 12-12-2002
},
{...etc}
]
These are implementation details, but when we 'zoom out' to the level of the architecture of the API itself, we see another way of creating a cluttered API by using GraphQL queries incorrectly: following the structure of the REST API directly. If you do that, then why have a GraphQL API at all?
To see what I mean, let's pretend we're GraphQL-izing a REST API with three endpoints:
- https://themovieapi.com/getmoviebyid
- https://themovieapi.com/getactorsbymovieid
- https://themovieapi.com/getactorspreviousrolesbyid
The developer wants to enter a movie id, and then get a list of actors with their previous roles.
So, to layer your GraphQL API, you create these queries:
getmoviebyid
getactorsbymovieid
getactorspreviousrolesbyid
This means that the developer must write a lot of frontend business logic. They must first retrieve the results of getmoviebyid
, then they must use that result in getactorsbymovieid
, and lastly they must use that result in getactorspreviousrolesbyid
. Why even use GraphQL in the first place, from the frontend developer's perspective?
This is where a GraphQL API designer can leverage StepZen to create an API that is easily consumed and understood by other developers.
StepZen provides many custom directives to users of its endpoints. One of them, @sequence, solves this problem within a few lines of code:
getActorsPreviousRolesByMovieID(movie_id: String!): MovieType
@sequence(
steps:
[
{query: "getmoviebyid"}
{query: "getactorsbymovieid"}
{query: "getactorspreviousrolesbyid"}
]
)
This condenses 3 queries to one-- each query
in the steps
argument automagically picks up its parameters from the previous step (while the first uses the parameters entered into the query).
Using these and other solutions like @materializer make connecting APIs with StepZen a matter of a few lines of code in the GraphQL layer, rather than hundreds of lines of business logic in the client.
An API Layer as Communication
Insofar as an API layer is a contract between developers, it represents a type of static communication between developers.
Much like in the case of software documentation, there are many principles of communication that then apply to API design.
- Communicate only what is needed.
- Documentation for an npm package needn't explain why a recursive function was needed to build it-- it only needs to explain how to use the package. Similarly, a GraphQL API does not need to make a call for REST endpoint it's layered over, if the API designer makes use of directives like @sequence.
- Create a smooth entry point.
- Documentation should take the reader clearly from one concept to the next. Similarly, GraphQL queries should create predictable patterns for the developer to follow: -- allStates -- statesByName -- statesByCapital -- allStateCapitals -- stateCapitalByName
- Consider the readability of your API.
- avoid meaningless nesting and inconsistent queries
Conclusion
GraphQL APIs partially evolved to help clean up messy REST APIs.
But how to redesign a confusing GraphQL API?
Developers can implement a StepZen GraphQL layer using @sequence
or a similar custom StepZen directive, to craft an API that will smooth out the developer experience.
It might also mean careful renaming of your queries.
Prevention is the best medicine-- patience and forethought in the process of adopting GraphQL, especially since incremental adoption is used at many organizations, can reduce your technical debt in the future.
If you've got questions about anything GraphQL API design, hit us up on Discord.