What is the Apollo Client?

Apollo is an open source GraphQL client built for Javascript and includes comprehensive integrations with Angular, React and Vue. The Angular Apollo Client integrates with RxJs and Observables out of the box and many of the patterns used by the package should be familiar to you if you have used the Angular Http Client. This design enables easy integration with state management solutions, such as NgRx, and makes the process of transitioning from a REST API to a GraphQL API as easy as possible.

How to install Apollo

Disclaimer: This article assumes that you have a working Angular app and node (12.13+) installed.

The simplest way to begin working with Apollo is to run the Apollo schematic. This will configure Apollo within your application and allow you to start writing GraphQL requests right away: ng add apollo-angular

If you are working within an NX repository or have a non-standard Angular repository structure you must setup Apollo manually. You can accomplish this in three steps:

  1. Install dependencies: npm install apollo-angular @apollo/client graphql
  2. Add esnext.asynciterable to your app tsconfig.json file:
 
{
  "compilerOptions": {
    // ...
    "lib": [
      "es2017",
      "dom",
      "esnext.asynciterable"
    ]
  }
}

Create a graphql.module.ts file and import it into your app.module.ts:

graphql.module.ts
------------------------

import { HttpClientModule} from '@angular/common/http';
import { APOLLO_OPTIONS } from 'apollo-angular';
import { HttpLink } from 'apollo-angular/http';
import { InMemoryCache } from '@apollo/client/core';

@NgModule({
   imports: [BrowserModule, HttpClientModule],
   providers: [
      {
         provide: APOLLO_OPTIONS,
         useFactory: (httpLink: HttpLink) => {
            return {
               cache: new InMemoryCache(),
               link: httpLink.create({
                  uri: '/* GRAPHQL_API_URL */',
               }),
            };
         },
         deps: [HttpLink],
      },
   ],
})
export class GraphQLModule {}

How to write GraphQL Query / Mutation definitions

The first step in sending a GraphQL request is defining the request that will be sent to the backend. We do this by creating a GraphQL query document. This query document is the body of the request that is sent to your GraphQL endpoint; it is the contract between you and the backend. It instructs your endpoint in what data should be returned and where it should look for said data.

Apollo supplies a tool, the gql template literal tag, to allow you to define GraphQL query documents within your typescript code in the form of query strings. This tag and its contents are parsed during the compilation step and converted into a GraphQL query document (GraphQL AST object): a format that your GraphQL endpoint can understand.

There are two types of requests that you can send to your GraphQL endpoint:

Queries: Request data from the backend. They do not mutate any data on the backend. You would typically use queries to fetch initial data to be displayed on a page.

Mutations: Mutate data on the backend such as Create, Update and Delete. You would typically use mutations when communicating user intent to the backend.

Query

Lets start with a simple query:

Note: This unusual syntax of putting backticks alongside a method is called a tagged template literal. If you’d like to learn more about template literals you can view the MDN documentation on the topic.

import { gql } from 'apollo-angular';

const itemsQuery = gql`
  query getItemsQuery {
    items {
      id
      name
      description
    }
  }
`;

This query document has three parts:

1. Query document name

  • getItemsQuery this is the name of your query document. This name functions similarly to a variable name; it does not affect how your request is parsed by the GraphQL backend. It is used as an identifier within the query document.

2. Query document content

  • items this is the name of the query type you want to return from the backend. It must match a query type definition in your backend schema.

3. Selected properties

  • You must list the properties that you would like the backend to return within the query definition body. The listed properties can either be a subset of properties or all available properties on the query type.
  • This query is requesting the id / name / description properties on the items query type definition.

Mutation

 
import { gql } from 'apollo-angular';

const addItemMutation = gql`
  mutation addItemMutation {
    addItem(item: {}) {
      id
      name
      description
    }
  }
`;

As you can see, mutation documents are very similar to query documents. They share all the same building blocks: the query document name, query document content and the selected properties.

