Angular and GraphQL: Integrating Apollo Client

In this tutorial, we will explore how to integrate Apollo Client with Angular for seamless communication with a GraphQL server. Angular is a popular JavaScript framework for building web applications, while GraphQL is a query language for APIs. By combining the two, we can efficiently retrieve and manipulate data in our Angular applications. Apollo Client is a powerful GraphQL client that simplifies the process of integrating GraphQL with various frontend frameworks, including Angular.

angular graphql integrating apollo client

Introduction

What is Angular?

Angular is a TypeScript-based open-source framework for building web applications. It provides a structured way to develop frontend applications by following the Model-View-Controller (MVC) architecture. With its extensive tooling and rich ecosystem, Angular is widely used by developers for creating robust and scalable web applications.

What is GraphQL?

GraphQL is a query language for APIs and a runtime for executing those queries with your existing data. It provides a flexible and efficient approach to retrieve and manipulate data from multiple sources through a single endpoint. Unlike traditional REST APIs, GraphQL allows clients to specify exactly what data they need, reducing over-fetching and under-fetching of data.

Why integrate Apollo Client?

Apollo Client is a fully-featured GraphQL client that simplifies working with GraphQL APIs. It provides tools for managing GraphQL queries, caching data, handling errors, and more. By integrating Apollo Client with Angular, we can take advantage of its features to seamlessly communicate with our GraphQL server and efficiently manage our application's data.

Setting Up Apollo Client

Installing Apollo Client

To get started with Apollo Client in Angular, we need to install the necessary packages. Open your terminal and navigate to your Angular project directory. Run the following command to install the required packages:

npm install @apollo/client graphql

The @apollo/client package contains the core Apollo Client functionality, while graphql provides the necessary utilities for working with GraphQL queries.

Configuring Apollo Client

Once we have installed the required packages, we need to configure Apollo Client in our Angular application. Create a new file called apollo.config.ts in the src/app directory and add the following code:

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

export const apolloClient = new ApolloClient({
  uri: '<YOUR_GRAPHQL_ENDPOINT>',
  cache: new InMemoryCache(),
});

In the above code, we import the necessary modules from @apollo/client. We then create a new instance of ApolloClient and pass in the GraphQL endpoint URL and an instance of InMemoryCache. Replace <YOUR_GRAPHQL_ENDPOINT> with the actual URL of your GraphQL server.

Creating GraphQL Queries

To perform GraphQL queries in our Angular application, we need to define them using the gql tag provided by graphql-tag. Create a new file called queries.ts in the src/app directory and add the following code:

import { gql } from '@apollo/client';

export const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`;

export const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`;

In the above code, we define two GraphQL queries: GET_USERS and GET_USER. The GET_USERS query retrieves a list of users with their id, name, and email fields. The GET_USER query retrieves a single user based on the provided id parameter.

Integrating Apollo Client with Angular

Creating GraphQL Services

To interact with our GraphQL server, we will create a service that encapsulates the logic for performing GraphQL queries. Create a new file called graphql.service.ts in the src/app directory and add the following code:

import { Injectable } from '@angular/core';
import { Apollo } from 'apollo-angular';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { GET_USERS, GET_USER } from './queries';

@Injectable({
  providedIn: 'root',
})
export class GraphqlService {
  constructor(private apollo: Apollo) {}

  getUsers(): Observable<any[]> {
    return this.apollo.query<any>({
      query: GET_USERS,
    }).pipe(
      map(result => result.data.users)
    );
  }

  getUser(id: string): Observable<any> {
    return this.apollo.query<any>({
      query: GET_USER,
      variables: { id },
    }).pipe(
      map(result => result.data.user)
    );
  }
}

In the above code, we import the necessary modules and define a GraphqlService class. The Apollo service provided by apollo-angular is injected into the constructor. We then define two methods: getUsers and getUser, which perform the GET_USERS and GET_USER queries, respectively. The results are mapped using the map operator from rxjs/operators.

Using Apollo Client in Components

Now that we have our GraphQL service, we can use it in our Angular components to fetch data from the GraphQL server. Open a component file, such as user-list.component.ts, and add the following code:

import { Component, OnInit } from '@angular/core';
import { GraphqlService } from '../graphql.service';

@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html',
  styleUrls: ['./user-list.component.css'],
})
export class UserListComponent implements OnInit {
  users: any[];

  constructor(private graphqlService: GraphqlService) {}

  ngOnInit(): void {
    this.graphqlService.getUsers().subscribe(users => {
      this.users = users;
    });
  }
}

In the above code, we import the GraphqlService and inject it into the constructor. In the ngOnInit method, we call the getUsers method of the GraphqlService and subscribe to the returned Observable. When the data is received, we assign it to the users property of the component.

Handling GraphQL Errors

When working with GraphQL, it's important to handle errors gracefully. Apollo Client provides error handling mechanisms that we can leverage in our Angular application. Update the getUser method in the GraphqlService as follows:

// ...

getUser(id: string): Observable<any> {
  return this.apollo.query<any>({
    query: GET_USER,
    variables: { id },
  }).pipe(
    map(result => result.data.user),
    catchError(error => {
      console.error('GraphQL Error:', error);
      // Handle error logic here
      return throwError(error);
    })
  );
}

// ...

In the above code, we import the catchError operator from rxjs/operators and the throwError function from rxjs. We then add the catchError operator to handle any errors that occur during the query. In the error handler, we log the error to the console and perform any necessary error handling logic.

Caching and State Management

Understanding Apollo Client Cache

Apollo Client provides a powerful caching mechanism for storing and retrieving GraphQL query results. By default, Apollo Client uses an in-memory cache that automatically normalizes and deduplicates data. This allows us to efficiently retrieve data from the cache instead of making unnecessary network requests.

Updating Cache after Mutations

When performing mutations that modify data on the server, we need to update the cache to reflect the changes. Apollo Client provides a mechanism to update the cache after mutations. Update the GraphqlService as follows:

// ...

import { ApolloCache, gql } from '@apollo/client';

const CREATE_USER = gql`
  mutation CreateUser($name: String!, $email: String!) {
    createUser(name: $name, email: $email) {
      id
      name
      email
    }
  }
`;

@Injectable({
  providedIn: 'root',
})
export class GraphqlService {
  // ...

  createUser(name: string, email: string): Observable<any> {
    return this.apollo.mutate<any>({
      mutation: CREATE_USER,
      variables: { name, email },
      update: (cache: ApolloCache<any>, { data }) => {
        cache.modify({
          fields: {
            users(existingUsers = []) {
              const newUserRef = cache.writeFragment({
                data: data.createUser,
                fragment: gql`
                  fragment NewUser on User {
                    id
                    name
                    email
                  }
                `,
              });
              return [...existingUsers, newUserRef];
            },
          },
        });
      },
    }).pipe(
      map(result => result.data.createUser)
    );
  }

  // ...
}

In the above code, we define a new mutation called CREATE_USER that creates a new user on the server. We then add a createUser method to the GraphqlService that performs the mutation. In the update function, we modify the users field in the cache by adding the newly created user. This ensures that the cache is updated with the latest data.

Managing Local State with Apollo Client

In addition to caching data from the server, Apollo Client also allows us to manage local state in our Angular application. This can be useful for storing client-side data or handling UI state. To demonstrate this, let's create a simple example of managing a theme preference. Update the GraphqlService as follows:

// ...

import { ApolloCache, gql } from '@apollo/client';

const GET_THEME_PREFERENCE = gql`
  query GetThemePreference {
    themePreference @client
  }
`;

const SET_THEME_PREFERENCE = gql`
  mutation SetThemePreference($theme: String!) {
    setThemePreference(theme: $theme) @client
  }
`;

@Injectable({
  providedIn: 'root',
})
export class GraphqlService {
  // ...

  getThemePreference(): Observable<string> {
    return this.apollo.query<any>({
      query: GET_THEME_PREFERENCE,
    }).pipe(
      map(result => result.data.themePreference)
    );
  }

  setThemePreference(theme: string): Observable<boolean> {
    return this.apollo.mutate<any>({
      mutation: SET_THEME_PREFERENCE,
      variables: { theme },
    }).pipe(
      map(() => true)
    );
  }

  // ...
}

In the above code, we define two new queries: GET_THEME_PREFERENCE and SET_THEME_PREFERENCE. The GET_THEME_PREFERENCE query retrieves the theme preference from the client-side cache, while the SET_THEME_PREFERENCE mutation updates the theme preference in the cache. We then add getThemePreference and setThemePreference methods to the GraphqlService to perform the queries and mutations, respectively.

Optimizing Performance

Batching Queries

To optimize the performance of our Angular application, we can batch multiple queries into a single network request. This reduces the overhead of making multiple network requests and improves the overall performance of our application. Apollo Client provides built-in support for batching queries. Let's update the GraphqlService to demonstrate this:

// ...

import { ApolloLink, BatchHttpLink } from '@apollo/client';

@Injectable({
  providedIn: 'root',
})
export class GraphqlService {
  constructor(private apollo: Apollo, private httpLink: HttpLink) {
    const batchLink = new BatchHttpLink({
      uri: '<YOUR_GRAPHQL_ENDPOINT>',
    });

    this.apollo.create({
      link: ApolloLink.from([batchLink, this.httpLink.create()]),
      cache: new InMemoryCache(),
    });
  }

  // ...
}

