DEV Community

Cover image for HTTP Interceptors: The Complete Guide to Request & Response Handling in React and Angular
Jack Pritom Soren
Jack Pritom Soren

Posted on AI-assisted

HTTP Interceptors: The Complete Guide to Request & Response Handling in React and Angular

Introduction: The Problem Before Interceptors

Imagine you're building a real-world application. Your API calls need authentication headers on every single request. Your error responses need to refresh expired tokens. Failed requests should retry. Loading states should show spinners. Response times should be logged for debugging. And oh, some requests need to cache their results to avoid hammering the server.

Now imagine doing all of that inside every component that makes an API call. You'd be copy-pasting the same validation, error-handling, and auth logic across fifty different fetch statements. Your codebase becomes a nightmare of repetition. Bugs multiply. Maintenance becomes a horror show.

This is why HTTP interceptors exist. They're middleware for your HTTP layer — a central place where you can inspect, modify, and handle every request and response flowing through your application. Once you set them up, every API call automatically benefits from your authentication logic, error handling, retry policies, and logging without a single line of duplicate code.

This guide will take you from "what even is an interceptor?" through production-grade implementations in both React and Angular. We'll build real examples, hit the edge cases, and show you how to think about interceptor architecture so you're not wrestling with them six months from now.


What We'll Cover

  1. Core Concepts — What interceptors actually are and why you need them
  2. The Mental Model — How to think about request and response flow
  3. React + Axios — Setting up interceptors in React with working examples
  4. React in Production — Token refresh, 401 handling, and common mistakes
  5. Angular Interceptors — The functional approach and architectural patterns
  6. Angular in Production — Multiple interceptors, ordering, and complex scenarios
  7. React vs Angular — Direct comparison and when to use each approach
  8. Limitations & Production Architecture — What interceptors can't do and how to handle it

Part 1: Core Concepts

What Is an HTTP Interceptor?

An HTTP interceptor is a function (or class, depending on the framework) that sits between your application and the network. Every HTTP request passes through it before hitting the server. Every response passes through it after leaving the server. You can intercept at either or both stages.

Think of it like a mail sorter at a post office:

  • Request interceptor = the sorter checks outgoing mail, adds postage and routing info, maybe redirects some packages
  • Response interceptor = the sorter receives incoming mail, verifies stamps, opens unopened packages, and sorts them by recipient