There is one difference though; we are passing an object into the mutation via the item variable on the addItem query type. Variables allow us to pass content into GraphQL requests enabling update, create and delete functionality. We’ll cover variables in the next section.

Variables

Variables are used to send data in a GraphQL request; most mutations will make use of variables to enable mutative functionality:

import { gql } from 'apollo-angular';

const addItemMutation = gql`
  mutation addItemMutation($newItem: !Item) {
    addItem(item: $newItem}) {
      id
      name
      description
    }
  }
`;

In this example we are defining a $newItem variable on the addItemMutation definition and passing that $newItem variable into the addItem query type.

All variables defined on the addItemMutation document can be assigned values when calling this mutation in a service. Just creating a variable on the mutation definition does not automatically send that data to the GraphQL endpoint, though. A variable must be passed into the mutation body to be included in your request.

  • addItemMutation($newItem: !Item): This is where you define what variables can be passed into the request when called by the Apollo service.
  • addItem(item: $newItem): This is how to pass variable data into the mutation request. Any parameters seen here must be defined by the backend schema.

How to send a query / mutation to your GraphQL endpoint

Once you have written a query or mutation document it must be sent to your GraphQL endpoint. Apollo provides base methods for sending each type of GraphQL request and a couple special Apollo specific methods.

The first step in sending a GraphQL request is injecting the Apollo Service into your class:

import { Apollo } from 'apollo-angular';

constructor(private apollo: Apollo) {}

Query

To send GraphQL queries you must use the query method on the Apollo service. The query method accepts a configuration object, as a parameter, on which you must define a query property and assign it to your GraphQL query string. This method returns a clean single-use observable that emits once the query request has completed:

const itemsQuery = gql`
  query getItemsQuery {
    items {
      id
      name
      description
    }
  }
`;

getItems() {
   return this.apollo
      .query({
        query: itemsQuery,
      })
      .pipe(map(response => response.data.items));
}

Mutations

To send GraphQL mutations you must use the mutate method on the Apollo service. This method returns a clean single-use observable that emits once the mutation request has completed:

const addItemMutation = gql`
  mutation addItemMutation($newItem: Item!) {
    addItem(item: $item) {
      id
      name
      description
    }
  }
`;

addItem(item: Item) {
   return this.apollo
      .mutate({
         mutation: addItemMutation,
         variables: {
            newItem: item
         }
      })
      .pipe(map(response => response.data.addItem));
}

If you have defined variables within your mutation query string you must pass those variables into the mutate method via the variables property.

As you can see, we defined a $newItem variable but are passing in a variables object that contains the property newItem. This is not a mistake. When defining variables in a query string they must always be prefixed with a %content%lt;/span>. But, once you call a mutation, you must pass in your variables through the variables object without the %content%lt;/span> prefix.

Special: watchQuery

watchQuery is a method on the Apollo service that contains a couple of special features. Instead of returning a single use observable stream, that simply returns data, it returns a QueryRef instance. This QueryRef instance contains many useful methods such as refetch, fetchMore and getLastResult. These methods allow you to manipulate the QueryRef enabling features such as refetching data, polling data at set intervals, modifying the passed in query and synchronously returning cached data.

To access the fetched data you must subscribe to the valueChanges observable; this is a clean RxJs observable that emits the query result. It also emits whenever the refetch method is called or when data is polled:

const itemsQuery = gql`
  query getItemsQuery {
    items {
      id
      name
      description
    }
  }
`;

itemsQueryRef: QueryRef<any> = this.apollo.watchQuery({ query: itemsQuery });

getItems$() {
   return this.itemsQueryRef.valueChanges
            .pipe(map(response => response.data.items));
}

refetch() {
   this.itemsQueryRef.refetch();
}

Error Handlings

There are two ways that you can handle GraphQL errors while using Apollo. I will run through both options and give guidance on the benefits and potential use cases of each.

You can handle errors natively within Apollo by creating an Apollo Link Interceptor. Apollo provides a onError link that, when injected into the Apollo config, executes the passed in callback function every time an error occurs with an Apollo GraphQL request:

