Experiments with Client Only GraphQL

Fake it till you make it, bridging GraphQL to REST

Patrick Winters
Sep 9, 2018 · 7 min read

Enthusiasm for GraphQL has come a long way in a few short years, but many of us remain locked out of adopting GraphQL for frontend development for a number of good reasons. Engineering organizations rightfully wish to leverage existing RESTful assets, avoid the cost of building yet another service, and lack a familiarity with GraphQL’s benefits. While the frontend team at Bronto built support and knowledge for GraphQL and the advantages of a GraphQL API, we used one GraphQL-like approach and two client side GraphQL techniques to resolve queries from existing, RESTful, resource-oriented services and locally cached data.

I’ll describe these approaches and how they have prepared us for a GraphQL API at Bronto. This article assumes a high level of familiarity with GraphQL and Apollo.

“A man can do as he wills, but not will as he wills”

In the early stages of “Building a Data-Driven Reporting User Interface,” we investigated the feasibility of using GraphQL for managing our data intensive, asynchronous UI. At the time (and this was over three years ago), React and client side tooling for GraphQL remained scant and the community didn’t have much more to choose from than Facebook’s Relay. There were suggestions that Relay might support resolution of queries from non-GraphQL sources using a custom Network Layer, but the documentation was spotty and the approach seemed off the trodden path. It was clear to us that our UI would need to be extremely reactive, re-fetching and re-visualizing time series data and metrics as users adjusted filters and date ranges. A lot of community examples and tutorials for “React data fetching” suggested the use of redux and asynchronous actions, but, unfortunately, we felt that this pattern wouldn’t scale well with the number of components and charts we were planning. Convinced that GraphQL (or at least its React patterns) would fit our requirements well, we looked for options that would allow us to leverage GraphQL in our client applications without the burden of building and maintaining a new service. We really wanted to use GraphQL and took conservative steps to make it a reality.

GraphQL-ish?

GraphQL-analogous patterns allowed us to declare queries and variables as a function of component input and produce highly reactive UIs.

I can’t remember how I discovered gyzerok/adrenaline, but it had been mentioned in a number of community discussions about React patterns and GraphQL clients and libraries (for example reduxjs/redux/issues/464 and reduxjs/redux/issues/775). The Apollo team, in fact, investigated Adrenaline early in their project’s development. We came to the conclusion that Adrenaline held promise for supporting RESTful queries with custom network adapters and contributed a PR to make it happen.

Fatefully, we opted to fork the project, identifying significant issues and determining that it hadn’t garnered enough community interest to make the support overheard pragmatic. I’m still proud of what we accomplished using Adrenaline, aware that there weren’t many alternatives; but I’m disappointed that it didn’t meet my expectations (however unrealistic they may have been).

How We Used Adrenaline

My initial interest in GraphQL for React focused on the prop driven, reactive resolution of data. Rather than manually initiating actions to fetch metrics, GraphQL-analogous patterns allowed us to declare queries and variables as a function of component input and produce highly reactive UIs. Adrenaline’s would manage a normalized, application data cache, and with a higher order component would asynchronously provide reporting metrics fetched from a RESTful service using a JSON protocol. We technically weren’t using GraphQL, but the declarative, asynchronous, and reactive nature of our components and the normalized data store all drew inspiration from client-side GraphQL tools. The following snippet roughly demonstrates what components looked like; our hoc wrapped Adrenaline to declare variables as a function of upstream props, reactively hydrating requirements as downstream props when variables changed or the cache updated.

This served the Reporting team at Bronto rather well, in part because the vast majority of our data requirements fetched from a single API. As the project advanced, we found it necessary to model additional API resources with custom “adapters.” Each had their own schemas and quirks, and we found ourselves re-implementing normalization logic using paularmstrong/normalizr for each API and adapter. The overhead made the pattern difficult to scale or recommend outside of our small team and project.

Resolving GraphQL Against RESTful Services Inside the Client

This feels like running a GraphQL service in the browser!