The key idea: you intercept, you inspect, you can modify, then you pass it along (or stop it entirely if something's wrong).

interceptors

Why Do We Need Interceptors?

Without interceptors, you'd handle every cross-cutting concern (auth, errors, logging) inside every component. With interceptors, you handle it once, globally.

Real-world scenarios interceptors solve:

  • Authentication — Add JWT tokens to every request without thinking about it
  • Error handling — Catch 401s globally, refresh the token, retry the request automatically
  • Loading states — Show spinners by incrementing a global counter, hide when count hits zero
  • Logging — Log every request/response for debugging and analytics
  • Request modification — Add timestamps, version headers, request IDs for tracing
  • Caching — Return cached data for GET requests, skip the network entirely
  • Retry logic — Automatically retry failed requests with exponential backoff
  • Response transformation — Unwrap API responses, normalize data shape

Each of these, without interceptors, would need to be duplicated across 20+ components. With interceptors, it's centralized, testable, and maintainable.


Part 2: The Interceptor Mental Model

Here's how to think about request/response flow:

┌─────────────────────────────────────────────────────────────┐
│ Your Component: this.http.get('/api/users')                 │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
        ┌──────────────────────────────────────┐
        │  Request Interceptor #1              │
        │  (add auth header, request ID, etc)  │
        └──────────────────┬───────────────────┘
                           │
                           ▼
        ┌──────────────────────────────────────┐
        │  Request Interceptor #2              │
        │  (maybe cache check, logging)        │
        └──────────────────┬───────────────────┘
                           │
                           ▼
                  ┌─────────────────┐
                  │ Network Request │
                  │ (actual HTTP)   │
                  └─────────┬───────┘
                           │
                           ▼
        ┌──────────────────────────────────────┐
        │  Response Interceptor #2             │
        │  (runs in *reverse* order)           │
        └──────────────────┬───────────────────┘
                           │
                           ▼
        ┌──────────────────────────────────────┐
        │  Response Interceptor #1             │
        │  (final processing)                  │
        └──────────────────┬───────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│ Your Component: .then(data => console.log(data))            │
└─────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Key insight: Multiple interceptors run in sequence on the request, then in reverse order on the response. This matters when one interceptor depends on another.


Part 3: Request Interceptors

A request interceptor runs before the HTTP request leaves your app. You can:

  • Modify the request (add headers, change the URL, add body data)
  • Reject the request entirely (throw an error or return a rejection)
  • Skip the network and return cached data
  • Track the request (for loading counters, analytics)

General structure:

// Pseudocode — exact syntax varies by framework
const requestInterceptor = (config) => {
  // Modify the config object
  config.headers.Authorization = `Bearer ${token}`;
  config.headers['X-Request-ID'] = generateRequestId();

  // Return the modified config to let it continue
  return config;

  // Or reject it (this stops the request):
  // throw new Error('Request blocked');
};
Enter fullscreen mode Exit fullscreen mode

Common Request Interceptor Patterns

Pattern 1: Adding auth headers

// Every request gets a JWT token
interceptor.request = (config) => {
  const token = localStorage.getItem('access_token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
};
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Adding metadata

// Add a unique request ID and timestamp for tracing
interceptor.request = (config) => {
  config.headers['X-Request-ID'] = `req-${Date.now()}-${Math.random()}`;
  config.headers['X-Timestamp'] = new Date().toISOString();
  return config;
};
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Modifying the URL

// Add query params or change the base URL dynamically
interceptor.request = (config) => {
  config.url = `${process.env.REACT_APP_API_URL}${config.url}`;
  // Also works: config.params = { ...config.params, v: '2' };
  return config;
};
Enter fullscreen mode Exit fullscreen mode

Part 4: Response Interceptors

A response interceptor runs after the HTTP response returns. Here's what you can do:

  • Transform the response (unwrap nested data, normalize shape)
  • Check the status code and decide what to do
  • Retry the request if it failed
  • Refresh auth tokens if they've expired
  • Log the response
  • Throw errors for upstream .catch() handlers

General structure:

const responseInterceptor = {
  success: (response) => {
    // Handle 2xx responses
    // Maybe unwrap the data
    return response.data; // or return response as-is
  },

  error: (error) => {
    // Handle 4xx, 5xx responses
    // Maybe retry, refresh token, or transform the error
    throw error; // or return recovery
  }
};
Enter fullscreen mode Exit fullscreen mode

Common Response Interceptor Patterns

Pattern 1: Unwrapping API responses

// Many APIs wrap data like: { status: 'ok', data: [...], errors: null }
// Unwrap to just get the data
interceptor.response.success = (response) => {
  return response.data.data; // Flatten the response
};
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Handling specific status codes

interceptor.response.error = (error) => {
  if (error.response?.status === 401) {
    // Token expired — refresh it
    return refreshTokenAndRetry(error.config);
  }

  if (error.response?.status === 429) {
    // Rate limited — retry after delay
    return retryWithBackoff(error.config);
  }

  // For other errors, rethrow
  throw error;
};
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Logging

interceptor.response.success = (response) => {
  console.log(`✅ ${response.config.method.toUpperCase()} ${response.config.url}`);
  return response.data;
};

interceptor.response.error = (error) => {
  console.error(
    `❌ ${error.config.method.toUpperCase()} ${error.config.url}`,
    error.response?.status,
    error.message
  );
  throw error;
};
Enter fullscreen mode Exit fullscreen mode

Part 5: Common Interceptor Use Cases

Use Case 1: Global Authentication

// Every request automatically includes the user's token
// No component needs to think about auth

const authInterceptor = (config) => {
  const token = getStoredToken();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
};
Enter fullscreen mode Exit fullscreen mode

Use Case 2: Handling 401 (Token Expired)

// If the server says "your token expired," refresh it and retry
const handleUnauthorized = async (error) => {
  if (error.response?.status === 401) {
    try {
      const newToken = await refreshAccessToken();
      storeToken(newToken);

      // Retry the original request with new token
      error.config.headers.Authorization = `Bearer ${newToken}`;
      return httpClient.request(error.config);
    } catch {
      // Refresh failed — log the user out
      redirectToLogin();
      throw error;
    }
  }
  throw error;
};
Enter fullscreen mode Exit fullscreen mode

Use Case 3: Loading State Management

let activeRequests = 0;

const loadingInterceptor = {
  request: (config) => {
    activeRequests++;
    showLoadingSpinner(); // Assume this function exists
    return config;
  },

  response: {
    success: (response) => {
      activeRequests--;
      if (activeRequests === 0) hideLoadingSpinner();
      return response;
    },

    error: (error) => {
      activeRequests--;
      if (activeRequests === 0) hideLoadingSpinner();
      throw error;
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Use Case 4: Retry with Exponential Backoff

const retryInterceptor = {
  response: {
    error: async (error) => {
      // Only retry on specific status codes (5xx, timeout, etc)
      const shouldRetry = 
        error.response?.status >= 500 || 
        error.code === 'ECONNABORTED';

      if (!shouldRetry || error.config.retryCount >= 3) {
        throw error;
      }

      error.config.retryCount = (error.config.retryCount || 0) + 1;
      const delay = Math.pow(2, error.config.retryCount) * 1000; // 2s, 4s, 8s

      await new Promise(resolve => setTimeout(resolve, delay));
      return httpClient.request(error.config);
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Use Case 5: Caching GET Requests

const cache = new Map();

const cachingInterceptor = {
  request: (config) => {
    // Only cache GET requests
    if (config.method === 'get' && cache.has(config.url)) {
      // Return cached response — skip the network entirely
      const cachedResponse = cache.get(config.url);
      return Promise.resolve(cachedResponse);
    }
    return config;
  },

  response: {
    success: (response) => {
      if (response.config.method === 'get') {
        cache.set(response.config.url, response);
      }
      return response;
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Part 6: Interceptor vs Middleware

Middleware (like Express middleware) runs on the server.
Interceptors run on the client, in the browser (or Node.js if you're using an HTTP client there).

Aspect Middleware Interceptor
Where Server-side Client-side
Runs on Every incoming request to the server Every outgoing/incoming request from the app
Controls Request routing, authentication, logging Request headers, response transformation, caching
Framework Express, Koa, Django, etc. Axios, Fetch API, Angular HTTP client
Use for Rate limiting, parsing body, CORS Auth tokens, error handling, retries

When you need both:

Client App
    │
    ├─ Interceptor (add auth token)
    │
    ▼ (Network)
Server
    ├─ Middleware (verify token, log request)
    │
    ▼ (Route handler)
Database
Enter fullscreen mode Exit fullscreen mode

Good architecture uses both: interceptors handle client-side concerns (auth, caching, retries), middleware handles server-side concerns (CORS, body parsing, rate limiting).


Part 7: React and HTTP Clients

React doesn't have built-in interceptors like Angular does. Instead, you use HTTP libraries like Axios or custom hooks wrapping the Fetch API.

Why Axios?

Axios is popular because:

  1. Simple interceptor syntax (built-in support)
  2. Automatic JSON transformation
  3. Request cancellation (useful for cleanup)
  4. Timeout support

Alternative: Fetch + Custom Hook

You can build interceptors with Fetch, but it requires more boilerplate. Here's the pattern:

// Custom HTTP client using Fetch
class HttpClient {
  constructor() {
    this.requestInterceptors = [];
    this.responseInterceptors = [];
  }

  async request(url, options = {}) {
    // Run request interceptors
    let config = { url, ...options };
    for (const interceptor of this.requestInterceptors) {
      config = await interceptor(config);
    }

    // Make request
    const response = await fetch(config.url, config);

    // Run response interceptors
    let result = response;
    for (const interceptor of this.responseInterceptors) {
      result = await interceptor(result);
    }

    return result;
  }
}
Enter fullscreen mode Exit fullscreen mode

For this guide, we'll focus on Axios because it's simpler and more widely used in React apps. The concepts apply to Fetch or other clients too.


Part 8: React + Axios Setup

Step 1: Install Axios

npm install axios
Enter fullscreen mode Exit fullscreen mode

Step 2: Create an Axios Instance

// api/client.js
import axios from 'axios';

export const httpClient = axios.create({
  baseURL: process.env.REACT_APP_API_URL,
  timeout: 10000, // 10 second timeout
});

export default httpClient;
Enter fullscreen mode Exit fullscreen mode

Why create an instance instead of using axios.default?

  • You can have multiple HTTP clients with different interceptors (e.g., one for your API, one for third-party services)
  • Easier to test (you can mock the instance)
  • Cleaner separation of concerns

Step 3: Add Your First Interceptor

// api/client.js
import axios from 'axios';

const httpClient = axios.create({
  baseURL: process.env.REACT_APP_API_URL,
});

// Request interceptor: add auth token
httpClient.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('access_token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    // If something fails *before* the request is sent, handle it here
    return Promise.reject(error);
  }
);

// Response interceptor: handle errors
httpClient.interceptors.response.use(
  (response) => {
    // Any 2xx status code comes here
    return response.data; // Unwrap the data
  },
  (error) => {
    // Any non-2xx status code comes here
    console.error('API Error:', error.response?.status, error.message);
    return Promise.reject(error);
  }
);

export default httpClient;
Enter fullscreen mode Exit fullscreen mode

Step 4: Use in Components

// components/UserList.jsx
import { useEffect, useState } from 'react';
import httpClient from '../api/client';

export default function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsers = async () => {
      setLoading(true);
      try {
        // httpClient automatically applies all interceptors
        const data = await httpClient.get('/users');
        setUsers(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

That's it. The token is automatically included. Errors are automatically logged. Your component stays simple.


Part 9: Axios Request Interceptor Deep Dive

Adding Multiple Headers

httpClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('access_token');

  // Add auth
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }

  // Add request ID for tracing (helps with debugging)
  config.headers['X-Request-ID'] = generateUUID();

  // Add API version
  config.headers['X-API-Version'] = '2.0';

  // Add user language preference
  config.headers['Accept-Language'] = getUserLanguage();

  return config;
});
Enter fullscreen mode Exit fullscreen mode

Conditionally Adding Headers

// Only add auth headers for your own API, not for third-party requests
httpClient.interceptors.request.use((config) => {
  const isInternalAPI = config.url.startsWith('/api');

  if (isInternalAPI) {
    const token = localStorage.getItem('access_token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
  }

  return config;
});
Enter fullscreen mode Exit fullscreen mode

Logging Request Details

httpClient.interceptors.request.use((config) => {
  const timestamp = new Date().toISOString();
  const method = config.method.toUpperCase();

  console.log(`[${timestamp}] 📤 ${method} ${config.url}`);
  console.log('Headers:', config.headers);
  if (config.data) console.log('Body:', config.data);

  return config;
});
Enter fullscreen mode Exit fullscreen mode

Handling Request Errors (Rare)

Request errors happen when:

  • Headers are invalid
  • Request can't be constructed
  • Local validation fails
httpClient.interceptors.request.use(
  (config) => {
    // ✅ Happy path
    return config;
  },
  (error) => {
    // ❌ Error path — this rarely happens, but handle it
    console.error('Request construction failed:', error);
    return Promise.reject(error);
  }
);
Enter fullscreen mode Exit fullscreen mode

Part 10: Axios Response Interceptor Deep Dive

Unwrapping Nested API Responses

Most APIs wrap their responses. Your interceptor can flatten them:

// API returns: { success: true, data: [...], errors: null, meta: { page: 1 } }
// Your app should just get the array

httpClient.interceptors.response.use(
  (response) => {
    // Assume this shape and unwrap it
    if (response.data?.data !== undefined) {
      return response.data.data;
    }
    // Fallback to raw response
    return response.data;
  }
);
Enter fullscreen mode Exit fullscreen mode

Logging Responses

httpClient.interceptors.response.use(
  (response) => {
    const method = response.config.method.toUpperCase();
    const status = response.status;
    const url = response.config.url;

    console.log(`✅ ${status} ${method} ${url}`);
    return response.data;
  },
  (error) => {
    const method = error.config?.method?.toUpperCase() || 'UNKNOWN';
    const status = error.response?.status || 'ERROR';
    const url = error.config?.url || 'UNKNOWN';

    console.error(`❌ ${status} ${method} ${url} - ${error.message}`);
    return Promise.reject(error);
  }
);
Enter fullscreen mode Exit fullscreen mode

Transforming Response Data

// Transform timestamps to Date objects, format names, etc.
httpClient.interceptors.response.use((response) => {
  const data = response.data;

  // If it's an array of users, transform each
  if (Array.isArray(data)) {
    return data.map(transformUser);
  }

  // If it's a single user
  if (data.id && data.name) {
    return transformUser(data);
  }

  return data;
});

function transformUser(user) {
  return {
    ...user,
    createdAt: new Date(user.created_at), // Parse timestamp
    displayName: user.name.toUpperCase(),  // Format name
  };
}
Enter fullscreen mode Exit fullscreen mode

Part 11: Handling 401 (Unauthorized) in React

This is the production scenario everyone hits: your JWT token expires mid-session. The server rejects it with 401. What do you do?

Option A: Simple approach — just log them out

httpClient.interceptors.response.use(
  (response) => response.data,
  (error) => {
    if (error.response?.status === 401) {
      // Token expired — clear auth and redirect to login
      localStorage.removeItem('access_token');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);
Enter fullscreen mode Exit fullscreen mode

Problem: Jarring user experience. They lose their work.

Option B: Smart approach — refresh the token and retry

let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) => {
  failedQueue.forEach(prom => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token);
    }
  });
  failedQueue = [];
};

httpClient.interceptors.response.use(
  (response) => response.data,
  async (error) => {
    const { config } = error;

    // Only handle 401 for specific endpoints
    if (error.response?.status === 401 && config.url !== '/auth/refresh') {

      if (!isRefreshing) {
        isRefreshing = true;

        try {
          // Make refresh request
          const { data } = await axios.post(
            `${process.env.REACT_APP_API_URL}/auth/refresh`,
            { refreshToken: localStorage.getItem('refresh_token') }
          );

          // Store new token
          localStorage.setItem('access_token', data.accessToken);

          // Update default header
          httpClient.defaults.headers.common.Authorization = 
            `Bearer ${data.accessToken}`;

          // Resolve queued requests with new token
          processQueue(null, data.accessToken);

          // Retry original request
          config.headers.Authorization = `Bearer ${data.accessToken}`;
          return httpClient(config);

        } catch (refreshError) {
          // Refresh failed — clear everything and log out
          localStorage.removeItem('access_token');
          localStorage.removeItem('refresh_token');
          processQueue(refreshError, null);
          window.location.href = '/login';
          return Promise.reject(refreshError);
        } finally {
          isRefreshing = false;
        }
      }

      // If already refreshing, queue this request until refresh completes
      return new Promise((resolve, reject) => {
        failedQueue.push({ resolve, reject });
      }).then((token) => {
        config.headers.Authorization = `Bearer ${token}`;
        return httpClient(config);
      });
    }

    return Promise.reject(error);
  }
);
Enter fullscreen mode Exit fullscreen mode

This is more complex, but it's production-grade. Here's what happens:

  1. First 401 arrives → Start refresh, queue other failed requests
  2. More 401s arrive → They get queued, not all sent at once
  3. Refresh completes → Process the queue, retry all queued requests
  4. Refresh fails → Log out the user

Part 12: Token Refresh in React (Complete Example)

Here's a complete, copy-paste-ready setup:

// api/client.js
import axios from 'axios';

const httpClient = axios.create({
  baseURL: process.env.REACT_APP_API_URL,
  timeout: 10000,
});

// State for token refresh queue
let isRefreshing = false;
let refreshSubscribers = [];

// Subscribe to token refresh completion
const subscribeTokenRefresh = (callback) => {
  refreshSubscribers.push(callback);
};

// Notify all subscribers that token was refreshed
const onRefreshed = (token) => {
  refreshSubscribers.forEach(callback => callback(token));
  refreshSubscribers = [];
};

// Request interceptor: add auth token
httpClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('access_token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor: handle 401 and refresh
httpClient.interceptors.response.use(
  (response) => response.data,

  async (error) => {
    const { config } = error;

    // Not a 401, or it's the refresh endpoint itself
    if (error.response?.status !== 401 || config.url.includes('/auth/refresh')) {
      return Promise.reject(error);
    }

    if (!isRefreshing) {
      isRefreshing = true;

      try {
        // Make the refresh request (use raw axios to avoid infinite loops)
        const { data } = await axios.post(
          `${process.env.REACT_APP_API_URL}/auth/refresh`,
          { refreshToken: localStorage.getItem('refresh_token') },
          { timeout: 10000 }
        );

        // Store new tokens
        localStorage.setItem('access_token', data.accessToken);
        if (data.refreshToken) {
          localStorage.setItem('refresh_token', data.refreshToken);
        }

        // Update client's default header for future requests
        httpClient.defaults.headers.common.Authorization = 
          `Bearer ${data.accessToken}`;

        // Notify subscribers and clear the queue
        onRefreshed(data.accessToken);

        // Retry the original request
        config.headers.Authorization = `Bearer ${data.accessToken}`;
        return httpClient(config);

      } catch (refreshError) {
        // Refresh failed — clear tokens and redirect
        localStorage.removeItem('access_token');
        localStorage.removeItem('refresh_token');

        onRefreshed(null); // Clear subscribers without token

        // Redirect to login
        if (typeof window !== 'undefined') {
          window.location.href = '/login';
        }

        return Promise.reject(refreshError);

      } finally {
        isRefreshing = false;
      }
    }

    // While refresh is happening, queue this request
    return new Promise((resolve) => {
      subscribeTokenRefresh((token) => {
        if (token) {
          // Refresh succeeded, retry with new token
          config.headers.Authorization = `Bearer ${token}`;
          resolve(httpClient(config));
        } else {
          // Refresh failed, reject
          resolve(Promise.reject(error));
        }
      });
    });
  }
);

export default httpClient;
Enter fullscreen mode Exit fullscreen mode

Usage in a component:

// components/Dashboard.jsx
import { useEffect, useState } from 'react';
import httpClient from '../api/client';

export default function Dashboard() {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        // If token expired, interceptor handles refresh automatically
        const result = await httpClient.get('/api/dashboard');
        setData(result);
      } catch (error) {
        // If refresh failed, we get an error
        console.error('Failed to fetch dashboard:', error);
      }
    };

    fetchData();
  }, []);

  return data ? <div>{/* render */}</div> : <p>Loading...</p>;
}
Enter fullscreen mode Exit fullscreen mode

Part 13: Common React Interceptor Mistakes

Mistake 1: Adding Interceptors in Every Component

Wrong:

// components/UserList.jsx
useEffect(() => {
  const interceptor = httpClient.interceptors.request.use((config) => {
    config.headers.Authorization = `Bearer ${token}`;
    return config;
  });

  return () => {
    httpClient.interceptors.response.eject(interceptor);
  };
}, [token]);
Enter fullscreen mode Exit fullscreen mode

This adds a new interceptor every time the component mounts. You'll have 50 interceptors by the end of the day.

Right:

// api/client.js (once, at app startup)
httpClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('access_token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});
Enter fullscreen mode Exit fullscreen mode

Set up interceptors once when the app starts, not in components.

Mistake 2: Infinite Refresh Loops

Wrong:

httpClient.interceptors.response.use(
  (response) => response.data,
  (error) => {
    // Every 401 tries to refresh, even if refresh itself gets 401
    if (error.response?.status === 401) {
      return refreshToken().then(() => httpClient(error.config));
    }
    return Promise.reject(error);
  }
);
Enter fullscreen mode Exit fullscreen mode

If refreshToken() returns 401 (refresh token expired), you loop forever.

Right:

if (error.response?.status === 401 && !error.config.url.includes('/auth/refresh')) {
  // Skip 401 handling if this *is* the refresh request
  return refreshToken().then(() => httpClient(error.config));
}
Enter fullscreen mode Exit fullscreen mode

Mistake 3: Not Unwrapping Responses

Wrong:

// Your API returns { data: [...], errors: null, success: true }
// You map over response.data.data everywhere
const users = response.data.data.map(u => u.name);
Enter fullscreen mode Exit fullscreen mode

Repeated everywhere. Inconsistent. Fragile.

Right:

// In the response interceptor
httpClient.interceptors.response.use((response) => {
  return response.data?.data || response.data;
});

// Now in components
const users = data.map(u => u.name); // Clean!
Enter fullscreen mode Exit fullscreen mode

Mistake 4: Swallowing Errors

Wrong:

httpClient.interceptors.response.use(
  (response) => response.data,
  (error) => response.data // 🚨 returns undefined!
);
Enter fullscreen mode Exit fullscreen mode

Also wrong:

httpClient.interceptors.response.use(
  (response) => response.data,
  (error) => null // Returns null instead of error
);
Enter fullscreen mode Exit fullscreen mode

Errors disappear silently. Debugging becomes a nightmare.

Right:

httpClient.interceptors.response.use(
  (response) => response.data,
  (error) => {
    // Log it for debugging
    console.error('API error:', error);

    // Handle specific cases
    if (error.response?.status === 401) {
      // Handle auth
    }

    // Always reject for upstream handlers
    return Promise.reject(error);
  }
);
Enter fullscreen mode Exit fullscreen mode

Mistake 5: Blocking on Interceptors

Wrong:

httpClient.interceptors.request.use(async (config) => {
  // Synchronous code, not async!
  const token = await getTokenFromServer(); // ❌ blocks everything
  config.headers.Authorization = `Bearer ${token}`;
  return config;
});
Enter fullscreen mode Exit fullscreen mode

Request interceptors should be synchronous (or resolve immediately). Async logic blocks all requests.

Right:

// Pre-fetch tokens before the request
httpClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('access_token');
  config.headers.Authorization = `Bearer ${token}`;
  return config;
});
Enter fullscreen mode Exit fullscreen mode

Or refresh tokens proactively before they expire, not in the interceptor.


Part 14: Angular and HTTP Interceptors

Angular has first-class interceptor support baked into its HTTP client. Unlike React (where you add Axios), Angular's HttpClient is designed around interceptors from the ground up.

Angular Interceptor API

// An interceptor implements the HttpInterceptor interface
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Modify request
    const authReq = req.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });

    // Pass to next interceptor, or network if this is the last one
    return next.handle(authReq);
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Angular concepts:

  • HttpRequest — immutable request object (you .clone() it to modify)
  • HttpHandler — next step in the chain (could be another interceptor or the network)
  • HttpEvent — response (could be a full response, a progress event, etc.)
  • Observable — RxJS stream (you can pipe, retry, catch, etc.)

Registering Interceptors

// app.config.ts (Angular 14+) or app.module.ts

import { HttpInterceptor, provideHttpClient, withInterceptors } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor, errorInterceptor, loadingInterceptor])
    ),
  ],
};
Enter fullscreen mode Exit fullscreen mode