import { onError } from "@apollo/client/link/error";

const errorLink = onError(({ graphQLErrors, networkError }) => {
   if (graphQLErrors)
      graphQLErrors.map(({ message, locations, path }) =>
        console.log(
         `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
        )
      );

   if (networkError) console.log(`[Network error]: ${networkError}`);
});

Once you have defined your Apollo onError link, you must add it into your Apollo Module link configuration alongside your Http link. This can be accomplished by using the from function exported by the @apollo/client/core package. This function allows you to define multiple links that are executed on each GraphQL request:

import { NgModule } from '@angular/core';
import { APOLLO_OPTIONS } from 'apollo-angular';
import { HttpLink } from 'apollo-angular/http';
import { from } from '@apollo/client/core';

const errorLink = onError(({ graphQLErrors, networkError }) => {
   if (graphQLErrors)
      graphQLErrors.map(({ message, locations, path }) =>
        console.log(
          `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
        ),
      );

   if (networkError) console.log(`[Network error]: ${networkError}`);
});

@NgModule({
   providers: [
      {
         provide: APOLLO_OPTIONS,
         useFactory: (httpLink: HttpLink) => {
            return {
               // ..
               link: from([
                  httpLink.create({
                     uri: 'GRAPHQL_API_ENDPOINT',
                  }),
                  errorLink
               ]),
            };
         }
   },
   ],
})
export class GraphQLModule {}

In the above example we are injecting both the Error and Http links into the link property in the Apollo configuration using the from function.

RxJs Catch Error

You can handle errors on a per-request basis by using the RxJs catchError operator. This operator allows you to catch errors within an individual request stream, allowing fine-grained error handling:

const handleError = ({ networkError, graphQLErrors, message }: ApolloError) => {
   if (graphQLErrors) {
      graphQLErrors.forEach(e => console.log('Apollo GraphQL Error', e));
   }
   if (networkError) {
      const { error: serverErrors, ...apolloNetworkError } = networkError as any;
      console.log('Apollo Network Error', apolloNetworkError);
      serverErrors.error?.errors.forEach(e => console.log('Apollo Network Error', e));
   }
   return throwError(message);
}

getItems() {
   return this.apollo
      .query({ query: itemsQuery })
      .pipe(
         map(response => response.data.items),
         catchError(handleError)
      );
}

We are defining a handleError method and passing it into the catchError operator on the apollo.query stream. This handleError method is an example of how we would console log all errors returned from the backend.

Tips / Tricks (Response Interface, Disable Caching)

Response Interface

When a successful Apollo GraphQL request occurs a response object is emitted. This object contains a data property that, in turn, contains an object with a property matching the query type that was requested from the backend. This data object is typed to any, by default, which results in a sub-par typescript experience and can cause linting errors when trying to select the query type property from the data object.

I recommend strictly typing all Apollo requests using this handy Typescript type.

It works by allowing you to pass in the query type key that will be returned, and the type of the data. Resulting in type safe Apollo requests:

export type GraphQLResponse<responseKey extends string, responseType> = {
   [key in responseKey]: responseType
}

const loadUserQuery = gql`
  query loadUserQuery($id: ID) {
    loadUser(id: $id) {
      ...userFragment
    }
  }
  ${userFragment}
`;

loadUser(id: string): Observable<User> {
   return this.apollo
        .query<GraphQLResponse<'loadUser', User>>({
           query: loadUserQuery,
           variables: { id },
        })
        .pipe(map((response) => response.data.loadUser));
}

Disable Caching

Apollo comes bundled with a powerful caching solution. This is useful in certain contexts and, if utilized correctly, can act as a state management layer. It can also be used in tandem with more established state management solutions, such as NgRx, to reduce the amount of API calls to your backend.

This sounds good in theory and might work for your team and application. But, for most non-trivial applications, trying to manage multiple state management systems can quickly become unwieldy. A single established state management solution is recommended to enable scaling and growth.

If you would like to disable Apollo’s caching solution you can add the following configuration options to your graphql.module file:

{
   provide: APOLLO_OPTIONS,
           useFactory: (httpLink: HttpLink) => {
      return {
         // ...
         defaultOptions: {
            watchQuery: {
               fetchPolicy: 'no-cache',
            },
            query: {
               fetchPolicy: 'no-cache',
            },
            mutate: {
               fetchPolicy: 'no-cache',
            },
         },
      };
   }
}

Unit Testing

Testing is a critical step in building and maintaining production enterprise level software. Apollo was built with testing in mind and provides many tools that empower you to write clean, readable and reliable tests for your GraphQL services.

In this section we will provide a unit testing example for the following GraphQL service call:

export const loadUserQuery = gql`
  query loadUserQuery($id: ID) {
    loadUser(id: $id) {
      ...userFragment
    }
  }
  ${userFragment}
`;

------------------------------------------------

constructor(private apollo: Apollo) {}

loadUser(id: string): Observable<User> {
   return this.apollo
        .query({
           query: loadUserQuery,
           variables: { id },
        })
        .pipe(map((response) => response.data.loadUser));
}

The first step is to import the ApolloTestingModule into your testing module. This module will provide all of the Apollo testing tools to your unit tests and is modeled after the built in Angular HttpTestingModule.

The next step is to create a reference to the ApolloTestingController. This controller will allow you to manipulate the Apollo service that is used to send GraphQL requests to your backend:

let service: UsersService;
let controller: ApolloTestingController

beforeEach(() => {
  TestBed.configureTestingModule({
    imports: [
      ApolloTestingModule,
    ],
    providers: [
      UsersService
    ]
  });
  service = TestBed.inject(UsersService);
  controller = TestBed.inject(ApolloTestingController);
});

Next we can write the test for our loadUser request. First we start by defining the mock server response that we expect the service to return:

it('should return user by its id', (done) => {
    const mockUsersServerResponse: any = { id: '1' };
    // ...
});

Then we subscribe to the service method that we are testing and call the unit test callback function within the subscribe block to run the test asynchronously:

it('should return user by its id', (done) => {
    // ...
    service.findById('1').subscribe(serverResponse => {
      expect(serverResponse).toEqual(mockUsersServerResponse);
      done();
    });
    // ...
});

Next we use the ApolloTestingController to mock the GraphQL server response.

First we expect that the service was called with the loadUserQuery. Then we expect that the operation name of that query is loadUserQuery. And finally, we provide a mock response to that query, which will emit from our service subscription and complete the test:

it('should return user by its id', (done) => {
    // ...
    const req = controller.expectOne(loadUserQuery);
    expect(req.operation.operationName).toBe('loadUserQuery');
    req.flush({
      data: {
        users: [
          mockUsersServerResponse
        ]
      }
    });
    // ...
});

The final step is verifying that there are no pending GraphQL requests:

it('should return user by its id', (done) => {
    // ...
    controller.verify();
});

All in all we should be left with the following unit test:

import { loadUserQuery } from './users.service';

describe('loadUser', () => {
  it('should return user by its id', (done) => {
    const mockUsersServerResponse: any = { id: '1' };

    service.findById('1').subscribe(serverResponse => {
      expect(serverResponse).toEqual(mockUsersServerResponse);
      done();
    });

    const req = controller.expectOne(loadUserQuery);
    expect(req.operation.operationName).toBe('loadUserQuery');
    req.flush({
      data: {
        users: [
          mockUsersServerResponse
        ]
      }
    });
    controller.verify();
  });
});

Conclusion

With this guide, you should be able to do the following with confidence:

  • Install and configure the Apollo client within your Angular project
  • Define a GraphQL query and mutation
  • Pass data into a GraphQL mutation
  • Handle errors returned from your GraphQL backend
  • Strictly type data returned from Apollo requests
  • Disable Apollo caching if desired
  • Unit test your Apollo services

The Apollo Client is a powerful tool that enables developers to seamlessly integrate their Angular apps with a GraphQL API.