šŸ”„ API Styles & Python REST Scripts

Comparing API Architectures and Building Python Applications

šŸ”„ 2.8 Compare Common API Styles

Overview of API Styles

Different API architectural styles exist to serve different needs. The most common styles are REST and RPC, which can be implemented using either synchronous or asynchronous communication patterns.

graph TD A["API Styles"] --> B["REST"] A --> C["RPC"] B --> D["Resource-Based"] B --> E["HTTP Methods"] B --> F["Stateless"] C --> G["Action-Based"] C --> H["Function Calls"] C --> I["Procedure-Oriented"] A --> J["Communication Pattern"] J --> K["Synchronous"] J --> L["Asynchronous"] K --> M["Wait for Response"] L --> N["Callback/Promise"] style A fill:#667eea,stroke:#764ba2,stroke-width:3px,color:#fff style B fill:#42a5f5,stroke:#1976d2,stroke-width:2px,color:#fff style C fill:#ab47bc,stroke:#6a1b9a,stroke-width:2px,color:#fff style K fill:#66bb6a,stroke:#2e7d32,stroke-width:2px,color:#fff style L fill:#ffa726,stroke:#e65100,stroke-width:2px,color:#fff

1. REST (Representational State Transfer)

REST Resource-Oriented Architecture

REST is an architectural style that treats everything as a resource (users, products, orders, etc.). Resources are identified by URLs and manipulated using standard HTTP methods.

Core Principles of REST:
  1. Client-Server: Separation of concerns between client and server
  2. Stateless: Each request contains all needed information
  3. Cacheable: Responses can be cached for performance
  4. Uniform Interface: Standard HTTP methods and status codes
  5. Layered System: Can have intermediary layers (proxies, load balancers)

REST Characteristics

Aspect Description
Focus Resources (nouns) - users, products, orders
Methods HTTP verbs: GET, POST, PUT, PATCH, DELETE
URL Structure /api/users/123 (resource-based)
Data Format Usually JSON, can be XML, HTML
State Stateless - no client context stored on server
Response HTTP status codes (200, 404, 500, etc.)

REST API Examples

# REST API Examples - Resource-based URLs with HTTP methods

# Get all users
GET /api/v1/users

# Get specific user
GET /api/v1/users/123

# Create new user
POST /api/v1/users
Body: {"name": "John Doe", "email": "john@example.com"}

# Update user (full replacement)
PUT /api/v1/users/123
Body: {"name": "John Smith", "email": "john.smith@example.com"}

# Update user (partial update)
PATCH /api/v1/users/123
Body: {"email": "newemail@example.com"}

# Delete user
DELETE /api/v1/users/123

# Nested resources (relationships)
GET /api/v1/users/123/orders        # Get orders for user 123
GET /api/v1/users/123/orders/456    # Get specific order for user

# Filtering, sorting, pagination
GET /api/v1/users?role=admin&sort=created_at&page=2&limit=20
āœ… REST Advantages:
  • Scalable: Stateless design allows horizontal scaling
  • Simple: Uses standard HTTP - easy to understand and implement
  • Cacheable: Built-in HTTP caching mechanisms
  • Flexible: Supports multiple data formats
  • Widely Used: Industry standard, extensive tooling
āŒ REST Disadvantages:
  • Over-fetching/Under-fetching: May get too much or too little data
  • Multiple Requests: Complex operations may need multiple API calls
  • No Standard Schema: API documentation varies widely
  • Versioning: Can be challenging to maintain multiple versions

2. RPC (Remote Procedure Call)

RPC Action-Oriented Architecture

RPC (Remote Procedure Call) is an architectural style that treats API calls like function calls. Instead of resources, RPC focuses on actions/procedures to be executed.

RPC Variants:
  • JSON-RPC: Uses JSON for encoding
  • XML-RPC: Uses XML for encoding
  • gRPC: Google's high-performance RPC framework using Protocol Buffers

RPC Characteristics

Aspect Description
Focus Actions/procedures (verbs) - getUser, createOrder, calculateTotal
Methods Usually POST for all operations
URL Structure /api/getUser (action-based) or single endpoint
Data Format JSON, XML, Protocol Buffers
State Can be stateful or stateless
Response Procedure-specific status in response body

RPC API Examples

