# Chapter 8: Interceptors & Middleware

*"Every request tells a story. Interceptors let you edit it."*

---

## The Authentication Nightmare

Sarah's Weather Buddy was type-safe, cached, and error-resistant. But then came the new requirement: user authentication for premium features.

"Every API call needs an auth token now," her PM announced. "And we need to track all requests for analytics. Oh, and can you add request signing for the payment endpoints?"

Sarah's heart sank. Would she have to modify every single API call?

"Not with interceptors," Marcus said, appearing with coffee. "Let me show you the power of middleware."

## Understanding Interceptors: The Request Pipeline

Think of interceptors like airport security. Every request passes through checkpoints where you can inspect, modify, or reject it:

```javascript
// Without interceptors - repetitive and error-prone
const token = getAuthToken()
const { data } = await tf.get('/api/user', {
  headers: {
    'Authorization': `Bearer ${token}`,
    'X-Request-ID': generateId(),
    'X-Client-Version': APP_VERSION
  }
})
logRequest('/api/user')

// With interceptors - clean and automatic
tf.addRequestInterceptor(config => ({
  ...config,
  headers: {
    ...config.headers,
    'Authorization': `Bearer ${getAuthToken()}`,
    'X-Request-ID': generateId(),
    'X-Client-Version': APP_VERSION
  }
}))

// Now every request is automatically enhanced!
const { data } = await tf.get('/api/user')  // Auth added automatically
```

## Request Interceptors: Transform Outgoing Requests

Request interceptors run before requests are sent:

```typescript
// Add authentication to all requests
tf.addRequestInterceptor(config => {
  const token = localStorage.getItem('authToken')
  
  if (token && !config.headers['Authorization']) {
    config.headers['Authorization'] = `Bearer ${token}`
  }
  
  return config
})

// Add request tracking
tf.addRequestInterceptor(config => {
  config.metadata = {
    ...config.metadata,
    requestId: crypto.randomUUID(),
    timestamp: Date.now(),
    userId: getCurrentUserId()
  }
  
  console.log(`[${config.metadata.requestId}] ${config.method} ${config.url}`)
  
  return config
})

// Modify requests based on environment
tf.addRequestInterceptor(config => {
  if (process.env.NODE_ENV === 'development') {
    // Add debug headers in dev
    config.headers['X-Debug-Mode'] = 'true'
    config.headers['X-Developer'] = os.username()
  }
  
  if (config.url.includes('/payment')) {
    // Extra security for payment endpoints
    config.timeout = 30000  // 30 seconds
    config.retries = 0      // No retries for payments
  }
  
  return config
})

// Request signing for secure endpoints
tf.addRequestInterceptor(async config => {
  if (config.url.includes('/secure')) {
    const signature = await signRequest(config)
    config.headers['X-Signature'] = signature
    config.headers['X-Timestamp'] = Date.now().toString()
  }
  
  return config
})
```

## Response Interceptors: Transform Incoming Data

Response interceptors process data before it reaches your code:

```typescript
// Transform snake_case to camelCase
tf.addResponseInterceptor(response => {
  if (response.data) {
    response.data = transformKeys(response.data, snakeToCamel)
  }
  return response
})

// Extract data from envelopes
tf.addResponseInterceptor(response => {
  // API returns { success: true, data: {...}, meta: {...} }
  // We just want the data
  if (response.data?.success && response.data?.data) {
    response.data = response.data.data
  }
  return response
})

// Add computed properties
tf.addResponseInterceptor(response => {
  if (response.config.url.includes('/users')) {
    // Add display name
    if (response.data.firstName && response.data.lastName) {
      response.data.displayName = `${response.data.firstName} ${response.data.lastName}`
    }
    
    // Add avatar URL
    if (response.data.avatarId && !response.data.avatarUrl) {
      response.data.avatarUrl = `https://cdn.example.com/avatars/${response.data.avatarId}`
    }
  }
  
  return response
})

