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.
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:
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 withHTTP_INTERCEPTORS(orwithInterceptorsFromDi()). 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.
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:
<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 manualsubscribe()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.
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:
<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.
