Client Side Caching With Interceptors
Tags: angular, webdev, javascript, beginners
Published: 2023-11-27 05:52:04

Angular is known for the vast amount of built-in feature it has, but sometimes we only use a few of them without knowing what their full potential could be.

Angular is renowned for its extensive array of built-in features. However, at times, users only scratch the surface, unaware of the full potential that lies within.

Taking inspiration from @armandotrue and his excellent series [Superpowers with Directives and Dependency Injection](https://dev.to/search?q=Superpowers+with+Directives+and+Dependency+Injection), in this series, we are going to explore what HTTP interceptors are in Angular, and what practical use cases can be solved with them.

## Today's Problem

Today let's imagine we have a piece of data that is fetched over and over.

For example, we could build a photos library app that is listing the user's albums. It's common for users to navigate back and forth within the application, revisiting their photos.

Without caching, this implies that every time a user accesses an album, all the details of each picture must be retrieved from the server anew.

This looks like something we could improve!

> If you would like to follow along, you can clone [the repository at this step](https://github.com/pBouillon/DEV.ClientSideCachingWithInterceptors/tree/7824b3baec5dad5461559c5569da122d4e94eb51)

## Initial State

Here is our app in action:

![Initial State](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5hq0k5sumvufebzkku38.gif)

For now it just displays the amount of photos in each album. But if we look at the network tab, we can see that whenever we access an album, the app fetches its details again, even if we just clicked into it a moment ago.

Since these albums belong to the user, chances are any changes will come from their side. As long as they're just browsing, it might not change a lot.

Sounds like caching could help!

## Bringing Interceptor to the Rescue

To add additional behavior to the `HttpClient` while it handles requests, we need to create a new interceptor to define the associated logic.

An [interceptor](https://angular.dev/guide/http/interceptors#interceptors) is a special type of service that let us configure a pipeline an HTTP request will pass through before an actual request is made to the server.

For caching, it means we could intercept the request if we already know what the response will be.

### Creating the Interceptor

To define an interceptor, we first need to create and register it.

An interceptor is of type `HttpInterceptorFn`, which takes the request and the next handler in the pipeline as parameters, and returns the result as an `Observable` of `HttpEvent`:

```ts
// 📁 app/caching.interceptor.ts

export const cachingInterceptor: HttpInterceptorFn = (
  req: HttpRequest<unknown>,
  next: HttpHandlerFn
): Observable<HttpEvent<unknown>> => {
  // For now, this is just a pass-through
  return next(req);
};
```

To register it, we need to add it to the interceptors of the `HttpClient`:

```ts
// 📁 main.ts

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes, withComponentInputBinding()),
    provideHttpClient(
      // 👇 Add the interceptor to the pipeline
      withInterceptors([cachingInterceptor])
    ),
  ],
}).catch((err) => console.error(err));
```

We're all set!

If we run our appliation again, nothing has changed for now. While this might not be really useful, at least it indicates that we didn't break anything and HTTP requests still flows into our app.

## Creating our Cache

The most straightforward approach for our cache is to have it as a `Map` of responses by urls:

```ts
// 📁 app/caching.interceptor.ts

const cache = new Map<string, HttpEvent<unknown>>();

export const cachingInterceptor: HttpInterceptorFn = (
  req: HttpRequest<unknown>,
  next: HttpHandlerFn
): Observable<HttpEvent<unknown>> => {
  const cached = cache.get(req.url);

  // 👇 If the response is known, return it without making a request
  const isCacheHit = cached !== undefined;
  if (isCacheHit) {
    return of(cached);
  }

  return next(req).pipe(
    // 👇 Cache the response as it flows back into our application
    tap((response) => cache.set(req.url, response))
  );
};
```

If we run our app again, we can see that the album details are requested only when accessed for the first time:

![Simple Caching](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2xdrs37dpswxeh51vyx7.gif)

### Restricting the Cache

While it does cache our requests, the downside of this approach is that *all* requests will be cached while we might want to cache only the ones related to the album.

Since we would like to add some logic to our cache, we can transform it to a smarter piece of logic and upgrade it to a service:

```ts
// 📁 app/caching.service.ts

@Injectable({ providedIn: "root" })
export class CachingService {
  readonly #cache = new Map<string, HttpEvent<unknown>>();

  get(key: string): HttpEvent<unknown> | undefined {
    return this.#cache.get(key);
  }

  set(key: string, value: HttpEvent<unknown>): void {
    if (key.includes("album")) {
      this.#cache.set(key, value);
    }
  }
}
```

> Here, we are only using a simple condition to check if this should be cached. In a real world application you might prefer relying on a smarter way of checking this.

Since interceptors function are called within an [injection context](https://angular.dev/guide/di/dependency-injection-context), we can use dependency injection into its definition:

```ts
// 📁 app/caching.interceptor.ts

export const cachingInterceptor: HttpInterceptorFn = (
  req: HttpRequest<unknown>,
  next: HttpHandlerFn
): Observable<HttpEvent<unknown>> => {
  // 👇 Rely on our dedicated service
  const cache = inject(CachingService);

  const cached = cache.get(req.url);

  const isCacheHit = cached !== undefined;
  if (isCacheHit) {
    return of(cached);
  }

  return next(req).pipe(tap((response) => cache.set(req.url, response)));
};
```

From the interceptor's point of view, nothing has changed. However, since we now have a dedicated service, adding more complex logic will be greatly simplified.

### Enhancing the Cache

Finally, we could want the cache to expire after some time.

To achieve this, we can enhance it a little bit more to include a time to live to each entry:

```ts
// 📁 app/caching.service.ts

interface CacheEntry {
  value: HttpEvent<unknown>;
  expiresOn: number;
}
```

And expire the associated value whenever needed:

```ts
// 📁 app/caching.service.ts

const TTL = 3_000;

@Injectable({ providedIn: "root" })
export class CachingService {
  readonly #cache = new Map<string, CacheEntry>();

  get(key: string): HttpEvent<unknown> | undefined {
    const cached = this.#cache.get(key);

    if (!cached) {
      return undefined;
    }

    // 👇 Remove the entry if expired
    const hasExpired = new Date().getTime() >= cached.expiresOn;
    if (hasExpired) {
      this.#cache.delete(key);
      return undefined;
    }

    return cached.value;
  }

  set(key: string, value: HttpEvent<unknown>): void {
    if (key.includes("album")) {
      this.#cache.set(key, {
        value,
        // 👇 Set its lifespan
        expiresOn: new Date().getTime() + TTL,
      });
    }
  }
}
```

If we look at the network tab of our browser again, we can now see that an each request is cached for a few seconds, and the cache expires afterwards:

![With TTL](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3r89f5t4qv76rsmgh31s.gif)

## Takeaways

In this article we saw how to intercept HTTP request made by our Angular application and how to manipulate them.

We also saw how to take advantage of Angular's dependency injection system to add custom logic to an interceptor.

Finally, we also built a small client-side caching system for our application to use.

> If you would like to check the resulting code, you can head on to the associated GitHub repository:
> {% embed https://github.com/pBouillon/DEV.ClientSideCachingWithInterceptors %}

---

I hope that you learn something useful there!

{% embed https://dev.to/pbouillon %}

---

> Photo by <a href="https://unsplash.com/@cristina_gottardi?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Cristina Gottardi</a> on <a href="https://unsplash.com/photos/man-painting-of-mosque-Rh-wSkhMn6k?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>