# JSON-RPC Example - All use POST to single endpoint

# Get user
POST /api/jsonrpc
Body: {
    "jsonrpc": "2.0",
    "method": "getUser",
    "params": {"userId": 123},
    "id": 1
}

# Create user
POST /api/jsonrpc
Body: {
    "jsonrpc": "2.0",
    "method": "createUser",
    "params": {
        "name": "John Doe",
        "email": "john@example.com"
    },
    "id": 2
}

# Update user
POST /api/jsonrpc
Body: {
    "jsonrpc": "2.0",
    "method": "updateUser",
    "params": {
        "userId": 123,
        "email": "newemail@example.com"
    },
    "id": 3
}

# Delete user
POST /api/jsonrpc
Body: {
    "jsonrpc": "2.0",
    "method": "deleteUser",
    "params": {"userId": 123},
    "id": 4
}

# Response format
{
    "jsonrpc": "2.0",
    "result": {
        "userId": 123,
        "name": "John Doe",
        "email": "john@example.com"
    },
    "id": 1
}

# Error response
{
    "jsonrpc": "2.0",
    "error": {
        "code": -32600,
        "message": "Invalid Request"
    },
    "id": 1
}
āœ… RPC Advantages:
  • Simple Concept: Like calling local functions
  • Efficient: Lightweight protocol, less overhead
  • Action-Oriented: Natural for command-based operations
  • Flexible: Can bundle multiple operations in one call
  • Strongly Typed: (gRPC) compile-time type checking
āŒ RPC Disadvantages:
  • Less Standard: No universal convention like REST
  • Tight Coupling: Client needs to know exact procedure names
  • Discovery: Harder to explore API without documentation
  • Caching: Difficult to cache since everything is POST
  • Not Resource-Oriented: Doesn't align with web architecture

3. Synchronous vs Asynchronous Communication

Synchronous Blocking Communication

Synchronous communication means the client sends a request and waits (blocks) until it receives a response from the server.

sequenceDiagram participant C as Client participant S as Server C->>S: Request Note over C: Client Waits Note over S: Processing S->>C: Response Note over C: Continue Execution

Synchronous Characteristics

  • Blocking: Client waits for response before continuing
  • Sequential: Operations happen one after another
  • Simple: Easy to understand and implement
  • Request-Response: Immediate response expected
  • Example: Traditional REST API calls, database queries
# Synchronous Example - Python
import requests

# Client waits here until response received
response = requests.get('https://api.example.com/users/123')

# This line doesn't execute until response is received
print(f"User: {response.json()['name']}")

# Another synchronous call - waits again
response2 = requests.get('https://api.example.com/users/456')
print(f"User 2: {response2.json()['name']}")

# Total time = time1 + time2 (sequential)
āœ… Synchronous Advantages:
  • Simple to understand and implement
  • Easy to debug and trace execution
  • Predictable execution order
  • No callback complexity
āŒ Synchronous Disadvantages:
  • Blocks execution while waiting
  • Inefficient for long-running operations
  • Can't handle multiple operations concurrently
  • Poor user experience in UI (freezes)

Asynchronous Non-Blocking Communication

Asynchronous communication means the client sends a request and continues executing without waiting. Response is handled via callback, promise, or event.

sequenceDiagram participant C as Client participant S as Server C->>S: Request 1 Note over C: Continue Execution C->>S: Request 2 Note over C: Still Executing Note over S: Processing S->>C: Response 1 Note over C: Handle Response 1 S->>C: Response 2 Note over C: Handle Response 2

Asynchronous Characteristics

  • Non-Blocking: Client continues execution immediately
  • Concurrent: Multiple operations can run simultaneously
  • Complex: Requires callbacks, promises, or async/await
  • Event-Driven: Response triggers an event or callback
  • Example: AJAX calls, WebSockets, message queues
# Asynchronous Example - Python with asyncio
import asyncio
import aiohttp

async def get_user(session, user_id):
    """Asynchronous function to get user"""
    async with session.get(f'https://api.example.com/users/{user_id}') as response:
        data = await response.json()
        print(f"User {user_id}: {data['name']}")
        return data

