Full Stack Learning Hub

Comprehensive guides, cheat sheets, and code examples for full stack development.

View on GitHub

APIs & HTTP Requests - Complete Reference Guide

Quick Reference Card

Component Syntax Example
GET request requests.get(url) requests.get('https://api.example.com/data')
POST request requests.post(url, json=data) requests.post(url, json={'key': 'value'})
PUT request requests.put(url, json=data) requests.put(url, json={'key': 'value'})
DELETE request requests.delete(url) requests.delete('https://api.example.com/item/1')
Query params params={'key': 'value'} requests.get(url, params={'page': 1})
Headers headers={'key': 'value'} headers={'Authorization': 'Bearer token'}
JSON response response.json() data = response.json()
Status code response.status_code if response.status_code == 200:
Check success response.ok if response.ok:
Timeout timeout=seconds requests.get(url, timeout=5)

Common Status Codes: 200 (OK), 201 (Created), 400 (Bad Request), 401 (Unauthorized), 404 (Not Found), 500 (Server Error)

Table of Contents


API Basics

What is an API?

# API - Application Programming Interface
# Allows different software applications to communicate

# REST API - Representational State Transfer
# Uses HTTP methods to perform CRUD operations:
# - Create (POST)
# - Read (GET)
# - Update (PUT/PATCH)
# - Delete (DELETE)

JSON Format

# JSON - JavaScript Object Notation
# Lightweight data interchange format

# Python dict ←→ JSON
import json

# Python to JSON
python_dict = {"name": "Wilson", "age": 30}
json_string = json.dumps(python_dict)
# '{"name": "Wilson", "age": 30}'

# JSON to Python
json_data = '{"name": "Bob", "age": 25}'
python_obj = json.loads(json_data)
# {'name': 'Bob', 'age': 25}

HTTP Status Codes

# 2xx - Success
200  # OK - Request successful
201  # Created - Resource created successfully
204  # No Content - Successful but no data returned

# 3xx - Redirection
301  # Moved Permanently
302  # Found (temporary redirect)

# 4xx - Client Errors (Your mistake)
400  # Bad Request - Malformed request
401  # Unauthorized - Authentication required
403  # Forbidden - No permission
404  # Not Found - Resource doesn't exist
429  # Too Many Requests - Rate limit exceeded

# 5xx - Server Errors (Their problem)
500  # Internal Server Error
502  # Bad Gateway
503  # Service Unavailable

HTTP Methods

GET - Retrieve Data

import requests

# GET request
response = requests.get("https://api.example.com/users")

# GET with parameters
params = {"page": 1, "limit": 10}
response = requests.get("https://api.example.com/users", params=params)
# Actual URL: https://api.example.com/users?page=1&limit=10

POST - Create Data

import requests

# POST request with JSON body
data = {
    "name": "Wilson",
    "email": "Wilson@example.com",
    "age": 30
}

response = requests.post(
    "https://api.example.com/users",
    json=data  # Automatically sets Content-Type: application/json
)

# Alternative: send as form data
response = requests.post(
    "https://api.example.com/users",
    data=data  # Content-Type: application/x-www-form-urlencoded
)

PUT - Update (Replace) Data

import requests

# PUT request - replaces entire resource
updated_data = {
    "name": "Wilson Johnson",
    "email": "Wilson.j@example.com",
    "age": 31
}

response = requests.put(
    "https://api.example.com/users/123",
    json=updated_data
)

PATCH - Partial Update

import requests

# PATCH request - updates specific fields
partial_update = {
    "age": 31  # Only update age
}

response = requests.patch(
    "https://api.example.com/users/123",
    json=partial_update
)

DELETE - Remove Data

import requests

# DELETE request
response = requests.delete("https://api.example.com/users/123")

# Check if successful
if response.status_code == 204:
    print("User deleted successfully")

Python Requests Library

Installation

pip install requests

Basic GET Request

import requests

# Make a GET request
response = requests.get("https://jsonplaceholder.typicode.com/posts/1")

# Check status
print(response.status_code)  # 200

# Get JSON data
data = response.json()
print(data)  # Python dictionary

# Get raw text
print(response.text)  # String

Request with Timeout

import requests

# Timeout in seconds
try:
    response = requests.get(
        "https://api.example.com/data",
        timeout=5  # Wait max 5 seconds
    )
except requests.Timeout:
    print("Request timed out")

Request with Retries

import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