Or with the old module-based approach:

// app.module.ts
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';

@NgModule({
  imports: [HttpClientModule],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
    { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Part 15: Angular Functional Interceptors (Modern Approach)

Angular 15+ introduced functional interceptors, which are simpler than class-based ones.

// interceptors/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const token = authService.getToken();

  if (token) {
    // Clone and add Authorization header
    req = req.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
  }

  return next(req);
};
Enter fullscreen mode Exit fullscreen mode

Much simpler! No class boilerplate, no @Injectable(), no interface to implement.

Registration:

// app.config.ts
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './interceptors/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor]) // Add as many as you want
    ),
  ],
};
Enter fullscreen mode Exit fullscreen mode

Part 16: Modifying Angular Requests

Adding Headers

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = localStorage.getItem('access_token');

  // HttpRequest is immutable — you clone it to modify
  const authReq = req.clone({
    setHeaders: {
      Authorization: token ? `Bearer ${token}` : '',
      'X-Request-ID': generateRequestId(),
      'X-API-Version': '2.0',
    }
  });

  return next(authReq);
};
Enter fullscreen mode Exit fullscreen mode

Adding Query Parameters

export const versionInterceptor: HttpInterceptorFn = (req, next) => {
  // Add v=2 to all requests
  const versionedReq = req.clone({
    setParams: {
      v: '2'
    }
  });

  return next(versionedReq);
};
Enter fullscreen mode Exit fullscreen mode

