Productionizing Apollo Links

Joan Vilà Cuñat
6 min readMar 5, 2019

Having a basic understanding of what apollo links are and what are they used for is not a big deal. It is known that if we want to add auth headers to every request or we want to handle response errors, the links are the place to do so.

However, when it comes to combining multiple links to achieve production readiness for our Apollo client, without a proper understanding, the implementation can go out of hand.

What is a link

In a really simplified way, a link is a middleware to a GraphQL operation that can modify it and perform side effects. The main use case for it is to perform an http request to a server to solve the GraphQL query.

https://www.apollographql.com/docs/link/overview.html

When combining different links, it feels natural to give each link a single responsibility, this improves testability, maintainability and reusability since different teams may want to use and reuse different links.

Each link of the chain will apply some logic to the GraphQL operation and call the next link of the chain with the forward operation. But let’s not worry too much about this now since we will see examples later.

Why are the links needed

As mentioned early, the main use case of the Apollo Links is to perform an http call to a backend service to resolve a GraphQL operation:

const httpLink = () => (createHttpLink({
uri: 'http://localhost:5000/graphql',
}));
const links = ApolloLink.from([
httpLink(),
]);
cons apolloClient = new ApolloClient({
links,
cache: new InMemoryCache(),
});

In this example we are using the ApolloLink.from interface to create a list of Apollo Links. For now, the only one in the list is the httpLink which is using the apollo-link-http library to perform a network request to localhost when receiving a GraphQL operation.

Logging

One of the first things to be done if we want to ship our GraphQL stack to production is to add logging around every network request. How many of them are performed and how much time do they take to resolve.

To do so, we will create a custom link:

import { ApolloLink } from 'apollo-link';const loggerLink = new ApolloLink((operation, forward) => {
console.log(`GraphQL Request: ${operation.operationName}`);
operation.setContext({ start: new Date() });
return forward(operation).map((response) => {
const responseTime = new Date() - operation.getContext().start;
console.log(`GraphQL Response took: ${responseTime}`);
return response;
});
});

This is the first link we see so before adding it to the list of links, let’s check how it works.

A custom link is an instance of ApolloLink that receives two parameters:

  • operation: Provides information and a context to the GraphQL operation being performed.
  • forward: A method to be called in order to continue the chain of links.

If we forget for a second about the links, logging should happen as follows:

  • Log that the http request is going to be performed.
  • Get the initial time.
  • Perform the http request
  • Subtract the initial time from the current one to calculate the response time.
  • Log that the http request has finished successfully.

According to the list, there is some logic that needs to happen before the request and some that needs to happen afterwards. The links by design allow us to do this by chaining them. If we take a close look to the code, we see how the first thing we do in the link, is to log and to store the initial time in the operation context.

Once this is done, we call the forward method which continues the chain of links and once the next links have been executed, it will receive their response. At this point, we will log the response time and return the response to continue the chain of links.

In order to chain our custom logger link with the http link we already had in place, we only need to add it to the list of Apollo Links:

const links = ApolloLink.from([
loggerLink,
httpLink(),
]);

Notice that the sorting of the list is very important since the links will execute in order from the first one to the last one. In this case the execution of the links when a GraphQl operation is triggered happens as:

  • The logger link receives the operation as the first link in the chain and stores the start time in the context
  • The logger calls the forward method passing the operation to the http link.
  • The http link performs an http request and sends the response as a result of the forward operation of the previous link (the loggerLink).
  • The logger link logs the response time and returns the response.

At this point, we already have Apollo Links working doing http requests and logging. Yay! 🚀

From this point on, we will only be adding more links to the list in order to do auth, error handling, timeouts and retry policies but we will build on top of the same concepts.

Auth

In most cases, our apps need to handle authorisation and authentication. Usually this consists on adding headers to the http calls to our backends. In order to do so, we’ll make use of apollo-link-context setContext to add those headers to the GraphQL operation:

import { setContext } from 'apollo-link-context';const authLink = (config) => (setContext(async (_, { headers }) => {
const apiKey = config.backendApiKey;
return {
headers: {
...headers,
apiKey,
},
};
}));
const links = ApolloLink.from([
loggerLink,
authLink(config),
httpLink(),
]);

At this point we are adding the apiKey as a header to be received by our backend for all the network requests.

Error handling

As responsible developers, we want to log any problem and error responses coming from the backend. In order to do so, we’ll use the onError link provided by apollo-link-error:

import { onError } from 'apollo-link-error'const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.map(({ message, locations, path }) =>
console.log(`GraphQL Error: ${message}`),
);
}
if (networkError) {
console.log(`Network Error: ${networkError.message}`);
}
};
const links = ApolloLink.from([
loggerLink,
authLink(config),
errorLink,
httpLink(),
]);

Notice that when the errorLink catches an error, the error variable of the Query component in the view will contain the error too.

Timeouts

Not letting the network request hang for a long period of time is vital for not breaking the user experience. For this reason, if a call takes too long, we will abort it and return an error. To do so, we are going to use apollo-link-timeout:

import ApolloLinkTimeout from 'apollo-link-timeout';const timeoutLink = (config) => new ApolloLinkTimeout(config.timeout);const links = ApolloLink.from([
loggerLink,
authLink(config),
errorLink,
timeoutLink(config),
httpLink(),
]);

The most important thing here is that since the timeoutLink is going to generate an error when it cancels the request, we want this error to be caught by the errorLink. For this reason, we must place it after the errorLink in the list. Otherwise, the errorLink won’t have visibility on the errors generated by the timeoutLink.

Retry policy

Depending on some errors (specially network errors) we may want the requests to be retried automatically. In order to achieve it, apoll-link-retry will do the job:

import { RetryLink } from 'apollo-link-retry';const retryIf = (error, operation) => {
const doNotRetryCodes = [500, 400];
return !!error && !doNotRetryCodes.includes(error.statusCode);
};
const retryLink = new RetryLink({
delay: {
initial: 100,
max: 2000,
jitter: true,
},
attempts: {
max: 5,
retryIf,
},
});
const links = ApolloLink.from([
loggerLink,
authLink(config),
retryLink,
errorLink,
timeoutLink(config),
httpLink(),
]);

This configuration of the retry link specifies that a request will be retried a maximum number of 5 times if the http response code is different than 400 or 500. As a delay we will use a random number of milliseconds between 100 and 2000.

As we did in the timeoutLink here we also need to think carefully where do we place the link in the list of links. In order to decide it, we need to have several things into consideration:

  • The retry must happen after we have detected and logged that an error has happened. This means that must be placed before the onError link.
  • Timeouts should be retried so it’s fine that the retry is placed before the timeoutLink.
  • We don’t want to recreate the auth headers for every retry in the policy so the retryLink should be placed after the authLink.

Conclusion

At this point, we already have a production ready setup to hit our backend with GraphQL queries while being safe and informed about what is happening to each request.

Each use case and problem we face will require different links to achieve different behaviours but now we should have a general understanding of how the links work together to be able to extend the current functionality.

If this is your first time reading about the topic, the Apollo docs on links are a really nice source of information and you will find some of the things explained here with greater detail.

--

--