Leveraging various endpoints with Apollo Client

Having mastered Apollo and GraphQL with the help of Odyssey, I am currently engrossed in creating my own project using Next.js. This involves fetching data from not one, but two different GraphQL endpoints.

The dilemma at hand: How can I efficiently retrieve data from multiple GraphQL endpoints using ApolloClient?

Displayed below is the code snippet for accessing my first endpoint:

import { ApolloClient, InMemoryCache, createHttpLink } from "@apollo/client";

const client = new ApolloClient({
  ssrMode: true,
  link: createHttpLink({
    uri: "https://api.hashnode.com/",
    credentials: "same-origin",
    headers: {
      Authorization: process.env.HASHNODE_AUTH,
    },
  }),
  cache: new InMemoryCache(),
});

export default client;

Answer №1

Your goal contradicts Apollo's "One Graph" approach, but there are workarounds you can explore. Consider using gateways and federation for a more cohesive solution - https://www.apollographql.com/docs/federation/

Although a hacky solution is possible, it may lead to complex maintenance and performance issues as it deviates from the intended mechanism.

// Define your endpoints
const endpoint1 = new HttpLink({
    uri: 'https://api.hashnode.com/graphql',
    ...
})
const endpoint2 = new HttpLink({
    uri: 'endpoint2/graphql',
    ...
})

// Configure apollo-client with the endpoints
const client = new ApolloClient({
    link: ApolloLink.split(
        operation => operation.getContext().clientName === 'endpoint2',
        endpoint2,
        endpoint1
    )
    ...
})

// Specify the client name in queries/mutations
useQuery(QUERY, {variables, context: {clientName: 'endpoint2'}})

You may find this package useful for handling multiple endpoints: https://github.com/habx/apollo-multi-endpoint-link

For further insights, refer to this discussion thread: https://github.com/apollographql/apollo-client/issues/84

Answer №2

Today, I faced the same issue and needed a dynamic solution. Here is what I came up with:

type DynamicLinkClient = "aApp" | "bApp" | "graphqlApp";
type LinkType = RestLink | HttpLink;
type DynamicLinkType = { link: LinkType; name: DynamicLinkClient };
const LINK_STORAGE: DynamicLinkType[] = [
  { link: aRestLink, name: "aApp" },
  { link: bAppRestLink, name: "bApp" },
  { link: graphqlAppLink, name: "graphqlApp" },
];

const determineClientFromContext = (client: string) => (operation: Operation) =>
  operation.getContext().client === client;

const DynamicApolloLinkHandler = LINK_STORAGE.reduce<ApolloLink | undefined>(
  (previousLink, nextDynamicLink) => {
    // If no name is provided, resort to defaultLink.
    if (!previousLink) {
      return ApolloLink.split(
        determineClientFromContext(nextDynamicLink.name),
        nextDynamicLink.link,
        defaultLink
      );
    }
    return ApolloLink.split(
      determineClientFromContext(nextDynamicLink.name),
      nextDynamicLink.link,
      previousLink
    );
  },
  undefined
) as ApolloLink;

Answer №3

I have identified this task as utilizing Apollo Links within the vanilla Apollo Client.

Specifically, it involves creating a directional composition link chain. https://i.sstatic.net/PQlx7.png

While my prototype may be verbose, the key points to focus on are:

  • Using ApolloLink.split during ApolloClient initialization to direct queries to different endpoints.
  • Implementing redirection in the Apollo Link using a boolean operation.
    • In the example provided, I am utilizing "context" as part of query options.

It's worth noting that excessive complexity is not necessary.

Below is my prototype (NextJS + TypeScript + ApolloClient):

libs/apollo/index.ts (where you define your apollo client)

import { ApolloClient, ApolloLink, HttpLink, InMemoryCache } from '@apollo/client'

// Define endpoints
const animeEndpoint = new HttpLink({
    uri: 'https://graphql.anilist.co',
})
const countriesEndpoint = new HttpLink({
    uri: 'https://countries.trevorblades.com/',
})

// Optional for type safety.
export enum Endpoint {
    anime = 'anime',
    country = 'country',
}

// Configure apollo-client with endpoints
const client = new ApolloClient({
    // Custom property "Version" determines which endpoint to use
    // Truthy = animeEndpoint (second) parameter, falsy = countriesEndpoint (third) parameter
    link: ApolloLink.split((operation) => operation.getContext().version === Endpoint.anime, animeEndpoint, countriesEndpoint),
    cache: new InMemoryCache(),
})

export default client

