Skip to content

Bits of .NET

Daily micro-tips for C#, SQL, performance, and scalable backend engineering.

  • Asp.Net Core
  • C#
  • SQL
  • JavaScript
  • CSS
  • About
  • ErcanOPAK.com
  • No Access
  • Privacy Policy
Ajax

AJAX Fetch API: The Request Interceptor Pattern That Saved Our Auth System

- 23.02.26 | 23.02.26 - ErcanOPAK

🔐 The Authentication Nightmare

Every API call needs an auth token. Every developer on your team copies the same 10 lines of code. Token refresh logic is duplicated 47 times across your codebase. Sound familiar?

The Solution: Fetch Interceptor Pattern

❌ The Repetitive Way (47 Places)

// Component A
const token = localStorage.getItem('token');
const response = await fetch('/api/users', {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  }
});

// Component B (same code copy-pasted)
const token = localStorage.getItem('token');
const response = await fetch('/api/posts', {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  }
});

// 45 more components with the same code...
// Token refresh? Good luck updating 47 places!

✅ Centralized API Client (1 Place)

// api.js - Your centralized API client
class ApiClient {
  constructor() {
    this.baseURL = process.env.REACT_APP_API_URL;
    this.refreshing = false;
  }

  async request(endpoint, options = {}) {
    // Interceptor: Add auth automatically
    const token = localStorage.getItem('token');
    const config = {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        'Authorization': token ? `Bearer ${token}` : '',
        ...options.headers
      }
    };

    try {
      const response = await fetch(`${this.baseURL}${endpoint}`, config);
      
      // Interceptor: Handle 401 (token expired)
      if (response.status === 401 && !this.refreshing) {
        const newToken = await this.refreshToken();
        if (newToken) {
          // Retry original request with new token
          return this.request(endpoint, options);
        }
      }

      return response.json();
    } catch (error) {
      console.error('API Error:', error);
      throw error;
    }
  }

  async refreshToken() {
    this.refreshing = true;
    try {
      const refresh = localStorage.getItem('refreshToken');
      const response = await fetch(`${this.baseURL}/auth/refresh`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refresh })
      });
      
      const { token } = await response.json();
      localStorage.setItem('token', token);
      return token;
    } catch (error) {
      // Refresh failed - log user out
      localStorage.clear();
      window.location = '/login';
      return null;
    } finally {
      this.refreshing = false;
    }
  }

  // Convenience methods
  get(endpoint) { return this.request(endpoint); }
  post(endpoint, data) { 
    return this.request(endpoint, { 
      method: 'POST', 
      body: JSON.stringify(data) 
    }); 
  }
  put(endpoint, data) { 
    return this.request(endpoint, { 
      method: 'PUT', 
      body: JSON.stringify(data) 
    }); 
  }
  delete(endpoint) { 
    return this.request(endpoint, { method: 'DELETE' }); 
  }
}

export const api = new ApiClient();

🎯 Usage: Clean & Simple

// Component A
import { api } from './api';

const users = await api.get('/users');
// ✅ Auth token added automatically
// ✅ Token refresh handled automatically
// ✅ Errors logged automatically

// Component B
const post = await api.post('/posts', { title: 'Hello' });
// Same benefits, zero boilerplate!

// Component C
const updated = await api.put('/posts/123', { title: 'Updated' });

// Component D
await api.delete('/posts/123');

// All 47 components: Clean, simple, DRY
Approach Code Duplication Token Refresh Maintainability
Direct fetch() calls 47 places Manual (breaks) Nightmare
Axios (old way) Some shared Interceptors OK (+35KB)
API Client Pattern Zero! Automatic Perfect (0KB)

🚀 Advanced Features to Add

class ApiClient {
  // ... previous code ...

  // Request deduplication
  pendingRequests = new Map();
  
  async request(endpoint, options = {}) {
    const key = `${options.method || 'GET'}-${endpoint}`;
    
    // If same request already in flight, return existing promise
    if (this.pendingRequests.has(key)) {
      return this.pendingRequests.get(key);
    }

    const promise = this._makeRequest(endpoint, options);
    this.pendingRequests.set(key, promise);
    
    promise.finally(() => this.pendingRequests.delete(key));
    return promise;
  }