Modifying URL

export const baseUrlInterceptor: HttpInterceptorFn = (req, next) => {
  // Only for relative URLs
  if (!req.url.startsWith('http')) {
    const apiUrl = 'https://api.example.com';
    req = req.clone({
      url: `${apiUrl}${req.url}`
    });
  }

  return next(req);
};
Enter fullscreen mode Exit fullscreen mode

Modifying Body

export const bodyInterceptor: HttpInterceptorFn = (req, next) => {
  if (req.method === 'POST' && req.body) {
    // Add timestamp to all POST requests
    const modifiedBody = {
      ...req.body,
      _timestamp: new Date().toISOString(),
    };

    req = req.clone({
      body: modifiedBody
    });
  }

  return next(req);
};
Enter fullscreen mode Exit fullscreen mode

Part 17: Angular Response Handling

Response interceptors in Angular are handled via RxJS operators on the Observable:

import { tap, catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';

export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
  console.log(`📤 ${req.method} ${req.url}`);

  return next(req).pipe(
    tap((event) => {
      // Runs on successful response
      if (event instanceof HttpResponse) {
        console.log(`✅ ${event.status} ${event.statusText}`);
      }
    }),

    catchError((error) => {
      // Runs on error
      console.error(`❌ ${error.status} ${error.statusText}`);
      return throwError(() => error);
    })
  );
};
Enter fullscreen mode Exit fullscreen mode

