Angular HTTP Configuration: Before vs Standalone APIs
These three questions are closely related because Angular moved from NgModule-based HTTP configuration to provider-based HTTP configuration.
The basic evolution is:
BEFORE
HttpClientModule
+
HTTP_INTERCEPTORS
↓
NgModule configuration
NOW
provideHttpClient()
+
withInterceptors(...)
↓
Standalone provider configuration
1. How Was HttpClientModule Configured Before Standalone APIs?
What is HttpClientModule?
Before standalone APIs, Angular's HttpClient was normally made available by importing:
HttpClientModule
from:
@angular/common/http
into an Angular NgModule, usually AppModule.
Without importing HttpClientModule, Angular's dependency-injection system would not have the providers needed for HttpClient.
Traditional Architecture
main.ts
↓
AppModule
↓
imports: [HttpClientModule]
↓
HttpClient available
↓
ProductService
↓
ASP.NET Core Web API
Full Simple Program — Old NgModule Approach
Assume Angular calls:
GET https://localhost:7001/api/products
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule
],
providers: [],
bootstrap: [
AppComponent
]
})
export class AppModule {
}
The important configuration was:
imports: [
HttpClientModule
]
Once imported, HttpClient could be injected into services.
product.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface Product {
id: number;
name: string;
price: number;
}
@Injectable({
providedIn: 'root'
})
export class ProductService {
private apiUrl =
'https://localhost:7001/api/products';
constructor(private http: HttpClient) {
}
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>(
this.apiUrl
);
}
}
Because HttpClientModule had been imported, Angular could resolve:
HttpClient
when constructing ProductService.
Old HTTP Flow
AppModule
↓
HttpClientModule
↓
registers HTTP providers
↓
HttpClient
↓
ProductService
↓
http.get()
↓
ASP.NET Core Web API
2. How Is HTTP Configured Using provideHttpClient()?
With standalone Angular, we generally don't need to import HttpClientModule into a root AppModule.
Instead, HTTP is configured using:
provideHttpClient()
Modern Standalone Example
app.config.ts
import {
ApplicationConfig
} from '@angular/core';
import {
provideHttpClient
} from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient()
]
};
That's enough to make HttpClient available application-wide.
main.ts
import {
bootstrapApplication
} from '@angular/platform-browser';
import { App } from './app/app';
import { appConfig } from './app/app.config';
bootstrapApplication(App, appConfig)
.catch(err => console.error(err));
The flow is:
main.ts
↓
bootstrapApplication(App, appConfig)
↓
app.config.ts
↓
provideHttpClient()
↓
HttpClient
↓
HTTP requests
product.service.ts
The service itself is almost unchanged:
import { Injectable } from '@angular/core';
import {
HttpClient
} from '@angular/common/http';
import {
Observable
} from 'rxjs';
export interface Product {
id: number;
name: string;
price: number;
}
@Injectable({
providedIn: 'root'
})
export class ProductService {
private apiUrl =
'https://localhost:7001/api/products';
constructor(private http: HttpClient) {
}
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>(
this.apiUrl
);
}
}
Notice something important:
Old service
constructor(private http: HttpClient) {}
Modern service
constructor(private http: HttpClient) {}
The way we use HttpClient did not fundamentally change.
What changed is primarily how its providers are configured.
Old vs Modern HTTP Configuration
Before
@NgModule({
imports: [
HttpClientModule
]
})
export class AppModule {
}
Modern
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient()
]
};
Easy way to remember:
OLD
HttpClientModule
↓
imports
MODERN
provideHttpClient()
↓
providers
3. How Were Interceptors Registered Using HTTP_INTERCEPTORS?
Before functional interceptors became the preferred standalone style, interceptors were commonly implemented as classes implementing:
HttpInterceptor
They were then registered with the DI token:
HTTP_INTERCEPTORS
What Is an HTTP Interceptor?
An interceptor sits between your Angular application and HTTP communication.
For example:
Component
↓
ProductService
↓
HttpClient
↓
Interceptor
↓
ASP.NET Core API
It can perform cross-cutting operations such as:
- Add JWT tokens
- Add correlation IDs
- Log requests
- Handle errors
- Show loading indicators
For example:
Angular
↓
AuthInterceptor
↓
Authorization: Bearer <token>
↓
ASP.NET Core Web API
Full Old-Style Interceptor Example
Suppose we want to add a JWT token to every API request.
auth.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class AuthInterceptor
implements HttpInterceptor {
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
const token = 'sample-jwt-token';
const clonedRequest = req.clone({
setHeaders: {
Authorization:
`Bearer ${token}`
}
});
return next.handle(clonedRequest);
}
}
Registering the Interceptor
In AppModule:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import {
HttpClientModule,
HTTP_INTERCEPTORS
} from '@angular/common/http';
import { AppComponent } from './app.component';
import {
AuthInterceptor
} from './auth.interceptor';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
],
bootstrap: [
AppComponent
]
})
export class AppModule {
}
The important code is:
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
Understanding This Registration
provide
provide: HTTP_INTERCEPTORS
This tells Angular:
Register something against Angular's HTTP interceptor injection token.
useClass
useClass: AuthInterceptor
This tells Angular:
Use
AuthInterceptoras an interceptor implementation.
multi: true
This is particularly important.
multi: true
means:
Allow multiple providers to be registered for
HTTP_INTERCEPTORS.
Without multi-provider behavior, multiple interceptor registrations would not compose as the intended interceptor collection.
For example:
HTTP_INTERCEPTORS
│
├── AuthInterceptor
├── LoggingInterceptor
└── ErrorInterceptor
Multiple Old-Style Interceptors
For example:
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: LoggingInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: ErrorInterceptor,
multi: true
}
]
Conceptually:
HTTP Request
↓
AuthInterceptor
↓
LoggingInterceptor
↓
ErrorInterceptor
↓
ASP.NET Core Web API
The response travels back through the interceptor chain.
What Is the Modern Approach?
Modern standalone Angular supports functional interceptors, configured with:
provideHttpClient(
withInterceptors(...)
)
For example:
provideHttpClient(
withInterceptors([
authInterceptor,
loggingInterceptor,
errorInterceptor
])
)
Modern Functional Interceptor
auth.interceptor.ts
import {
HttpInterceptorFn
} from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn =
(req, next) => {
const token = 'sample-jwt-token';
const clonedRequest = req.clone({
setHeaders: {
Authorization:
`Bearer ${token}`
}
});
return next(clonedRequest);
};
Notice:
HttpInterceptorFn
instead of:
class AuthInterceptor
implements HttpInterceptor
app.config.ts
import {
ApplicationConfig
} from '@angular/core';
import {
provideHttpClient,
withInterceptors
} from '@angular/common/http';
import {
authInterceptor
} from './auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([
authInterceptor
])
)
]
};
The important code is:
provideHttpClient(
withInterceptors([
authInterceptor
])
)
Complete Modern Example With Multiple Interceptors
Suppose we have:
authInterceptor
loggingInterceptor
errorInterceptor
Configuration:
provideHttpClient(
withInterceptors([
authInterceptor,
loggingInterceptor,
errorInterceptor
])
)
Architecture:
Angular Component
↓
ProductService
↓
HttpClient
↓
authInterceptor
↓
loggingInterceptor
↓
errorInterceptor
↓
ASP.NET Core Web API
Old vs Modern Interceptors
Old Class-Based Approach
Interceptor
@Injectable()
export class AuthInterceptor
implements HttpInterceptor {
intercept(
req: HttpRequest<any>,
next: HttpHandler
) {
return next.handle(req);
}
}
Registration
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
Modern Functional Approach
Interceptor
export const authInterceptor: HttpInterceptorFn =
(req, next) => {
return next(req);
};
Registration
provideHttpClient(
withInterceptors([
authInterceptor
])
)
This is substantially simpler.
Important: Can Class-Based Interceptors Still Be Used?
Yes. This is useful when maintaining or migrating an older application.
With provider-based HTTP configuration, DI-provided/class-based interceptors can be included using:
withInterceptorsFromDi()
For example:
import {
HTTP_INTERCEPTORS,
provideHttpClient,
withInterceptorsFromDi
} from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
},
provideHttpClient(
withInterceptorsFromDi()
)
]
};
So:
Class interceptor
↓
HTTP_INTERCEPTORS
↓
withInterceptorsFromDi()
↓
provideHttpClient()
However, for new standalone code, functional interceptors are generally the cleaner approach.
Complete Evolution
Traditional Angular
AppModule
│
├── imports
│ ↓
│ HttpClientModule
│
└── providers
↓
HTTP_INTERCEPTORS
↓
AuthInterceptor
Code:
@NgModule({
imports: [
HttpClientModule
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
]
})
export class AppModule {
}
Modern Standalone Angular
app.config.ts
↓
provideHttpClient()
↓
withInterceptors()
↓
Functional interceptors
Code:
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([
authInterceptor,
errorInterceptor
])
)
]
};
Side-by-Side Comparison
| Before standalone APIs | Modern standalone approach |
|---|---|
HttpClientModule |
provideHttpClient() |
NgModule imports |
Application providers |
| Class interceptor | Functional interceptor preferred |
HttpInterceptor |
HttpInterceptorFn |
HTTP_INTERCEPTORS |
withInterceptors() |
useClass |
Function reference |
multi: true |
Interceptor array |
next.handle(req) |
next(req) |
Key Points
Old HTTP configuration
imports: [
HttpClientModule
]
was used to configure Angular HTTP support.
Modern HTTP configuration
providers: [
provideHttpClient()
]
is the provider-based approach.
Old interceptor
@Injectable()
export class AuthInterceptor
implements HttpInterceptor {
}
registered using:
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
Modern interceptor
export const authInterceptor: HttpInterceptorFn =
(req, next) => {
return next(req);
};
registered using:
provideHttpClient(
withInterceptors([
authInterceptor
])
)
Easy way to remember
OLD
HttpClientModule
HTTP_INTERCEPTORS
HttpInterceptor
next.handle(req)
NOW
provideHttpClient()
withInterceptors()
HttpInterceptorFn
next(req)
Interview Questions and Answers
1. How was HttpClient configured before standalone APIs?
HttpClientModule was imported into an NgModule, usually AppModule.
imports: [
HttpClientModule
]
This registered the providers required for HttpClient.
2. How is HttpClient configured in standalone Angular?
Use:
provideHttpClient()
typically in app.config.ts:
providers: [
provideHttpClient()
]
3. Does using HttpClient inside a service change?
Not significantly.
You can still inject it:
constructor(private http: HttpClient) {}
or use:
private http = inject(HttpClient);
The main change is how HTTP support is provided/configured.
4. How were interceptors traditionally created?
They were classes implementing:
HttpInterceptor
Example:
@Injectable()
export class AuthInterceptor
implements HttpInterceptor {
}
5. How were class-based interceptors registered?
Using:
HTTP_INTERCEPTORS
as a multi-provider:
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
6. Why was multi: true required?
Because Angular needed to allow multiple interceptors to be registered under the same HTTP_INTERCEPTORS token.
HTTP_INTERCEPTORS
├── AuthInterceptor
├── LoggingInterceptor
└── ErrorInterceptor
7. What is the modern interceptor approach?
Modern Angular commonly uses functional interceptors:
export const authInterceptor: HttpInterceptorFn =
(req, next) => next(req);
registered with:
provideHttpClient(
withInterceptors([
authInterceptor
])
)
8. What is withInterceptors()?
It configures functional HTTP interceptors for the HttpClient instance provided by provideHttpClient().
provideHttpClient(
withInterceptors([
authInterceptor,
errorInterceptor
])
)
9. Can multiple functional interceptors be registered?
Yes.
withInterceptors([
authInterceptor,
loggingInterceptor,
errorInterceptor
])
The order matters because they form an interceptor chain.
10. Can old class-based interceptors still work with provideHttpClient()?
Yes. DI-provided interceptors can be included using:
provideHttpClient(
withInterceptorsFromDi()
)
along with their HTTP_INTERCEPTORS provider registrations.
Interview Quick Answer
Before standalone APIs, Angular HTTP support was configured by importing
HttpClientModuleinto an NgModule, and class-based interceptors were registered using theHTTP_INTERCEPTORSmulti-provider. In modern standalone Angular, HTTP is configured withprovideHttpClient(), and functional interceptors are commonly registered usingwithInterceptors().