async def main():
    """Run multiple requests concurrently"""
    async with aiohttp.ClientSession() as session:
        # Both requests start immediately (concurrent)
        task1 = get_user(session, 123)
        task2 = get_user(session, 456)
        
        # Wait for both to complete
        await asyncio.gather(task1, task2)
        
        # Total time ā‰ˆ max(time1, time2) (concurrent, not sequential)

# Run async code
asyncio.run(main())
āœ… Asynchronous Advantages:
  • Non-blocking - better resource utilization
  • Handle multiple operations concurrently
  • Better performance for I/O-bound operations
  • Responsive UI - doesn't freeze
  • Efficient for long-running operations
āŒ Asynchronous Disadvantages:
  • More complex to implement and understand
  • Harder to debug (execution order unclear)
  • Callback hell / Promise chaining complexity
  • Error handling more complicated
  • Race conditions possible

Comprehensive Comparison

Feature REST RPC Synchronous Asynchronous
Focus Resources (nouns) Actions (verbs) Request-Response Event-driven
Methods GET, POST, PUT, DELETE POST (usually) Any HTTP method Any HTTP method
URL Pattern /users/123 /getUser or /rpc N/A N/A
Execution Stateless Can be stateful Blocking Non-blocking
Caching āœ… Easy āŒ Difficult āœ… Possible āš ļø Complex
Learning Curve āš ļø Moderate āœ… Easy āœ… Easy āŒ Steep
Performance āš ļø Good āœ… Excellent āš ļø Slower āœ… Faster
Use Case Web APIs, CRUD operations Microservices, actions Simple requests Multiple concurrent operations
Example GitHub API, Twitter API Slack API, Discord API Form submission Real-time chat, notifications
šŸ’” Key Takeaways:
  • REST is resource-focused, perfect for CRUD operations on entities
  • RPC is action-focused, better for command-based operations
  • Synchronous is simple but blocks execution
  • Asynchronous is complex but enables concurrent operations
  • You can use REST with async or RPC with sync - they're independent concepts

šŸ 2.9 Construct Python Scripts that Call REST APIs

The Python Requests Library

The requests library is the de facto standard for making HTTP requests in Python. It provides a simple, elegant interface for interacting with REST APIs.

šŸ“¦ Installation:
# Install using pip
pip install requests

# Or using pip3
pip3 install requests

# Verify installation
python -c "import requests; print(requests.__version__)"

Basic Request Structure

#!/usr/bin/env python3
"""
Basic structure of a REST API call using requests library
"""

import requests

# Make a request
response = requests.get('https://api.example.com/users')

# Access response components
status_code = response.status_code    # HTTP status code (200, 404, etc.)
headers = response.headers            # Response headers (dict-like)
content = response.content            # Response body as bytes
text = response.text                  # Response body as string
data = response.json()                # Parse JSON response to dict
ok = response.ok                      # True if status < 400

# Check if request was successful
if response.ok:
    print("Success!")
    print(data)
else:
    print(f"Error: {response.status_code}")

1. GET Requests - Retrieve Data

GET - Reading Resources

GET requests retrieve data from the server without modifying anything.

#!/usr/bin/env python3
"""
GET Request Examples - Retrieving data from REST API
"""

import requests
import json


# =============================================================================
# EXAMPLE 1: Simple GET Request
# =============================================================================

def get_all_users():
    """Get list of all users"""
    url = "https://jsonplaceholder.typicode.com/users"
    
    response = requests.get(url)
    
    if response.status_code == 200:
        users = response.json()
        print(f"āœ… Retrieved {len(users)} users")
        for user in users[:3]:  # Show first 3
            print(f"  - {user['name']} ({user['email']})")
        return users
    else:
        print(f"āŒ Error: {response.status_code}")
        return None


# =============================================================================
# EXAMPLE 2: GET with Path Parameter (Specific Resource)
# =============================================================================

def get_user_by_id(user_id):
    """Get specific user by ID"""
    url = f"https://jsonplaceholder.typicode.com/users/{user_id}"
    
    response = requests.get(url)
    
    if response.status_code == 200:
        user = response.json()
        print(f"āœ… User found:")
        print(f"   Name: {user['name']}")
        print(f"   Email: {user['email']}")
        print(f"   City: {user['address']['city']}")
        return user
    elif response.status_code == 404:
        print(f"āŒ User {user_id} not found")
        return None
    else:
        print(f"āŒ Error: {response.status_code}")
        return None