app/page.tsx (for NextJS implementation)

'use client'
import { useQuery } from '@apollo/client'
import GetAnimeQuery from '@/libs/gql/GetAnime' // GraphQL query
import { Endpoint } from '@/libs/apollo'
import GetCountriesQuery from '@/libs/gql/GetCountries' // GraphQL query

export default function Home() {
  // Fetch Anime data
    const { loading: loadingAnime, error: errorAnime, data: dataAnime } = useQuery(GetAnimeQuery, { context: { version: Endpoint.anime } })
  // Fetch Countries data
    const {
        loading: loadingCountries,
        error: errorCountries,
        data: dataCountries,
    } = useQuery(GetCountriesQuery, { context: { version: Endpoint.country } })
    console.log('Countries', dataCountries)
    console.log('Anime', dataAnime)
    return (
        <main>
            {loadingAnime && <p>Loading Anime...</p>}
            {errorAnime && <p>Error Anime :{errorAnime.message}</p>}
            {loadingCountries && <p>Loading Countries...</p>}
            {errorCountries && <p>Error Countries:{errorCountries.message}</p>}
        </main>
    )
}

Appendix (skip unless requiring graphql queries)

libs/gql/GetAnime.ts

import { gql } from '@apollo/client'

const GetAnimeQuery = gql`
    query Get {
        Page(page: 1, perPage: 5) {
            pageInfo {
                total
                currentPage
                lastPage
                hasNextPage
                perPage
            }
            media {
                id
                title {
                    romaji
                }
            }
        }
    }
`

export default GetAnimeQuery

libs/gql/GetCountries.ts

import { gql } from '@apollo/client'

const GetCountriesQuery = gql`
    query GetAllCountries {
        countries {
            code
            currency
            name
        }
    }
`

export default GetCountriesQuery

Answer №4

Really impressed with Pete's solution that allows for more than just 2 endpoints.

I decided to create my own version in order to enhance type checking.

This is my take on his implementation:

Typescript:

const defaultClient: keyof typeof clients = "heroku";

const clients = {
    "heroku": new HttpLink({ uri: "https://endpointURLForHeroku" }),
    "lists": new HttpLink({uri: "https://endpointURLForLists" })
}

const isRequestedClient = (clientName: string) => (op: Operation) =>
    op.getContext().clientName === clientName;

const ClientResolverLink = Object.entries(clients)
.map(([clientName, Link]) => ([clientName, ApolloLink.from([Link])] as const))
.reduce(([_, PreviousLink], [clientName, NextLink]) => {

    const ChainedLink = ApolloLink.split(
        isRequestedClient(clientName),
        NextLink,
        PreviousLink
    )

    return [clientName, ChainedLink];
}, ["_default", clients[defaultClient]])[1]

declare module "@apollo/client" {
interface DefaultContext {
clientName: keyof typeof clients
}
}

JS:

const defaultClient = "heroku";

const clients = {
"heroku": new HttpLink({ uri: "https://endpointURLForHeroku" }),
"lists": new HttpLink({uri: "https://endpointURLForLists" })
}

const isRequestedClient = (clientName) => (op) =>
op.getContext().clientName === clientName;

const ClientResolverLink = Object.entries(clients)
.reduce(([_, PreviousLink], [clientName, NextLink]) => {

const ChainedLink = ApolloLink.split(
isRequestedClient(clientName),
NextLink,
PreviousLink
)

return [clientName, ChainedLink];
},["_default", clients[defaultClient]])[1]

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The Jasmine tests seem to have a mind of their own,

Encountering a peculiar problem during the execution of my Jasmine tests in the Bamboo build. The tests are failing sporadically with the error message below: Failed: can't convert undefined to object on 18-Nov-2019 at 03:08:56 ./node_modules/@a ...

When attempting to import the OrbitControls.js file, Three.js encounters an error and fails

I am completely new to javascript and unfamiliar with working with libraries. I am currently experimenting with basic three.js code, but unfortunately facing issues that I cannot seem to resolve. Following the documentation on Threejs.org, I have set up a ...

What is the best way to retrieve an object value within an if statement?