In the above code, we import the ApolloLink and BatchHttpLink classes from @apollo/client. We then create a new instance of BatchHttpLink and pass in the GraphQL endpoint URL. We then create a new instance of ApolloClient and set the link property to the batched link and the cache property to a new instance of InMemoryCache.

Using Fragments

Fragments allow us to define reusable selections of fields in our GraphQL queries. By using fragments, we can reduce duplication in our queries and make them more maintainable. Let's update the GET_USER query in the queries.ts file to use a fragment:

// ...

const USER_FRAGMENT = gql`
  fragment UserFields on User {
    id
    name
    email
  }
`;

export const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      ...UserFields
    }
  }
`;

// ...

In the above code, we define a new fragment called UserFields that contains the id, name, and email fields of a user. We then use the ...UserFields syntax in the GET_USER query to include the fragment in the query. This allows us to reuse the fragment in other queries that require the same fields.

Pagination with Apollo Client

When working with large datasets, it's often necessary to implement pagination to improve performance and reduce the amount of data transferred over the network. Apollo Client provides built-in support for pagination. Let's update the GET_USERS query in the queries.ts file to include pagination:

// ...

export const GET_USERS = gql`
  query GetUsers($limit: Int!, $offset: Int!) {
    users(limit: $limit, offset: $offset) {
      id
      name
      email
    }
  }
`;

// ...

In the above code, we add two variables to the GET_USERS query: limit and offset. These variables allow us to specify the number of users to retrieve and the starting position of the retrieval. By using these variables, we can implement pagination in our application.

Testing Apollo Client Integration

Unit Testing GraphQL Services

To ensure the correctness of our GraphQL services, we can write unit tests that cover different scenarios and edge cases. Angular provides a testing framework that allows us to easily test our services. Let's create a unit test for the GraphqlService to test the getUsers method:

// ...

describe('GraphqlService', () => {
  let service: GraphqlService;
  let apollo: Apollo;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        ApolloTestingModule,
      ],
      providers: [
        GraphqlService,
      ],
    });

    service = TestBed.inject(GraphqlService);
    apollo = TestBed.inject(Apollo);
  });

  it('should return list of users', () => {
    const mockUsers = [
      { id: '1', name: 'John Doe', email: '[email protected]' },
      { id: '2', name: 'Jane Smith', email: '[email protected]' },
    ];

    spyOn(apollo, 'query').and.returnValue(of({ data: { users: mockUsers } }));

    service.getUsers().subscribe(users => {
      expect(users).toEqual(mockUsers);
    });
  });
});

// ...

In the above code, we import the necessary modules and define a test suite for the GraphqlService. In the beforeEach block, we configure the testing module by importing the ApolloTestingModule and providing the GraphqlService. We then write a test case that checks if the getUsers method returns the expected list of users. We use the spyOn function to mock the query method of Apollo and return a mock response using the of function from rxjs.

Mocking GraphQL Responses

In addition to unit testing our GraphQL services, we may also need to mock GraphQL responses in our component tests. This allows us to test our components in isolation without relying on a real GraphQL server. Let's create a component test for the UserListComponent to demonstrate this:

// ...

describe('UserListComponent', () => {
  let component: UserListComponent;
  let fixture: ComponentFixture<UserListComponent>;
  let graphqlService: GraphqlService;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [UserListComponent],
      providers: [
        {
          provide: GraphqlService,
          useValue: {
            getUsers: () => of([
              { id: '1', name: 'John Doe', email: '[email protected]' },
              { id: '2', name: 'Jane Smith', email: '[email protected]' },
            ]),
          },
        },
      ],
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(UserListComponent);
    component = fixture.componentInstance;
    graphqlService = TestBed.inject(GraphqlService);
    fixture.detectChanges();
  });

  it('should display list of users', () => {
    const userListElement: HTMLElement = fixture.nativeElement.querySelector('.user-list');
    expect(userListElement.textContent).toContain('John Doe');
    expect(userListElement.textContent).toContain('Jane Smith');
  });
});

// ...

In the above code, we import the necessary modules and define a test suite for the UserListComponent. In the beforeEach block, we configure the testing module by providing a mock implementation of the GraphqlService that returns a mock list of users using the of function from rxjs. We then create the component, inject the GraphqlService, and trigger change detection. Finally, we write a test case that checks if the list of users is displayed correctly in the component's template.

Conclusion

In this tutorial, we have explored how to integrate Apollo Client with Angular for seamless communication with a GraphQL server. We started by understanding what Angular and GraphQL are and why integrating Apollo Client is beneficial. We then learned how to set up Apollo Client in an Angular application, create GraphQL services, and use Apollo Client in components. We also covered topics such as caching and state management, performance optimization, and testing. By following this tutorial, you should now be able to integrate Apollo Client with Angular and leverage its features to enhance your Angular applications with GraphQL.