HTTP in a standalone Angular application with provideHttpClient

Angular v14 introduced the concept of standalone components/directives/pipes and allows writing applications without modules.

How do you use and test HTTP in a standalone application?

provideHttpClient

For a long time, the HttpClient was provided by the HttpClientModule that you imported into your application module.

@NgModule({
  imports: [BrowserModule, HttpClientModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

When the standalone APIs were introduced in Angular v14, it opened the door to writing applications without modules.

The Angular team introduced an importProvidersFrom() function, that you could use in the bootstrapApplication function to import providers from an existing module, as most of the ecosystem was structured around modules.

So to provide the HttpClient in a standalone application, you could do:

import { bootstrapApplication, importProvidersFrom } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

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

But since Angular v15, this can be replaced by provideHttpClient(), a new function that does the same thing as importProvidersFrom(HttpClientModule):

import { bootstrapApplication } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app.component';

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

HttpClient is then available for injection in your application.

provideHttpClient() is more “tree-shakable” than importing HttpClientModule, as you can enable the features you want by giving it some parameters.

For example, if you want JSONP support, you can write:

import { bootstrapApplication } from '@angular/core';
import { provideHttpClient, withJsonpSupport } from '@angular/common/http';
import { AppComponent } from './app.component';

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

In the same vein, Angular provides XSRF protection (cross-site request forgery) out-of-the-box, by adding a custom header containing a random token provided by the server in a cookie (which is a common technic to mitigate these attacks).

As you probably want to keep this security, it’s enabled by default in provideHttpClient(), but you can configure it with withXsrfConfiguration() to specify a custom header name and cookie name:

import { bootstrapApplication } from '@angular/core';
import { provideHttpClient, withXsrfConfiguration } from '@angular/common/http';
import { AppComponent } from './app.component';

bootstrapApplication(AppComponent, {
  providers: [provideHttpClient(withXsrfConfiguration({
    cookieName: 'TOKEN', // default is 'XSRF-TOKEN'
    headerName: 'X-TOKEN' // default is 'X-XSRF-TOKEN'
  }))]
});

or you can disable it completely with withNoXsrfProtection():

import { bootstrapApplication } from '@angular/core';
import { provideHttpClient, withNoXsrfProtection } from '@angular/common/http';
import { AppComponent } from './app.component';

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

There is another feature that you can enable, but first I need to introduce the concept of functional interceptors.

Functional interceptors

In Angular, interceptors are classes that implement the HttpInterceptor interface. They are used to intercept HTTP requests and responses and can be used to add headers, log requests, etc.

@Injectable()
export class LoggerInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.log(`Request is on its way to ${req.url}`);
    return next.handle(req);
  }
}

Since Angular v15, you can also use functional interceptors. They are functions that take an HttpRequest and a HttpHandlerFn as parameters:

export function loggerInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
  console.log(`Request is on its way to ${req.url}`);
  return next(req);
}

As this is a function, you can’t use the usual dependency injection via constructor parameters to inject services in it. But you can use the inject() function:

export const loggerInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> => {
  const logger = inject(Logger);
  logger.log(`Request is on its way to ${req.url}`);
  return next(req);
}

Functional interceptors have to be registered via withInterceptors():

import { bootstrapApplication } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app.component';

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

Note that you can also register class-based interceptors via withInterceptorsFromDi():

import { bootstrapApplication } from '@angular/core';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { AppComponent } from './app.component';

bootstrapApplication(AppComponent, {
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: LoggerInterceptor, multi: true },
    provideHttpClient(withInterceptorsFromDi())
  ]
});

But this API may be phased out in the future, so it’s better to use withInterceptors() and functional interceptors.

Interceptors and lazy-loading

A long-standing issue with Angular was that interceptors are not inherited in lazy-loaded modules (see this issue for more context).

For example, let’s say we have a lazy-loaded part of the application for the administration of our website:

{
  path: 'admin',
  loadChildren: () => import('./admin/admin.routes').then(c => c.ADMIN_ROUTES)
},

If it provides the HttpClient as well (which is not a good idea to be honest, but let’s say that’s the case for this example):

export const ADMIN_ROUTES: Routes = [
  {
    path: '',
    pathMatch: 'prefix',
    providers: [provideHttpClient()], // <--
    children: [{ path: '', component: AdminComponent }]
  }
];

then all requests made in the AdminComponent will not be intercepted by the interceptors registered in our application.

If we want them to go through our loggerInterceptor, we can use withRequestsMadeViaParent():

export const ADMIN_ROUTES: Routes = [
  {
    path: '',
    pathMatch: 'prefix',
    providers: [provideHttpClient(withRequestsMadeViaParent(), withInterceptors([adminInterceptor()]))],
    children: [{ path: '', component: AdminComponent }]
  }
];

Then the requests made in the AdminComponent will then go through the adminInterceptor and are then handed off to the parent HttpClient, and will be intercepted by the loggerInterceptor registered at the application bootstrap.

Testing HTTP

When using HttpClientModule in your application, you can import the HttpClientTestingModule in your tests to mock HTTP requests.

But if you use provideHttpClient() instead, you can use provideHttpClientTesting() to mock HTTP requests in your tests (in addition to provideHttpClient()):

import { TestBed } from '@angular/core/testing';
import { provideHttpClientTesting } from '@angular/common/http/testing';

beforeEach(() =>
  TestBed.configureTestingModule({
    providers: [provideHttpClient(), provideHttpClientTesting()]
  })
);

You can then inject HttpController to mock HTTP requests as you usually do.

Summary

The provideHttpClient() API is the way to go if you work with an Angular application and don’t want to use NgModule. When migrating an existing application to the standalone APIs, you will need to replace the usage of HttpClientModule with provideHttpClient() and the usage of HttpClientTestingModule with provideHttpClientTesting() in your tests. You can also gradually migrate your class-based interceptors to functional interceptors.

All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!



blog comments powered by Disqus