With dacz/apollo-bridge-link, we found the holy grail! An unassuming library, this Apollo Link allows clients to implement custom resolvers for arbitrary schema fields. We had plans to develop a proof of concept app to demonstrate the viability of a new architecture and technologies, and this link held promise for letting us bridge the gap between GraphQL oriented components and RESTful APIs. Thankfully, our frontend project was able to make use of a publicly accessible API that we could build a GraphQL bridge to.

Using our new “micro frontend” architecture, we were able to develop in a decoupled fashion, querying a stub API built with mountebank and boo1ean/casual. This allowed us to write integration tests verifying our resolvers and schema and continue UI development with GraphQL in veritable isolation. As we designed our schema and implemented resolvers, we found ourselves realizing, “this feels like running a GraphQL service in the browser!” Analysis of our bundle size would confirm that, indeed, we were running the entirety of GraphQL tooling on the client. If bundle size isn’t a major concern for you, this comes with benefits. First, the bridge link validates queries and resolvers against the declared schema. Second, the schema and resolver implementations are largely portable and can be easily migrated into a service. Last but not least, it requires zero knowledge or special syntax from the perspective of the client requesting data.

Here’s an example of a resolver that can be stitched into a client-executable schema with the bridge link.

Bootstrapping an Application with a Primed GraphQL Cache

Our next project required resolving a number of data resources currently inaccessible from any structured, RESTful API but available server side to our bootstrapping php application. Some resources may have have been available from a scattering of URLS in our legacy PHP application, but the semantics of these APIs were inconsistent. Given that these routes were built for specific use cases and features, we opted to avoid relying on them and accidentally stumbling into booby traps.

Instead, we acknowledged constraints in the amount of reactivity we could support in the UI and used Apollo’s State link to bootstrap an application with data marshaled in the server’s html response as embedded and encoded JSON hidden in the DOM.

Custom Resolvers to Adapt the Cache and Perform Mutations

I found it difficult to find examples of how to adapt local state to arbitrary fields and types. The Apollo documentation provides some help, demonstrating custom resolvers with cache reads and writes, but we wanted to present a facade/schema that would be future compatible with standards we’d adopted around filters and cursor based pagination.

The State link requires the use of @client directives on fields to instruct it to step in and use its configured resolvers. It doesn’t perform schema validation nor make use of a schema for matching selection sets to its resolvers. This has the effect of feeling looser and less reliable.

Here’s an example of custom field and mutation resolvers using local state. The state link initializes and primes the apollo cache, making queries accessible that reference the fields implied by the shape of the bootstrapped state. The “TASKS_CACHE_QUERY” below can be used to resolve the array of bootstrapped tasks directly from the cache, but when a client query selects the tasks field, we provide a custom resolver that adapts the result to our intended schema.

Additional Use Cases

In “Building a Data-Driven Reporting User Interface,” I indirectly revealed that we’ve implemented “derived metrics” in our client. Our reporting API maintains rather simple counts of events, and we query for and reduce these counts to compute metrics like ratios inside our client. I believe that the state link and its custom resolvers could be used for resolving and computing these kinds of fields, when our APIs cannot provide the data directly. In a manner of speaking, this could support client specific materialized views.

Client GraphQL and Patterns Have Prepared Us For Where We’re Heading

It’s fair to say that we’ve been using and learning GraphQL proper, despite the absence of a GraphQL, Node service. Much of the client-side adaptations we’ve implemented will be useful when we build a proper API, and each resolver, component, fragment, query, and mutation that we write has taught us something useful that we continue to build upon.

In the end, we got what we were looking for.

In the coming weeks, we’re going to cut the bridge, drop our bootstrapped state, and build a proper GraphQL service! I’m convinced that our experiences with GraphQL have not only helped us prepare our team but also instruct our organization in the value of what we’ve advocated building. We’re well positioned now to provide guidance to others, make impactful recommendations for APIs and schemas, and interface meaningfully with many of Bronto’s other teams.

Patrick Winters

Written by

Engineer @ Citrix

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade