By Nimrod Grinvald (@ngrinvald)
If you're a frontend engineer, you've likely heard of GraphQL. It's a powerful tool for building APIs that can simplify frontend communication with the back-end. But when is it not the right choice?
In this post, we'll share our frontend engineering team's process for choosing to use Connect-Query over GraphQL. The team discussed using GraphQL but had skepticism about adding another server sitting between frontend and back-end API. We ultimately chose a simpler implementation that didn't require an intermediate server, hence Connect-Query.
Who are we?
Our team consists of seven contributors, mostly all senior, most of whom are based in Japan with a couple based in the US. Hirano is our team lead. Rito spearheaded the effort to investigate GraphQL before he left. Sher joined and stepped up to continue Rito's work. Sher's experience in both backend and frontend work allowed him to act as a bridge between the two groups and helped us evaluate the options with a more holistic view.
What are we building?
Our team is building a prototype for a new ID management system. We need to test technologies in preparation for our MVP.
What was the problem we had?
Our frontend functions as a generic single-page application (SPA): it is a statically bundled React TypeScript application relying on a backend server for persistent data storage.
The backend is currently written in Go and uses gRPC Protocol Buffers (protobuf) to define our backend API. To communicate with the backend server, the frontend imports our own protobuf package.
However, the backend's asynchronous state management presents a challenge for developers integrating new features.
TanStack Query’s documentation summarizes the issue of asynchronous state management well:
Most core web frameworks do not come with an opinionated way of fetching or updating data in a holistic way. Because of this developers end up building either meta-frameworks which encapsulate strict opinions about data-fetching, or they invent their own ways of fetching data. This usually means cobbling together component-based state and side-effects, or using more general purpose state management libraries to store and provide asynchronous data throughout their apps.
While most traditional state management libraries are great for working with client state, they are not so great at working with async or server state. This is because server state is totally different. For starters, server state:
- Is persisted remotely in a location you do not control or own
- Requires asynchronous APIs for fetching and updating
- Implies shared ownership and can be changed by other people without your knowledge
- Can potentially become "out of date" in your applications if you're not careful
Once you grasp the nature of server state in your application, even more challenges will arise as you go, for example:
- Caching... (possibly the hardest thing to do in programming)
- Deduping multiple requests for the same data into a single request
- Updating "out of date" data in the background
- Knowing when data is "out of date"
- Reflecting updates to data as quickly as possible
- Performance optimizations like pagination and lazy loading data
- Managing memory and garbage collection of server state
- Memoizing query results with structural sharing
These issues leave our frontend engineering team with a problem: they must coordinate on and then implement their own logic for loading data, handling errors, refetching, caching, and invalidating.
Our initial homegrown solution has resulted in multiple implementations and repeated boilerplate code, causing unnecessary complexity.
And for a small team of seven contributors, each working on a different portion of the codebase, we need a lightweight solution requiring minimal maintenance.
So, in order to address these issues and standardize asynchronous state management, we began exploring alternative approaches.
GraphQL was a natural first place to start.
Why GraphQL?
GraphQL solves common development challenges like over-fetching and under-fetching of data, while also providing flexibility and performance benefits (and GraphQL also acts as a unified access layer for multiple data sources, though this benefit doesn’t apply to our use case).
In its own words, GraphQL is
a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools…
Send a GraphQL query to your API and get exactly what you need, nothing more and nothing less. GraphQL queries always return predictable results. Apps using GraphQL are fast and stable because they control the data they get, not the server…
GraphQL queries access not just the properties of one resource but also smoothly follow references between them. While typical REST APIs require loading from multiple URLs, GraphQL APIs get all the data your app needs in a single request. Apps using GraphQL can be quick even on slow mobile network connections.
And it’s popular, which gives us a healthy ecosystem with plenty of community support. According to the 2021 Stack Overflow Developer Survey, GraphQL is one of the most loved and wanted technologies among developers. GraphQL has a growing community of developers who are actively contributing to its development and improvement. And a variety of programming languages and platforms (JavaScript, Java, Python, Ruby, web, mobile, desktop…) use GraphQL.
But we wanted to be sure GraphQL would be the best fit for our MVP, so Sher continued the investigative effort Rito began. That’s when he found Connect-Query.
What is Connect-Query?
Connect-Query is:
an expansion pack for TanStack Query (react-query), written in TypeScript and thoroughly tested. It enables effortless communication with servers that speak the Connect Protocol.
But that begs the question, what is TanStack Query?
Remember, the frontend team needs a lightweight library that provides an easy-to-use query language for fetching data from APIs that solves the asynchronous state management issue.
TanStack Query (formerly known as React Query) does just that. It provides methods to query data from the server, mutate data on the server, and intelligently mark query data as stale (and potentially prompt a refetch).
Tell TanStack Query where to get your data and how fresh you need it to be and the rest is automatic. It handles caching, background updates and stale data out of the box with zero-configuration.
If you know how to work with promises or async/await, then you already know how to use TanStack Query. There's no global state to manage, reducers, normalization systems or heavy configurations to understand. Simply pass a function that resolves your data (or throws an error) and the rest is history.
TanStack Query is configurable down to each observer instance of a query with knobs and options to fit every use-case. It comes wired up with dedicated devtools, infinite-loading APIs, and first class mutation tools that make updating your data a breeze. Don't worry though, everything is pre-configured for success!
TanStack Query’s documentation suggests it can:
- Help you remove many lines of complicated and misunderstood code from your application and replace with just a handful of lines of [...] logic.
- Make your application more maintainable and easier to build new features without worrying about wiring up new server state data sources
- Have a direct impact on your end-users by making your application feel faster and more responsive than ever before.
- Potentially help you save on bandwidth and increase memory performance
Sounds great so far, BUT…
TanStack Query does not play nicely out of the box with Protocol Buffers!
While the frontend has access to protobuf files that define the backend API, turning those protobuf backend API definitions into usable frontend Typescript means creating and maintaining boilerplate code. There’s also more boilerplate code involved in managing the keys of the created TanStack queries. Not fun.
Connect-Query enhances TanStack Query here. Connect-Query creates TanStack Query-compatible hooks out of protobuf-generated code. The Connect-Query code generator generates Typescript types and TSDoc documentation from the protobuf schema. As the library developers clarified,
Developing with Connect-Query provides several key benefits:
- Type-safe methods: Through code generation, Connect-Query ensures that query keys are always correct and consistent.
- Flexible: All hooks provided by Connect-Query return only the parameters required by TanStack Query, allowing for easy customization and overrides for all default behavior. This also provides enough flexibility for handling edge cases that fall outside of the usual fetch/mutate patterns.
- Un-opinionated: Connect-Query can be adapted to work with almost any other query library that accepts a key and query function.
- Method discovery: Strong types make it easy to discover RPC method sources in the IDE when typing its name.
Before Connect-Query, we found that it became repetitive to manually write all the glue code for connecting our RPCs, which also opened the door to potential programming errors.
Connect-Query simplifies this work by generating [...]outputs which include the query key and request/response message types[...]
With this set of generated code, our original implementation becomes simpler, more concise, and type-safe
So it looked like we could get a lot of benefit without needing to maintain another server. Seemed worth a closer work.
Our process for evaluating GraphQL vs. Connect-Query
Remember our problem: the frontend team must coordinate on and then implement their own logic for loading data, handling errors, refetching, caching, and invalidating.
Before deciding to use Connect-Query, we evaluated both Connect-Query and GraphQL using the following criteria:
- Provides a simplified and unified approach to asynchronous state management
- Seamlessly integrates with React applications
- Handles edge cases like errors, refetches, and caching
- Offers flexibility and customization options
- Eliminates boilerplate code and promotes standardization
- Is likely to remain reliably maintained
We initially favored GraphQL due to its popularity, as well as its:
- Schema-first development
- Ability to make complex queries in a simple markup
- Ability to batch requests
- Partial mutations
- Easy mocking in CI or test environment
But GraphQL requires a dedicated service in front of the backend server, which would create a potential bottleneck and maintenance burden. We would also introduce a need to learn GraphQL, which could create additional bottlenecks in hiring or training.
Connect-Query seemed to provide the better approach. In addition to hitting all our criteria:
- Our frontend could consume the backend-generated protobuf package as-is
- The familiar hooks paradigm integrates smoothly with our React Typescript application
- Granular state control means the ability to specify separate statuses for data fetching and network activity
- Possible to fallback to direct protobuf service method calls with TanStack Query if Connect-Query is not well maintained
There were a few downsides to Connect-Query, though. It, too, requires a moderate learning curve. Some of our team members would need to familiarize themselves with its fine-tuning configuration options such as retries, dynamic cache keys, and parallel query optimization.. And it has all the risks and limitations associated with a new library prior to a semantic version 1 release.
We Chose Connect-Query over GraphQL
You knew we would… we say so right in the title of this blog post.
While GraphQL is easy to debug, improves performance by aggregating requests, and lives in a mature ecosystem, there would be additional ongoing work to learn and maintain the system. Adoption would also require an initial effort to map protobuf to GraphQL.
Where Connect-Query shines for us is its easy code migration, low effort to learn, and official support from the builders of the Connect protocol. However, those advantages were somewhat offset by its current experimental status, immature ecosystem, and inability to optimize the number of requests.
Ultimately, our team decided to use connect-query. What it came down to for our small team is the need to optimize for our time and effort. GraphQL would mean additional server maintenance cost, learning, and more tooling to convert protobuf to the GraphQL scheme.
Here’s the takeaway for your team:
Takeaway
While GraphQL is a powerful and popular tool that can help revolutionize how your front-end communicates with your back-end, it's not always the right choice!
We ultimately chose to use Connect-Query due to its simplicity and maintainability given our existing tools.
However, every team's needs are different, and it's important to evaluate each solution based on your specific use case.
We hope this post helps you evaluate whether Connect-Query is the right choice for your team. Happy coding!
JOIN OUR TEAM!
Do you want to help make teamwork and collaboration better? Are you interested in creating a new IAM product? Do you want to guide the future of next-generation software?
Read our open positions. We are looking forward to working with you.