Skip to content
intermediatePhase 37 · Frontend Architecture

API Layer

Design clean API abstraction layers for data fetching and mutation.

45m
0 problems
Topic Progress0%

API Abstraction

API Abstraction

A centralized API layer provides consistent error handling, authentication, and configuration across your application.

Base API Client

// api/client.js
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || '/api';

class ApiClient {
  constructor(baseURL = API_BASE_URL) {
    this.baseURL = baseURL;
    this.defaultHeaders = {
      'Content-Type': 'application/json',
    };
  }

  setAuthToken(token) {
    this.defaultHeaders['Authorization'] = `Bearer ${token}`;
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const config = {
      headers: { ...this.defaultHeaders, ...options.headers },
      ...options,
    };

    const response = await fetch(url, config);
    
    if (!response.ok) {
      const error = await this.handleError(response);
      throw error;
    }

    if (response.status === 204) return null;
    return response.json();
  }

  async handleError(response) {
    const errorData = await response.json().catch(() => ({}));
    return {
      status: response.status,
      message: errorData.message || response.statusText,
      errors: errorData.errors || [],
    };
  }

  get(endpoint, options = {}) {
    return this.request(endpoint, { ...options, method: 'GET' });
  }

  post(endpoint, data, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  put(endpoint, data, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  delete(endpoint, options = {}) {
    return this.request(endpoint, { ...options, method: 'DELETE' });
  }
}

export const api = new ApiClient();

API Endpoints

// api/products.js
import { api } from './client';

export const productsApi = {
  getAll: (params = {}) => {
    const query = new URLSearchParams(params).toString();
    return api.get(`/products?${query}`);
  },

  getById: (id) => api.get(`/products/${id}`),

  create: (product) => api.post('/products', product),

  update: (id, product) => api.put(`/products/${id}`, product),

  delete: (id) => api.delete(`/products/${id}`),

  search: (query) => api.get(`/products/search?q=${encodeURIComponent(query)}`),
};

// Usage in components
import { productsApi } from '../api/products';

function useProducts() {
  return useQuery({
    queryKey: ['products'],
    queryFn: productsApi.getAll,
  });
}

Error Handling

Error Handling

Consistent error handling improves debugging and user experience.

Error Types

// api/errors.js
export class ApiError extends Error {
  constructor(status, message, errors = []) {
    super(message);
    this.name = 'ApiError';
    this.status = status;
    this.errors = errors;
  }

  isUnauthorized() { return this.status === 401; }
  isForbidden() { return this.status === 403; }
  isNotFound() { return this.status === 404; }
  isValidationError() { return this.status === 422; }
  isServerError() { return this.status >= 500; }
}

export class NetworkError extends Error {
  constructor(message = 'Network error occurred') {
    super(message);
    this.name = 'NetworkError';
  }
}

export class TimeoutError extends Error {
  constructor(message = 'Request timed out') {
    super(message);
    this.name = 'TimeoutError';
  }
}

Error Boundaries with API

// components/ApiErrorBoundary.jsx
import React from 'react';

class ApiErrorBoundary extends React.Component {
  state = { error: null };

  static getDerivedStateFromError(error) {
    return { error };
  }

  componentDidCatch(error, errorInfo) {
    console.error('API Error:', error, errorInfo);
    errorReporter.captureException(error, { extra: errorInfo });
  }

  render() {
    if (this.state.error) {
      return (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          <p>{this.state.error.message}</p>
          <button onClick={() => window.location.reload()}>
            Retry
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

Error Handling in Hooks

// hooks/useApi.js
import { useState, useCallback } from 'react';

export function useApi(apiFn) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  const execute = useCallback(async (...args) => {
    try {
      setLoading(true);
      setError(null);
      const result = await apiFn(...args);
      setData(result);
      return result;
    } catch (err) {
      setError(err);
      throw err;
    } finally {
      setLoading(false);
    }
  }, [apiFn]);

  return { data, error, loading, execute };
}

// Usage
function ProductForm() {
  const { execute: createProduct, loading } = useApi(productsApi.create);

  const handleSubmit = async (data) => {
    try {
      await createProduct(data);
      showToast('Product created!');
    } catch (error) {
      if (error.isValidationError()) {
        setErrors(error.errors);
      } else {
        showToast('Failed to create product', 'error');
      }
    }
  };
}

Request/Response Interceptors

Request/Response Interceptors

Interceptors allow you to transform requests and responses globally.

Axios Interceptors

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

const api = axios.create({
  baseURL: '/api',
  timeout: 10000,
});

// Request interceptor
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    // Add request timestamp
    config.metadata = { startTime: new Date() };
    return config;
  },
  (error) => Promise.reject(error)
);

// Response interceptor
api.interceptors.response.use(
  (response) => {
    // Log slow requests
    const duration = new Date() - response.config.metadata.startTime;
    if (duration > 1000) {
      console.warn(`Slow request: ${response.config.url} took ${duration}ms`);
    }
    return response;
  },
  async (error) => {
    const originalRequest = error.config;

    // Handle token refresh
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        const { data } = await axios.post('/auth/refresh', {
          refreshToken: localStorage.getItem('refreshToken'),
        });

        localStorage.setItem('token', data.token);
        originalRequest.headers.Authorization = `Bearer ${data.token}`;

        return api(originalRequest);
      } catch (refreshError) {
        localStorage.removeItem('token');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }

    return Promise.reject(error);
  }
);

export default api;

Custom Interceptor Pattern

// api/interceptors.js
export function createInterceptors() {
  const requestInterceptors = [];
  const responseInterceptors = [];

  return {
    addRequestInterceptor: (interceptor) => {
      requestInterceptors.push(interceptor);
    },
    addResponseInterceptor: (interceptor) => {
      responseInterceptors.push(interceptor);
    },
    processRequest: async (config) => {
      let processedConfig = config;
      for (const interceptor of requestInterceptors) {
        processedConfig = await interceptor(processedConfig);
      }
      return processedConfig;
    },
    processResponse: async (response) => {
      let processedResponse = response;
      for (const interceptor of responseInterceptors) {
        processedResponse = await interceptor(processedResponse);
      }
      return processedResponse;
    },
  };
}

// Usage
const interceptors = createInterceptors();

interceptors.addRequestInterceptor((config) => {
  config.headers['X-Request-ID'] = crypto.randomUUID();
  return config;
});

interceptors.addResponseInterceptor((response) => {
  console.log('Response:', response.status);
  return response;
});

Common Interceptor Use Cases

  • Authentication: Add tokens to requests
  • Logging: Log requests and responses
  • Metrics: Track request duration
  • Retry: Retry failed requests
  • Transform: Convert data formats

Practice Problems

0/3solved
Build API Layer Component

Create a reusable React component implementing API Layer. Include proper state management and accessibility.

Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where needed
API Layer Testing

Write unit and integration tests for API Layer using React Testing Library.

Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility tests
API Layer Performance

Optimize API Layer for performance. Consider memoization, code splitting, and bundle size.

Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysis

Quiz

1. What is the primary purpose of an API abstraction layer?

Question 1 options

2. What does a request interceptor do?

Question 2 options

3. How should 401 errors be handled in an API layer?

Question 3 options

4. What is the benefit of creating separate API endpoint modules?

Question 4 options

Flashcards

Question

What is an API abstraction layer?

Answer

A centralized module that handles API configuration, error handling, and common request/response transformations.

Question

What do request interceptors do?

Answer

Transform requests before they're sent, such as adding authentication headers or logging.

Question

How do you handle token refresh with interceptors?

Answer

Catch 401 errors in response interceptor, refresh token, update headers, and retry original request.

Question

What are common interceptor use cases?

Answer

Authentication, logging, metrics tracking, retry logic, and data transformation.

Question

What is API Layer?

Answer

API Layer is a key concept in frontend development.

Revision Notes

Key Takeaways

  • 1.Create a centralized API client for consistent configuration
  • 2.Define custom error classes for different error types
  • 3.Use interceptors for cross-cutting concerns like auth and logging
  • 4.Separate API endpoints into domain-specific modules
  • 5.Always handle token refresh in response interceptors

Interview Tips

  • Explain how you would structure an API layer in a large application
  • Discuss token refresh strategies and how to handle 401 errors
  • Know the difference between request and response interceptors

Cheat Sheet

API Layer Cheat Sheet

Base Client Structure

class ApiClient {
  async request(endpoint, options) { ... }
  get(endpoint) { ... }
  post(endpoint, data) { ... }
}

Error Types

  • ApiError (4xx/5xx)
  • NetworkError (connection issues)
  • TimeoutError (request timeout)

Interceptor Pattern

api.interceptors.request.use(config => {
  config.headers.Authorization = `Bearer ${token}`;
  return config;
});

api.interceptors.response.use(
  response => response,
  error => handleTokenRefresh(error)
);