# =============================================================================
# EXAMPLE 3: GET with Query Parameters (Filtering)
# =============================================================================

def get_posts_by_user(user_id):
    """Get all posts by specific user"""
    url = "https://jsonplaceholder.typicode.com/posts"
    
    # Query parameters as dictionary
    params = {
        'userId': user_id
    }
    
    response = requests.get(url, params=params)
    
    if response.status_code == 200:
        posts = response.json()
        print(f"āœ… User {user_id} has {len(posts)} posts")
        for post in posts[:2]:  # Show first 2
            print(f"   - {post['title']}")
        return posts
    else:
        print(f"āŒ Error: {response.status_code}")
        return None


# =============================================================================
# EXAMPLE 4: GET with Multiple Query Parameters
# =============================================================================

def search_posts(user_id=None, limit=None, title_contains=None):
    """Search posts with multiple filters"""
    url = "https://jsonplaceholder.typicode.com/posts"
    
    # Build params dynamically
    params = {}
    if user_id:
        params['userId'] = user_id
    if limit:
        params['_limit'] = limit
    
    response = requests.get(url, params=params)
    
    if response.status_code == 200:
        posts = response.json()
        
        # Client-side filtering if needed
        if title_contains:
            posts = [p for p in posts if title_contains.lower() in p['title'].lower()]
        
        print(f"āœ… Found {len(posts)} posts")
        return posts
    else:
        print(f"āŒ Error: {response.status_code}")
        return None


# =============================================================================
# EXAMPLE 5: GET with Headers (Authentication)
# =============================================================================

def get_protected_resource(api_key):
    """Get resource that requires authentication"""
    url = "https://api.example.com/protected/data"
    
    headers = {
        'Authorization': f'Bearer {api_key}',
        'Accept': 'application/json',
        'User-Agent': 'MyApp/1.0'
    }
    
    response = requests.get(url, headers=headers)
    
    if response.status_code == 200:
        print("āœ… Access granted")
        return response.json()
    elif response.status_code == 401:
        print("āŒ Unauthorized - check API key")
        return None
    else:
        print(f"āŒ Error: {response.status_code}")
        return None


# =============================================================================
# RUN EXAMPLES
# =============================================================================

if __name__ == "__main__":
    print("="*70)
    print("GET REQUEST EXAMPLES")
    print("="*70)
    
    print("\n1. Get all users:")
    get_all_users()
    
    print("\n2. Get specific user:")
    get_user_by_id(1)
    
    print("\n3. Get posts by user:")
    get_posts_by_user(1)
    
    print("\n4. Search posts:")
    search_posts(user_id=1, limit=3)

2. POST Requests - Create Data

POST - Creating Resources

POST requests send data to the server to create new resources.

#!/usr/bin/env python3
"""
POST Request Examples - Creating new resources
"""

import requests
import json


# =============================================================================
# EXAMPLE 1: Simple POST with JSON Data
# =============================================================================

def create_post(title, body, user_id):
    """Create a new post"""
    url = "https://jsonplaceholder.typicode.com/posts"
    
    # Data to send (Python dict, will be converted to JSON)
    data = {
        "title": title,
        "body": body,
        "userId": user_id
    }
    
    # Headers
    headers = {
        "Content-Type": "application/json"
    }
    
    # Make POST request (json= automatically converts dict to JSON)
    response = requests.post(url, json=data, headers=headers)
    
    if response.status_code == 201:  # 201 Created
        new_post = response.json()
        print(f"āœ… Post created successfully!")
        print(f"   Post ID: {new_post['id']}")
        print(f"   Title: {new_post['title']}")
        return new_post
    else:
        print(f"āŒ Error: {response.status_code}")
        print(f"   Message: {response.text}")
        return None


# =============================================================================
# EXAMPLE 2: POST with Authentication
# =============================================================================

def create_user(name, email, api_key):
    """Create new user with authentication"""
    url = "https://api.example.com/users"
    
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {api_key}"
    }
    
    data = {
        "name": name,
        "email": email,
        "role": "user"
    }
    
    response = requests.post(url, json=data, headers=headers)
    
    if response.status_code == 201:
        print(f"āœ… User created: {response.json()}")
        return response.json()
    elif response.status_code == 401:
        print("āŒ Unauthorized - invalid API key")
        return None
    elif response.status_code == 400:
        print(f"āŒ Bad request: {response.json().get('error', 'Unknown error')}")
        return None
    else:
        print(f"āŒ Error: {response.status_code}")
        return None


