Most Angular apps are still stuck in 2019’s interceptor model. Angular 20 quietly killed it — here’s why your pipeline needs a rewrite
Most Angular applications still use interceptor architecture designed for Angular 8.
The old pattern:
@NgModule({
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
]
})
Three problems with this:
- Order is implicit — You can't visually trace the pipeline
- Tree shaking is impossible — All interceptors bundle regardless of usage
- Testing requires TestBed — No pure function testing
Angular 20+ changed everything with provideHttpClient().
The modern approach:
// main.ts
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([
authInterceptor,
retryInterceptor,
loggingInterceptor,
errorInterceptor,
cacheInterceptor
])
)
]
};
// functional interceptor
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.getToken();
const reqWithAuth = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`)
});
return next(reqWithAuth);
};
Why this wins:
✅ Composition is explicit — Order matches array sequence.
✅ Tree shakeable — Unused interceptors never hit the bundle.
✅ Pure testable — No TestBed required for unit tests.
✅ Standalone-first — No NgModule wrapper needed.
✅ inject() works — DI inside functional interceptors.
Here’s how the new explicit pipeline visually compares to the old implicit stack

Visual comparison: implicit NgModule stack vs explicit provideHttpClient array order.
The Senior Architect Golden Rule:
Networking architecture should scale as cleanly as UI architecture.
Your interceptor chain is middleware. Treat it like one.
🧱 From syntax to system design
Enterprise reality check:
Large Angular apps fail when networking concerns:
- Centralize into one monolithic interceptor.
- Mix authentication, logging, retry, and caching.
- Create circular DI dependencies.
- Block SSR with browser-only APIs.
Modern solution: Composable pipelines with isolated concerns.
// Each interceptor does ONE thing
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
retry({
count: 3,
delay: exponentialBackoff(1000, 5000)
})
);
};
Performance impact:
Old NgModule pattern: All interceptors bundle → ~8-12KB dead code
Functional pattern: Only used interceptors → 0KB dead code

Bundle size diff: Old NgModule pattern vs Functional Interceptors — 8.2 kB saved 🚀
SSR consideration:
Your interceptors need to check the execution environment:
export const browserOnlyInterceptor: HttpInterceptorFn = (req, next) => {
if (isPlatformServer(inject(PLATFORM_ID))) {
return next(req);
}
// Browser-specific logic
return next(req).pipe(tap(/* analytics */));
};
What's the most complex interceptor chain you've built in production? And is it still using HTTP_INTERCEPTORS?
If you’ve migrated to functional interceptors, share your bundle diff or lessons below — I’d love to see how your pipeline evolved.
🌐 Connect With Me
If you enjoyed this deep dive into Angular architecture and want more insights on scalable frontend systems, follow my work across platforms:
🔗 LinkedIn — Professional discussions, architecture breakdowns, and engineering insights.
📸 Instagram — Visuals, carousels, and design‑driven posts under the Terminal Elite aesthetic.
🧠 Website — Articles, tutorials, and project showcases.
🎥 YouTube — Deep‑dive videos and live coding sessions.
Top comments (0)