Distributed GraphQL Schemas with NPM Modules
HomeAway uses the simplicity and flexibility of GraphQL to insulate applications from change and accelerate UI and API development.
For a little over two years, we‘ve been busy replatforming our web applications at HomeAway to Node.js using hapi and React. Last year, we sought to simplify reuse for data fetching and orchestration between our native mobile applications and the many new web applications being developed.
GraphQL lets developers provide data to both web and native experiences while allowing the resolution of how that data is provided to evolve over time, mitigating the impact to the many, many UI components we have developed. This is, in part, why Facebook developed GraphQL.
Although we were already impressed by the power and simplicity of GraphQL, the typical process of schema creation meant that they were not easy to share between applications unless shared as common services, which introduced a model prone to issues at scale (see Killing BFFs with GraphQL).
Although anyone could have begun adopting GraphQL at any time for their application, we sought to operationalize GraphQL at scale for all of HomeAway. To do this, we wanted to develop tooling that allowed us, among other things, to:
- Provide support for internal concerns such as logging and metrics.
- Enable reuse between applications through modules.
- Enable developers to pick the types of queries they needed for their application.
This led us to the development of a convention we refer to internally as a “GraphQL partial”. While breaking up schemas into multiple files isn’t a new thing, componentizing them requires a little glue.
A GraphQL partial is simply an npm module that exports enough information for us to construct an executable schema with. That means a partial needs to export some types as well as the type resolvers as needed.
You will notice that the query type in this example uses the
extend keyword. This is because there will be many partials defining queries or mutations and to allow this, an empty root query and mutation will be provided by our tooling for these type definitions to extend.
Once a partial has been defined, what remains is to declare the partial schemas to use and stitch them together into a single executable schema.
As mentioned earlier, HomeAway uses the hapi framework for building applications. In addition, we use a module for bootstrapping the hapi server through an environment-aware hapi configuration engine called
steerage makes it easy to configure the partials and setup a GraphQL server in a consistent fashion, and once the partials have been specified, they can be stitched together. HomeAway uses Apollo to serve GraphQL, although we wrap it to inject context and accept and merge GraphQL partials.
Apollo also makes some other useful tools, one of which is the
makeExecutableSchema in the
makeExecutableSchema brings together type definitions and resolvers into a single schema.
So far, we haven’t done anything particularly different from a well known pattern for breaking up schemas. The challenge in breaking up schemas really surfaces when you want to publish them as separate modules, especially when it comes to root types.
This brings us back to our use of the
extend keyword and the little bit of utility we wrapped on top of the GraphQL server. Our server adds the empty root types and merges the different types and resolvers exported by the partials. Lastly, it uses
makeExecutableSchema and passes the result onward. We also use additional tooling to detect type conflicts ahead of time.
The final bit is providing the empty root types for each partial to extend. Rather than provide an entirely empty root type, we use a
_null attribute with a no-op resolver to enable merging multiple schemas.
The result is a simple utility that enables different applications to pick and choose their query capabilities.
Although the capability to build and reuse partials empowers teams to more easily craft schemas for their use cases, there are additional challenges to overcome.
As the number of partials grows, encouraging wide-spread reuse and discouraging redefining existing types can be challenging without good discovery practices, such as collocation of partial modules and excellent documentation.
The GraphiQL IDE presents another challenge. GraphiQL is intended for interacting with a single schema; with many partials this schema can grow very large. This may make it difficult to view all possible partials in a single place.
Finally, testing presents additional considerations. Since the partials are separate modules, applications incorporating them may not know how they are resolved upstream. Services, for example, must be accessible or mocked, and this requires discovery of what these upstream services are.
Today GraphQL — and our partials paradigm — has become our de facto standard for UIs to interact with and query data. We use GraphQL in our native mobile applications as well as multiple web applications.
To date, we have used GraphQL primarily to fulfill UI requirements, but we have begun to experiment with GraphQL for our public APIs as well. While REST and Swagger/OpenAPI have been the go-to for public API platforms for years, I believe we will begin to see more and more general purpose APIs developed with GraphQL.
Follow us here for a future post describing our adoption of Apollo 2 and the changes we’re making to make our partials more powerful and composable. I hope you’ve enjoyed this article. See you soon!