Using multiple endpoints in Apollo Client

Using multiple endpoints in Apollo Client


18

this is my first discussion post here. I have learned Apollo + GraphQL through Odyssey. Currently, I am building my own project using Next.js which required fetching data from 2 GraphQL endpoints.

My problem: How can I fetch data from multiple GraphQL endpoints with ApolloClient?

Below is my code for 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;

4

  • Can you provide your ApolloClient configuration code?

    – Jacek Walasik

    Oct 19, 2021 at 10:27

  • Hi, here is what I have written for my first client endpoint. I'm planning to add another endpoint but I can't really find any solution on the Internet.

    – Eugene

    Oct 19, 2021 at 10:48

  • 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; @JacekWalasik Above is my code. 🙂

    – Eugene

    Oct 19, 2021 at 10:49

  • @JacekWalasik Can you check my comment below? I'm facing issue my headers is not working anymore when I put my token into .env.local file & use the token in other file.

    – Eugene

    Oct 20, 2021 at 6:26


4 Answers
4


16

What you are trying to accomplish is kinda against Apollo’s "One Graph" approach.
Take a look at gateways and federation – https://www.apollographql.com/docs/federation/

With that being said, some hacky solution is possible but you will need to maintain a more complex structure and specify the endpoint in every query, which undermines the built-in mechanism and might cause optimization issues.

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

//pass them to apollo-client config
const client = new ApolloClient({
    link: ApolloLink.split(
        operation => operation.getContext().clientName === 'endpoint2',
        endpoint2, //if above 
        endpoint1
    )
    ...
})

//pass client name in query/mutation
useQuery(QUERY, {variables, context: {clientName: 'endpoint2'}})

This package seems to do what you want: https://github.com/habx/apollo-multi-endpoint-link

Also, check the discussion here: https://github.com/apollographql/apollo-client/issues/84

6

  • May I know how to add headers? //Declare your endpoints const endpoint1 = new HttpLink({ uri: 'https://api.hashnode.com/graphql', ... }) const endpoint2 = new HttpLink({ uri: 'endpoint2/graphql', ... }) //pass them to apollo-client config const client = new ApolloClient({ link: ApolloLink.split( operation => operation.getContext().clientName === 'endpoint2', endpoint2, //if above endpoint1 ) ... }) //pass client name in query/mutation useQuery(QUERY, {variables, context: {clientName: 'endpoint2'}})

    – Eugene

    Oct 19, 2021 at 14:31


  • I assume in the same way you did before: const endpoint1 = new HttpLink({ uri: '...', credentials: credentials, headers: headers}). Doesnt work like this?

    – Jacek Walasik

    Oct 19, 2021 at 14:49


  • thank you. I solved my problems.

    – Eugene

    Oct 19, 2021 at 15:30

  • My headers is not working anymore if I put it in this way: const endpoint2 = new HttpLink({ uri: "https://api.github.com/graphql", headers: { Authorization: Bearer ${process.env.GITHUB_ACCESS_TOKEN}, }, });

    – Eugene

    Oct 20, 2021 at 6:24


  • 1

    That's unexpected, maybe incorrect use of Template literals – try headers: { authorization: `Bearer ${process.env.GITHUB_ACCESS_TOKEN}`}

    – Jacek Walasik

    Oct 20, 2021 at 8:23


2

Encountered the same problem today. I wanted to have it dynamic so this is what I came out with:

export type DynamicLinkClientName = "aApp" | "bApp" | "graphqlApp";
type Link = RestLink | HttpLink;
type DynamicLink = { link: Link; name: DynamicLinkClientName };
const LINK_MAP: DynamicLink[] = [
  { link: aRestLink, name: "aApp" },
  { link: bAppRestLink, name: "bApp" },
  { link: graphqlAppLink, name: "graphqlApp" },
];

const isClientFromContext = (client: string) => (op: Operation) =>
  op.getContext().client === client;

const DynamicApolloLink = LINK_MAP.reduce<ApolloLink | undefined>(
  (prevLink, nextLink) => {
    // When no name is specified, fallback to defaultLink.
    if (!prevLink) {
      return ApolloLink.split(
        isClientFromContext(nextLink.name),
        nextLink.link,
        defaultLink
      );
    }
    return ApolloLink.split(
      isClientFromContext(nextLink.name),
      nextLink.link,
      prevLink
    );
  },
  undefined
) as ApolloLink;


1

Really like the solution by Pete for allowing more than just 2 endpoints.

decided to write my own version for better type checking.

Here is my take on his solution:

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]


0

I see this as an Apollo Links task which is part of the vanilla Apollo Client.

Specifically a directional composition link chain

Using multiple endpoints in Apollo Client

My prototype may be a bit too verbose, so all you need to worry about is:

  • use ApolloLink.split in the initialisation of your ApolloClient to direct queries to a different endpoint.
  • redirect in the Apollo Link using some boolean operation.
    • In the example I am using "context" (something you put in your query options).

Note that you don’t need to

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

libs/apollo/index.ts (wherever you want to define your apollo client)

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

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

// Not necessary. Just helps with type safety.
export enum Endpoint {
    anime = 'anime',
    country = 'country',
}

//pass them to apollo-client config
const client = new ApolloClient({
    // Version here is just a custom property that we can use to determine 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 (because I am using NextJS for this)

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

export default function Home() {
  // Anime data
    const { loading: loadingAnime, error: errorAnime, data: dataAnime } = useQuery(GetAnimeQuery, { context: { version: Endpoint.anime } })
  // 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 me unless you want the graphql query)

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



Leave a Reply

Your email address will not be published. Required fields are marked *