DEV Community

Cover image for 🔀Advanced provideHttpClient Interceptors
abdelaaziz ouakala
abdelaaziz ouakala

Posted on

🔀Advanced provideHttpClient Interceptors

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 }
  ]
})
Enter fullscreen mode Exit fullscreen mode

Three problems with this:

  1. Order is implicit — You can't visually trace the pipeline
  2. Tree shaking is impossible — All interceptors bundle regardless of usage
  3. 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);
};
Enter fullscreen mode Exit fullscreen mode

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

Interceptor pipeline diagram
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)
    })
  );
};
Enter fullscreen mode Exit fullscreen mode

Performance impact:

Old NgModule pattern: All interceptors bundle → ~8-12KB dead code
Functional pattern: Only used interceptors → 0KB dead code

Angular bundle size comparison
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 */));
};
Enter fullscreen mode Exit fullscreen mode

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)