Unwrapping API Responses

import { map } from 'rxjs/operators';

export const unwrapInterceptor: HttpInterceptorFn = (req, next) => {
  return next(req).pipe(
    map((event) => {
      // Only transform the actual response, not upload/download progress events
      if (event instanceof HttpResponse) {
        // Unwrap from API response structure
        return event.clone({
          body: event.body?.data || event.body
        });
      }
      return event;
    })
  );
};
Enter fullscreen mode Exit fullscreen mode

Transforming Data

export const transformInterceptor: HttpInterceptorFn = (req, next) => {
  return next(req).pipe(
    map((event) => {
      if (event instanceof HttpResponse && Array.isArray(event.body)) {
        // Transform each item
        return event.clone({
          body: event.body.map(item => ({
            ...item,
            createdAt: new Date(item.created_at),
          }))
        });
      }
      return event;
    })
  );
};
Enter fullscreen mode Exit fullscreen mode

Part 18: Multiple Angular Interceptors and Ordering

Order matters. Interceptors run in the order they're registered:

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([
        // 1. First: add request ID for tracing
        requestIdInterceptor,

        // 2. Second: add auth token
        authInterceptor,

        // 3. Third: log requests
        loggingInterceptor,

        // 4. Network request happens here

        // 5. Fourth (reverse): handle errors globally
        errorInterceptor,

        // 6. Fifth (reverse): transform responses
        transformInterceptor,
      ])
    ),
  ],
};
Enter fullscreen mode Exit fullscreen mode