# =============================================================================
# EXAMPLE 3: POST with Form Data (not JSON)
# =============================================================================

def upload_form_data():
    """Submit form data (application/x-www-form-urlencoded)"""
    url = "https://httpbin.org/post"
    
    # Form data as dictionary (use data= not json=)
    form_data = {
        "username": "johndoe",
        "password": "secret123",
        "remember": "true"
    }
    
    response = requests.post(url, data=form_data)
    
    if response.ok:
        print("āœ… Form submitted successfully")
        return response.json()
    else:
        print(f"āŒ Error: {response.status_code}")
        return None


# =============================================================================
# EXAMPLE 4: POST with File Upload
# =============================================================================

def upload_file(file_path):
    """Upload file to server"""
    url = "https://httpbin.org/post"
    
    # Open and upload file
    with open(file_path, 'rb') as f:
        files = {'file': f}
        data = {'description': 'My file upload'}
        
        response = requests.post(url, files=files, data=data)
    
    if response.ok:
        print("āœ… File uploaded successfully")
        return response.json()
    else:
        print(f"āŒ Error: {response.status_code}")
        return None


# =============================================================================
# RUN EXAMPLES
# =============================================================================

if __name__ == "__main__":
    print("="*70)
    print("POST REQUEST EXAMPLES")
    print("="*70)
    
    print("\n1. Create new post:")
    create_post(
        title="My Test Post",
        body="This is the content of my post",
        user_id=1
    )
    
    print("\n2. Submit form data:")
    upload_form_data()

3. PUT & PATCH Requests - Update Data

PUT & PATCH - Updating Resources

PUT replaces entire resource. PATCH updates specific fields.

#!/usr/bin/env python3
"""
PUT and PATCH Request Examples - Updating resources
"""

import requests


# =============================================================================
# EXAMPLE 1: PUT - Full Update (Replace Entire Resource)
# =============================================================================

def update_post_full(post_id, title, body, user_id):
    """Update entire post (PUT replaces all fields)"""
    url = f"https://jsonplaceholder.typicode.com/posts/{post_id}"
    
    # ALL fields must be provided
    data = {
        "id": post_id,
        "title": title,
        "body": body,
        "userId": user_id
    }
    
    headers = {"Content-Type": "application/json"}
    
    response = requests.put(url, json=data, headers=headers)
    
    if response.status_code == 200:
        updated = response.json()
        print(f"āœ… Post {post_id} updated (PUT)")
        print(f"   Title: {updated['title']}")
        return updated
    else:
        print(f"āŒ Error: {response.status_code}")
        return None


# =============================================================================
# EXAMPLE 2: PATCH - Partial Update (Update Specific Fields)
# =============================================================================

def update_post_partial(post_id, **fields):
    """Update specific fields only (PATCH for partial update)"""
    url = f"https://jsonplaceholder.typicode.com/posts/{post_id}"
    
    # Only send fields to update
    data = fields
    
    headers = {"Content-Type": "application/json"}
    
    response = requests.patch(url, json=data, headers=headers)
    
    if response.status_code == 200:
        updated = response.json()
        print(f"āœ… Post {post_id} updated (PATCH)")
        print(f"   Updated fields: {list(fields.keys())}")
        return updated
    else:
        print(f"āŒ Error: {response.status_code}")
        return None


# =============================================================================
# RUN EXAMPLES
# =============================================================================

if __name__ == "__main__":
    print("="*70)
    print("PUT & PATCH REQUEST EXAMPLES")
    print("="*70)
    
    print("\n1. PUT - Full update:")
    update_post_full(
        post_id=1,
        title="Updated Title",
        body="Updated body content",
        user_id=1
    )
    
    print("\n2. PATCH - Partial update (title only):")
    update_post_partial(1, title="Only Title Changed")
    
    print("\n3. PATCH - Partial update (multiple fields):")
    update_post_partial(1, title="New Title", body="New Body")

4. DELETE Requests - Remove Data

DELETE - Removing Resources