What can I do to ensure that the line within the if statement functions properly? const player1 = { name: "Ashley", color: "purple", isTurn: true, play: function() { if (this.isTrue) { return `this["name"] is current ...

The visibility of buttons can be controlled based on the selected radio option

I have just completed setting up 4 buttons (add, edit, delete, cancel) and a table that displays data received via ajax. Each row in the table contains a radio button identified by the name "myRadio". When a radio button is clicked, I need the buttons to ...

onTouch event causing problems with gesture scrolling

Currently, I am utilizing the following Javascript code to implement ontouchstart/move/end callbacks on div elements in an iOS web application. The issue I am facing is that when attempting to scroll through the page, it triggers the ontouchstart event and ...

What is the best way to utilize the react hook useEffect just once in my scenario?

After looking into it, I realized that my question may seem like a duplicate at first glance, but upon further inspection, I found that it is not. I have come across many questions with the same title but different cases. The issue I am facing is that I h ...

Error: Unable to access unknown properties (reading 'extend')

Struggling to integrate the Vuetify library into my current Vue 3 project, encountering complications. An error message popped up post-compilation: vuetify.js?ce5b:42021 Uncaught TypeError: Cannot read properties of undefined (reading 'extend') ...

How does setting 0 as the initial element of an array prevent the execution of a "for" loop in JavaScript?

Take a look at the JavaScript code snippet below: var words = delIdx = [0, 1, 2, 3]; for(let i=0; delIdx[i]; i++) { console.log('DELIDX: ', delIdx[i]); } for(let i=0; words[i]; i++) { console.log('Word: ', words[i]); } The arrays ...

Resizing columns in HTML table remains effective even after the page is refreshed

I've created HTML pages with tables that have multiple columns, but I'm experiencing an issue. The columns are not resizable until I refresh the page. Could someone assist me in fixing this problem and explaining why it's happening? ...

What could be the reason behind my inability to initiate this PHP file through jQuery/AJAX?

I'm experiencing an issue with a piece of PHP code (located at ../wp-content/plugins/freework/fw-freework.php). <div id="new-concept-form"> <form method="post" action="../wp-admin/admin.php?page=FreeWorkSlug" class="form-inline" role ...

Establishing connections to numerous databases using ArangoDB

I am currently developing a product that involves the dynamic creation of a new database for each project, as new teams will be creating new projects based on their specific needs. The backend of the product is built using Node.js, Express.js, TypeScript, ...

Selenium - Tips for entering text in a dynamically generated text field using Javascript!

I'm fairly new to the world of web scraping and browser automation, so any guidance would be greatly appreciated! Using Python's Selenium package, my objective is: Navigate to Login using the provided username & password Complete my order thr ...

Maintaining responsiveness while initiating an HTTP request in a separate component

I have a specific app structure <header-bar></header-bar> <bag :bag="bag"></bag> <!--Main Section--> <section class="MainSection"> <div class="MainSection__wrap"> <router-view ...

What is the reasoning behind an empty input value being considered as true?

I am facing an issue with the following code that is supposed to execute oninput if the input matches the answer. However, when dealing with a multiplication problem that equals 0, deleting the answer from the previous calculation (leaving the input empt ...

Dealing with the hAxis number/string dilemma in Google Charts (Working with Jquery ajax JSON data)

My Objective I am attempting to display data from a MySQL database in a "ComboChart" using Google Charts. To achieve this, I followed a tutorial, made some modifications, and ended up with the code provided at the bottom of this post. Current Behavior T ...

Having trouble editing a form with React Hooks and Redux?

Seeking assistance with creating an edit form in React that utilizes data stored in Redux. The current form I have created is displaying the values correctly, but it appears to be read-only as no changes are being reflected when input is altered. Any advic ...

Challenges with variable scopes and passing variables in Ionic 2 (Typescript)

In my Ionic 2 TypeScript file, I am facing an issue with setting the value of a variable from another method. When I close the modal, I get undefined as the value. I'm encountering difficulty in setting the value for coord. export class RegisterMapP ...

Issue in Vuetify: The value of the first keypress event is consistently an empty string

I need to restrict the user from entering numbers greater than 100. The code snippet below represents a simplified version of my production code. However, I am facing an issue where the first keypress always shows an empty string result. For example, if ...

javascript/jquery form validation problems/support needed (jQuery)

Long story short, I am facing an issue with my code and seeking some guidance. I have various functions to perform different checks. For this particular example, I have a form with a default value of "enter name here" for one field. Here is the HTML snipp ...

The content from http://x.com/js/bootstrap.min.js.map could not be retrieved due to an HTTP error with a status code 404, showing a net::ERR_HTTP_RESPONSE_CODE_FAILURE

I'm working on an HTML CSS Website and encountering a consistent error. Some solutions suggest using Developer Tools in the browser to resolve it, but after trying multiple browsers, I suspect the issue lies within the code itself. Can anyone offer as ...