# Configure retry strategy
retry_strategy = Retry(
    total=3,  # Total number of retries
    backoff_factor=1,  # Wait 1, 2, 4 seconds between retries
    status_forcelist=[429, 500, 502, 503, 504]
)

adapter = HTTPAdapter(max_retries=retry_strategy)
session = requests.Session()
session.mount("https://", adapter)

# Make request with retries
response = session.get("https://api.example.com/data")

Request Components

Headers

import requests

# Add custom headers
headers = {
    "User-Agent": "MyApp/1.0",
    "Accept": "application/json",
    "Content-Type": "application/json"
}

response = requests.get(
    "https://api.example.com/data",
    headers=headers
)

# View request headers
print(response.request.headers)

Query Parameters

import requests

# Method 1: In URL
response = requests.get("https://api.example.com/search?q=python&page=1")

# Method 2: Using params (recommended)
params = {
    "q": "python",
    "page": 1,
    "limit": 10
}

response = requests.get("https://api.example.com/search", params=params)

# With list values
params = {
    "tags": ["python", "api", "tutorial"]
}
# Becomes: ?tags=python&tags=api&tags=tutorial

Request Body

import requests

# JSON body (most common for APIs)
data = {
    "username": "Wilson",
    "email": "Wilson@example.com"
}

response = requests.post(
    "https://api.example.com/users",
    json=data  # Automatically serializes and sets Content-Type
)

# Form data
form_data = {
    "username": "Wilson",
    "password": "secret123"
}

response = requests.post(
    "https://api.example.com/login",
    data=form_data  # Sent as form-encoded
)

# Raw string body
response = requests.post(
    "https://api.example.com/webhook",
    data="raw string data"
)

Cookies

import requests

# Send cookies
cookies = {"session_id": "abc123"}
response = requests.get(
    "https://api.example.com/profile",
    cookies=cookies
)

# Get cookies from response
print(response.cookies)

Response Handling

Accessing Response Data

import requests

response = requests.get("https://jsonplaceholder.typicode.com/posts/1")

# Status code
status = response.status_code  # 200

# JSON data (raises error if not JSON)
data = response.json()

# Raw text
text = response.text

# Raw bytes
content = response.content

# Headers
headers = response.headers
content_type = response.headers['Content-Type']

# URL (useful after redirects)
url = response.url

# Check if successful (2xx status code)
is_success = response.ok  # True for 200-299

Checking Response Status

import requests

response = requests.get("https://api.example.com/data")

# Method 1: Check status code
if response.status_code == 200:
    data = response.json()
    print("Success:", data)
elif response.status_code == 404:
    print("Resource not found")
else:
    print(f"Error: {response.status_code}")

# Method 2: Use ok property
if response.ok:
    data = response.json()
else:
    print(f"Request failed: {response.status_code}")

# Method 3: Raise exception for error status
try:
    response.raise_for_status()  # Raises HTTPError for 4xx/5xx
    data = response.json()
except requests.HTTPError as e:
    print(f"HTTP Error: {e}")

Parsing JSON Safely

import requests

response = requests.get("https://api.example.com/data")

# Check if response is JSON
if response.headers.get('Content-Type') == 'application/json':
    try:
        data = response.json()
    except ValueError:
        print("Invalid JSON response")
else:
    print("Response is not JSON")

# Or use try-except
try:
    data = response.json()
except requests.JSONDecodeError:
    print("Failed to parse JSON")

Authentication

API Key Authentication

import requests

# Method 1: In query parameters
api_key = "your_api_key_here"
response = requests.get(
    "https://api.example.com/data",
    params={"api_key": api_key}
)

# Method 2: In headers (more common)
headers = {
    "X-API-Key": api_key
}
response = requests.get(
    "https://api.example.com/data",
    headers=headers
)

Bearer Token Authentication

import requests

# Common for OAuth 2.0
token = "your_access_token_here"

headers = {
    "Authorization": f"Bearer {token}"
}

response = requests.get(
    "https://api.example.com/protected",
    headers=headers
)

Basic Authentication

import requests

# Method 1: Using auth parameter (recommended)
response = requests.get(
    "https://api.example.com/data",
    auth=("username", "password")
)

# Method 2: Manual header
import base64

credentials = "username:password"
encoded = base64.b64encode(credentials.encode()).decode()

headers = {
    "Authorization": f"Basic {encoded}"
}

response = requests.get(
    "https://api.example.com/data",
    headers=headers
)

