In a recent React project I had to implement another, external GraphQL API in addition to the existing default main GraphQL API. Everything related to the main API (schema, etc.) was already generated by using GraphQL Code Generator. This makes your life very easy if you always get the latest API changes automatically and also get generated functions (React hooks) for your queries and other operations.

During my research I stumbled across different approaches on how to handle this situation, since GraphQL is usually all about a single endpoint. In the end I came up with a different solution, mainly because none of them really dealt with GraphQL Code Generator.

I have created a very basic and rough example project that can be found on GitHub. The README already provides a basic overview but in this post I will go into a little more into detail and provide a step-by-step guide.

GraphQL Operations

In this example I’ll focus on simple queries, but whenever I write about queries the same basically applies to mutations.

Prerequisites

  1. Create a new React project with Typescript, e.g. via create-react-app
npx create-react-app my-app --template typescript
  1. Install the following packages
npm i @apollo/client graphql
npm i -D @graphql-codegen/cli @graphql-codegen/introspection @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
  1. Make sure that you have the necessary GraphQL language features ready for your code editor (e.g. the necessary extensions for VSCode).

Starting point with a single Apollo Client

Setup Apollo Client

So, let’s start with the bare minimum. First, we’ll create a new Apollo Client to retrieve some data from a GraphQL API. In this case we will use the public SpaceX API.

Create a graphql folder in the src directory and create an ApolloClients.ts file in it. Add the following to this file:

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

export const spacexURL = "https://spacex-production.up.railway.app/";

export const spacexApolloClient = new ApolloClient({
  uri: spacexURL,
  cache: new InMemoryCache(),
});

We create the API URL as a separate variable and export it, because we will need it elsewhere later. In a real project, we would rather locate this URL in an environment file, for example to distinguish between staging and production.

To be able to use the client in our project, we need to add the ApolloProvider to the index.tsx:

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { spacexApolloClient } from "./graphql/ApolloClients";
import { ApolloProvider } from "@apollo/client";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <ApolloProvider client={spacexApolloClient}>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </ApolloProvider>
);

At this point we are already able to retrieve data from the API. To do this, we create a query in App.tsx:

import React from "react";
import logo from "./logo.svg";
import "./App.css";
import { gql, useQuery } from "@apollo/client";