Request flow (↓):

Component
  ↓
requestIdInterceptor (add X-Request-ID)
  ↓
authInterceptor (add Authorization)
  ↓
loggingInterceptor (log to console)
  ↓
Network
Enter fullscreen mode Exit fullscreen mode

Response flow (↑):

Network Response
  ↓
transformInterceptor (unwrap/transform data)
  ↓
errorInterceptor (catch/retry)
  ↓
loggingInterceptor (log response)
  ↓
Component
Enter fullscreen mode Exit fullscreen mode

Part 19: Authentication Interceptor (Angular)

// interceptors/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);

  // Get token (could be from localStorage, service, etc.)
  const token = authService.getAccessToken();

  if (token) {
    // Clone and add Authorization header
    req = req.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
  }

  return next(req);
};
Enter fullscreen mode Exit fullscreen mode

Service:

// services/auth.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class AuthService {
  constructor(private http: HttpClient) {}

  getAccessToken(): string | null {
    return localStorage.getItem('access_token');
  }

  refreshToken(): Observable<{ accessToken: string }> {
    const refreshToken = localStorage.getItem('refresh_token');
    return this.http.post<{ accessToken: string }>('/auth/refresh', {
      refreshToken
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Part 20: Error Interceptor (Angular)

// interceptors/error.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { AuthService } from '../services/auth.service';
import { Router } from '@angular/router';

export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  return next(req).pipe(
    catchError((error) => {
      // Handle specific error codes
      if (error.status === 401) {
        // Token expired
        authService.logout();
        router.navigate(['/login']);
      } else if (error.status === 403) {
        // Forbidden
        router.navigate(['/forbidden']);
      } else if (error.status >= 500) {
        // Server error
        console.error('Server error:', error);
      }

      return throwError(() => error);
    })
  );
};
Enter fullscreen mode Exit fullscreen mode

Part 21: Loading Interceptor (Angular)

// interceptors/loading.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { tap } from 'rxjs/operators';
import { LoadingService } from '../services/loading.service';

export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
  const loadingService = inject(LoadingService);

  // Start loading
  loadingService.show();

  return next(req).pipe(
    tap(
      () => {
        // Response received
        loadingService.hide();
      },
      (error) => {
        // Error
        loadingService.hide();
      }
    )
  );
};
Enter fullscreen mode Exit fullscreen mode

Service:

// services/loading.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class LoadingService {
  private loadingCount = 0;
  isLoading$ = new BehaviorSubject(false);

  show(): void {
    this.loadingCount++;
    this.isLoading$.next(true);
  }

  hide(): void {
    this.loadingCount = Math.max(0, this.loadingCount - 1);
    if (this.loadingCount === 0) {
      this.isLoading$.next(false);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Component:

// components/loading-bar.component.ts
import { Component } from '@angular/core';
import { LoadingService } from '../services/loading.service';

@Component({
  selector: 'app-loading-bar',
  template: `
    <div *ngIf="loadingService.isLoading$ | async" class="loading-bar">
      Loading...
    </div>
  `,
})
export class LoadingBarComponent {
  constructor(public loadingService: LoadingService) {}
}
Enter fullscreen mode Exit fullscreen mode

Part 22: Retry Interceptor (Angular)

// interceptors/retry.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { catchError, retry, delay } from 'rxjs/operators';
import { throwError } from 'rxjs';

export const retryInterceptor: HttpInterceptorFn = (req, next) => {
  // Only retry GET requests on server errors (5xx)
  if (req.method === 'GET') {
    return next(req).pipe(
      retry({
        count: 3,
        delay: (error) => {
          // Only retry on 5xx, not on client errors
          if (error.status >= 500) {
            console.log(`Retrying failed request: ${req.url}`);
            return timer(1000); // Wait 1 second before retry
          }
          return throwError(() => error);
        }
      })
    );
  }

  return next(req);
};
Enter fullscreen mode Exit fullscreen mode

With exponential backoff:

import { timer } from 'rxjs';

export const retryInterceptor: HttpInterceptorFn = (req, next) => {
  if (req.method === 'GET') {
    return next(req).pipe(
      retry({
        count: 3,
        delay: (error, retryCount) => {
          if (error.status >= 500) {
            const waitTime = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s
            console.log(`Retrying in ${waitTime}ms...`);
            return timer(waitTime);
          }
          return throwError(() => error);
        }
      })
    );
  }

  return next(req);
};
Enter fullscreen mode Exit fullscreen mode

Part 23: Request-Specific Interceptor Behavior

Sometimes you want an interceptor to behave differently for specific requests. Two approaches:

Approach 1: Check URL

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  // Only add auth for your API, not for third-party services
  const isInternalAPI = req.url.includes('/api/');

  if (isInternalAPI) {
    const token = localStorage.getItem('access_token');
    if (token) {
      req = req.clone({
        setHeaders: { Authorization: `Bearer ${token}` }
      });
    }
  }

  return next(req);
};
Enter fullscreen mode Exit fullscreen mode

Approach 2: Use Custom Headers as Flags

In your component:

// component.ts
this.http.get('/api/users', {
  headers: {
    'X-Skip-Auth': 'true' // Custom flag
  }
}).subscribe(...);
Enter fullscreen mode Exit fullscreen mode

In the interceptor:

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  if (req.headers.has('X-Skip-Auth')) {
    // Remove the flag and skip auth
    return next(req.clone({ 
      headers: req.headers.delete('X-Skip-Auth') 
    }));
  }

  const token = localStorage.getItem('access_token');
  if (token) {
    req = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` }
    });
  }

  return next(req);
};
Enter fullscreen mode Exit fullscreen mode

Part 24: Caching Interceptor (Angular)

// interceptors/cache.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { of } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
class CacheService {
  private cache = new Map<string, any>();

  get(key: string): any {
    return this.cache.get(key);
  }

  set(key: string, value: any): void {
    this.cache.set(key, value);
  }

  has(key: string): boolean {
    return this.cache.has(key);
  }
}

export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
  const cacheService = inject(CacheService);

  // Only cache GET requests
  if (req.method !== 'GET') {
    return next(req);
  }

  // Check cache
  if (cacheService.has(req.url)) {
    console.log(`Cache hit: ${req.url}`);
    return of(cacheService.get(req.url));
  }

  // Not in cache — fetch and cache
  return next(req).pipe(
    tap((event) => {
      if (event instanceof HttpResponse) {
        cacheService.set(req.url, event);
      }
    })
  );
};
Enter fullscreen mode Exit fullscreen mode

Part 25: Complex Token Refresh (Angular)

// interceptors/token-refresh.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { BehaviorSubject, filter, take, switchMap, catchError, throwError } from 'rxjs';
import { AuthService } from '../services/auth.service';

// Shared subject for refresh state
const isRefreshing$ = new BehaviorSubject(false);

export const tokenRefreshInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);

  return next(req).pipe(
    catchError((error) => {
      // Only handle 401 and skip if it's the refresh endpoint itself
      if (error.status !== 401 || req.url.includes('/auth/refresh')) {
        return throwError(() => error);
      }

      if (!isRefreshing$.value) {
        isRefreshing$.next(true);

        return authService.refreshToken().pipe(
          switchMap((response) => {
            isRefreshing$.next(false);
            // Retry the original request with new token
            return next(req.clone({
              setHeaders: {
                Authorization: `Bearer ${response.accessToken}`
              }
            }));
          }),

          catchError((refreshError) => {
            isRefreshing$.next(false);
            authService.logout();
            return throwError(() => refreshError);
          })
        );
      }

      // If already refreshing, wait for it to complete
      return isRefreshing$.pipe(
        filter(value => !value), // Wait until refresh completes
        take(1),
        switchMap(() => {
          // Retry with updated token
          const newToken = authService.getAccessToken();
          return next(req.clone({
            setHeaders: {
              Authorization: `Bearer ${newToken}`
            }
          }));
        })
      );
    })
  );
};
Enter fullscreen mode Exit fullscreen mode

Part 26: Interceptor Limitations

Interceptors are powerful, but they have limits.

❌ Can't intercept:

  1. Requests from other tabs/windows — Interceptors are per-instance
  2. WebSocket connections — Different protocol, different API
  3. Worker/Service Worker requests — Different thread context
  4. Form submissions — Use <form> element directly, not XHR
  5. GraphQL subscriptions — Real-time, not interceptable like regular HTTP
  6. CORS pre-flight requests — OPTIONS requests handled by browser, not your code

❌ Limited scope:

  • Large file uploads — Interceptors see the whole request, can't stream progress easily
  • Streaming responses — You intercept the whole response, not chunks
  • Cancellation — Some interceptors can't properly cancel in-flight requests

✅ Workarounds:

For WebSocket auth:

// Add token to WebSocket handshake manually
const token = localStorage.getItem('access_token');
const socket = io('https://api.example.com', {
  auth: { token }
});
Enter fullscreen mode Exit fullscreen mode

For file uploads with progress:

// Use progress events, not interceptors
const formData = new FormData();
formData.append('file', file);

this.http.post('/upload', formData, {
  reportProgress: true,
  observe: 'events'
}).subscribe((event) => {
  if (event.type === HttpEventType.UploadProgress) {
    const percentDone = (event.loaded / event.total) * 100;
    console.log(`Uploaded ${percentDone}%`);
  }
});
Enter fullscreen mode Exit fullscreen mode

Part 27: Production Architecture

Here's how a production app structures interceptors:

src/
├── api/
│   ├── client.ts                    # Axios instance (React)
│   ├── http.service.ts              # HttpClient setup (Angular)
│   └── endpoints.ts                 # API endpoint definitions
│
├── interceptors/
│   ├── auth.interceptor.ts          # Add JWT tokens
│   ├── error.interceptor.ts         # Global error handling
│   ├── loading.interceptor.ts       # Loading state
│   ├── retry.interceptor.ts         # Retry failed requests
│   ├── transform.interceptor.ts     # Unwrap responses
│   ├── logging.interceptor.ts       # Development logging
│   └── index.ts                     # Export all
│
├── services/
│   ├── auth.service.ts              # Token management
│   ├── loading.service.ts           # Global loading state
│   └── error.service.ts             # Error toasts/notifications
│
└── App.tsx (React) / app.config.ts (Angular)
Enter fullscreen mode Exit fullscreen mode

Best practices:

  1. One interceptor per concern — Auth, errors, loading, retries, etc. each get their own
  2. Service-backed — Interceptors delegate to services for state management
  3. Minimal logic — Keep interceptors thin; put business logic in services
  4. Testable — Services are easier to test than interceptor chains
  5. Documented order — Comments showing interceptor execution sequence
  6. Error handling — Every error interceptor must rethrow or handle explicitly
  7. No side effects — Avoid async operations in request interceptors if possible

Example well-structured setup:

// app.config.ts (Angular)
export const appConfig: ApplicationConfig = {
  providers: [
    // HTTP with interceptors in order
    provideHttpClient(
      withInterceptors([
        // Layer 1: Request preparation
        requestIdInterceptor,           // Add tracing ID
        authInterceptor,                // Add auth token
        contentTypeInterceptor,         // Add content-type

        // Layer 2: Response handling (reverse order on response)
        errorInterceptor,               // Catch and handle errors
        transformInterceptor,           // Unwrap responses
        cacheInterceptor,               // Cache GET responses
      ])
    ),

    // Services
    AuthService,
    ErrorService,
    LoadingService,
    CacheService,
  ],
};
Enter fullscreen mode Exit fullscreen mode

Part 28: React vs Angular Comparison

Aspect React (Axios) Angular (HttpClient)
Setup Create instance + add interceptors Register via provider or module
Syntax httpClient.interceptors.request.use() Functional export const or class-based
Request flow Synchronous middleware chain Observable-based with RxJS
Error handling Promises and .catch() RxJS catchError operator
Typing Generics on Axios methods Strong TypeScript support with generics
State sharing External state management (Zustand, Redux) RxJS Subjects/Services
Learning curve Easier for Promises-based developers Steeper, requires RxJS knowledge
Built-in features Fewer, more DIY Complete HTTP toolkit
Testing Mock axios instance Mock HttpTestingController
Multiple instances Can have multiple axios clients One HttpClient per module/config

When to use each:

  • React + Axios: You like simplicity, prefer Promises, want full control
  • Angular HttpClient: You're in Angular, need RxJS, want built-in patterns

Part 29: Final Mental Model

Think of HTTP interceptors as a request/response gateway:

┌─────────────────────────────────────────────────────────────┐
│                      Your App                               │
├─────────────────────────────────────────────────────────────┤
│
│  Interceptor Chain = Security + Transformation Pipeline
│
│  Request Path:    Auth → Logging → Error Handling → Network
│  Response Path:   Network → Transform → Logging → App
│
└─────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Key mental models:

  1. Interceptors run in sequence — First added, first to run (on requests)
  2. Responses run in reverse — Last added, first to run (on responses)
  3. Each interceptor either passes it along or handles it — No silent failures
  4. Immutable requests — Clone and return, don't mutate
  5. Errors can be handled or rethrown — You're in control

Conclusion

HTTP interceptors are one of those architectural patterns that seem simple at first ("just add a header to every request") but reveal their power once you understand the full flow. With interceptors, you move cross-cutting concerns out of your components and into a centralized, testable layer.

In React: Axios interceptors are straightforward Promise-based middleware. Perfect for adding auth tokens, handling 401 refreshes, and global error handling.

In Angular: HttpClient interceptors are RxJS-native, giving you the full power of reactive streams. Slightly more complex syntax, but incredibly flexible once you grok Observables.

The production pattern: One interceptor per concern (auth, errors, loading, retry, transform). Each interceptor is thin and delegates to a service for state management. This keeps your code testable, maintainable, and sane.

Start simple — add auth, then unwrap responses, then handle 401s. Each addition teaches you something new about the flow. Before long, you'll have a rock-solid HTTP layer that makes your entire app more reliable.

Now go build something great.

Top comments (0)