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.
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.