function App() {
  const { data } = useQuery(gql`
    query GetAllSpacexDragons {
      dragons {
        name
        active
        description
      }
    }
  `);

  console.log(data);

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

This would be the most basic setup when working with a GraphQL API and Apollo Client. One of the main criticisms would be that the queried data is not typed. Of course we could create a type manually, but where would be the fun and convenience in that? This is where GraphQL Code Generator comes in!

Setup GraphQL Code Generator

We have already installed the necessary packages at the very beginning. So we can start directly with creating a configuration file. To do this, create a codegen folder in the graphql directory and create a file called spacexConfig.ts. In this file we add the following lines:

import { CodegenConfig } from "@graphql-codegen/cli";
import { spacexURL } from "../ApolloClients";

const spacexConfig: CodegenConfig = {
  schema: spacexURL,
  documents: ["src/**/*.graphql"],
  overwrite: true,
  generates: {
    "src/graphql/generated/spacex.ts": {
      plugins: [
        "typescript",
        "typescript-operations",
        "typescript-react-apollo",
      ],
    },
    "src/graphql/generated/spacex.schema.json": {
      plugins: ["introspection"],
    },
  },
};

export default spacexConfig;

To run the code generation, we need to add a script to our package.json:

"update-graphql": "graphql-codegen --config src/graphql/codegen/spacexConfig.ts"

If you now run the script via npm run update-graphql you will get some errors:

✔ Parse Configuration
⚠ Generate outputs
  ❯ Generate to src/graphql/generated/spacex.ts
    ✔ Load GraphQL schemas
    ✖ Unable to find any GraphQL type definitions for the following…
      - src/**/*.graphql
    ◼ Generate
  ❯ Generate to src/graphql/generated/spacex.schema.json
    ✔ Load GraphQL schemas
    ✖ Unable to find any GraphQL type definitions for the following…
      - src/**/*.graphql
    ◼ Generate

This happens because we do not currently have any existing .graphql files. We can either comment out the documents: ["src/**/*.graphql"] line in the spacexConfig.tsx file, or we can create a new .graphql file with a query.

For the initial setup, however, the nicest approach is to comment out the line and run the script. This will create a generated folder containing the two files spacex.schema.json and spacex.ts.

As a last step we will use the generated schema and create a .graphqlrc.json file with the following content:

{
  "projects": {
    "spacex": {
      "schema": "src/graphql/generated/spacex.schema.json",
      "documents": "src/**/*.graphql"
    }
  }
}

This is our config file for the GraphQL language features that give us auto-completion and suggestions when writing new queries. You may need to restart your IDE for these changes to take effect.

Create your first query

As a first step we add back the previously commented out line regarding the documents property in spacexConfig.ts.

We also extend the scripts in package.json to ensure that we do not have to call update-graphql every time we create a new query or make changes.

"start": "npm run serve & npm run update-graphql -- --watch",
"serve": "react-scripts start",

This way the code generation will run every time the application is started, then goes into watch mode and runs again when changes are made.

Now we can create the GetAllDragons.graphql file in the src directory and insert this query:

query GetAllDragons {
  dragons {
    name
    active
    description
  }
}

If you manually type in the query field names, such as description, you should now get auto-completion.

Once the file is saved, the code generation will run again.

Open the App.tsx file and remove the previously created query and all related imports. Now we should be able to import the following things:

  • GetAllDragonsDocument: The raw query
  • useGetAllDragonsQuery: A hook that wraps the Apollo query function and all necessary types
  • useGetAllDragonsLazyQuery: A hook that wraps the Apollo lazy query function and all necessary types

The same would happen if we had created a mutation instead of a query (except for the lazy part): <MutationName>Document and use<MutationName>Mutation.

We can now replace the previous query with this one:

const { data } = useGetAllDragonsQuery();
console.log(data);

Ta-da! Now we automatically generate hooks with typed data for our queries and also get any changes made to the API. Sure, the initial setup is a bit of a hassle, but in my opinion it’s totally worth it for the convenience it gives you.

Add a second Apollo Client

After creating the SpaceX API as our default main API, we will add the public Star Wars API next.

Setup the second Apollo Client

As before, we add a new client for the second API to ApolloClient.ts. Additionally we create an enum for the API names, which will be useful in later steps.

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

export enum API {
  SpaceX = "SpaceX",
  StarWars = "StarWars",
}

export const spacexURL = "https://spacex-production.up.railway.app/";
export const starWarsURL =
  "https://swapi-graphql.netlify.app/.netlify/functions/index";

export const spacexApolloClient = new ApolloClient({
  uri: spacexURL,
  cache: new InMemoryCache(),
});

export const starWarsApolloClient = new ApolloClient({
  uri: starWarsURL,
  cache: new InMemoryCache(),
});

Add an Apollo Multi Client Provider

Before we can start generating code for the new API, we need to create a replacement for the ApolloProvider in index.tsx. The current provider can only handle a single client, which is what we are passing through the props. Therefore, we need to create our own provider that can handle multiple clients. However, this provider is only half the solution and we also need to extend the generated hooks to handle multiple clients.

First we create the ApolloMultiClientProvider.tsx in the src directory:

import { ApolloClient } from "@apollo/client";
import React from "react";
import {
  API,
  spacexApolloClient,
  starWarsApolloClient,
} from "./graphql/ApolloClients";

const apolloMultiClientContext = React.createContext<{
  getClient(clientName: string | undefined): ApolloClient<any> | undefined;
}>({
  getClient() {
    return undefined;
  },
});

export const useApolloMultiClient = () => {
  return React.useContext(apolloMultiClientContext);
};

export const ApolloMultiClientProvider: React.FC<{
  children: React.ReactNode;
}> = ({ children }) => {
  const getClient = (clientName: string) => {
    return (
      {
        [API.StarWars]: starWarsApolloClient,
      }[clientName] ?? spacexApolloClient
    );
  };

  return (
    <apolloMultiClientContext.Provider
      value={{ getClient }}
      children={children}
    />
  );
};

Then we can replace the old Apollo Provider with our new Apollo Multi Client Provider in index.tsx:

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { ApolloMultiClientProvider } from "./ApolloMultiClientProvider";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <ApolloMultiClientProvider>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </ApolloMultiClientProvider>
);

To ensure that the correct client is used in the corresponding generated hooks, we will create our own hooks or rather extend the existing hooks with the client from the provider’s context. Therefore we create a CustomApolloHooks.ts file in the codegen folder and fill it with this:

import * as Apollo from "@apollo/client";
import { TaggedTemplateExpression } from "typescript";
import { useApolloMultiClient } from "../ApolloMultiClientProvider";

// Re-export everything else that has not been changed
export * from "@apollo/client";

// For this example project we will limit the hooks to normal queries.
// Here you would also add custom hooks for lazy queries, mutations, etc.
export function useQuery<
  TData = any,
  TVariables extends Apollo.OperationVariables = Apollo.OperationVariables
>(
  query:
    | Apollo.DocumentNode
    | Apollo.TypedDocumentNode<TaggedTemplateExpression, TVariables>,
  options?: Apollo.QueryHookOptions<TData, TVariables>
): Apollo.QueryResult<TData, TVariables> {
  const ctx = useApolloMultiClient();
  const client = ctx.getClient(options?.context?.clientName);
  const newOptions: Apollo.QueryHookOptions<TData, TVariables> = {
    ...options,
    client,
  };

  return Apollo.useQuery<TData, TVariables>(query, newOptions);
}

As already mentioned in the code comments, in this example we limit ourselves to the regular queries. In a final implementation, we would also create useMutation, useLazyQuery, etc. there and extend them with the client from the context.

In the next step you will see how we use these custom hooks in our code generation.

Add and update GraphQL Code Generator Configurations

We have already laid the groundwork for both the provider and the hooks to handle multiple clients in principle. All that is missing now is the actual use of the custom hooks for the queries and additionally the differentiation between the APIs during GraphQL code generation.

