Working with Web APIs

Learn how to interact with external data sources using the Fetch API and handle responses like a pro.

#intermediate

#apis

#fetch

#async

The modern web is built on connections between applications. Web APIs (Application Programming Interfaces) are the bridges that allow different systems to communicate with each other, enabling you to fetch data from servers, send data to databases, and integrate third-party services into your applications.

What are Web APIs?

A Web API is a set of rules and protocols that allow different software applications to communicate with each other. APIs define the methods and data formats that applications can use to request and exchange information.

There are many types of APIs you’ll encounter:

  1. REST APIs - The most common type, using HTTP requests to GET, POST, PUT, DELETE data
  2. GraphQL APIs - A more flexible alternative to REST that allows clients to request exactly the data they need
  3. WebSocket APIs - Enable two-way communication channels over a single TCP connection
  4. Browser APIs - Built into your browser (like Geolocation, Web Storage, Canvas)

The Fetch API

The Fetch API is a modern interface for making HTTP requests in JavaScript. It’s more powerful and flexible than the older XMLHttpRequest, and it uses Promises to handle asynchronous operations.

Basic Fetch Request

Here’s a simple GET request:

fetch("https://api.example.com/data")
  .then((response) => {
    // Check if the request was successful
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    return response.json(); // Parse the JSON in the response
  })
  .then((data) => {
    // Work with the data
    console.log(data);
  })
  .catch((error) => {
    // Handle errors
    console.error("Fetch error:", error);
  });

Using Fetch with Async/Await

For cleaner code, you can use 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);
    return data;
  } catch (error) {
    console.error("Fetch error:", error);
  }
}

// Call the function
fetchData();

Handling Different Response Types

The Fetch API can handle various response formats:

// JSON (most common)
const jsonData = await response.json();

// Plain text
const textData = await response.text();

// Form data
const formData = await response.formData();

// Binary data as Blob
const blobData = await response.blob();

// Array buffer for binary data
const arrayBufferData = await response.arrayBuffer();

Making Different Types of Requests

POST Request (Sending Data)

async function postData(url, data) {
  const response = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(data), // Convert JavaScript object to JSON string
  });

  return response.json();
}

// Example usage
const newUser = {
  name: "Jane Doe",
  email: "jane@example.com",
};

postData("https://api.example.com/users", newUser)
  .then((data) => console.log("Success:", data))
  .catch((error) => console.error("Error:", error));

PUT Request (Updating Data)

async function updateUser(id, userData) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(userData),
  });

  return response.json();
}

DELETE Request

async function deleteUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: "DELETE",
  });

  // Some APIs might return no content on DELETE
  if (response.status === 204) {
    return { success: true };
  }

  return response.json();
}

Working with Headers

Headers allow you to send additional information with your requests:

const response = await fetch("https://api.example.com/protected-data", {
  headers: {
    Authorization: "Bearer YOUR_ACCESS_TOKEN",
    "Content-Type": "application/json",
    Accept: "application/json",
    "X-Custom-Header": "CustomValue",
  },
});

You can also read headers from responses:

const response = await fetch("https://api.example.com/data");

// Single header
const contentType = response.headers.get("Content-Type");

// Iterate through all headers
for (const [key, value] of response.headers.entries()) {
  console.log(`${key}: ${value}`);
}

Handling Timeouts and Cancellation

The Fetch API doesn’t directly support timeouts, but you can implement them:

async function fetchWithTimeout(url, options = {}, timeout = 5000) {
  // Create abort controller for cancelling the fetch
  const controller = new AbortController();
  const { signal } = controller;

  // Create the promise that rejects after the timeout
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => {
      controller.abort();
      reject(new Error(`Request timed out after ${timeout}ms`));
    }, timeout);
  });

  // Race the fetch against the timeout
  return Promise.race([fetch(url, { ...options, signal }), timeoutPromise]);
}

// Usage
try {
  const response = await fetchWithTimeout(
    "https://api.example.com/data",
    {},
    3000
  );
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error("Error:", error.message);
}

Practical Example: Building a Weather App

Let’s put everything together by creating a simple weather app using a public API:

async function getWeather(city) {
  const apiKey = "your_api_key_here"; // You would need to get an API key
  const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`;

  try {
    const response = await fetch(url);

    if (!response.ok) {
      throw new Error(`Weather API error: ${response.status}`);
    }

    const data = await response.json();

    // Extract the information we need
    const weather = {
      city: data.name,
      country: data.sys.country,
      temp: data.main.temp,
      description: data.weather[0].description,
      icon: data.weather[0].icon,
      humidity: data.main.humidity,
      windSpeed: data.wind.speed,
    };

    return weather;
  } catch (error) {
    console.error("Failed to fetch weather:", error);
    throw error;
  }
}

// UI code to display the weather
function displayWeather(weather) {
  const weatherDiv = document.getElementById("weather");

  weatherDiv.innerHTML = `
    <h2>${weather.city}, ${weather.country}</h2>
    <div class="temp">${Math.round(weather.temp)}°C</div>
    <div class="description">${weather.description}</div>
    <img src="https://openweathermap.org/img/w/${weather.icon}.png" alt="${
    weather.description
  }">
    <div class="details">
      <div>Humidity: ${weather.humidity}%</div>
      <div>Wind: ${weather.windSpeed} m/s</div>
    </div>
  `;
}

// Event handler for the form
document
  .getElementById("weather-form")
  .addEventListener("submit", async (e) => {
    e.preventDefault();
    const city = document.getElementById("city-input").value;

    try {
      const weatherData = await getWeather(city);
      displayWeather(weatherData);
    } catch (error) {
      document.getElementById("weather").innerHTML = `
      <div class="error">Failed to fetch weather data. Please try again.</div>
    `;
    }
  });
|GET /users| B[Server]
A -->|POST /users| B
A -->|GET /users/123| B
A -->|PUT /users/123| B
A -->|DELETE /users/123| B
B -->|Response| A

—>

Common HTTP Status Codes

When working with APIs, you’ll encounter these status codes:

  • 200 OK: Success
  • 201 Created: Resource created successfully
  • 204 No Content: Success but no content to return
  • 400 Bad Request: Invalid input
  • 401 Unauthorized: Authentication required
  • 403 Forbidden: Not allowed to access this resource
  • 404 Not Found: Resource doesn’t exist
  • 500 Internal Server Error: Server error

Error Handling Best Practices

Robust error handling is critical when working with APIs:

async function fetchAPI(url) {
  try {
    const response = await fetch(url);

    if (response.status === 404) {
      throw new Error("Resource not found");
    }

    if (response.status === 401) {
      // Maybe trigger a new login
      throw new Error("Authentication required");
    }

    if (!response.ok) {
      // Try to get error details from response
      try {
        const errorData = await response.json();
        throw new Error(errorData.message || `API error: ${response.status}`);
      } catch (jsonError) {
        // If parsing JSON fails, just use the status
        throw new Error(`API error: ${response.status}`);
      }
    }

    return response.json();
  } catch (error) {
    // Log the error for debugging
    console.error("API request failed:", error);

    // Provide user-friendly message
    if (error.message.includes("Failed to fetch")) {
      throw new Error("Network error. Please check your internet connection.");
    }

    // Re-throw the error for the caller to handle
    throw error;
  }
}

Rate Limiting and Throttling Requests

Many APIs limit how many requests you can make in a given time period. Here’s a simple way to throttle requests:

class APIThrottler {
  constructor(requestsPerMinute) {
    this.requestsPerMinute = requestsPerMinute;
    this.queue = [];
    this.processing = false;
  }

  async fetch(url, options = {}) {
    return new Promise((resolve, reject) => {
      this.queue.push({
        url,
        options,
        resolve,
        reject,
      });

      if (!this.processing) {
        this.processQueue();
      }
    });
  }

  async processQueue() {
    this.processing = true;

    while (this.queue.length > 0) {
      const { url, options, resolve, reject } = this.queue.shift();

      try {
        const response = await fetch(url, options);
        const data = await response.json();
        resolve(data);
      } catch (error) {
        reject(error);
      }

      // Wait to respect rate limit
      await new Promise((r) =>
        setTimeout(r, (60 * 1000) / this.requestsPerMinute)
      );
    }

    this.processing = false;
  }
}

// Usage
const apiClient = new APIThrottler(30); // 30 requests per minute

async function getMultipleUsers() {
  const userIds = [1, 2, 3, 4, 5];
  const promises = userIds.map((id) =>
    apiClient.fetch(`https://api.example.com/users/${id}`)
  );

  return Promise.all(promises);
}

Conclusion

Web APIs are the backbone of modern web development, connecting applications and enabling rich, data-driven experiences. The Fetch API provides a powerful way to interact with these services from your JavaScript code.

As you work with APIs, remember these key points:

  1. Always handle errors gracefully
  2. Use async/await for cleaner, more readable code
  3. Respect API rate limits and implement proper throttling
  4. Validate and sanitize data both before sending and after receiving
  5. Keep sensitive information like API keys secure

With these skills, you’ll be able to integrate any third-party service or build your own API-powered applications with confidence.