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);
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);
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());
}
}
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()))
}
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);
withInterceptors is not the only function that returns an HttpFeature. There are several others, including:
withXsrfConfigurationfor fine-tuning how XSRF protection behaveswithJsonpSupportfor 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
}
and register them with the http client instance.
bootstrapApplication(AppComponent, {
providers: [ provideHttpClient(
withInterceptorsFromDi())
]
}).catch(console.error);
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.
