Connecting to multiple GraphQL endpoints with URQL

Connecting to multiple GraphQL endpoints with URQL

If you're familiar with GraphQL or URQL and there are times you need to connect to more than one client (not common) in your app, or you've probably worked with EVM (Ethereum Virtual Machine) chains where you had to connect to multiple subgraph endpoints in your app. In this article, I summarize how to do that and challenges you might come across.

INTRODUCTION

To answer the question from the title, I would say Yes and No, I will be using URQL and React to explain this.
URQL is similar to Apollo client, so if you know Apollo you will totally get the hang of the code. you can check the docs for more information on how to create a client and add authorization exchanges. I will be moving straight to making a request which is the main purpose of this article.

FIRST SCENARIO

import { Client, createClient } from 'urql'

export function createGraphQLClient() {
  return createClient({
    url: 'your-api-url', // The default client url
    fetchOptions: {
      credentials: 'same-origin',
      method: 'GET',
      headers: {
        Authorization: `Bearer ...`,
        'Content-Type': 'application/json',
      }
    },
    preferGetMethod: true,
    exchanges: [],
  })
}

This method above connects to your client to make a simple get request which is easy to understand. You just replace 'your-api-endpoint' with your endpoint, and you're good to go, then you can call that function anywhere in your app.

const fetchedData = async () => {
    const { data, error } = await createGraphQLClient(MY_QUERY).toPromise()
    // your result lives in your returned data
    // you can also check for error state 
}

You could pass your data to your app state from there and display it.This is pretty straight forward.

SECOND SCENARIO

The First scenario is common to everyone using GraphQL, now imagine you have to fetch data from more than one endpoint and display them in your app, you could conditionally choose what the URL you want to call at particular points or pages of your app.

export const graphQLClient = createClient({
  url: "default-url",
  exchanges: [
    dedupExchange,
    cacheExchange,
    authExchange({
      addAuthToOperation({ operation }) {
        // if clientName does not exist, we return operation without modifications
        if (!operation.context.clientName) {
          return operation;
        }

        const { clientName, fetchOptions } = operation.context;
        const options = typeof fetchOptions === "function" ? fetchOptions() : fetchOptions ?? {};

        switch (clientName) {
          case "firstCase": {
            const context = {
              ...operation.context,
              url: "first-case-url",
              fetchOptions: {
                ...options,
              },
            };
            return makeOperation(operation.kind, operation, context);
          }
          case "secondCase": {
            const context = {
              ...operation.context,
              url: "second-case-url",
              fetchOptions: {
                ...options,
              },
            };
            return makeOperation(operation.kind, operation, context);
          }
          default: {
            return operation;
          }
        }
      },
      getAuth: async () => {},
    }),
    fetchExchange,
  ],
});

This is how you would connect to the client in this scenario. It's pretty complex, but let me explain. Firstly, I am assuming you already have an idea how switch statement works in JavaScript, so when this function is called, it looks for either one of the cases (“firstCase” or “secondCase”) then it connects to that client, if it doesn't find any of the cases it uses the default URL set. So how exactly do you call these cases, the same function as the one earlier but called with extra parameters.

 const { data, error } = await graphQLClient
        .query(
          MY_QUERY,
          {},
          {
            clientName: "firstCase",
          }
        )
        .toPromise();

The second object takes a field called clientName which checks to see if any of the switch cases passed above has a case with "firstCase" and it hits that client, if it doesn't find any case like that it uses the default URL. The first object is usually empty, unless your query requires variables. That's it, with this you can connect to multiple clients in your app.

THIRD SCENARIO

This scenario is not common, but I ran into this recently. The downside with the second scenario is that you can only connect to one client per instance, meaning even if you have multiple endpoints you can only select one and connect to that one at a time and not all, What do I mean? Imagine your app has a marketplace page with different sections, and you need to source data from different endpoints, this won't work. I know you must be thinking, why not have all data live in one endpoint, why have multiple? Well, this problem is not common unless you're working with something like subgraph because you have to index data from the blockchain, additionally you might be working on multiple evm chains (Polygon, Celo, Aurora e.t.c). So you would need to deploy for all chains needed in your app, for instance if you're working with 3 chains you would probably need endpoints for both testnet and mainnet making it 6 endpoints in total, if this data is displayed at different places in your app, you're good – scenario two would work fine (passing all the endpoints in your switch statement). Imagine it's just a single page you want to display all data (probably NFTs or the likes) and you want users to be able to filter by different chains. The second scenario would not work, it would keep hitting one endpoint. There is a solution I came up with, it's not ideal but works fine and good without bugs or issues, your app works as expected, but I would be writing about this next because I don't want to make this article too long. Thank you!