Angular 15 introduces functional HTTP interceptors

Less boilerplate and more tree-shakable. Let’s compare.

Rafael Mestre
HeroDevs

--

Angular logo climbing a graph of lean success

Angular continues on its recent trend to improve developer experience and reduce boilerplate by welcoming HTTP interceptors, and client, on the functional train. This article will go over what’s new and why you’d want to jump on it too.

To learn about using a similar pattern to simplify your route guards, take a look at our friend Kate’s article, Functional router guards in Angular 15 open the door to happier code.

Angular is getting leaner and so is HttpClient

Angular’s packages are undergoing significant API refactoring to make them more extensible and tree-shakable, including @angular/common/http. The preferred way to configure the router in your app, module, or component is now the provideHttpClient() function.

bootstrapApplication(AppComponent, {
providers: [provideHttpClient()],
});

This function supersedes HttpClientModule and returns the set of providers your app needs with just the specified features.

One of these new HttpFeatures is withInterceptors(): the new way to add interceptors to your HttpClient. Pass in a list of HttpInterceptorFns to configure your interceptor pipeline, similar to the way it used to be done.

Did you say HttpInterceptorFn?

That’s right: going forward, you can write functional interceptors. This means class boilerplate isn’t required anymore thanks to several factors. Let’s compare.

// Before
@Injectable()
export class LoggerInterceptor implements HttpInterceptor {
loggingService = inject(MyLogger);
// or
constructor(private loggingService: MyLogger) {}

intercept(req: HttpRequest<Type>, next: HttpHandler): Observable<HttpEvent<Type>> {
console.log('do something here', req);
return next.handle(req);
}
}

The HttpInterceptor interface only required one method be implemented, so the only reason we were writing classes for this was dependency injection; not only the type of DI we define in our classes, but the logic going on under the hood in the HttpClient. With the inject function now usable in more contexts, lots of functionality can be simplified, including these.

// After
export const loggerInterceptor: HttpInterceptorFn = (req, next) => {
const loggingService = inject(MyLogger);
loggingService.log('do something here', req);
return next(req);
}

This is a functional interceptor in its simplest form that will do something with the request and return it to the handler. HttpInterceptorFn types the arguments and return value, so multiple type imports are simplified into the one. Services or other providers needed for the logic can be injected, too. The second argument, next, was also changed to the simpler HttpHandlerFn that's just called directly. Use it by adding it to the interceptors feature in your provided HttpClient:

bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withInterceptors([
loggerInterceptor,
// They can be inlined too, with full type safety!
(req, next) => next(req),
]),
),
],
});

Classes vs. functions, or inheritance vs. composition

In the vast majority of cases, functions should be preferred over classes and inheritance. I won’t go too deep into this subject, but composition unlocks advanced patterns and promotes reusability. A great example of this is higher-order functions, and interceptors can take advantage of it.

export const logByLevelInterceptor = (level: LogLevel): HttpInterceptorFn => {
const loggingService = inject(MyLogger);

return (req, next) => {
loggingService[level]('do something here', req);
return next(req);
}
}

bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withInterceptors([
logByLevelInterceptor('WARN')
]),
),
],
});

Interceptors from parent HttpClients

Another neat feature they’ve added is the ability to inherit interceptors from parent injectors. A lazy loaded module creates a separate environment injector that can provide its own HttpClient but have requests routed through its parent clients and their interceptors when withRequestsMadeViaParent is configured.

@Component({
selector: 'i-am-lazy-loaded',
providers: [
provideHttpClient(
// Pass through parent interceptors until an
// HttpClient without this feature is reached.
withRequestsMadeViaParent(),
withInterceptors([myInterceptorFn]),
)
]
})
export ...

What happens with existing class-based interceptors?

One of the things the Angular team does best is create a migration path for large-scale changes. If a project has any number of class interceptors that can’t be refactor immediately, they’ve exposed a feature called withInterceptorsFromDi() that'll inject HTTP_INTERCEPTORS provided “the old way”. You can use it in isolation or combine it with functional interceptors.

@Component({
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: MyOldInterceptor, multi: true },
provideHttpClient(
// Both are optional! Can use either or both as needed.
// Without the next line, the provider above would be ignored.
withInterceptorsFromDi(),
withInterceptors([ ... ]),
)
]
})

Other newly configurable features

Some other features worth mentioning are XSRF configuration and JSONP support. These already existed with the original HttpClientModule, in the form of extra module imports HttpClientXsrfModule and HttpClientJsonpModule, but are now also provided in the nicer (and more tree-shakable) feature function way.

bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withXsrfConfiguration({ cookieName, headerName }),
// or to disable,
withNoXsrfProtection(),
withJsonpSupport(),
),
],
});

Wrap-up

There’re many ways to improve DX, and these features definitely help in many of them. Smaller bundles, better types, more readable code, and more powerful functionality will always be well received by everyone. HttpClient, Router, Forms, and Angular as a whole are getting better every day, and I can't wait to see what's next!

About HeroDevs
HeroDevs is a software engineering and consulting studio that specializes in frontend development. Our team has authored or co-authored projects like the Angular CLI, Angular Universal, Scully, XLTS — extended long-term support for AngularJS, Vue2, Protractor, and many others. We work with fast-growing startups and some of the world’s largest companies like Google, GE, Capital One, Experian, T-Mobile, Corteva, and others. Learn more about us at herodevs.com

--

--