// Track response times
tf.addResponseInterceptor(response => {
  const duration = Date.now() - response.config.metadata.timestamp
  
  console.log(
    `[${response.config.metadata.requestId}] ` +
    `${response.status} ${response.config.url} - ${duration}ms`
  )
  
  // Add to metrics
  metrics.recordApiCall({
    endpoint: response.config.url,
    method: response.config.method,
    status: response.status,
    duration
  })
  
  return response
})
```

## Error Interceptors: Handle Failures Gracefully

Error interceptors catch and transform errors:

```typescript
// Retry on auth failure
tf.addErrorInterceptor(async error => {
  if (error.response?.status === 401) {
    // Try refreshing the token
    try {
      const newToken = await refreshAuthToken()
      localStorage.setItem('authToken', newToken)
      
      // Retry the original request
      error.config.headers['Authorization'] = `Bearer ${newToken}`
      return tf.request(error.config)
    } catch (refreshError) {
      // Refresh failed, redirect to login
      window.location.href = '/login'
      throw error
    }
  }
  
  throw error
})

// Transform error messages
tf.addErrorInterceptor(error => {
  // Make errors more user-friendly
  if (error.response?.status === 404) {
    error.userMessage = 'The requested resource was not found.'
    error.suggestions = ['Check the URL', 'Try again later']
  } else if (error.response?.status === 500) {
    error.userMessage = 'Something went wrong on our end.'
    error.suggestions = ['Try again in a few minutes', 'Contact support']
  }
  
  // Log to error service
  errorReporter.log(error)
  
  throw error
})

// Handle network errors
tf.addErrorInterceptor(error => {
  if (error.code === 'NETWORK_ERROR') {
    // Check if we're offline
    if (!navigator.onLine) {
      error.userMessage = 'You appear to be offline.'
      error.canRetry = true
      
      // Queue for later
      offlineQueue.add(error.config)
    }
  }
  
  throw error
})
```

## Building Complex Middleware Chains

Interceptors can work together to create powerful pipelines:

```typescript
// 1. Authentication interceptor
class AuthInterceptor {
  constructor(private auth: AuthService) {}
  
