One of the common tasks in almost any web application is making HTTP requests to a backend API. In previous versions of Angular, developers had to explicitly import HttpClientModule into their AppModule or use provideHttpClient() in their app.config.ts for standalone applications. While a small step, it was an extra piece of boilerplate for every new project.

Angular v21 streamlines this process by making HttpClient available by default. This means less initial configuration, especially for new projects, and a slightly smoother onboarding experience for new Angular developers.

The Old Way (Angular ≤ v20)

Before Angular v21, if you wanted to use HttpClient in a standalone component, your app.config.ts would look something like this:

// src/app/app.config.ts (Angular <= v20 example)
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http'; // This line was crucial

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient() // Had to remember this every time!
  ]
};

And for NgModule-based applications, you’d import HttpClientModule:

// src/app/app.module.ts (Angular <= v20 NgModule example)
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http'; // This was imported here

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule // And added to imports
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

The New Way (Angular v21)

In Angular v21, HttpClient is provided at the root level automatically for new applications. It just works! This means you can simply inject HttpClient into your services or components without any prior configuration in app.config.ts or AppModule.

Let’s see this in action:

  1. Create a new Angular v21 project (if you haven’t already):

    ng new angular-http-demo --standalone --routing --strict
    cd angular-http-demo
    
  2. Generate a new service to fetch data:

    ng generate service user --skip-tests
    
  3. Implement the service: Open src/app/user.service.ts and add the following code. Notice there’s no special setup for HttpClient.

    // src/app/user.service.ts
    import { Injectable, inject } from '@angular/core';
    import { HttpClient } from '@angular/common/http'; // Just import and use!
    import { Observable } from 'rxjs';
    
    export interface User {
      id: number;
      name: string;
      email: string;
    }
    
    @Injectable({
      providedIn: 'root'
    })
    export class UserService {
      private http = inject(HttpClient);
      private apiUrl = 'https://jsonplaceholder.typicode.com/users'; // Public API for demonstration
    
      getUsers(): Observable<User[]> {
        console.log('Fetching users...');
        return this.http.get<User[]>(this.apiUrl);
      }
    
      getUserById(id: number): Observable<User> {
        return this.http.get<User>(`${this.apiUrl}/${id}`);
      }
    }
    
  4. Create a component to display users:

    ng generate component user-list --skip-tests
    
  5. Implement the component: Open src/app/user-list/user-list.component.ts.

    // src/app/user-list/user-list.component.ts
    import { Component, OnInit, inject, signal } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { UserService, User } from '../user.service'; // Import the service
    
    @Component({
      selector: 'app-user-list',
      standalone: true,
      imports: [CommonModule],
      template: `
        <h2>User List</h2>
        <p *ngIf="loading()">Loading users...</p>
        <p *ngIf="error()">Error fetching users: {{ error() }}</p>
        <ul *ngIf="!loading() && users().length > 0">
          <li *ngFor="let user of users()">
            <strong>{{ user.name }}</strong> ({{ user.email }})
          </li>
        </ul>
        <p *ngIf="!loading() && users().length === 0">No users found.</p>
        <button (click)="fetchUsers()" [disabled]="loading()">Refresh Users</button>
      `,
      styles: [`
        ul { list-style-type: none; padding: 0; }
        li { background-color: #f9f9f9; margin-bottom: 5px; padding: 10px; border-radius: 4px; }
        button { margin-top: 15px; padding: 8px 15px; background-color: #007bff; color: white; border: none; cursor: pointer; }
        button:disabled { background-color: #cccccc; cursor: not-allowed; }
      `]
    })
    export class UserListComponent implements OnInit {
      private userService = inject(UserService);
    
      users = signal<User[]>([]);
      loading = signal(false);
      error = signal<string | null>(null);
    
      ngOnInit(): void {
        this.fetchUsers();
      }
    
      fetchUsers(): void {
        this.loading.set(true);
        this.error.set(null);
        this.userService.getUsers().subscribe({
          next: (data) => {
            this.users.set(data);
            this.loading.set(false);
          },
          error: (err) => {
            console.error('Failed to fetch users:', err);
            this.error.set(err.message || 'Unknown error');
            this.loading.set(false);
          }
        });
      }
    }
    
  6. Add the component to app.component.ts:

    // src/app/app.component.ts
    import { Component } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { RouterOutlet } from '@angular/router';
    import { UserListComponent } from './user-list/user-list.component'; // Import the new component
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [CommonModule, RouterOutlet, UserListComponent], // Add it to imports
      template: `
        <h1>Angular v21 HttpClient Demo</h1>
        <app-user-list></app-user-list>
        <router-outlet></router-outlet>
      `,
      styles: []
    })
    export class AppComponent {
    

title = ‘angular-http-demo’; } ```

  1. Run the application:
    ng serve
    
    Open your browser to http://localhost:4200, and you should see the list of users fetched from the API! All without explicitly adding provideHttpClient() in app.config.ts.

Why This Matters

  • Less Boilerplate: One less configuration step for every new project, making the initial setup quicker and cleaner.
  • Easier Onboarding: New developers learning Angular won’t encounter the “HttpClient not provided” error as a common stumbling block.
  • Aligns with “Just Works” Philosophy: Angular aims to provide sensible defaults, and HTTP communication is fundamental to most applications.
  • Reduced Mental Overhead: No need to recall if you’ve enabled it; you can just start injecting and using HttpClient.

When You Still Need provideHttpClient()

While HttpClient is now available by default, there are still scenarios where you’ll need to use provideHttpClient():

  • Configuring Interceptors: If you need to add HTTP interceptors (e.g., for authentication, logging, error handling), you’ll use provideHttpClient() along with withInterceptorsFromDi() or withInterceptors():

    // src/app/app.config.ts (Example for adding an interceptor)
    import { provideHttpClient, withInterceptors } from '@angular/common/http';
    import { authInterceptor } from './auth.interceptor'; // Your custom interceptor
    
    export const appConfig: ApplicationConfig = {
      providers: [
        // ...other providers
        provideHttpClient(withInterceptors([authInterceptor]))
      ]
    };
    
  • Customizing Other HTTP Client Features: If you need to configure other HttpClient features via providers (e.g., withJsonpSupport(), withXsrfConfiguration()), you’ll still use provideHttpClient() with those options.

  • Specific Testing Scenarios: While testing is also simplified, you might still use provideHttpClient() or provideHttpClientTesting() for more fine-grained control in test setups.

Mini-Challenge: Add an HTTP Interceptor (Conceptual)

Think about how you would add a simple HTTP interceptor that adds an Authorization header to every outgoing request. You don’t need to implement the full interceptor logic, just consider how you would update app.config.ts to include it, leveraging provideHttpClient().

Hint: Your auth.interceptor.ts might look like this:

// src/app/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authToken = 'YOUR_SECRET_TOKEN'; // In a real app, this would come from a service
  const authReq = req.clone({
    headers: req.headers.set('Authorization', `Bearer ${authToken}`)
  });
  return next(authReq);
};

How would app.config.ts incorporate this interceptor?

Summary/Key Takeaways

  • Angular v21 makes HttpClient available by default for new applications, eliminating the need for provideHttpClient() in basic usage.
  • This reduces boilerplate, simplifies onboarding, and aligns with Angular’s “just works” philosophy.
  • You still need to use provideHttpClient() when configuring HttpClient with features like interceptors or other specialized settings.
  • This change is a small but welcome improvement that streamlines a very common development task.

Next, we’ll dive into the intriguing, experimental world of Signal Forms, Angular’s new approach to reactive form management.