DELETE requests remove resources from the server.

#!/usr/bin/env python3
"""
DELETE Request Examples - Removing resources
"""

import requests


# =============================================================================
# EXAMPLE 1: Simple DELETE
# =============================================================================

def delete_post(post_id):
    """Delete a post by ID"""
    url = f"https://jsonplaceholder.typicode.com/posts/{post_id}"
    
    response = requests.delete(url)
    
    if response.status_code == 200:
        print(f"āœ… Post {post_id} deleted successfully")
        return True
    elif response.status_code == 204:  # No Content - also success
        print(f"āœ… Post {post_id} deleted (no content returned)")
        return True
    elif response.status_code == 404:
        print(f"āŒ Post {post_id} not found")
        return False
    else:
        print(f"āŒ Error: {response.status_code}")
        return False


# =============================================================================
# EXAMPLE 2: DELETE with Authentication
# =============================================================================

def delete_user(user_id, api_key):
    """Delete user (requires authentication)"""
    url = f"https://api.example.com/users/{user_id}"
    
    headers = {
        "Authorization": f"Bearer {api_key}"
    }
    
    response = requests.delete(url, headers=headers)
    
    if response.status_code in [200, 204]:
        print(f"āœ… User {user_id} deleted")
        return True
    elif response.status_code == 401:
        print("āŒ Unauthorized")
        return False
    elif response.status_code == 403:
        print("āŒ Forbidden - insufficient permissions")
        return False
    elif response.status_code == 404:
        print(f"āŒ User {user_id} not found")
        return False
    else:
        print(f"āŒ Error: {response.status_code}")
        return False


# =============================================================================
# RUN EXAMPLES
# =============================================================================

if __name__ == "__main__":
    print("="*70)
    print("DELETE REQUEST EXAMPLES")
    print("="*70)
    
    print("\n1. Delete post:")
    delete_post(1)

5. Complete REST API Client Example

#!/usr/bin/env python3
"""
Complete REST API Client
Demonstrates all CRUD operations with error handling, retries, and best practices
"""

import requests
import time
from typing import Optional, Dict, List


class RESTAPIClient:
    """Generic REST API client with full CRUD operations"""
    
    def __init__(self, base_url: str, api_key: Optional[str] = None):
        """
        Initialize API client
        
        Args:
            base_url: Base URL for API (e.g., https://api.example.com/v1)
            api_key: Optional API key for authentication
        """
        self.base_url = base_url.rstrip('/')
        self.api_key = api_key
        self.session = requests.Session()
        
        # Set default headers
        self.session.headers.update({
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'User-Agent': 'Python-REST-Client/1.0'
        })
        
        # Add authentication if provided
        if api_key:
            self.session.headers.update({
                'Authorization': f'Bearer {api_key}'
            })
    
    def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
        """
        Make HTTP request with error handling and retries
        
        Args:
            method: HTTP method (GET, POST, PUT, PATCH, DELETE)
            endpoint: API endpoint (e.g., '/users/123')
            **kwargs: Additional arguments for requests
        
        Returns:
            Response object
        """
        url = f"{self.base_url}{endpoint}"
        max_retries = 3
        retry_delay = 1
        
        for attempt in range(max_retries):
            try:
                response = self.session.request(method, url, **kwargs)
                
                # If rate limited, wait and retry
                if response.status_code == 429:
                    retry_after = int(response.headers.get('Retry-After', retry_delay))
                    print(f"ā±ļø  Rate limited. Waiting {retry_after}s...")
                    time.sleep(retry_after)
                    continue
                
                # If server error, retry
                if response.status_code >= 500 and attempt < max_retries - 1:
                    print(f"āš ļø  Server error. Retry {attempt + 1}/{max_retries}...")
                    time.sleep(retry_delay * (2 ** attempt))  # Exponential backoff
                    continue
                
                return response
                
            except requests.exceptions.ConnectionError:
                if attempt < max_retries - 1:
                    print(f"āš ļø  Connection error. Retry {attempt + 1}/{max_retries}...")
                    time.sleep(retry_delay * (2 ** attempt))
                else:
                    raise
        
        return response
    
    def get(self, endpoint: str, params: Optional[Dict] = None) -> Optional[Dict]:
        """GET request - Retrieve resource(s)"""
        response = self._make_request('GET', endpoint, params=params)
        
        if response.ok:
            return response.json() if response.content else None
        else:
            print(f"āŒ GET failed: {response.status_code} - {response.text}")
            return None
    
    def post(self, endpoint: str, data: Dict) -> Optional[Dict]:
        """POST request - Create new resource"""
        response = self._make_request('POST', endpoint, json=data)
        
        if response.status_code == 201:
            print("āœ… Resource created")
            return response.json() if response.content else None
        elif response.ok:
            return response.json() if response.content else None
        else:
            print(f"āŒ POST failed: {response.status_code} - {response.text}")
            return None
    
    def put(self, endpoint: str, data: Dict) -> Optional[Dict]:
        """PUT request - Update entire resource"""
        response = self._make_request('PUT', endpoint, json=data)
        
        if response.ok:
            print("āœ… Resource updated (PUT)")
            return response.json() if response.content else None
        else:
            print(f"āŒ PUT failed: {response.status_code} - {response.text}")
            return None
    
    def patch(self, endpoint: str, data: Dict) -> Optional[Dict]:
        """PATCH request - Partially update resource"""
        response = self._make_request('PATCH', endpoint, json=data)
        
        if response.ok:
            print("āœ… Resource updated (PATCH)")
            return response.json() if response.content else None
        else:
            print(f"āŒ PATCH failed: {response.status_code} - {response.text}")
            return None
    
    def delete(self, endpoint: str) -> bool:
        """DELETE request - Remove resource"""
        response = self._make_request('DELETE', endpoint)
        
        if response.ok or response.status_code == 204:
            print("āœ… Resource deleted")
            return True
        else:
            print(f"āŒ DELETE failed: {response.status_code} - {response.text}")
            return False