  // Request caching
  cache = new Map();
  
  async getCached(endpoint, ttl = 60000) {
    const cached = this.cache.get(endpoint);
    if (cached && Date.now() - cached.timestamp < ttl) {
      return cached.data;
    }

    const data = await this.get(endpoint);
    this.cache.set(endpoint, { data, timestamp: Date.now() });
    return data;
  }

  // Request retry with exponential backoff
  async requestWithRetry(endpoint, options = {}, maxRetries = 3) {
    for (let i = 0; i <= maxRetries; i++) { try { return await this.request(endpoint, options); } 
        catch (error) { if (i === maxRetries) throw error; const delay = Math.pow(2, i) * 1000; 
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }
}

💡 Real-World Benefits

  • Token refresh: Changed once, works everywhere
  • Error handling: Centralized logging to Sentry
  • Loading states: Global loading indicator
  • Rate limiting: Add throttling in one place
  • API versioning: Switch v1 to v2 globally
  • Mock API: Swap real API with mock for testing

“We had auth logic scattered across 63 components. Migrating to API Client took 2 hours. Saved us 100+ hours in future maintenance. Best refactor ever.”

— Senior Frontend Dev, E-commerce Startup

⚠️ When NOT to Use This Pattern

  • Tiny apps with 1-2 API calls (overkill)
  • Already using tRPC, GraphQL, or React Query (they handle this)
  • Team prefers Axios (interceptors work similarly)

But for most SPAs with RESTful APIs? This pattern is gold.

Related posts:

Ajax Calls Succeed but UI Feels Delayed

Throttle Requests to Save Backend

AJAX Requests Timeout Randomly — Browser Connection Limits

Post Views: 4

Post navigation

JavaScript Performance: The One Weird Trick That Made Our App 10x Faster
Git Worktrees: Work on Multiple Branches Simultaneously Without Stashing

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

March 2026
M T W T F S S
 1
2345678
9101112131415
16171819202122
23242526272829
3031  
« Feb    

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (938)
  • How to add default value for Entity Framework migrations for DateTime and Bool (836)
  • Get the First and Last Word from a String or Sentence in SQL (828)
  • How to select distinct rows in a datatable in C# (801)
  • How to make theater mode the default for Youtube (736)
  • Add Constraint to SQL Table to ensure email contains @ (575)
  • How to enable, disable and check if Service Broker is enabled on a database in SQL Server (554)
  • Average of all values in a column that are not zero in SQL (523)
  • How to use Map Mode for Vertical Scroll Mode in Visual Studio (477)
  • Find numbers with more than two decimal places in SQL (441)

Recent Posts

  • C#: Saving Memory with yield return (Lazy Streams)
  • C#: Why Records are Better Than Classes for Data DTOs
  • C#: Creating Strings Without Memory Pressure with String.Create
  • SQL: Protecting Sensitive Data with Dynamic Data Masking
  • SQL: Writing Readable Queries with Common Table Expressions (CTE)
  • .NET Core: Handling Errors Gracefully with Middleware
  • .NET Core: Mastering Service Lifetimes (A Visual Guide)
  • Git: Surgical Stashing – Don’t Save Everything!
  • Git: Writing Commits That Your Future Self Won’t Hate
  • Ajax: Improving Perceived Speed with Skeleton Screens

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (938)
  • How to add default value for Entity Framework migrations for DateTime and Bool (836)
  • Get the First and Last Word from a String or Sentence in SQL (828)
  • How to select distinct rows in a datatable in C# (801)
  • How to make theater mode the default for Youtube (736)

Recent Posts

  • C#: Saving Memory with yield return (Lazy Streams)
  • C#: Why Records are Better Than Classes for Data DTOs
  • C#: Creating Strings Without Memory Pressure with String.Create
  • SQL: Protecting Sensitive Data with Dynamic Data Masking
  • SQL: Writing Readable Queries with Common Table Expressions (CTE)

Social

  • ErcanOPAK.com
  • GoodReads
  • LetterBoxD
  • Linkedin
  • The Blog
  • Twitter
© 2026 Bits of .NET | Built with Xblog Plus free WordPress theme by wpthemespace.com