We’ve discussed GraphQL at length previously – and while the discussions on how GraphQL works are obviously very powerful, we’ve yet to dive into some of the best practices that should be adopted when developing a GraphQL-centric API.
Today, we’re going to do exactly that. We’re going to discuss the best practices for healthy GraphQL implementation, and describe why these practices are so advised. By properly integrating these best practices as a matter of course, your GraphQL-driven implementation and its associated API structure can be more powerful, efficient, and practical.
Dogma vs Practices
Before we dive too heavily into this, it must be said — these are suggested best practices, not absolute dogma. While the majority of the practices herein are applicable to most API installation, use case, and design, there are some situations in which your API design or application will not work well with some aspects of GraphQL, while requiring other aspects of it.
In such cases, these best practices should be considered a guideline, not a dogmatic instruction on how GraphQL should be implemented.
URIs and Routes
While GraphQL is fundamentally RESTful, it does drop some elements that have become core to RESTful design, such as resources. Wherein most REST APIs use resources as a basic conceptualization for how data is processed and returned, GraphQL eschews this and instead depends on entities as defined by an entity graph. This entity graph drives all traffic to a single URL or endpoint, which is the forward facing entrance into the system.
Accordingly, URIs and routes should all lead back to a single endpoint — this is the fundamental use case of GraphQL, and is principle in its functionality.
Endpoint Collation and HTTP
By its nature, GraphQL serves verb requests over a single endpoint in HTTP. This aspect of GraphQL is fundamental to its design and approach towards endpoint collation — as such, using a methodology other than a collated endpoint in HTTP is locking a lot of functionality away.
In classic REST format, resources are exposed via a suite of URLs, and each URL has a specified endpoint that is tied into as part of the formatted request. While GraphQL can certainly co-exist with this kind of setup, having such a collection of resources behind URIs defies the purpose of GraphQL implementation in the first place.
Accordingly, if you need to adopt such a practice of hybrid linking (aka a single collated point and then a group of URLs representing individual resources), it would be advisable to look into your design, schema, and format, and address whether or not this is actually intended before proceeding with such a system in GraphQL.
GraphQL is a schema, and as such, there’s nothing stopping your API from versioning however it desires. That being said, GraphQL has stated a strong avoidance of versioning from a philosophical standpoint. GraphQL’s stated viewpoint is as follows:
“Why do most APIs version? When there’s limited control over the data that’s returned from an API endpoint, any change can be considered a breaking change, and breaking changes require a new version. If adding new features to an API requires a new version, then a tradeoff emerges between releasing often and having many incremental versions versus the understandability and maintainability of the API.”
Accordingly, GraphQL takes the view that, since the solution only returns explicitly requested data, there’s no such thing as a “breaking change” in properly designed GraphQL APIs. This is a definite shift from versioned API to versionless, and is part of the design ethos of GraphQL itself.
That’s not to say of course that versioning needs to be an all or nothing practice. It’s very easy to simply version by documenting changes and referring to the API by a stated version number – this is much more an internal organizational consideration, though, and as thus falls outside of the common definition of true versioning. As such, it’s better to call this a revision process rather than a versioning process.
Nullability and Default Typing
APIs typically handle nullability in two steps: with a “null” common type, and then a “nullable” version of that type, specifically to separate the two types from one another while allowing for nullification by specific declaration of said type. This is all well and good, and in fact is a good practice in and of itself, but because of how GraphQL is structured and defined, this actually does not work within the schema.
Accordingly, the best practice for nullification in GraphQL is to remember that this null type is in fact a default setting for every single field. This was integrated into GraphQL as a methodology for managing failures from a variety of internal and external causes, and as such, works well for its intended purposes.
As a corollary to this, keep in mind that if a non-null value is required, it can be set as a non-null type — regardless, it’s important to remember this default nullability approach when addressing issues in the return stage of data retrieval.
Pagination in GraphQL is almost entirely the purview of the developer. GraphQL allows for listed values as a return, and as those listed values grow in length, how they are returned is dictated by the strictures created by the API owner themselves.
GraphQL allows for a pattern known as “Connections.” This element of GraphQL allows for not only discovering information about specific related items, but about the relationships themselves. Adopting Collections is a best practice, as it ties into other GraphQL suites and tools like Relay, which can automatically support client-side pagination in that common format.
JSON and GZIP Dependency
GraphQL is designed to respond to requests using JSON. Despite this, it’s not expressly required in the specification, and in fact GraphQL can work with a variety of response types. That being said, it’s a best practice to adopt JSON due to its text-centric organization, as GraphQL is designed expressly to work with high-ratio GZIP compression.
Interestingly, GraphQL is designed with the syntax of JSON in mind, and as such, it is a good idea to keep this syntax consistent through both design and response. Likewise, GraphQL suggests that clients respond with the following appended in the header:
This will allow for even further compression of the response data, and should result in lower overhead and greater network performance. You don’t necessarily have to adopt JSON, especially if you are adopting GraphQL after your API has already been designed and given a specified output format, but failure to adopt JSON can result in decreased efficiency and confusion amongst the user base when comparing output with the code that generates said output.
Batching to Address Chattiness
GraphQL is, by default, extremely chatty. This is simply due to how GraphQL is formed, how the data is collected and returned, and how the server address requests. This chattiness can be a hindrance, especially when doing large scale data requests from a database.
Under GraphQL, the solution is to simply batch these requests over a given period of time, collating them, and then submitting the multiple requests as a single, collated request to the system in question.
This in turn eliminates much of the chattiness, as you are no longer issuing multiple packed, collated requests to multiple elements of a GraphQL enabled server — instead, you are bundling bundles of requests, resulting in a passive GraphQL into GraphQL solution that creates a relatively “quiet” system for the amount of data that is being requested.
This does not have to be enabled, of course, and for smaller projects, it might make sense not to do so, so that issues in requesting can be identified and singled out. That being said, when handling any substantial amount of data, adopting batching is probably a good idea.
GraphQL is designed in a way that allows you to write clean code on the server, where every field on every type has a focused single-purpose function for resolving that value. However without additional consideration, a naive GraphQL service could be very “chatty” or repeatedly load data from your databases.
Disabling GraphiQL in Production Environments
GraphiQL is a big selling point for many GraphQL adoptees, but even though it’s extremely powerful, it should be disabled in production. This recommendation comes from the official GraphQL documentation. GraphiQL should be disabled due to various vulnerabilities that could inadvertently expose internal API functionality by exploring the primary forward-facing endpoint. Enabling GraphiQL essentially negates many of the reasons you are integrating GraphQL in the first place.
GraphQL Authorization Practices
GraphQL specifically notes in its specification documentation that all authorization must be done in the business logic layer. This is specified due to how GraphQL functions. Using authorization logic that checks for elements of the entry point validation process, such as “has author ID” or “carries correct token” would mean that, for every possible entry point, this logic would have to be duplicated, which leads to incredibly complex implementations for even small amounts of authorization controls.
Accordingly, GraphQL dictates that this should be done at the business logic layer so that the authorization controls only need to be specified once for the entirety of that specific class. GraphQL notes that this is fundamentally considered having a “single source of truth for authorization.”
GraphQL Pipeline Model
As part of this approach towards Authorization, GraphQL advises that all authentication middleware should come before the GraphQL implementation to avoid the same issues with authentication. Any filters, plugins, extensions, etc. should all be placed before GraphQL to allow session and user information in its final form for granular and singular control.
As stated at the beginning of this piece, we’ve aimed to share advice rather than dogma when we discuss these best practices. The best practice of all is to adopt solutions that fit with your specific use case. Consider these GraphQL best practices as guidelines rather than set-in-stone rules, but for the most part, adopting these best practices and implementing GraphQL as designed will lead to a more powerful, leaner, and more efficient system. Failure to adhere can result in many of the benefits to GraphQL disappearing as quickly as your number of endpoints.