# =============================================================================
# USAGE EXAMPLE
# =============================================================================

if __name__ == "__main__":
    # Initialize client
    client = RESTAPIClient('https://jsonplaceholder.typicode.com')
    
    print("="*70)
    print("COMPLETE REST API CLIENT DEMONSTRATION")
    print("="*70)
    
    # CREATE (POST)
    print("\n1ļøāƒ£  CREATE - POST new post:")
    new_post = client.post('/posts', {
        'title': 'My New Post',
        'body': 'This is the content',
        'userId': 1
    })
    if new_post:
        print(f"   Created post ID: {new_post.get('id')}")
    
    # READ (GET)
    print("\n2ļøāƒ£  READ - GET all posts:")
    posts = client.get('/posts', params={'_limit': 3})
    if posts:
        print(f"   Retrieved {len(posts)} posts")
    
    print("\n3ļøāƒ£  READ - GET specific post:")
    post = client.get('/posts/1')
    if post:
        print(f"   Title: {post.get('title')}")
    
    # UPDATE (PUT)
    print("\n4ļøāƒ£  UPDATE - PUT (full update):")
    updated = client.put('/posts/1', {
        'id': 1,
        'title': 'Updated Title',
        'body': 'Updated body',
        'userId': 1
    })
    
    # UPDATE (PATCH)
    print("\n5ļøāƒ£  UPDATE - PATCH (partial update):")
    patched = client.patch('/posts/1', {
        'title': 'Partially Updated Title'
    })
    
    # DELETE
    print("\n6ļøāƒ£  DELETE - Remove post:")
    deleted = client.delete('/posts/1')
    
    print("\n" + "="*70)
    print("āœ… All CRUD operations completed!")
    print("="*70)
āœ… Best Practices Demonstrated:
  • Session Reuse: Use requests.Session() for connection pooling
  • Error Handling: Check status codes and handle errors gracefully
  • Retry Logic: Implement exponential backoff for transient failures
  • Rate Limiting: Respect Retry-After headers
  • Authentication: Centralize header management
  • Type Hints: Use type annotations for clarity

šŸ Interactive Python Interpreter

šŸ’” How to use:
  • Write Python code in the editor below
  • Click "ā–¶ Run Code" to execute
  • Output appears in the console below
  • Use sample codes for quick testing
  • Experiment with REST API patterns and styles
šŸ“ Python Code Editor
⚔ Console Output
Ready to execute Python code...
āš ļø Note: This is a client-side Python interpreter. Network requests are simulated for demonstration purposes.