← Back to the seriesThe Galactic Guide to Angular · 9 / 10

Interceptors and pipes: the finishing tools

Interceptors and pipes: the finishing tools

The penultimate chapter of the basic module is dedicated to two tools that do not build pages but improve everything else:


  • Interceptors, for handling every HTTP call in one place: tokens, logs, error handling.
  • Pipes, for transforming data directly in the template: dates, currencies, text that needs shortening.

They are the classic tools that leave you wondering how you ever managed without them.


🚦 Interceptors: customs for requests


An interceptor sits between your app and the server: every outgoing HTTP request and incoming response passes through it. Like customs, it can allow, modify, record or reject.


The four real-world uses are always the same, and eventually you will need them all:


  • Adding the authentication token to every call.
  • Logging requests and response times.
  • Handling errors centrally (401 → redirect to login).
  • Showing a global loader while traffic is in progress.

Without an interceptor, this logic would be copied into every service. With one, you write it once.


🛠 Example: the token on every request


A modern interceptor is a simple HttpInterceptorFn function: it receives the request and a next function to continue processing it.

TYPESCRIPT
1// auth.interceptor.ts
2import { HttpInterceptorFn } from '@angular/common/http';
3
4export const authInterceptor: HttpInterceptorFn = (req, next) => {
5  const token = localStorage.getItem('token');
6
7  // HTTP requests are immutable: clone rather than mutate
8  const reqClone = token
9    ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
10    : req;
11
12  return next(reqClone);
13};

Two details to notice. First: requests are immutable, so you do not modify them; you clone them with the changes (req.clone(...)). Second: because an interceptor runs in Angular's context, you can use inject() inside it. A real AuthService, instead of localStorage, is injected just like in previous chapters.


Register it in app.config.ts, attaching it to provideHttpClient, familiar from chapter 7:

TYPESCRIPT
1import { provideHttpClient, withInterceptors } from '@angular/common/http';
2import { authInterceptor } from './auth.interceptor';
3
4export const appConfig: ApplicationConfig = {
5  providers: [provideHttpClient(withInterceptors([authInterceptor]))],
6};

Older projects contain interceptors as classes implementing HttpInterceptor, registered with HTTP_INTERCEPTORS (or withInterceptorsFromDi()). Same idea, more ceremony: use functions for new code.

A second example, to show we can also act on the response: logging each call's duration.

TYPESCRIPT
1import { HttpInterceptorFn } from '@angular/common/http';
2import { finalize } from 'rxjs';
3
4export const timingInterceptor: HttpInterceptorFn = (req, next) => {
5  const start = Date.now();
6  return next(req).pipe(
7    finalize(() => console.log(`${req.method} ${req.url}: ${Date.now() - start}ms`))
8  );
9};

Interceptors form a chain: withInterceptors([authInterceptor, timingInterceptor]) runs them in the listed order.


🪄 Pipes: formatting without making a mess


Let us move from HTTP requests to templates. Pipes transform values when displaying them, using the | syntax:

HTML
<p>{{ oggi | date:'longDate' }}</p>
<p>{{ prezzo | currency:'EUR' }}</p>
<p>{{ nome | uppercase }}</p>

The component's data stays intact; only presentation changes. That is the difference between keeping prezzo = 59.99 and ending up with a string such as "€59.99" that you can no longer use in calculations.


The built-in pipes I use most often:


  • date: date formatting, with configurable locale and format.
  • currency: numbers as currency.
  • uppercase / lowercase / titlecase: capitalization.
  • number: thousands separators and decimals.
  • json: prints an object as JSON, invaluable for debugging.
  • async: subscribes to an Observable for you. If chapter 7's manual subscribe() felt noisy, this is the alternative.

Pipes also need to be imported into their component, like everything in standalone Angular: imports: [DatePipe, CurrencyPipe], and so on. If a template reports a missing pipe "that should be there", this is almost always why.

✍️ Writing a custom pipe


When built-in pipes are not enough, writing one takes minutes. Classic example: shortening an overly long text.

TYPESCRIPT
1import { Pipe, PipeTransform } from '@angular/core';
2
3@Pipe({ name: 'shorten' })
4export class ShortenPipe implements PipeTransform {
5  transform(value: string, limit = 10): string {
6    return value.length > limit ? value.slice(0, limit) + '…' : value;
7  }
8}

In the template, after importing it:

HTML
<p>{{ descrizione | shorten:20 }}</p>

One detail explains pipes' efficiency: by default they are pure, meaning Angular reruns them only when the input value changes. You can put twenty in a list without worrying: that is not where the app slows down.


Where we are now


With interceptors and pipes, the basic module is complete: components, router, signals, forms, HTTP, shared state and now tools spanning requests and presentation. What follows is craft and practice.


Practice starts immediately: in the next chapter, the last of the series, we combine every piece into a complete app: a digital video game library. Bring your dice.

Fullstack developer in Milan. Writes about Angular, the JavaScript ecosystem and AI.