  async request(config: RequestConfig) {
    const token = await this.auth.getToken()
    
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`
    }
    
    return config
  }
  
  async error(error: Error) {
    if (error.response?.status === 401) {
      await this.auth.refresh()
      return tf.request(error.config)
    }
    throw error
  }
}

// 2. Logging interceptor
class LoggingInterceptor {
  private logger = new Logger('API')
  
  request(config: RequestConfig) {
    this.logger.info(`→ ${config.method} ${config.url}`)
    config.metadata.startTime = performance.now()
    return config
  }
  
  response(response: Response) {
    const duration = performance.now() - response.config.metadata.startTime
    this.logger.info(
      `← ${response.status} ${response.config.url} (${duration.toFixed(2)}ms)`
    )
    return response
  }
  
  error(error: Error) {
    this.logger.error(`✗ ${error.config.url}: ${error.message}`)
    throw error
  }
}

// 3. Retry interceptor
class RetryInterceptor {
  private retryCount = new Map<string, number>()
  
  async error(error: Error) {
    const key = `${error.config.method}:${error.config.url}`
    const attempts = this.retryCount.get(key) || 0
    
    if (this.shouldRetry(error) && attempts < 3) {
      this.retryCount.set(key, attempts + 1)
      
      await this.delay(attempts)
      return tf.request(error.config)
    }
    
    this.retryCount.delete(key)
    throw error
  }
  
  private shouldRetry(error: Error) {
    return (
      error.response?.status >= 500 ||
      error.code === 'NETWORK_ERROR' ||
      error.code === 'TIMEOUT'
    )
  }
  
  private delay(attempt: number) {
    const ms = Math.min(1000 * Math.pow(2, attempt), 10000)
    return new Promise(resolve => setTimeout(resolve, ms))
  }
}

// Register all interceptors
const auth = new AuthInterceptor(authService)
const logging = new LoggingInterceptor()
const retry = new RetryInterceptor()

tf.addRequestInterceptor(config => auth.request(config))
tf.addRequestInterceptor(config => logging.request(config))

tf.addResponseInterceptor(response => logging.response(response))

tf.addErrorInterceptor(error => auth.error(error))
tf.addErrorInterceptor(error => retry.error(error))
tf.addErrorInterceptor(error => logging.error(error))
```

## Weather Buddy 8.0: Enterprise Ready

Let's add professional-grade interceptors to Weather Buddy:

```typescript
// weather-buddy-8.ts
import { tf } from 'typedfetch'

// Configuration
interface Config {
  apiKey: string
  analyticsId: string
  environment: 'development' | 'staging' | 'production'
}

const config: Config = {
  apiKey: process.env.WEATHER_API_KEY!,
  analyticsId: process.env.ANALYTICS_ID!,
  environment: (process.env.NODE_ENV as any) || 'development'
}

// Analytics tracking
class AnalyticsInterceptor {
  private queue: AnalyticsEvent[] = []
  private flushInterval = 5000
  
  constructor(private analyticsId: string) {
    setInterval(() => this.flush(), this.flushInterval)
  }
  
  request(config: RequestConfig) {
    config.metadata.analyticsId = crypto.randomUUID()
    config.metadata.timestamp = Date.now()
    
    this.track({
      type: 'api_request',
      id: config.metadata.analyticsId,
      endpoint: config.url,
      method: config.method,
      timestamp: config.metadata.timestamp
    })
    
    return config
  }
  
  response(response: Response) {
    const duration = Date.now() - response.config.metadata.timestamp
    
    this.track({
      type: 'api_response',
      id: response.config.metadata.analyticsId,
      endpoint: response.config.url,
      status: response.status,
      duration,
      cached: response.cached || false
    })
    
    return response
  }
  
  error(error: Error) {
    this.track({
      type: 'api_error',
      id: error.config.metadata.analyticsId,
      endpoint: error.config.url,
      error: error.message,
      status: error.response?.status
    })
    
    throw error
  }
  
  private track(event: AnalyticsEvent) {
    this.queue.push({
      ...event,
      analyticsId: this.analyticsId,
      sessionId: getSessionId(),
      userId: getCurrentUserId()
    })
  }
  
  private async flush() {
    if (this.queue.length === 0) return
    
    const events = [...this.queue]
    this.queue = []
    
    try {
      await fetch('https://analytics.example.com/events', {
        method: 'POST',
        body: JSON.stringify({ events })
      })
    } catch (error) {
      // Re-queue on failure
      this.queue.unshift(...events)
    }
  }
}

// API versioning
class VersioningInterceptor {
  constructor(private version: string) {}
  
  request(config: RequestConfig) {
    // Add version to URL
    if (config.url.includes('/api/')) {
      config.url = config.url.replace('/api/', `/api/${this.version}/`)
    }
    
    // Add version header
    config.headers['API-Version'] = this.version
    
    return config
  }
  
  response(response: Response) {
    // Check if API version is deprecated
    const deprecation = response.headers.get('X-API-Deprecation')
    if (deprecation) {
      console.warn(`API deprecation warning: ${deprecation}`)
      
      // Show user notification
      showNotification({
        type: 'warning',
        message: 'This app needs to be updated soon.',
        action: 'Update Now',
        onAction: () => window.location.href = '/update'
      })
    }
    
    return response
  }
}

// Request signing for premium features
class SigningInterceptor {
  constructor(private secret: string) {}
  
  async request(config: RequestConfig) {
    if (config.url.includes('/premium')) {
      const timestamp = Date.now()
      const payload = `${config.method}:${config.url}:${timestamp}`
      const signature = await this.sign(payload)
      
      config.headers['X-Signature'] = signature
      config.headers['X-Timestamp'] = timestamp.toString()
    }
    
    return config
  }
  
  private async sign(payload: string): Promise<string> {
    const encoder = new TextEncoder()
    const data = encoder.encode(payload)
    const key = await crypto.subtle.importKey(
      'raw',
      encoder.encode(this.secret),
      { name: 'HMAC', hash: 'SHA-256' },
      false,
      ['sign']
    )
    
    const signature = await crypto.subtle.sign('HMAC', key, data)
    return btoa(String.fromCharCode(...new Uint8Array(signature)))
  }
}

// Rate limiting with backpressure
class RateLimitInterceptor {
  private requests = new Map<string, number[]>()
  private limits = {
    '/api/weather': { requests: 60, window: 60000 },      // 60/minute
    '/api/premium': { requests: 100, window: 60000 },     // 100/minute
    'default': { requests: 120, window: 60000 }           // 120/minute
  }
  
  async request(config: RequestConfig) {
    const endpoint = this.getEndpoint(config.url)
    const limit = this.limits[endpoint] || this.limits.default
    
    const now = Date.now()
    const requests = this.requests.get(endpoint) || []
    
    // Clean old requests
    const recent = requests.filter(time => now - time < limit.window)
    
    if (recent.length >= limit.requests) {
      const oldestRequest = recent[0]
      const waitTime = limit.window - (now - oldestRequest)
      
      console.warn(`Rate limit approaching, waiting ${waitTime}ms`)
      await new Promise(resolve => setTimeout(resolve, waitTime))
    }
    
    recent.push(now)
    this.requests.set(endpoint, recent)
    
    return config
  }
  
  response(response: Response) {
    // Check rate limit headers
    const remaining = response.headers.get('X-RateLimit-Remaining')
    const reset = response.headers.get('X-RateLimit-Reset')
    
    if (remaining && parseInt(remaining) < 10) {
      console.warn(`Low API quota: ${remaining} requests remaining`)
      
      // Slow down requests
      tf.configure({
        requestDelay: 1000  // Add 1s delay between requests
      })
    }
    
    return response
  }
  
  private getEndpoint(url: string): string {
    const match = url.match(/\/api\/\w+/)
    return match ? match[0] : 'default'
  }
}

// Development tools
class DevToolsInterceptor {
  private enabled = config.environment === 'development'
  private requests: RequestLog[] = []
  
  request(config: RequestConfig) {
    if (!this.enabled) return config
    
    const log: RequestLog = {
      id: config.metadata.requestId,
      method: config.method,
      url: config.url,
      headers: config.headers,
      data: config.data,
      timestamp: Date.now(),
      stack: new Error().stack
    }
    
    this.requests.push(log)
    
    // Dev UI integration
    window.postMessage({
      type: 'API_REQUEST',
      payload: log
    }, '*')
    
    return config
  }
  
  response(response: Response) {
    if (!this.enabled) return response
    
    const log = this.requests.find(r => r.id === response.config.metadata.requestId)
    if (log) {
      log.response = {
        status: response.status,
        headers: Object.fromEntries(response.headers.entries()),
        data: response.data,
        duration: Date.now() - log.timestamp
      }
      
      window.postMessage({
        type: 'API_RESPONSE',
        payload: log
      }, '*')
    }
    
    return response
  }
  
  getRequests() {
    return this.requests
  }
  
  clear() {
    this.requests = []
  }
}

// Register all interceptors
const analytics = new AnalyticsInterceptor(config.analyticsId)
const versioning = new VersioningInterceptor('v2')
const signing = new SigningInterceptor(config.apiKey)
const rateLimit = new RateLimitInterceptor()
const devTools = new DevToolsInterceptor()

// Request pipeline
tf.addRequestInterceptor(config => versioning.request(config))
tf.addRequestInterceptor(config => analytics.request(config))
tf.addRequestInterceptor(config => rateLimit.request(config))
tf.addRequestInterceptor(config => signing.request(config))
tf.addRequestInterceptor(config => devTools.request(config))

// Response pipeline
tf.addResponseInterceptor(response => devTools.response(response))
tf.addResponseInterceptor(response => analytics.response(response))
tf.addResponseInterceptor(response => versioning.response(response))
tf.addResponseInterceptor(response => rateLimit.response(response))

// Error pipeline
tf.addErrorInterceptor(error => analytics.error(error))

// Weather Buddy Premium Features
class WeatherBuddyPremium {
  private subscriptionActive = false
  
  async checkSubscription(): Promise<boolean> {
    try {
      const { data } = await tf.get('/api/premium/subscription')
      this.subscriptionActive = data.active
      return data.active
    } catch {
      return false
    }
  }
  
  async getDetailedForecast(city: string): Promise<DetailedForecast> {
    if (!this.subscriptionActive) {
      throw new Error('Premium subscription required')
    }
    
    const { data } = await tf.get(`/api/premium/forecast/${city}`, {
      params: {
        days: 14,
        hourly: true,
        includes: ['uv', 'pollen', 'airQuality', 'astronomy']
      }
    })
    
    return data
  }
  
  async getWeatherAlerts(location: { lat: number, lon: number }): Promise<Alert[]> {
    const { data } = await tf.get('/api/premium/alerts', {
      params: location
    })
    
    return data.alerts
  }
  
  async getHistoricalWeather(city: string, date: Date): Promise<HistoricalData> {
    const { data } = await tf.get(`/api/premium/historical/${city}`, {
      params: {
        date: date.toISOString().split('T')[0]
      }
    })
    
    return data
  }
}

// Export for DevTools extension
if (config.environment === 'development') {
  (window as any).__WEATHER_BUDDY_DEV__ = {
    tf,
    interceptors: {
      analytics,
      versioning,
      signing,
      rateLimit,
      devTools
    },
    getRequests: () => devTools.getRequests(),
    clearRequests: () => devTools.clear(),
    config
  }
}
```

## Creating Plugin Systems with Interceptors

Build extensible applications with interceptor-based plugins:

```typescript
// Plugin system
interface Plugin {
  name: string
  version: string
  init?(tf: TypedFetch): void
  request?(config: RequestConfig): RequestConfig | Promise<RequestConfig>
  response?(response: Response): Response | Promise<Response>
  error?(error: Error): Error | Promise<Error>
}

class PluginManager {
  private plugins: Plugin[] = []
  
  register(plugin: Plugin) {
    console.log(`Loading plugin: ${plugin.name} v${plugin.version}`)
    
    this.plugins.push(plugin)
    
    if (plugin.init) {
      plugin.init(tf)
    }
    
    if (plugin.request) {
      tf.addRequestInterceptor(config => plugin.request!(config))
    }
    
    if (plugin.response) {
      tf.addResponseInterceptor(response => plugin.response!(response))
    }
    
    if (plugin.error) {
      tf.addErrorInterceptor(error => plugin.error!(error))
    }
  }
  
  unregister(pluginName: string) {
    this.plugins = this.plugins.filter(p => p.name !== pluginName)
    // Note: Interceptors can't be removed in this example
    // In real implementation, track interceptor IDs
  }
  
  list() {
    return this.plugins.map(p => ({
      name: p.name,
      version: p.version
    }))
  }
}

// Example plugins
const compressionPlugin: Plugin = {
  name: 'compression',
  version: '1.0.0',
  
  request(config) {
    if (config.data && config.method !== 'GET') {
      const json = JSON.stringify(config.data)
      if (json.length > 1024) {
        // Compress large payloads
        config.headers['Content-Encoding'] = 'gzip'
        config.data = gzip(json)
      }
    }
    return config
  },
  
  response(response) {
    if (response.headers.get('Content-Encoding') === 'gzip') {
      response.data = JSON.parse(gunzip(response.data))
    }
    return response
  }
}

const cacheSyncPlugin: Plugin = {
  name: 'cache-sync',
  version: '1.0.0',
  
  init(tf) {
    // Sync cache across tabs
    window.addEventListener('storage', (e) => {
      if (e.key?.startsWith('tf-cache-')) {
        tf.cache.sync(e.key, e.newValue)
      }
    })
  },
  
  response(response) {
    if (response.config.method === 'GET' && response.cached === false) {
      // Share across tabs
      localStorage.setItem(
        `tf-cache-${response.config.url}`,
        JSON.stringify({
          data: response.data,
          timestamp: Date.now()
        })
      )
    }
    return response
  }
}

// Use plugins
const plugins = new PluginManager()
plugins.register(compressionPlugin)
plugins.register(cacheSyncPlugin)
```

## Advanced Interceptor Patterns

### 1. Conditional Interceptors

Apply interceptors based on conditions:

```typescript
// Only for specific endpoints
tf.addRequestInterceptor(config => {
  if (config.url.includes('/admin')) {
    config.headers['X-Admin-Token'] = getAdminToken()
  }
  return config
})

// Only in certain environments
if (process.env.NODE_ENV === 'production') {
  tf.addRequestInterceptor(config => {
    config.headers['X-Source'] = 'production'
    return config
  })
}

// Based on feature flags
tf.addRequestInterceptor(config => {
  if (featureFlags.get('new-api')) {
    config.url = config.url.replace('/api/v1', '/api/v2')
  }
  return config
})
```

### 2. Interceptor Composition

Combine multiple interceptors elegantly:

```typescript
function compose(...interceptors: RequestInterceptor[]) {
  return async (config: RequestConfig) => {
    let result = config
    
    for (const interceptor of interceptors) {
      result = await interceptor(result)
    }
    
    return result
  }
}

// Combine auth + logging + metrics
const combined = compose(
  addAuth,
  addLogging,
  addMetrics
)

tf.addRequestInterceptor(combined)
```

### 3. Stateful Interceptors

Interceptors that maintain state:

```typescript
class SequenceInterceptor {
  private sequence = 0
  
  request(config: RequestConfig) {
    config.headers['X-Sequence'] = (++this.sequence).toString()
    return config
  }
  
  response(response: Response) {
    const expected = response.config.headers['X-Sequence']
    const received = response.headers.get('X-Sequence-Echo')
    
    if (expected !== received) {
      console.warn('Response out of sequence!')
    }
    
    return response
  }
}
```

### 4. Priority-Based Interceptors

Control interceptor execution order:

```typescript
class PriorityInterceptorManager {
  private interceptors: Array<{
    priority: number
    handler: Function
  }> = []
  
  add(handler: Function, priority = 0) {
    this.interceptors.push({ priority, handler })
    this.interceptors.sort((a, b) => b.priority - a.priority)
  }
  
  async execute(config: RequestConfig) {
    let result = config
    
    for (const { handler } of this.interceptors) {
      result = await handler(result)
    }
    
    return result
  }
}

// Usage
const manager = new PriorityInterceptorManager()
manager.add(authInterceptor, 100)     // Run first
manager.add(loggingInterceptor, 50)   // Run second  
manager.add(metricsInterceptor, 10)   // Run last
```

## Testing with Interceptors

Use interceptors to make testing easier:

```typescript
// Mock interceptor for testing
class MockInterceptor {
  private mocks = new Map<string, any>()
  
  mock(url: string, response: any) {
    this.mocks.set(url, response)
  }
  
  clear() {
    this.mocks.clear()
  }
  
  request(config: RequestConfig) {
    const mock = this.mocks.get(config.url)
    
    if (mock) {
      // Bypass network, return mock
      return Promise.reject({
        config,
        mockResponse: mock
      })
    }
    
    return config
  }
  
  error(error: any) {
    if (error.mockResponse) {
      // Convert to response
      return {
        config: error.config,
        data: error.mockResponse,
        status: 200,
        headers: new Headers(),
        mocked: true
      }
    }
    throw error
  }
}

// Testing
describe('WeatherAPI', () => {
  const mock = new MockInterceptor()
  
  beforeAll(() => {
    tf.addRequestInterceptor(config => mock.request(config))
    tf.addErrorInterceptor(error => mock.error(error))
  })
  
  beforeEach(() => {
    mock.clear()
  })
  
  test('getWeather returns data', async () => {
    mock.mock('/api/weather/london', {
      temp: 15,
      condition: 'Cloudy'
    })
    
    const weather = await getWeather('london')
    expect(weather.temp).toBe(15)
  })
})
```

## Best Practices for Interceptors 🎯

### 1. Keep Interceptors Focused
```typescript
// ✅ Good: Single responsibility
tf.addRequestInterceptor(addAuthentication)
tf.addRequestInterceptor(addRequestId)
tf.addRequestInterceptor(addVersioning)

// ❌ Bad: Doing too much
tf.addRequestInterceptor(config => {
  // Add auth
  // Add logging
  // Add versioning
  // Transform data
  // 100 lines of code...
})
```

### 2. Handle Errors Gracefully
```typescript
// ✅ Good: Safe interceptor
tf.addRequestInterceptor(async config => {
  try {
    const token = await getToken()
    config.headers['Authorization'] = `Bearer ${token}`
  } catch (error) {
    console.error('Failed to get token:', error)
    // Continue without auth rather than failing
  }
  return config
})
```

### 3. Make Interceptors Configurable
```typescript
// ✅ Good: Configurable behavior
function createLoggingInterceptor(options = {}) {
  const {
    logLevel = 'info',
    includeHeaders = false,
    includeBody = false
  } = options
  
  return (config: RequestConfig) => {
    const log = {
      method: config.method,
      url: config.url,
      ...(includeHeaders && { headers: config.headers }),
      ...(includeBody && { body: config.data })
    }
    
    logger[logLevel]('API Request:', log)
    return config
  }
}
```

### 4. Document Side Effects
```typescript
/**
 * Adds authentication to requests
 * Side effects:
 * - Reads from localStorage
 * - May redirect to /login on 401
 * - Refreshes token automatically
 */
function authInterceptor(config: RequestConfig) {
  // Implementation
}
```

## Practice Time! 🏋️

### Exercise 1: Build a Caching Interceptor
Create an interceptor that caches responses:

```typescript
class CacheInterceptor {
  // Your code here:
  // - Cache GET responses
  // - Respect cache headers
  // - Handle cache invalidation
  // - Add cache info to response
}
```

### Exercise 2: Create a Retry Queue
Build an interceptor that queues failed requests:

```typescript
class RetryQueueInterceptor {
  // Your code here:
  // - Queue failed requests
  // - Retry with backoff
  // - Handle offline/online
  // - Prevent duplicates
}
```

### Exercise 3: Request Batching
Implement request batching:

```typescript
class BatchInterceptor {
  // Your code here:
  // - Batch multiple requests
  // - Send as single request
  // - Distribute responses
  // - Handle partial failures
}
```

## Key Takeaways 🎯

1. **Interceptors modify requests and responses in flight** - Transform data automatically
2. **Request interceptors run before sending** - Add auth, headers, tracking
3. **Response interceptors run after receiving** - Transform data, add computed fields
4. **Error interceptors handle failures** - Retry, refresh tokens, queue offline
5. **Chain interceptors for complex behaviors** - Compose simple functions
6. **Use interceptors for cross-cutting concerns** - Auth, logging, analytics
7. **Interceptors enable plugin systems** - Extensible architectures
8. **Keep interceptors focused and safe** - Single responsibility, graceful errors

## Common Pitfalls 🚨

1. **Modifying config without cloning** - Always return new object
2. **Forgetting async interceptors** - Handle promises properly
3. **Interceptor order matters** - Auth before signing
4. **Infinite loops in error handlers** - Detect retry loops
5. **Heavy processing in interceptors** - Keep them fast
6. **Not documenting side effects** - Hidden behaviors confuse

## What's Next?

You've mastered interceptors! But what about real-time data? In Chapter 9, we'll explore streaming and real-time features:

- Server-Sent Events (SSE)
- WebSocket integration
- Streaming JSON responses
- Real-time data synchronization
- Live updates and subscriptions
- Handling connection states

Ready to make your app real-time? See you in Chapter 9! 🚀

---

## Chapter Summary

- Interceptors form a pipeline that every request/response passes through
- Request interceptors add auth, headers, and modify outgoing data
- Response interceptors transform data and add computed properties
- Error interceptors handle failures, retry logic, and token refresh
- Chain multiple interceptors for complex behaviors like analytics + auth + versioning
- Interceptors enable plugin systems and extensible architectures
- Keep interceptors focused, safe, and well-documented
- Weather Buddy 8.0 is enterprise-ready with auth, analytics, versioning, and rate limiting

**Next Chapter Preview**: Real-Time & Streaming - Server-Sent Events, WebSockets, streaming responses, and building live-updating applications.