Working with APIs in JavaScript

What are APIs?

Application Programming Interfaces (APIs) allow different software systems to communicate with each other. In web development, APIs typically provide data in JSON or XML format that can be consumed by JavaScript applications.

The Fetch API

The modern way to make HTTP requests in JavaScript:

// Basic GET request
fetch('https://api.example.com/data')
  .then(response => {
    // Check if the response is successful
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    return response.json(); // Parse JSON response
  })
  .then(data => {
    console.log('Data received:', data);
    // Process the data
  })
  .catch(error => {
    console.error('Fetch error:', error);
  });

Request Configuration

// POST request with headers and body
fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  body: JSON.stringify({
    name: 'John Doe',
    email: 'john@example.com'
  })
})
.then(response => response.json())
.then(data => console.log('User created:', data));

// Other HTTP methods
const updateUser = {
  method: 'PUT', // or 'PATCH' for partial updates
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'John Updated' })
};

fetch('https://api.example.com/users/123', updateUser)
  .then(response => response.json())
  .then(data => console.log('User updated:', data));

// DELETE request
fetch('https://api.example.com/users/123', { method: 'DELETE' })
  .then(response => {
    if (response.ok) console.log('User deleted successfully');
  });

Response Handling

fetch('https://api.example.com/data')
  .then(response => {
    // Response properties
    console.log('Status:', response.status); // 200, 404, etc.
    console.log('OK?', response.ok); // true if status 200-299
    console.log('Status text:', response.statusText);
    console.log('Headers:', response.headers);
    console.log('Type:', response.type); // basic, cors, etc.
    
    // Response methods (each returns a Promise)
    // response.json() - Parse as JSON
    // response.text() - Get as text
    // response.blob() - Get as Blob (binary)
    // response.formData() - Get as FormData
    // response.arrayBuffer() - Get as ArrayBuffer
    
    return response.json();
  });

Using Async/Await with Fetch

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    
    const data = await response.json();
    console.log('Data received:', data);
    return data;
  } catch (error) {
    console.error('Fetch error:', error);
    throw error; // Re-throw to handle at call site if needed
  }
}

// Usage
fetchData()
  .then(data => {
    // Use the data
  })
  .catch(error => {
    // Handle any errors
  });

Request Cancellation

Using AbortController to cancel fetch requests:

const controller = new AbortController();
const { signal } = controller;

// Start fetch with signal
fetch('https://api.example.com/large-data', { signal })
  .then(response => response.json())
  .then(data => console.log('Data received'))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Fetch was aborted');
    } else {
      console.error('Fetch error:', error);
    }
  });

// Cancel the fetch after 5 seconds
setTimeout(() => {
  controller.abort();
  console.log('Fetch aborted');
}, 5000);

Handling Multiple Requests

Parallel Requests

// Execute all requests simultaneously
Promise.all([
  fetch('https://api.example.com/users').then(res => res.json()),
  fetch('https://api.example.com/posts').then(res => res.json()),
  fetch('https://api.example.com/comments').then(res => res.json())
])
.then(([users, posts, comments]) => {
  console.log('Users:', users);
  console.log('Posts:', posts);
  console.log('Comments:', comments);
})
.catch(error => {
  // If any request fails, the entire operation fails
  console.error('One or more requests failed:', error);
});

// Continue even if some requests fail
Promise.allSettled([
  fetch('https://api.example.com/users').then(res => res.json()),
  fetch('https://api.example.com/posts').then(res => res.json()),
  fetch('https://api.example.com/comments').then(res => res.json())
])
.then(results => {
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`Request ${index} succeeded:`, result.value);
    } else {
      console.log(`Request ${index} failed:`, result.reason);
    }
  });
});

Race Condition

// Use the first response that resolves
Promise.race([
  fetch('https://api-1.example.com/data').then(res => res.json()),
  fetch('https://api-2.example.com/data').then(res => res.json())
])
.then(data => {
  console.log('First API to respond:', data);
})
.catch(error => {
  console.error('Both requests failed:', error);
});

Error Handling and Retries

async function fetchWithRetry(url, options = {}, retries = 3, backoff = 300) {
  let lastError;
  
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      const response = await fetch(url, options);
      
      if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
      }
      
      return await response.json();
    } catch (error) {
      console.warn(`Attempt ${attempt + 1} failed:`, error);
      lastError = error;
      
      // Wait before retrying (exponential backoff)
      await new Promise(resolve => setTimeout(resolve, backoff * Math.pow(2, attempt)));
    }
  }
  
  throw lastError;
}

// Usage
fetchWithRetry('https://api.example.com/data')
  .then(data => console.log('Data received after retries:', data))
  .catch(error => console.error('All retry attempts failed:', error));

Working with Authentication

// Basic Auth
fetch('https://api.example.com/protected', {
  headers: {
    'Authorization': 'Basic ' + btoa('username:password')
  }
});

// Bearer Token (JWT)
fetch('https://api.example.com/protected', {
  headers: {
    'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
  }
});

// API Key
fetch('https://api.example.com/data?api_key=your_api_key');

CORS (Cross-Origin Resource Sharing)

// Request with CORS credentials
fetch('https://api.example.com/user-data', {
  credentials: 'include' // Send cookies with cross-origin requests
});

// Mode options
fetch('https://api.example.com/data', {
  mode: 'cors' // Default: allow CORS requests
  // Other modes: 'same-origin', 'no-cors'
});

Working with FormData

// Create FormData from a form element
const form = document.getElementById('upload-form');
const formData = new FormData(form);

// Or create and append manually
const formData = new FormData();
formData.append('username', 'john_doe');
formData.append('avatar', fileInput.files[0]);
formData.append('details', JSON.stringify({ age: 30, role: 'admin' }));

// Send with fetch
fetch('https://api.example.com/upload', {
  method: 'POST',
  body: formData
  // No need to set Content-Type header, it's set automatically
})
.then(response => response.json())
.then(data => console.log('Upload successful:', data));

API Client Libraries

Popular libraries for API requests:

// Axios example
import axios from 'axios';

// GET request
axios.get('https://api.example.com/data')
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.error('Error:', error);
  });

// POST request
axios.post('https://api.example.com/users', {
  name: 'John Doe',
  email: 'john@example.com'
})
.then(response => console.log(response.data));

// Request with config
axios({
  method: 'post',
  url: 'https://api.example.com/users',
  data: {
    name: 'John Doe',
    email: 'john@example.com'
  },
  headers: {
    'Authorization': 'Bearer token123'
  }
})
.then(response => console.log(response.data));

Best Practices

  1. Use try/catch with async/await for better error handling
  2. Implement request timeouts to prevent hanging requests
  3. Add retry logic for transient failures
  4. Cache responses when appropriate
  5. Validate API responses before using the data
  6. Use appropriate HTTP methods (GET, POST, PUT, DELETE)
  7. Handle loading and error states in the UI
  8. Implement proper authentication and secure sensitive data
  9. Consider rate limiting to avoid API throttling

Interview Tips

  • Explain the difference between the Fetch API and older XMLHttpRequest
  • Describe how to handle different response formats (JSON, text, binary)
  • Discuss strategies for error handling and retries
  • Explain CORS and how to handle cross-origin requests
  • Demonstrate knowledge of authentication methods for APIs
  • Describe how to optimize API requests for performance

Test Your Knowledge

Take a quick quiz to test your understanding of this topic.