Session 11 External APIs - Dyotson/START-Coding-in-3-Weeks GitHub Wiki
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.
- 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
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.
- 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.
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
- GET: Retrieve data (Read)
- POST: Create new data (Create)
- PUT/PATCH: Update existing data (Update)
- DELETE: Remove data (Delete)
- 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
// 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));
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);
});
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}')
Most APIs require authentication to ensure that only authorized users can access them. Common authentication methods include:
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));
// 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));
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));
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;
}
}
}
}
Let's explore some popular APIs that you can integrate into your applications:
-
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}`); });
-
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 } });
-
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!' });
- Twitter API: Tweet data, user information, post tweets
- Facebook Graph API: User data, posts, pages
- Amazon S3: Cloud storage for files
- Google Drive API: File storage and manipulation
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>
);
}
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;
}
});
- Create a weather dashboard using the OpenWeatherMap API
- Build an application that integrates with a public API of your choice (e.g., movie database, news API, cryptocurrency data)
- Implement error handling and loading states for API requests
- Create a unified API client for multiple services
- Add authentication to your API requests
- Implement rate limiting and retry logic for API calls
- Public APIs Directory - Extensive list of free public APIs
- MDN Fetch API Documentation
- Axios Documentation
- Python Requests Library
- REST API Design Best Practices
- OAuth 2.0 Simplified
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.