This is done by using a suffix in the GraphQL filename. The main API (SpaceX) considers all files with the schema <filename>.graphql. For the Star Wars API, however, we will be working with the <filename>.sw.graphql schema.

Therefore, we need to add an exception to the documents property in spacexConfig.ts:

documents: ["src/**/*.graphql", "!src/**/*.sw.graphql"],

Remember our own Apollo Hooks? Now is the time to add them to the SpaceX Config:

generates: {
  "src/graphql/generated/spacex.ts": {
    plugins: [
      "typescript",
      "typescript-operations",
      "typescript-react-apollo",
    ],
    config: {
      apolloReactHooksImportFrom: "../CustomApolloHooks",
    },
  },
  "src/graphql/generated/spacex.schema.json": {
    plugins: ["introspection"],
  },
},

For the Star Wars API we also need a configuration file. So we create the file starWarsConfig.ts in the codegen folder:

import { CodegenConfig } from "@graphql-codegen/cli";
import { API, starWarsURL } from "../ApolloClients";

const starWarsConfig: CodegenConfig = {
  schema: starWarsURL,
  documents: "src/**/*.sw.graphql",
  overwrite: true,
  generates: {
    "src/graphql/generated/starwars.ts": {
      plugins: [
        "typescript",
        "typescript-operations",
        "typescript-react-apollo",
      ],
      config: {
        apolloReactHooksImportFrom: "../CustomApolloHooks",
        defaultBaseOptions: {
          context: {
            clientName: API.StarWars,
          },
        },
      },
    },
    "src/graphql/generated/starwars.schema.json": {
      plugins: ["introspection"],
    },
  },
};

export default starWarsConfig;

We also need to extend and tweak our scripts in package.json:

"update-graphql": "npm run update-graphql:spacex & npm run update-graphql:starwars",
"update-graphql:spacex": "graphql-codegen --config src/graphql/codegen/spacexConfig.ts",
"update-graphql:starwars": "graphql-codegen --config src/graphql/codegen/starWarsConfig.ts"

We are almost there! Now we need to comment out the line concerning the documents property in the Star Wars config and run the code generation for both APIs with npm run update-graphql. This will update the existing query hook useGetAllDragons to use the correct client and create the files starwars.schema.json and starwars.ts in the generated directory.

Now we can update our .graphqlrc.json file accordingly:

{
  "projects": {
    "starwars": {
      "schema": "src/graphql/generated/starwars.schema.json",
      "documents": ["src/**/*.sw.graphql", "src/generated/starwars.ts"],
      "exclude": ["src/generated/spacex.ts"]
    },
    "spacex": {
      "schema": "src/graphql/generated/spacex.schema.json",
      "documents": "src/**/*.graphql",
      "exclude": ["src/**/*.sw.graphql", "src/generated/starwars.ts"]
    }
  }
}

As before, you will probably need to restart your IDE for the changes to take effect.

Finally, add the line for the documents property back into the starWarsConfig.ts file. Now we are ready to create a query for our Star Wars API!

Create your first query for the second API

So here we go, we want to query some data from the Star Wars universe! Therefore we create the file GetAllPlanets.sw.graphql in the src directory:

query GetAllPlanets {
  allPlanets {
    planets {
      name
      gravity
      population
    }
  }
}

When you try to enter field names manually here, you should get auto-completion and no overlap between the two APIs!

If the start script is still running, the code should be generated automatically when the file is saved. Otherwise, restart the app with npm start.

After that we can use the newly created query in the App.tsx:

const { data: planets } = useGetAllPlanetsQuery();
console.log(planets);

Yeah! Now we can query data from two completely different APIs and get automated code generation for changes to the GraphQL API and hooks for our GraphQL operations at the same time.

Add a third Apollo Client (maybe?)

In my example project on GitHub, I also created a third Apollo Client to demonstrate that this is not just a solution for two different APIs, but works with any number of clients.

I won’t go into the details of integrating a third API at this point, as this post is already way too long already as it’s more or less exactly the same process as for the second client.

Therefore a short summary:

  1. Add a third client to ApolloClients.ts
  2. Add the client to ApolloMultiClientProvider.tsx
  3. Create a new GraphQL Code Generator configuration in codegen
    • Don’t forget to add the custom hooks and the corresponding client name in the configuration
    • Also don’t forget to extend the file exceptions for the new suffix in the already existing configs. It should follow the scheme of <filename>.<suffix>.graphql.
  4. Comment out the part about the documents property and run the code generation
  5. Extend the .graphqlrc.json file according to the newly generated files and don’t forget to add exceptions to the existing entries
  6. Add the documents part back to the config again
  7. Create a new query
  8. The code generation should run automatically and you can now use the corresponding query hook as before

Conclusion

That was quite an effort, wasn’t it? Well, that’s definitely true, but isn’t that the case with all initial setups? The initial effort is always a bit more, but we benefit from it in the long run!

We can now easily include as many Apollo Clients as we want and thus address as many GraphQL APIs as our hearts desire. In addition, we have the luxury of automated code generation, which always provides us with any changes to the GraphQL API and creates typed hooks for newly created or changed GraphQL operations.

Happy querying!