Session 11 External APIs - Dyotson/START-Coding-in-3-Weeks GitHub Wiki

Session 11: Connect to the Real World (External APIs)

Overview

Welcome to Session 11! In this session, we'll learn how to integrate external APIs into our applications. APIs (Application Programming Interfaces) allow our applications to communicate with other services and retrieve or send data, enabling us to build feature-rich applications that leverage existing services rather than building everything from scratch.

Learning Objectives

  • Understand what APIs are and how they work
  • Learn about RESTful API principles
  • Make API requests using different HTTP methods
  • Handle API responses and errors
  • Implement authentication for secure API access
  • Integrate external APIs into our applications

Topics Covered

1. Understanding APIs

An API (Application Programming Interface) is a set of rules that allows one software application to interact with another. It defines the methods and data formats that applications can use to request and exchange data.

Types of APIs:

  • REST APIs: Use standard HTTP methods and are the most common
  • GraphQL APIs: Allow clients to request exactly what they need
  • SOAP APIs: Use XML for message format and are typically more rigid
  • WebSocket APIs: Enable bidirectional communication for real-time applications

In this session, we'll focus primarily on REST APIs, which are the most widely used.

2. REST API Principles

REST (Representational State Transfer) is an architectural style for designing networked applications. RESTful APIs follow these key principles:

  • Client-Server Architecture: Separation of concerns between client and server
  • Statelessness: Each request contains all the information needed to complete it
  • Cacheable: Responses must be explicitly labeled as cacheable or non-cacheable
  • Uniform Interface: Resources are identified in requests, manipulated through representations, and include self-descriptive messages
  • Layered System: Architecture composed of hierarchical layers

HTTP Methods (CRUD Operations):

  • GET: Retrieve data (Read)
  • POST: Create new data (Create)
  • PUT/PATCH: Update existing data (Update)
  • DELETE: Remove data (Delete)

HTTP Status Codes:

  • 2xx (Success): 200 OK, 201 Created, 204 No Content
  • 3xx (Redirection): 301 Moved Permanently, 302 Found
  • 4xx (Client Error): 400 Bad Request, 401 Unauthorized, 404 Not Found
  • 5xx (Server Error): 500 Internal Server Error, 503 Service Unavailable

3. Making API Requests

Using the fetch API (JavaScript):

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

// POST request with headers and body
fetch('https://api.example.com/items', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer your-token-here'
  },
  body: JSON.stringify({
    name: 'New Item',
    description: 'This is a new item'
  })
})
  .then(response => response.json())
  .then(data => console.log('Item created:', data))
  .catch(error => console.error('Error creating item:', error));

Using Axios (JavaScript):

import axios from 'axios';

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

// POST request
axios.post('https://api.example.com/items', {
  name: 'New Item',
  description: 'This is a new item'
}, {
  headers: {
    'Authorization': 'Bearer your-token-here'
  }
})
  .then(response => {
    console.log('Item created:', response.data);
  })
  .catch(error => {
    console.error('Error creating item:', error);
  });

Using the requests Library (Python):

import requests

# GET request
response = requests.get('https://api.example.com/data')
if response.status_code == 200:
    data = response.json()
    print('Data received:', data)
else:
    print(f'Error: {response.status_code}')

# POST request
headers = {
    'Authorization': 'Bearer your-token-here'
}
data = {
    'name': 'New Item',
    'description': 'This is a new item'
}
response = requests.post('https://api.example.com/items', json=data, headers=headers)
if response.status_code == 201:  # Created
    new_item = response.json()
    print('Item created:', new_item)
else:
    print(f'Error: {response.status_code}')

4. API Authentication

Most APIs require authentication to ensure that only authorized users can access them. Common authentication methods include:

API Keys:

fetch('https://api.example.com/data?api_key=your-api-key-here')
  .then(response => response.json())
  .then(data => console.log(data));

// Or in headers
fetch('https://api.example.com/data', {
  headers: {
    'X-API-Key': 'your-api-key-here'
  }
})
  .then(response => response.json())
  .then(data => console.log(data));

OAuth 2.0:

// After obtaining an access token through the OAuth flow
fetch('https://api.example.com/protected-resource', {
  headers: {
    'Authorization': 'Bearer your-access-token'
  }
})
  .then(response => response.json())
  .then(data => console.log(data));

Basic Authentication:

const credentials = btoa('username:password');  // Base64 encoding
fetch('https://api.example.com/data', {
  headers: {
    'Authorization': `Basic ${credentials}`
  }
})
  .then(response => response.json())
  .then(data => console.log(data));

5. Handling API Responses and Errors

Proper error handling is crucial when working with external APIs:

async function fetchData(url) {
  try {
    const response = await fetch(url);
    
    // Check if the request was successful
    if (!response.ok) {
      // Get more error info from the response
      const errorData = await response.json().catch(() => null);
      throw new Error(`API error: ${response.status} ${response.statusText} ${errorData ? JSON.stringify(errorData) : ''}`);
    }
    
    // Parse and return the data
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error fetching data:', error);
    throw error; // Re-throw to let the caller handle it
  }
}

// With rate limiting and retry logic
async function fetchWithRetry(url, maxRetries = 3, delay = 1000) {
  let retries = 0;
  
  while (retries < maxRetries) {
    try {
      return await fetchData(url);
    } catch (error) {
      // Retry on rate limiting (429) or server errors (5xx)
      if ((error.message.includes('429') || error.message.includes('5')) && retries < maxRetries - 1) {
        retries++;
        console.log(`Retrying (${retries}/${maxRetries}) in ${delay}ms...`);
        await new Promise(resolve => setTimeout(resolve, delay));
        delay *= 2; // Exponential backoff
      } else {
        throw error;
      }
    }
  }
}

6. Popular External APIs

Let's explore some popular APIs that you can integrate into your applications:

Weather APIs:

  • OpenWeatherMap: Weather data, forecasts, historical data
    const API_KEY = 'your-openweathermap-api-key';
    const city = 'London';
    
    fetch(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric`)
      .then(response => response.json())
      .then(data => {
        console.log(`Current temperature in ${city}: ${data.main.temp}°C`);
        console.log(`Weather conditions: ${data.weather[0].description}`);
      });

Payment APIs:

  • Stripe: Online payment processing
    // Using Stripe.js
    const stripe = Stripe('your-publishable-key');
    
    stripe.createPaymentMethod({
      type: 'card',
      card: elements.getElement('card'),
      billing_details: {
        name: 'Customer Name',
      },
    })
    .then(result => {
      if (result.error) {
        console.error(result.error.message);
      } else {
        // Send payment method ID to your server
        // server creates payment intent and confirms payment
      }
    });

Maps and Location APIs:

  • Google Maps API: Maps, geocoding, directions
    // Initialize map
    const map = new google.maps.Map(document.getElementById('map'), {
      center: { lat: -34.397, lng: 150.644 },
      zoom: 8
    });
    
    // Add a marker
    new google.maps.Marker({
      position: { lat: -34.397, lng: 150.644 },
      map: map,
      title: 'Hello World!'
    });

Social Media APIs:

  • Twitter API: Tweet data, user information, post tweets
  • Facebook Graph API: User data, posts, pages

File Storage APIs:

  • Amazon S3: Cloud storage for files
  • Google Drive API: File storage and manipulation

7. Building a Weather Dashboard with OpenWeatherMap API

Let's build a simple Next.js component that fetches and displays weather data:

'use client';

import { useState, useEffect } from 'react';
import axios from 'axios';

export default function WeatherDashboard() {
  const [city, setCity] = useState('');
  const [weather, setWeather] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  
  const API_KEY = process.env.NEXT_PUBLIC_OPENWEATHERMAP_API_KEY;
  
  const fetchWeather = async (e) => {
    e.preventDefault();
    
    if (!city.trim()) {
      setError('Please enter a city name');
      return;
    }
    
    setLoading(true);
    setError('');
    setWeather(null);
    
    try {
      const response = await axios.get(
        `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric`
      );
      
      setWeather(response.data);
    } catch (err) {
      if (err.response && err.response.status === 404) {
        setError('City not found. Please check the spelling and try again.');
      } else {
        setError('Failed to fetch weather data. Please try again later.');
      }
      console.error('Error fetching weather:', err);
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <div className="max-w-md mx-auto my-8 p-6 bg-white rounded-lg shadow-lg">
      <h1 className="text-2xl font-bold mb-4 text-center">Weather Dashboard</h1>
      
      <form onSubmit={fetchWeather} className="mb-4">
        <div className="flex">
          <input
            type="text"
            value={city}
            onChange={(e) => setCity(e.target.value)}
            placeholder="Enter city name"
            className="flex-1 px-4 py-2 border border-gray-300 rounded-l focus:outline-none focus:ring-2 focus:ring-blue-500"
          />
          <button
            type="submit"
            className="px-4 py-2 bg-blue-500 text-white rounded-r hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
            disabled={loading}
          >
            {loading ? 'Loading...' : 'Get Weather'}
          </button>
        </div>
      </form>
      
      {error && (
        <div className="p-3 bg-red-100 text-red-700 rounded mb-4">
          {error}
        </div>
      )}
      
      {weather && (
        <div className="bg-gray-100 p-4 rounded">
          <div className="text-center mb-2">
            <h2 className="text-xl font-bold">{weather.name}, {weather.sys.country}</h2>
            <p className="text-gray-500">{new Date().toLocaleDateString()}</p>
          </div>
          
          <div className="flex justify-center items-center mb-4">
            <img
              src={`http://openweathermap.org/img/wn/${weather.weather[0].icon}@2x.png`}
              alt={weather.weather[0].description}
              width="80"
              height="80"
            />
            <div className="text-center">
              <p className="text-4xl font-bold">{Math.round(weather.main.temp)}°C</p>
              <p className="capitalize">{weather.weather[0].description}</p>
            </div>
          </div>
          
          <div className="grid grid-cols-2 gap-4">
            <div className="text-center p-2 bg-white rounded shadow">
              <p className="text-gray-500">Feels Like</p>
              <p className="font-bold">{Math.round(weather.main.feels_like)}°C</p>
            </div>
            <div className="text-center p-2 bg-white rounded shadow">
              <p className="text-gray-500">Humidity</p>
              <p className="font-bold">{weather.main.humidity}%</p>
            </div>
            <div className="text-center p-2 bg-white rounded shadow">
              <p className="text-gray-500">Wind Speed</p>
              <p className="font-bold">{weather.wind.speed} m/s</p>
            </div>
            <div className="text-center p-2 bg-white rounded shadow">
              <p className="text-gray-500">Pressure</p>
              <p className="font-bold">{weather.main.pressure} hPa</p>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

8. Building a RESTful API Client

When working with multiple APIs, it's a good practice to create a unified API client:

// src/utils/apiClient.js
import axios from 'axios';

// Create API client instances for different services
const createApiClient = (baseURL, options = {}) => {
  const client = axios.create({
    baseURL,
    timeout: options.timeout || 10000,
    headers: {
      'Content-Type': 'application/json',
      ...options.headers
    }
  });
  
  // Request interceptor
  client.interceptors.request.use(
    config => {
      // Add auth token if needed
      if (options.authTokenProvider) {
        const token = options.authTokenProvider();
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
      }
      return config;
    },
    error => Promise.reject(error)
  );
  
  // Response interceptor
  client.interceptors.response.use(
    response => response,
    async error => {
      // Handle token refresh if 401 error and refreshTokenProvider exists
      if (error.response?.status === 401 && options.refreshTokenProvider) {
        try {
          const newToken = await options.refreshTokenProvider();
          if (newToken) {
            error.config.headers.Authorization = `Bearer ${newToken}`;
            return axios(error.config);
          }
        } catch (refreshError) {
          return Promise.reject(refreshError);
        }
      }
      return Promise.reject(error);
    }
  );
  
  return client;
};

// Create API clients for different services
export const weatherApi = createApiClient('https://api.openweathermap.org/data/2.5', {
  // Add API key to every request
  timeout: 5000,
  paramsSerializer: params => {
    return Object.entries({
      ...params,
      appid: process.env.NEXT_PUBLIC_OPENWEATHERMAP_API_KEY
    })
      .map(([key, value]) => `${key}=${value}`)
      .join('&');
  }
});

export const backendApi = createApiClient(process.env.NEXT_PUBLIC_API_URL, {
  authTokenProvider: () => {
    // Get token from local storage or state management
    return localStorage.getItem('auth_token');
  },
  refreshTokenProvider: async () => {
    // Implement token refresh logic here
    const refreshToken = localStorage.getItem('refresh_token');
    if (refreshToken) {
      try {
        const response = await axios.post('/auth/refresh', { refreshToken });
        const newToken = response.data.access_token;
        localStorage.setItem('auth_token', newToken);
        return newToken;
      } catch (error) {
        // Handle refresh error (e.g., redirect to login)
        localStorage.removeItem('auth_token');
        localStorage.removeItem('refresh_token');
        window.location.href = '/login';
        return null;
      }
    }
    return null;
  }
});

Practice Exercises

  1. Create a weather dashboard using the OpenWeatherMap API
  2. Build an application that integrates with a public API of your choice (e.g., movie database, news API, cryptocurrency data)
  3. Implement error handling and loading states for API requests
  4. Create a unified API client for multiple services
  5. Add authentication to your API requests
  6. Implement rate limiting and retry logic for API calls

Additional Resources

Next Steps

Now that you've learned how to integrate external APIs into your application, let's move on to Session 12: Make it Pretty where you'll learn about UI/UX design principles and how to create beautiful user interfaces using shadcn/ui.

⚠️ **GitHub.com Fallback** ⚠️