OAuth 2.0 Example (Spotify)

import requests
import base64

# Step 1: Get access token
client_id = "your_client_id"
client_secret = "your_client_secret"

# Encode credentials
credentials = f"{client_id}:{client_secret}"
encoded_creds = base64.b64encode(credentials.encode()).decode()

# Request token
token_url = "https://accounts.spotify.com/api/token"

headers = {
    "Authorization": f"Basic {encoded_creds}",
    "Content-Type": "application/x-www-form-urlencoded"
}

data = {
    "grant_type": "client_credentials"
}

response = requests.post(token_url, headers=headers, data=data)
token_data = response.json()
access_token = token_data["access_token"]

# Step 2: Use token for API requests
api_headers = {
    "Authorization": f"Bearer {access_token}"
}

response = requests.get(
    "https://api.spotify.com/v1/search",
    headers=api_headers,
    params={"q": "Beatles", "type": "artist"}
)

data = response.json()

Error Handling

Comprehensive Error Handling

import requests
from requests.exceptions import (
    Timeout,
    ConnectionError,
    HTTPError,
    RequestException
)

def make_api_request(url):
    try:
        response = requests.get(url, timeout=5)
        response.raise_for_status()  # Raise HTTPError for bad status
        return response.json()

    except Timeout:
        print("Request timed out")
        return None

    except ConnectionError:
        print("Failed to connect to server")
        return None

    except HTTPError as e:
        print(f"HTTP error occurred: {e}")
        print(f"Status code: {e.response.status_code}")
        return None

    except RequestException as e:
        # Catch all other requests exceptions
        print(f"An error occurred: {e}")
        return None

    except ValueError:
        # JSON decode error
        print("Invalid JSON in response")
        return None

# Use the function
data = make_api_request("https://api.example.com/data")
if data:
    print("Success:", data)

Handling Rate Limits

import requests
import time

def api_call_with_retry(url, max_retries=3):
    for attempt in range(max_retries):
        response = requests.get(url)

        if response.status_code == 200:
            return response.json()

        elif response.status_code == 429:  # Rate limited
            retry_after = int(response.headers.get('Retry-After', 60))
            print(f"Rate limited. Waiting {retry_after} seconds...")
            time.sleep(retry_after)

        else:
            print(f"Error: {response.status_code}")
            break

    return None

Best Practices

Using Sessions

import requests

# Session persists cookies and connection pooling
session = requests.Session()

# Set default headers for all requests
session.headers.update({
    "User-Agent": "MyApp/1.0",
    "Authorization": "Bearer token123"
})

# All requests use the session
response1 = session.get("https://api.example.com/users")
response2 = session.get("https://api.example.com/posts")

# Close session when done
session.close()

# Or use context manager
with requests.Session() as session:
    session.headers.update({"Authorization": "Bearer token123"})
    response = session.get("https://api.example.com/data")

Environment Variables for Secrets

import os
import requests

# Store API keys in environment variables, not in code!

# Set environment variable (in terminal):
# export API_KEY="your_api_key_here"

# Or use .env file with python-dotenv
# pip install python-dotenv

from dotenv import load_dotenv
load_dotenv()

api_key = os.getenv("API_KEY")

if not api_key:
    raise ValueError("API_KEY not found in environment variables")

headers = {"X-API-Key": api_key}
response = requests.get("https://api.example.com/data", headers=headers)

Response Caching

import requests
from datetime import datetime, timedelta

class SimpleCache:
    def __init__(self, ttl_seconds=300):
        self.cache = {}
        self.ttl = ttl_seconds

    def get(self, key):
        if key in self.cache:
            data, timestamp = self.cache[key]
            if datetime.now() - timestamp < timedelta(seconds=self.ttl):
                return data
            else:
                del self.cache[key]
        return None

    def set(self, key, value):
        self.cache[key] = (value, datetime.now())

# Usage
cache = SimpleCache(ttl_seconds=300)  # 5 minute cache

def get_data(url):
    # Check cache first
    cached = cache.get(url)
    if cached:
        print("Using cached data")
        return cached

    # Make request
    response = requests.get(url)
    data = response.json()

    # Cache the result
    cache.set(url, data)
    return data

Practical Examples

Weather API

import requests

