Overview

With the arrival of Angular 14, the inject function gained the ability to be invoked outside an injection context. That opened the door for several new patterns, such as:

  • composing dependencies in a more flexible way
  • defining route guards as plain functions

Now that Angular 15 is here, interceptors can also take advantage of this functional approach. However, because interceptors are tied to the httpClient instance and modules have become optional, a few questions naturally surface:

  • what is the current recommended way to register the http client?
  • how can an interceptor be written as a plain function?
  • how does registration work for these function-based interceptors?
  • can class-based interceptors and function-based interceptors coexist in one project?

Registering the http client with the new API

Prior to Angular 15, bootstrapping a standalone component with the Angular http client meant relying on the importProvidersFrom(module) helper, which handled:

  • pulling in the providers that the module exported
  • placing those providers into the environment injector
bootstrapApplication(AppComponent, {
  providers: [ importProvidersFrom(HttpClientModule) ]
}).catch(console.error);
Enter fullscreen mode Exit fullscreen mode

With Angular 15, a dedicated provideHttpClient function has been introduced, making http client registration more straightforward. This function accepts a list of HttpFeature entries. A common example of an HttpFeature is the configuration for interceptors.

bootstrapApplication(AppComponent, {
  providers: [ provideHttpClient() ]
}).catch(console.error);
Enter fullscreen mode Exit fullscreen mode

The modern approach to writing and registering interceptors

In versions before Angular 15, an interceptor was required to be a class decorated with @Injectable. That structure was convenient because it allowed dependencies to be injected directly into the interceptor.

@Injectable()
export class AuthorizationInterceptorDI implements HttpInterceptor {
  constructor(private loadingService: LoadingService){}
  intercept(request: HttpRequest<any>>, next: HttpHandler): Observable<HttpEvent<any> {
    this.loadingService.startLoader();
    const clonedRequest = request.clone({ setHeaders: { Authorization: 'this_is_angular' } });
    return next.handle(clonedRequest)
      .pipe(finalize(() => this.loadingService.stopLoader());
  }
}
Enter fullscreen mode Exit fullscreen mode

Because the inject function now works outside an injection context, the same interceptor can be expressed as a standalone function.

export function AuthorizationInterceptor(req: HttpRequest<unknown>,
next: HttpHandlerFn){
    const loadingService = inject(LoadingService);
    loadingService.startLoader();
    const clonedRequest = request.clone({ setHeaders: { 
      Authorization: 'this_is_angular' } });
    return next(clonedRequest)
      .pipe(finalize(() => this.loadingService.stopLoader()))
}
Enter fullscreen mode Exit fullscreen mode

This interceptor function signature includes two arguments:

  • the request that the interceptor will process
  • a callback for forwarding the request after any modifications

The return value of the interceptor function should be an HttpEvent observable, which allows you to work with the http response as well.

To attach a function-based interceptor to the http client, Angular supplies the withInterceptors helper.

This helper takes an array of interceptor functions and produces an HttpFeature specifically of type interceptor. That return type is particularly convenient because it can be passed directly into the provideHttpClient function.

bootstrapApplication(AppComponent, {
  providers: [ provideHttpClient(
    withInterceptors([AuthorizationInterceptor])) 
  ]
}).catch(console.error);
Enter fullscreen mode Exit fullscreen mode

withInterceptors is not the only function that returns an HttpFeature. There are several others, including:

  • withXsrfConfiguration for fine-tuning how XSRF protection behaves
  • withJsonpSupport for enabling JSONP requests through the http client

Additional details can be found in the official documentation.

Can the two styles be combined?

An Angular application these days can include both standalone components and traditional modules. That flexibility is useful when you want to gradually shift a module-based codebase toward a pure standalone setup.

Given that scenario, if your app is bootstrapped on a standalone component and you register the http client via provideHttpClient, do you have to convert every interceptor to the functional form?

The short answer is NO. You can absolutely have interceptors in the new function style while keeping older class-based interceptors intact. Angular provides the withInterceptorsFromDi function specifically for this purpose.

The purpose of withInterceptorsFromDi is to pull in interceptors that were declared using the older DI-based format:

{ provide: HTTP_INTERCEPTORS,
  useClass: AuthorizationInterceptorDI
  multi: true
}
Enter fullscreen mode Exit fullscreen mode

and register them with the http client instance.

bootstrapApplication(AppComponent, {
  providers: [ provideHttpClient(
    withInterceptorsFromDi()) 
  ]
}).catch(console.error);
Enter fullscreen mode Exit fullscreen mode

These two distinct functions let you run both interceptor syntaxes side by side in the same application, which smooths the path for a gradual migration.

Final thoughts

Angular 15 solidifies standalone components and continues the push to cut down on boilerplate. The function-based interceptor syntax is noticeably more concise than what came before.

With the combination of withInterceptors and withInterceptorsFromDi, you have a clear route to migrate interceptors incrementally.

That said, going forward the functional form via withInterceptors is the recommended choice, since withInterceptorsFromDi is likely to be deprecated in upcoming Angular releases.