def get_weather(city, api_key):
    url = "https://api.openweathermap.org/data/2.5/weather"

    params = {
        "q": city,
        "appid": api_key,
        "units": "metric"
    }

    response = requests.get(url, params=params)

    if response.status_code == 200:
        data = response.json()
        return {
            "temperature": data["main"]["temp"],
            "description": data["weather"][0]["description"],
            "humidity": data["main"]["humidity"]
        }
    else:
        return None

# weather = get_weather("London", "your_api_key")

Pokemon API - Complete Example

This real-world example demonstrates proper API integration with error handling and nested JSON parsing.

import requests

def get_pokemon_data(pokemon_identifier):
    """
    Get Pokemon data from PokeAPI and extract game-relevant information.

    Args:
        pokemon_identifier: Pokemon name (str) or ID (int)
            Examples: "pikachu", "charizard", 25, 6

    Returns:
        dict: Pokemon information with keys: name, id, hp, attack, sprite_url, type
        None: if Pokemon not found or error occurred

    Example:
        >>> pikachu = get_pokemon_data("pikachu")
        >>> print(f"{pikachu['name']} has {pikachu['hp']} HP")
        pikachu has 35 HP
    """

    # Step 1: Construct URL with identifier (can be name or ID)
    url = f"https://pokeapi.co/api/v2/pokemon/{pokemon_identifier}"

    # Step 2: Make GET request
    response = requests.get(url)

    # Step 3: Check if request was successful
    if response.status_code == 200:
        data = response.json()  # Convert JSON to Python dictionary

        # Step 4: Extract relevant data from nested JSON structure
        pokemon = {
            "name": data['name'],
            "id": data['id'],
            "hp": data["stats"][0]["base_stat"],  # HP is first stat
            "attack": data["stats"][1]["base_stat"],  # Attack is second stat
            "sprite_url": data["sprites"]["front_default"],
            "type": data["types"][0]["type"]["name"]  # Primary type
        }
        return pokemon

    elif response.status_code == 404:
        # Pokemon not found
        print(f"Pokemon '{pokemon_identifier}' not found")
        return None

    else:
        # Other errors
        print(f"Failed to catch {pokemon_identifier}: Status {response.status_code}")
        return None


# Usage Examples:
if __name__ == "__main__":
    # Example 1: Get Pokemon by name
    pikachu = get_pokemon_data("pikachu")
    if pikachu:
        print(f"\n{pikachu['name'].title()}")
        print(f"   Type: {pikachu['type']}")
        print(f"   HP: {pikachu['hp']}")
        print(f"   Attack: {pikachu['attack']}")

    # Example 2: Get Pokemon by ID
    charizard = get_pokemon_data(6)
    if charizard:
        print(f"\n{charizard['name'].title()}")
        print(f"   Type: {charizard['type']}")
        print(f"   HP: {charizard['hp']}")
        print(f"   Attack: {charizard['attack']}")

    # Example 3: Handle invalid Pokemon
    invalid = get_pokemon_data("invalidpokemon")
    if not invalid:
        print("\nPokemon not found!")

Key Features of This Example:

Understanding the JSON Structure:

# PokeAPI returns deeply nested JSON:
{
    "name": "pikachu",
    "id": 25,
    "stats": [
        {"base_stat": 35, "stat": {"name": "hp"}},      # Index 0
        {"base_stat": 55, "stat": {"name": "attack"}},   # Index 1
        # ... more stats
    ],
    "sprites": {
        "front_default": "https://...pikachu.png"
    },
    "types": [
        {"type": {"name": "electric"}}  # Primary type at index 0
    ]
}

# We navigate this structure to extract just what we need

RESTful CRUD Operations

import requests

BASE_URL = "https://jsonplaceholder.typicode.com"

# Create
def create_post(title, body, user_id):
    url = f"{BASE_URL}/posts"
    data = {"title": title, "body": body, "userId": user_id}
    response = requests.post(url, json=data)
    return response.json()

# Read
def get_post(post_id):
    url = f"{BASE_URL}/posts/{post_id}"
    response = requests.get(url)
    return response.json() if response.ok else None

# Update
def update_post(post_id, title, body):
    url = f"{BASE_URL}/posts/{post_id}"
    data = {"title": title, "body": body}
    response = requests.put(url, json=data)
    return response.json()

# Delete
def delete_post(post_id):
    url = f"{BASE_URL}/posts/{post_id}"
    response = requests.delete(url)
    return response.status_code == 200

# Usage
# new_post = create_post("My Title", "My content", 1)
# post = get_post(1)
# update_post(1, "Updated Title", "Updated content")
# delete_post(1)

See Also