Chapter 5: HTTP Methods - Beyond Reading Data

Creating, Updating, and Deleting Resources with Professional Practices

1. Beyond Read-Only APIs

So far, you've been reading data from APIs using GET requests. This is powerful for retrieving information, but real applications need to do more. They need to create new posts, update user profiles, and delete old content. These operations require different HTTP methods that tell the server exactly what kind of action you want to perform.

Think of HTTP methods as verbs in a conversation with a server. GET means "show me," POST means "create this," PUT means "replace that," and DELETE means "remove this." Learning these methods transforms you from someone who can only read APIs to someone who can build interactive applications that actually change data on servers.

There’s one important twist compared to Chapter 3: in this chapter you’ll be using HTTP methods that modify data, not just read it. That makes your defensive techniques from Chapter 4 (timeouts, status checks, validation, and careful error handling) even more important. When your code creates, updates, or deletes resources, you want to be very sure you know what happened.

Learning Objectives

By the end of this chapter, you’ll be able to:

  • Choose the right HTTP method for a given task and explain why it’s the right choice.
  • Send data with POST requests using both JSON and form-encoded formats.
  • Update existing resources completely using PUT requests.
  • Remove resources safely using DELETE, with clear feedback to the user.
  • Apply Chapter 4’s defensive patterns (timeouts, status checks, validation) to all HTTP methods.
  • Handle method-specific error scenarios (validation failures, conflicts, permissions) in a professional way.
  • Combine these methods into full CRUD flows that create, read, update, and delete data cleanly.

Why Reading Data Isn't Enough

Imagine social media where you could only read posts but never create your own, or email where you could view messages but never send replies. Real applications require the ability to create, update, and delete data. Understanding when and how to use different HTTP methods is essential for building interactive, data-driven applications. Section 2 maps out exactly how each method works — for now, the important thing is that every interactive app you've ever used is built on just four operations.

Building on Chapter 4

In Chapter 4, you learned the Make → Check → Extract pattern and how to validate responses defensively. These practices aren't just for GET requests, in fact they're even more critical when your requests modify data. Throughout this chapter, you'll see how professional developers apply validation, error handling, and status code checking to POST, PUT, and DELETE operations.

In this chapter you'll use httpbin.org, the safe testing service you've used before. You'll learn method concepts first with simple examples, then see how to apply professional practices to make your code production-ready.

2. Understanding HTTP Methods and CRUD

Every interactive application—from social media to e-commerce to project management—performs the same four fundamental operations on data: creating new items, reading existing items, updating current items, and deleting old items. HTTP provides specific methods for each of these operations, forming the foundation of modern web APIs.

Understanding how HTTP methods map to data operations isn't just about making requests work. It's about learning the professional vocabulary that developers use to discuss API design, recognizing patterns across different APIs, and understanding why certain methods behave the way they do. This knowledge helps you build applications that feel intuitive and work reliably.

CRUD: Professional Vocabulary for Data Operations

CRUD is a well-known acronym in software development that stands for Create, Read, Update, and Delete—the four basic operations you can perform on any data. When developers talk about "implementing CRUD operations" or "building a CRUD API," they're describing systems that provide these four fundamental capabilities.

HTTP methods map directly to CRUD operations. This mapping is so standard that once you understand it, you can predict how any REST API will work:

CRUD Operations and HTTP Methods
CRUD Operation HTTP Method What It Does Example
Create POST Make a new resource Register new user account
Read GET Retrieve existing data Load user profile information
Update PUT Replace entire resource Update complete user profile
Delete DELETE Remove a resource Delete user account
Why CRUD Matters

Understanding CRUD gives you a mental framework for API design. When you encounter a new API, you can immediately ask: "Does this API support full CRUD operations? Can I create resources, or only read them? Are updates supported?" This vocabulary appears everywhere in professional development—job descriptions ask for "CRUD API experience," technical discussions reference "CRUD endpoints," and API documentation organizes operations around CRUD operations.

Now that you understand what CRUD operations represent, let's examine how each HTTP method implements these operations and what makes each method unique.

HTTP Methods and Their Characteristics

Each HTTP method has specific characteristics that determine how it should be used and what behavior to expect. These characteristics matter because they affect how you write error handling code, when you can safely retry requests, and what success responses look like.

HTTP Method Characteristics
Method CRUD Sends Data? Modifies Server? Idempotent? Success Codes
GET Read No (URL only) No Yes 200 OK
POST Create Yes (body) Yes No 200 OK, 201 Created
PUT Update Yes (body) Yes Yes 200 OK, 204 No Content
DELETE Delete Optional Yes Yes 200 OK, 204 No Content

Two characteristics in this table deserve special attention: whether methods modify the server (which determines risk level) and whether they're idempotent (which affects retry safety). Let's explore both.

Safe vs Unsafe Methods

HTTP methods are classified by whether they change server state:

  • Safe methods (GET): Read data without modifying anything. You can call them repeatedly with no side effects. Safe methods are low-risk—failed GET requests don't corrupt data or leave systems in inconsistent states.
  • Unsafe methods (POST, PUT, DELETE): Change server state by creating, modifying, or removing data. These require more careful error handling because failures might leave data in inconsistent conditions or create partially-completed operations.

Understanding Idempotency

Idempotency is a critical concept for building reliable APIs. An operation is idempotent if calling it multiple times with the same data produces the same result as calling it once. This property determines whether it's safe to automatically retry failed requests.

Think about it this way: if your request times out and you don't know if it succeeded, can you safely retry? The answer depends on idempotency.

1.

GET is Idempotent (Safe to Retry)

Requesting the same data 100 times returns the same data 100 times. No harm done. Your browser automatically retries failed GET requests.

2.

POST is NOT Idempotent (Dangerous to Retry)

Sending the same POST request twice creates two resources. If your "create user" request times out, blindly retrying might create duplicate accounts. POST requests require careful handling and manual retry decisions.

3.

PUT is Idempotent (Safe to Retry)

Replacing a resource with the same data multiple times has the same effect as doing it once. The resource ends up in the same state regardless of retry count. If your PUT request times out, retrying is safe.

4.

DELETE is Idempotent (Safe to Retry)

Deleting a resource once or ten times produces the same result: the resource is gone. The first DELETE succeeds (200 or 204), subsequent DELETEs return 404 (not found), but the goal—resource removal—is achieved either way.

Why Idempotency Matters in Production

Network issues are common in production systems. Requests time out, connections drop, and packets get lost. Idempotent operations (GET, PUT, DELETE) can be safely retried by automated systems without checking what happened—worst case, you do the same thing twice. Non-idempotent operations (POST) require special handling: checking if the operation succeeded before retry, using unique request IDs, or asking users whether to retry. Professional developers design retry logic around idempotency properties.

Method Selection Quick Guide

When working with any API, use this decision framework to choose the right HTTP method for your operation:

1.

Are you changing anything on the server?

No: Use GET to read, search, or download data.
Yes: Continue to question 2.

2.

Are you creating something new?

Yes: Use POST to create new resources.
No: Continue to question 3.

3.

Are you updating an existing resource?

Yes: Use PUT to replace the entire resource.
No: Continue to question 4.

4.

Are you removing something?

Yes: Use DELETE to remove the resource.
No: Review your operation—you might need POST for complex actions.

The rest of this chapter explores each method in detail, showing both basic usage and professional implementation patterns. You'll see how to apply Chapter 4's defensive programming to data-modifying operations, handle method-specific error scenarios, and build complete CRUD applications that work reliably in production.

3. POST Requests - Creating New Resources

POST requests create new resources by sending data to servers in the request body. Unlike GET requests that append data to URLs, POST sends data separately, which is more secure and can handle larger amounts of information. This makes POST perfect for user registration, content creation, and form submissions.

POST requests carry more risk than GET because they modify server state, making proper error handling essential. Professional POST implementations always include status code checking and input validation to catch problems immediately rather than discovering them in production.

Your First POST Request

Let's start with a simple POST request to understand the basic pattern. This example demonstrates the concept clearly, then we'll add professional error handling.

Basic POST Request (Concept Demonstration)
Python
import requests

# Data to create
new_user = {
    "username": "alice_coder",
    "email": "alice@example.com",
    "age": 25
}

# Send POST request with JSON data
response = requests.post(
    "https://httpbin.org/post",
    json=new_user,  # Automatically sets Content-Type: application/json
    timeout=10
)

print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
Output (example)
Status: 200
Response: {'args': {}, 'data': '{"username": "alice_coder", "email": "alice@example.com", "age": 25}', ...}
What Just Happened
  • JSON parameter: Using json=data automatically serializes the dictionary and sets Content-Type header
  • Request body: Data travels in the request body, not the URL, making it more secure
  • Server processing: httpbin echoes back what it received, showing your data was transmitted correctly

This basic example works, but it's missing the defensive programming patterns from Chapter 4. Let's see what happens when things go wrong.

When POST Requests Fail

POST requests can fail in ways that GET requests don't. Understanding these failure modes helps you write code that handles them appropriately.

1.

Validation Failures (400 Bad Request)

Server rejects your data because required fields are missing, formats are invalid, or values don't meet constraints. The resource was never created.

2.

Duplicate Resources (409 Conflict)

You're trying to create something that already exists (like a username that's taken). The server prevents duplicate creation.

3.

Authorization Failures (401/403)

You lack permission to create resources. This happens with missing API keys or insufficient access levels.

4.

Network Timeouts

Request takes too long or connection drops. The resource might or might not have been created—you can't tell without checking.

The Duplicate Creation Problem

Here's a scenario that happens in production: Your POST request times out after 30 seconds. Did the server create the resource before timing out? Or did the timeout happen during transmission? Without proper error handling and status checking, you might retry and create duplicates—or give up when the resource actually exists.

Professional POST Pattern

Here's how professional developers write POST requests, applying Chapter 4's defensive patterns to creation operations. This pattern handles all the failure modes we just discussed.

Production-Grade POST Request
Python
import requests

def create_user(username, email, age):
    """
    Create a new user with professional error handling.
    
    Returns:
        tuple: (success: bool, user_data: dict or None, message: str)
    """
    
    # Validate input before sending
    if not username or not email:
        return (False, None, "Username and email are required")
    
    if not isinstance(age, int) or age < 0:
        return (False, None, "Age must be a positive number")
    
    user_data = {
        "username": username,
        "email": email,
        "age": age
    }
    
    try:

        # STEP 1: Make the request with timeout
        response = requests.post(
            "https://httpbin.org/post",
            json=user_data,
            timeout=10
        )
        
        # STEP 2: Check status code
        if response.status_code == 200 or response.status_code == 201:

            # Success - resource created
            
            # STEP 3: Validate content type
            content_type = response.headers.get("Content-Type", "")
            if "application/json" not in content_type:
                return (False, None, f"Expected JSON but received {content_type}")
            
            # STEP 4: Extract and validate response data
            try:
                response_data = response.json()
                return (True, response_data, "User created successfully")
            except ValueError:
                return (False, None, "Server returned invalid JSON")
        
        elif response.status_code == 400:

            # Bad request - invalid data
            return (False, None, "Invalid user data - check format and required fields")
        
        elif response.status_code == 409:

            # Conflict - username already exists
            return (False, None, f"Username '{username}' is already taken")
        
        elif response.status_code == 401 or response.status_code == 403:

            # Authentication/authorization failure
            return (False, None, "Not authorized to create users")
        
        else:

            # Unexpected status code
            return (False, None, f"Request failed with status {response.status_code}")
    
    except requests.exceptions.Timeout:
        return (False, None, "Request timed out - server took too long to respond")
    
    except requests.exceptions.ConnectionError:
        return (False, None, "Could not connect to server - check your internet connection")
    
    except requests.exceptions.RequestException as e:
        return (False, None, f"Network error: {e}")

# Test the function
success, user_data, message = create_user("alice_coder", "alice@example.com", 25)

if success:
    print(f"✅ {message}")
    print(f"Created user: {user_data.get('json', {}).get('username')}")
else:
    print(f"❌ {message}")
Output
✅ User created successfully
Created user: alice_coder
Why This Pattern Works
  • Input validation: Catches problems before making the request, saving bandwidth and time
  • Status code checking: Distinguishes between different failure types for appropriate responses
  • Content-Type validation: Ensures you're parsing the expected format (JSON, not HTML error page)
  • Network error handling: Separates network problems from application errors
  • Structured returns: Callers get consistent results (success flag, data, message) regardless of outcome

This pattern is verbose compared to the basic example, but it prevents production issues that plague applications without proper error handling. Users get clear messages instead of cryptic stack traces, and you can debug problems by checking status codes and error messages.

POST Data Formats: JSON vs Form Data

APIs accept POST data in different formats. Modern APIs prefer JSON, but many still use traditional form encoding. Understanding both formats helps you work with any API.

JSON Format (Most Common)
Python
import requests

# JSON format - best for complex data structures
user_data = {
    "username": "alice",
    "profile": {
        "email": "alice@example.com",
        "preferences": {
            "theme": "dark",
            "notifications": True
        }
    },
    "tags": ["python", "apis", "coding"]
}

response = requests.post(
    "https://httpbin.org/post",
    json=user_data,  # Handles nested structures naturally
    timeout=10
)

print(f"Status: {response.status_code}")
Form Data Format (Legacy/Simple Forms)
Python
import requests

# Form data - simple key-value pairs only
form_data = {
    "name": "Alice Smith",
    "email": "alice@example.com",
    "subject": "API Question",
    "message": "How do I learn more about POST requests?"
}

response = requests.post(
    "https://httpbin.org/post",
    data=form_data,  # Sends as application/x-www-form-urlencoded
    timeout=10
)

print(f"Status: {response.status_code}")
Format When to Use Advantages Limitations
JSON
json=data
Modern APIs, complex data Supports nesting, arrays, multiple types Slightly more bandwidth
Form Data
data=data
Simple forms, legacy APIs Widely supported, simple Flat key-value pairs only
How to Choose

Check the API documentation first—it will specify which format to use. If the docs show JSON examples with curly braces and nested objects, use json=data. If examples show simple key=value pairs, use data=data. When in doubt, JSON is the safer bet for modern APIs.

Understanding POST Response Status Codes

POST requests return different status codes than GET because they create resources. Learning to interpret these codes helps you understand whether creation succeeded and what happened on the server.

Status Code Meaning What to Do
200 OK Request processed successfully Resource created or processed; check response for details
201 Created New resource created successfully Check Location header for new resource URL
400 Bad Request Invalid data format or missing fields Fix data validation errors; check API docs for requirements
401 Unauthorized Missing or invalid authentication Check API key or login credentials
409 Conflict Resource already exists Use PUT to update or choose different identifier
422 Unprocessable Data syntax correct but semantically invalid Check business rule violations (e.g., invalid date range)
500 Server Error Server problem Not your fault; retry after delay or contact support
201 vs 200: What's the Difference?

Both indicate success, but they mean different things:

  • 201 Created: A new resource was created. The response often includes a Location header pointing to the new resource.
  • 200 OK: Request succeeded but might not have created a new resource. Could mean the request was processed, queued, or the resource already existed.

Professional code checks for both 200 and 201 as success, then examines the response body or Location header to understand exactly what happened.

POST requests form the foundation of interactive applications. By learning to send both JSON and form data with proper error handling, you can work with most APIs that accept user-generated content. The next section explores PUT requests, which update existing resources rather than creating new ones.

In Production: The Duplicate Order Problem

An e-commerce company's mobile app had a critical bug that cost them thousands in refunds and customer service time. When users tapped "Place Order," the POST request would sometimes timeout on slow networks. Frustrated users tapped the button again. The result? Duplicate orders, double charges, and angry customers calling support.

The bug wasn't in the server. It was in the client's retry logic. The app blindly retried POST requests without checking if the first one had succeeded. Each retry created a new order because POST isn't idempotent.

The fix used three defensive patterns:

  • Generate a unique order ID client-side before sending the POST request
  • Show a "Processing..." spinner and disable the button to prevent double-taps
  • Before retry, check if an order with that ID already exists

The duplicate order rate dropped from 3% to near-zero. The defensive POST patterns you learned in this section prevent exactly this category of production bugs. When you validate input, handle status codes properly, and provide clear user feedback, you're not being paranoid. You're preventing real financial losses.

4. PUT Requests - Replacing Resources

PUT requests update existing resources by replacing them entirely with new data. Think of PUT as "replace this resource with this new version." This complete replacement approach makes PUT perfect for updating user profiles, modifying settings, or changing document contents when you have the complete new state.

The key distinction between POST and PUT: POST creates something new ("add this to the collection"), while PUT replaces something that already exists ("replace item #123 with this new version"). Understanding this difference helps you choose the right method and handle the unique error scenarios each operation encounters.

When to Use PUT

PUT works best when you're providing complete, updated information about a resource. If you're only changing specific fields, you might want PATCH instead (covered in advanced chapters), but PUT is appropriate when you have the full resource state.

1.

Complete Profile Updates

Replacing an entire user profile with updated name, bio, preferences, and settings all at once.

2.

Document Replacement

Replacing the entire content of a document, configuration file, or data record with a new version.

3.

Configuration Changes

Updating application settings or preferences where you specify the complete new configuration state.

4.

File Updates

Replacing an entire file with a new version, such as updating a profile picture or document.

⚠️ Critical Warning: The "Partial Update" Trap

A common beginner mistake is treating PUT like a partial update. Remember: PUT is a complete replacement.

Imagine a user profile has a name, email, and age. If you only want to update the email, and you send a PUT request containing only the new email address:

  • What you expect: The email updates; the name and age remain unchanged.
  • What actually happens: The server replaces the entire resource with your data. The name and age are deleted because you didn't send them back!

If you need to update just one field without sending the whole object, you normally use the PATCH method (which we will cover in advanced chapters). For PUT, you must always send the complete resource data.

PUT is Idempotent

An important PUT characteristic: calling it multiple times with the same data produces the same result. If you PUT the same profile update three times, the resource ends up in the same state as putting it once. This property makes PUT safe to retry after network failures—unlike POST, which might create duplicates.

Basic PUT Request Pattern

Let's start with a simple PUT request to understand the basic mechanics, then we'll add professional error handling.

Basic PUT Request (Concept Demonstration)
Python
import requests

# Complete updated user profile
updated_profile = {
    "user_id": 123,
    "username": "alice_developer",  # Changed
    "email": "alice.dev@example.com",  # Updated
    "age": 26,  # Birthday happened
    "bio": "Python developer passionate about APIs",  # New field
    "interests": ["python", "apis", "machine learning"]  # Expanded
}

# Send PUT request to replace the resource
response = requests.put(
    "https://httpbin.org/put",
    json=updated_profile,
    timeout=10
)

print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
Output (example)
Status: 200
Response: {'args': {}, 'data': '{"user_id": 123, "username": "alice_developer", ...}', ...}
What Just Happened
  • Complete replacement: PUT sends the entire updated resource—all fields, not just changed ones
  • Idempotent operation: Sending the same PUT multiple times produces the same result (unlike POST which creates duplicates)
  • Resource targeting: You specify which resource to update (user_id: 123)—PUT requires knowing what you're replacing

This basic example demonstrates the PUT pattern: send complete resource data to replace what exists. But it's missing error handling for scenarios unique to updates. Let's explore what can go wrong with PUT requests.

PUT-Specific Failure Modes

PUT requests fail differently than POST because they operate on existing resources. Understanding these scenarios helps you write appropriate error handling.

1.

Resource Not Found (404)

You're trying to update something that doesn't exist. This is the most common PUT-specific error—you can't replace what isn't there.

2.

Concurrent Modification (409)

Someone else modified the resource after you retrieved it. Your update would overwrite their changes. Some APIs use ETags or version numbers to detect this.

3.

Permission Denied (403)

You can read the resource but lack permission to modify it. Common in collaborative applications with read-only access.

4.

Validation Failures (400)

Your update data is invalid. Unlike POST (which creates), PUT validation failures mean you're trying to replace valid data with invalid data.

The Lost Update Problem

Here's a classic scenario: Alice loads user profile #123 at 2:00 PM. Bob updates the same profile at 2:05 PM. Alice makes changes and sends a PUT request at 2:10 PM. Her PUT replaces Bob's changes—his modifications are lost. Professional APIs prevent this with versioning or ETags, but your code should handle 409 conflicts appropriately.

Professional PUT Pattern

Here's how professional developers write PUT requests, applying Chapter 4's defensive patterns and handling PUT-specific failure scenarios.

Production-Grade PUT Request
Python
import requests

def update_user_profile(user_id, profile_data):
    """
    Update complete user profile with professional error handling.
    
    Args:
        user_id: ID of user to update
        profile_data: Complete profile data dictionary
        
    Returns:
        tuple: (success: bool, updated_data: dict or None, message: str)
    """
    
    # Validate input before sending
    if not user_id:
        return (False, None, "User ID is required")
    
    if not profile_data:
        return (False, None, "Profile data cannot be empty")
    
    # Ensure user_id is in the data
    profile_data["user_id"] = user_id
    
    try:

        # STEP 1: Make the request with timeout
        response = requests.put(
            f"https://httpbin.org/put",
            json=profile_data,
            timeout=10
        )
        
        # STEP 2: Check status code
        if response.status_code == 200:

            # Success with response data
            
            # STEP 3: Validate content type
            content_type = response.headers.get("Content-Type", "")
            if "application/json" not in content_type:
                return (False, None, f"Expected JSON but received {content_type}")
            
            # STEP 4: Extract response data
            try:
                response_data = response.json()
                return (True, response_data, "Profile updated successfully")
            except ValueError:
                return (False, None, "Server returned invalid JSON")
        
        elif response.status_code == 204:

            # Success with no response content (common for PUT)
            return (True, None, "Profile updated successfully")
        
        elif response.status_code == 404:

            # Resource doesn't exist
            return (False, None, f"User {user_id} not found - cannot update")
        
        elif response.status_code == 409:

            # Conflict - concurrent modification
            return (False, None, "Profile was modified by someone else - please reload and try again")
        
        elif response.status_code == 403:

            # Permission denied
            return (False, None, "You don't have permission to update this profile")
        
        elif response.status_code == 400:

            # Validation failure
            return (False, None, "Invalid profile data - check required fields and formats")
        
        else:

            # Unexpected status
            return (False, None, f"Update failed with status {response.status_code}")
    
    except requests.exceptions.Timeout:
        return (False, None, "Request timed out - server took too long to respond")
    
    except requests.exceptions.ConnectionError:
        return (False, None, "Could not connect to server - check your internet connection")
    
    except requests.exceptions.RequestException as e:
        return (False, None, f"Network error: {e}")

# Test the function
profile_update = {
    "username": "alice_developer",
    "email": "alice.dev@example.com",
    "age": 26,
    "bio": "Python developer passionate about APIs"
}

success, data, message = update_user_profile(123, profile_update)

if success:
    print(f"✅ {message}")
else:
    print(f"❌ {message}")
Output
✅ Profile updated successfully
Why This Pattern Works
  • Resource validation: Checks that user_id exists before making the request
  • 404 handling: Distinguishes "resource not found" from other errors for clear user feedback
  • Conflict detection: Handles concurrent modification gracefully with actionable message
  • No content response: Some APIs return 204 for successful updates without response body—code handles both patterns
  • Permission checking: Separates authentication (401) from authorization (403) errors

This pattern handles the complexities of updating existing resources while maintaining the defensive programming practices from Chapter 4. The structured return format (success, data, message) makes it easy for calling code to respond appropriately to any outcome.

PUT vs POST: Understanding the Difference

New developers often confuse POST and PUT because both send data to servers. Understanding when to use each prevents bugs and makes your code more predictable.

Characteristic POST PUT
Purpose Create new resource Replace existing resource
Requires ID No (server assigns ID) Yes (you specify which resource)
Idempotent No (multiple calls create multiple resources) Yes (multiple calls same result)
Success Codes 201 Created, 200 OK 200 OK, 204 No Content
Common Errors 409 Conflict (duplicate), 400 Bad Request 404 Not Found, 409 Conflict (concurrent)
Safe to Retry No (might create duplicates) Yes (produces same result)
Decision Framework
When to Choose POST vs PUT
# Use POST when creating new resources

# Example: User registration, creating new blog posts

response = requests.post(
    "https://api.example.com/users",  # Collection endpoint
    json={"username": "alice", "email": "alice@example.com"},
    timeout=10
)

# Server assigns new user ID, returns 201 Created


# Use PUT when updating existing resources
# Example: Updating user profile, modifying blog post

response = requests.put(
    "https://api.example.com/users/123",  # Specific resource endpoint
    json={"username": "alice_updated", "email": "alice.new@example.com"},
    timeout=10
)

# Updates user 123, returns 200 OK or 204 No Content
The URL Difference

Notice the URL pattern difference:

  • POST: Send to collection endpoint (/users) - server decides new resource location
  • PUT: Send to specific resource (/users/123) - you specify which resource to replace

This URL pattern is a strong hint about which method to use. If the URL includes a resource ID, you're probably updating with PUT. If it's just a collection name, you're probably creating with POST.

🚗 Visual Analogy: The Parking Lot

If you are struggling to remember the difference, imagine your API is a parking lot:

  • POST (The Valet): You hand your car to the valet. You don't know where it will go. The valet parks it and gives you a ticket saying "Parked in Spot #501".
  • PUT (The Swap): You drive a new car directly to Spot #123. You remove the old car that was there and park the new one in its place.
In Production: The Concurrent Edit Race

A customer support platform had a subtle but serious bug. Two representatives would sometimes work on the same support ticket simultaneously. Rep A would add important notes like "Refund approved - $150." Rep B would update the status to "Closed." When Rep B's PUT request completed, it overwrote the entire ticket resource, and Rep A's refund note vanished. Customers didn't get their refunds because the information was lost.

This is called a concurrent modification problem, and it's common in collaborative systems. The second PUT overwrites changes made after the first user loaded the data. Neither rep saw an error. The data just silently disappeared.

Production systems solve this with optimistic locking:

  • Each resource includes a version number or timestamp
  • PUT requests include the version they're updating
  • Server returns 409 Conflict if versions don't match
  • User sees: "Someone else modified this ticket. Please refresh and try again."

This is why your PUT error handling checks for 409 status codes. In production, concurrent edits happen constantly in collaborative applications. The defensive patterns you learned don't just prevent crashes. They prevent data loss that directly impacts users and business operations.

Idempotency in Practice: The Double Charge Problem

Now that you understand both GET and PUT, you can see why idempotency matters beyond theory. These two examples show the difference in real code — GET is safe to retry at any time, while a PUT that triggers a payment can charge a customer twice if handled carelessly.

GET — Safe to Call Any Number of Times

No matter how many times this runs, the server is unchanged. If it times out, retry with confidence:

Python
import requests

def get_customer_order(order_id):
    response = requests.get(
        f"https://api.shop.com/orders/{order_id}",
        timeout=5
    )
    response.raise_for_status()
    return response.json()

# Safe to call repeatedly — nothing changes on the server
order = get_customer_order(42)
print(order)

PUT Gone Wrong — The Double Charge Problem

This is where idempotency theory collides with real-world side effects. PUT is idempotent in principle, but if the operation inside it — like charging a card — isn't, blind retries become dangerous:

Python
import requests

def update_order_and_charge(order_id, new_items):
    try:
        response = requests.put(
            f"https://api.shop.com/orders/{order_id}",
            json={
                "items": new_items,
                "charge_card": True   # triggers payment
            },
            timeout=5
        )
        response.raise_for_status()
        return response.json()

    except requests.Timeout:
        # The request timed out — but did it go through?
        # The server may have:
        #   1. Never received it (safe to retry)
        #   2. Processed it AND charged the card (retry = double charge!)
        print("Timed out. Retrying...")
        return update_order_and_charge(order_id, new_items)  # DANGEROUS


update_order_and_charge(42, ["shoes", "belt"])
Why This Fails in Production

The recursive retry on line 22 is the problem. When a timeout fires, you have no way of knowing whether the server processed the request before the connection dropped. If it did, calling the function again charges the customer a second time. PUT is theoretically idempotent — but only if the entire operation, including any side effects like payments, is idempotent too. The professional fix is an idempotency key: a unique token sent with every request so the server can recognise a duplicate and return the original result instead of processing it again.

5. DELETE Requests - Removing Resources

DELETE requests remove resources from servers permanently. They're the most straightforward HTTP method in purpose but require the most careful consideration because deletion is usually irreversible. DELETE requests are essential for content management, account cleanup, and maintaining tidy applications.

Unlike GET (safe to call anytime), POST (creates new things), or PUT (replaces existing things), DELETE is destructive. Once executed successfully, the resource is gone. Professional DELETE implementations always include confirmation workflows and clear feedback about what was deleted.

When to Use DELETE

DELETE requests should be used when you're certain a resource is no longer needed and safe to remove. Understanding when deletion is appropriate helps prevent accidental data loss.

1.

User-Requested Removal

Removing items from shopping carts, deleting social media posts, or canceling subscriptions when users explicitly request deletion.

2.

Content Cleanup

Removing old blog posts, deleting outdated photos, or cleaning up files that are no longer needed.

3.

Account Management

Deactivating user accounts, removing access permissions, or cleaning up expired sessions.

4.

Automated Maintenance

Removing test data, cleaning up temporary files, or deleting expired content through scheduled jobs.

Soft Delete vs Hard Delete

Many production systems don't actually DELETE resources—they mark them as deleted but keep the data. This "soft delete" approach:

  • Allows undo functionality (users can recover deleted items)
  • Maintains referential integrity (other records still reference the "deleted" item)
  • Preserves data for auditing and compliance
  • Prevents cascading deletion problems

Your DELETE requests might trigger soft deletes on the server, even though you're using the DELETE method. The API documentation will clarify the deletion behavior.

Basic DELETE Request Pattern

DELETE requests are typically simple because they just need to identify what to remove. Most DELETE requests don't send data in the body—the resource ID is usually in the URL.

Basic DELETE Request (Concept Demonstration)
Python
import requests

# Delete a specific resource by ID
resource_id = 123

response = requests.delete(
    f"https://httpbin.org/delete",
    timeout=10
)

print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
Output (example)
Status: 200
Response: {'args': {}, 'data': '', 'files': {}, ...}
What Just Happened
  • Minimal request: DELETE typically doesn't send data—the URL identifies what to remove
  • Empty response: Many APIs return 204 No Content for successful deletions (httpbin returns 200 with minimal data)
  • Permanent action: Unlike GET or PUT, DELETE operations can't be "undone" with another request—data is gone

This basic example shows the DELETE mechanism, but it's missing critical safety checks. Let's explore what can go wrong with DELETE requests and how to handle failures professionally.

DELETE-Specific Failure Modes

DELETE requests have unique failure scenarios that require special handling. Understanding these helps you write code that responds appropriately to each situation.

1.

Resource Already Gone (404)

You're trying to delete something that doesn't exist. This might not be an error—if the goal was to ensure the resource is gone, mission accomplished. Handle 404s gracefully for DELETE.

2.

Dependency Conflicts (409)

Other resources depend on what you're trying to delete. Example: can't delete a user who owns blog posts. The server prevents deletion to maintain data integrity.

3.

Permission Denied (403)

You lack permission to delete this resource. Common in collaborative systems where you can view but not delete others' content.

4.

Protected Resources (423 Locked)

Resource is locked or protected from deletion. Some systems use 423 to indicate resources under active use that can't be deleted.

The Idempotency Question

Is DELETE idempotent? The answer is nuanced: The *operation* is idempotent (after the first DELETE, subsequent DELETEs leave the system in the same state), but the *response code* might differ:

  • First DELETE: 200 OK or 204 No Content (resource deleted)
  • Second DELETE: 404 Not Found (resource already gone)

Professional code treats both as successful outcomes—the important thing is the resource is gone, not how many times you deleted it.

Professional DELETE Pattern

Here's how professional developers write DELETE requests, applying Chapter 4's defensive patterns and handling DELETE-specific failure scenarios.

Production-Grade DELETE Request
Python
import requests

def delete_resource(resource_type, resource_id):
    """
    Delete a resource with professional error handling.
    
    Args:
        resource_type: Type of resource (e.g., "post", "photo", "comment")
        resource_id: ID of resource to delete
        
    Returns:
        tuple: (success: bool, message: str)
    """
    
    # Validate input
    if not resource_id:
        return (False, "Resource ID is required")
    
    if not resource_type:
        return (False, "Resource type is required")
    
    try:

        # STEP 1: Make the request with timeout
        response = requests.delete(
            f"https://httpbin.org/delete",
            json={"type": resource_type, "id": resource_id},
            timeout=10
        )
        
        # STEP 2: Check status code
        if response.status_code == 200:

            # Success with confirmation response
            return (True, f"{resource_type} {resource_id} deleted successfully")
        
        elif response.status_code == 204:

            # Success with no content (common for DELETE)
            return (True, f"{resource_type} {resource_id} deleted successfully")
        
        elif response.status_code == 404:

            # Resource doesn't exist - treating as success since goal is accomplished
            return (True, f"{resource_type} {resource_id} not found (already deleted or never existed)")
        
        elif response.status_code == 409:

            # Conflict - dependencies prevent deletion
            return (False, f"Cannot delete {resource_type} {resource_id} - other resources depend on it")
        
        elif response.status_code == 403:

            # Permission denied
            return (False, f"You don't have permission to delete {resource_type} {resource_id}")
        
        elif response.status_code == 423:

            # Locked - resource protected from deletion
            return (False, f"{resource_type} {resource_id} is locked and cannot be deleted")
        
        else:

            # Unexpected status
            return (False, f"Deletion failed with status {response.status_code}")
    
    except requests.exceptions.Timeout:
        return (False, "Request timed out - server took too long to respond")
    
    except requests.exceptions.ConnectionError:
        return (False, "Could not connect to server - check your internet connection")
    
    except requests.exceptions.RequestException as e:
        return (False, f"Network error: {e}")

# Test the function
success, message = delete_resource("blog_post", 123)

if success:
    print(f"✅ {message}")
else:
    print(f"❌ {message}")
Output
✅ blog_post 123 deleted successfully
Why This Pattern Works
  • 404 handling: Treats "not found" as success since the goal (resource gone) is achieved
  • Dependency detection: Explains why deletion failed (409) and what to do about it
  • Permission checking: Clearly distinguishes authentication from authorization failures
  • Lock detection: Handles protected resources that temporarily can't be deleted
  • No content response: Many APIs return 204 for successful deletion—code handles both 200 and 204
⚠️ Python Pitfall: The 204 Crash

There is a specific reason why the code above separates 200 and 204 checks.

Never call .json() on a 204 response.

Because 204 means "No Content," the response body is empty. If you try to parse it with response.json(), Python will raise a JSONDecodeError and crash your program.

# ❌ BAD: This crashes if the server returns 204
response = requests.put(...)
data = response.json()  # CRASH! JSONDecodeError

# ✅ GOOD: Check status first
if response.status_code == 204:
    print("Success, but no data returned")
else:
    data = response.json()  # Safe to parse

Safe Deletion Workflow

Because deletion is permanent, professional applications implement confirmation workflows. Here's a pattern that demonstrates the complete safety checks before deletion.

Complete Safe Deletion Pattern
Python
import requests

def safe_delete_workflow(resource_type, resource_id, resource_name):
    """
    Demonstrate complete safe deletion workflow with all safety checks.
    
    Args:
        resource_type: Type of resource to delete
        resource_id: ID of resource
        resource_name: Human-readable name for confirmation
        
    Returns:
        tuple: (success: bool, message: str)
    """
    
    print(f"=== Safe Deletion Workflow ===")
    print(f"Resource: {resource_name} ({resource_type} #{resource_id})")
    
    # STEP 1: Verify resource exists
    print("\n1. Verifying resource exists...")
    
    try:
        verify_response = requests.get(
            f"https://httpbin.org/get",
            params={"type": resource_type, "id": resource_id},
            timeout=10
        )
        
        if not verify_response.ok:
            return (False, "Resource not found - nothing to delete")
        
        print("   ✓ Resource found")
    
    except requests.exceptions.RequestException as e:
        return (False, f"Could not verify resource existence: {e}")
    
    # STEP 2: Check for dependencies (in real apps, call a dependencies endpoint)
    print("\n2. Checking for dependencies...")

    # In production: call API to check what depends on this resource
    print("   ✓ No dependencies found")
    
    # STEP 3: User confirmation
    print(f"\n3. Confirming deletion...")
    print(f"   WARNING: This will permanently delete '{resource_name}'")
    print(f"   This action cannot be undone.")
    
    # In real applications: user_confirmed = input("Type 'DELETE' to confirm: ") == "DELETE"
    user_confirmed = True  # Simulated confirmation for demo
    
    if not user_confirmed:
        return (False, "Deletion cancelled by user")
    
    print("   ✓ User confirmed deletion")
    
    # STEP 4: Perform deletion with error handling
    print(f"\n4. Deleting resource...")
    
    try:
        delete_response = requests.delete(
            f"https://httpbin.org/delete",
            json={"type": resource_type, "id": resource_id},
            timeout=10
        )
        
        if delete_response.status_code in (200, 204):
            print(f"   ✓ Resource deleted successfully")
            return (True, f"'{resource_name}' has been permanently removed")
        
        elif delete_response.status_code == 409:
            return (False, "Deletion failed - dependencies found (may have been created since check)")
        
        else:
            return (False, f"Deletion failed with status {delete_response.status_code}")
    
    except requests.exceptions.RequestException as e:
        return (False, f"Network error during deletion: {e}")

# Test the workflow
success, message = safe_delete_workflow("photo", 123, "vacation_sunset.jpg")

print(f"\n{'='*50}")
if success:
    print(f"✅ SUCCESS: {message}")
else:
    print(f"❌ FAILED: {message}")
print(f"{'='*50}")
Output
=== Safe Deletion Workflow ===
Resource: vacation_sunset.jpg (photo #123)

1. Verifying resource exists...
   ✓ Resource found

2. Checking for dependencies...
   ✓ No dependencies found

3. Confirming deletion...
   WARNING: This will permanently delete 'vacation_sunset.jpg'
   This action cannot be undone.
   ✓ User confirmed deletion

4. Deleting resource...
   ✓ Resource deleted successfully

==================================================
✅ SUCCESS: 'vacation_sunset.jpg' has been permanently removed
==================================================
Professional Deletion Practices
  • Verify existence: Check resource exists before attempting deletion to provide clear error messages
  • Check dependencies: Ensure deletion won't break other resources or violate data integrity
  • Require confirmation: Make users explicitly confirm destructive actions, especially for important data
  • Provide context: Show what's being deleted (name, not just ID) so users understand the action
  • Log deletions: Production systems log who deleted what and when for auditing and recovery

This complete workflow demonstrates how professional applications handle deletion safely. While the example uses httpbin for demonstration, the pattern applies to any production API that supports resource deletion. The multi-step verification process prevents accidental data loss while providing clear feedback at each stage.

In Production: The Accidental Deletion Disaster

A social media platform's admin panel had a simple "Delete User" button next to each account. One click, account gone forever. No confirmation dialog. No safety net. Support received hundreds of tickets weekly from users (and admins) who had accidentally clicked the wrong button. "I meant to delete my test account but deleted my real one." "I was scrolling on mobile and my thumb hit Delete."

The cost was enormous: customer service time, lost user data, angry users, and sometimes legal complications when businesses lost their accounts. The engineering team assumed users would be careful. They weren't. People make mistakes, especially on mobile devices or when tired.

They implemented the multi-step deletion workflow you learned:

  • Confirmation dialog showing what will be deleted (account name, not just ID)
  • Requirement to type the account name to confirm intention
  • 30-day soft delete period where accounts can be recovered
  • Final "Are you absolutely sure?" confirmation for permanent deletion
  • Email notification sent immediately when deletion is initiated

Accidental deletion tickets dropped by 95%. The remaining 5% were quickly resolved using the soft-delete recovery feature. The multi-step verification pattern you learned isn't paranoia or bad UX. It's protection against human error in destructive operations. Professional developers assume mistakes will happen and design systems that make them recoverable.

6. Applying Chapter 4's Patterns to All Methods

In Chapter 4, you learned the Make → Check → Extract pattern and how to validate responses defensively. This section shows how these practices scale to POST, PUT, and DELETE operations, where the stakes are higher because failures can leave data in inconsistent states.

You've seen defensive programming in every example so far. Now let's make the universal pattern explicit so you can apply it to any HTTP method.

The Universal Pattern for All Methods

Remember Chapter 4's Make → Check → Extract pattern? It applies to every HTTP method, with method-specific variations in what you check and extract.

Step GET POST/PUT/DELETE
1. Make Send request with timeout Send request with timeout + validate input first
2. Check Status code, Content-Type Status code, Content-Type, method-specific codes (201, 204, 409)
3. Extract Parse JSON, validate structure Parse response if present (some return 204 No Content)
Building on Chapter 4

The four validation layers from Chapter 4 apply to all methods:

  • Network layer: try/except for Timeout, ConnectionError, RequestException
  • HTTP layer: Status code checking (200/201/204 vs 400/404/409)
  • Format layer: Content-Type validation when expecting JSON
  • Structure layer: Validate response data structure before using it

Complete CRUD Example with Defensive Programming

Let's see all four HTTP methods working together in a blog post management system that demonstrates professional error handling throughout.

Production-Grade CRUD Operations
Python
import requests

class BlogPostManager:
    """
    Demonstrates all HTTP methods with professional error handling.
    Applies Chapter 4's defensive patterns to all operations.
    """
    
    def __init__(self,base_url="https://httpbin.org"):
        self.base_url = base_url
        self.timeout = 10
    
    def _validate_content_type(self, response, expected="application/json"):
        """Validate response content type (Chapter 4 pattern)."""
        content_type = response.headers.get("Content-Type", "")
        if expected not in content_type:
            return False, f"Expected {expected} but received {content_type}"
        return True, ""
    
    def create_post(self, title, content, author):
        """
        CREATE: Use POST to create a new blog post.
        Demonstrates input validation and status code checking.
        """

        # Validate input before making request
        if not all([title, content, author]):
            return (False, None, "Title, content, and author are required")
        
        post_data = {
            "title": title,
            "content": content,
            "author": author
        }
        
        try:

            # Make request with timeout
            response = requests.post(
                f"{self.base_url}/post",
                json=post_data,
                timeout=self.timeout
            )
            
            # Check status codes specific to POST
            if response.status_code in (200, 201):

                # Validate content type
                valid, error = self._validate_content_type(response)
                if not valid:
                    return (False, None, error)
                
                # Extract response data
                try:
                    data = response.json()
                    return (True, data, f"Post '{title}' created successfully")
                except ValueError:
                    return (False, None, "Server returned invalid JSON")
            
            elif response.status_code == 409:
                return (False, None, f"Post '{title}' already exists")
            
            elif response.status_code == 400:
                return (False, None, "Invalid post data")
            
            else:
                return (False, None, f"Creation failed: {response.status_code}")
        
        except requests.exceptions.Timeout:
            return (False, None, "Request timed out")
        except requests.exceptions.RequestException as e:
            return (False, None, f"Network error: {e}")
    
    def read_post(self, post_id):
        """
        READ: Use GET to retrieve a blog post.
        Demonstrates Chapter 4's complete validation pattern.
        """
        if not post_id:
            return (False, None, "Post ID is required")
        
        try:

            # Make request with timeout
            response = requests.get(
                f"{self.base_url}/get",
                params={"post_id": post_id},
                timeout=self.timeout
            )
            
            # Check status
            if response.status_code == 200:

                # Validate content type
                valid, error = self._validate_content_type(response)
                if not valid:
                    return (False, None, error)
                
                # Extract and validate structure
                try:
                    data = response.json()

                    # In real API, validate data has expected structure
                    return (True, data, f"Retrieved post {post_id}")
                except ValueError:
                    return (False, None, "Server returned invalid JSON")
            
            elif response.status_code == 404:
                return (False, None, f"Post {post_id} not found")
            
            else:
                return (False, None, f"Request failed: {response.status_code}")
        
        except requests.exceptions.Timeout:
            return (False, None, "Request timed out")
        except requests.exceptions.RequestException as e:
            return (False, None, f"Network error: {e}")
    
    def update_post(self, post_id, title, content, author):
        """
        UPDATE: Use PUT to replace entire blog post.
        Demonstrates handling of PUT-specific status codes.
        """
        if not post_id:
            return (False, None, "Post ID is required")
        
        if not all([title, content, author]):
            return (False, None, "All fields required for complete replacement")
        
        updated_data = {
            "post_id": post_id,
            "title": title,
            "content": content,
            "author": author
        }
        
        try:

            # Make request with timeout
            response = requests.put(
                f"{self.base_url}/put",
                json=updated_data,
                timeout=self.timeout
            )
            
            # Check status codes specific to PUT
            if response.status_code == 200:

                # Success with response data
                valid, error = self._validate_content_type(response)
                if not valid:
                    return (False, None, error)
                
                try:
                    data = response.json()
                    return (True, data, f"Post {post_id} updated successfully")
                except ValueError:
                    return (False, None, "Server returned invalid JSON")
            
            elif response.status_code == 204:

                # Success without response content (common for PUT)
                return (True, None, f"Post {post_id} updated successfully")
            
            elif response.status_code == 404:
                return (False, None, f"Post {post_id} not found - cannot update")
            
            elif response.status_code == 409:
                return (False, None, "Post was modified by someone else - please reload")
            
            else:
                return (False, None, f"Update failed: {response.status_code}")
        
        except requests.exceptions.Timeout:
            return (False, None, "Request timed out")
        except requests.exceptions.RequestException as e:
            return (False, None, f"Network error: {e}")
    
    def delete_post(self, post_id):
        """
        DELETE: Remove a blog post.
        Demonstrates treating 404 as success for DELETE.
        """
        if not post_id:
            return (False, "Post ID is required")
        
        try:

            # Make request with timeout
            response = requests.delete(
                f"{self.base_url}/delete",
                json={"post_id": post_id},
                timeout=self.timeout
            )
            
            # Check status codes specific to DELETE
            if response.status_code in (200, 204):
                return (True, f"Post {post_id} deleted successfully")
            
            elif response.status_code == 404:

                # Treat as success - goal accomplished
                return (True, f"Post {post_id} not found (already deleted)")
            
            elif response.status_code == 409:
                return (False, f"Cannot delete post {post_id} - dependencies exist")
            
            elif response.status_code == 403:
                return (False, f"Not authorized to delete post {post_id}")
            
            else:
                return (False, f"Deletion failed: {response.status_code}")
        
        except requests.exceptions.Timeout:
            return (False, "Request timed out")
        except requests.exceptions.RequestException as e:
            return (False, f"Network error: {e}")


# Demonstrate complete CRUD workflow
print("="*60)
print("PRODUCTION-GRADE CRUD DEMONSTRATION")
print("="*60)

blog = BlogPostManager()

# CREATE
print("\n1. CREATE - Making a new blog post")
success, data, msg = blog.create_post(
    "Learning HTTP Methods",
    "Today I learned about GET, POST, PUT, and DELETE...",
    "Alice"
)
print(f"   {'✅' if success else '❌'} {msg}")

# READ
print("\n2. READ - Retrieving the blog post")
success, data, msg = blog.read_post(123)
print(f"   {'✅' if success else '❌'} {msg}")

# UPDATE
print("\n3. UPDATE - Modifying the blog post")
success, data, msg = blog.update_post(
    123,
    "Mastering HTTP Methods",
    "I've now mastered GET, POST, PUT, and DELETE with defensive programming!",
    "Alice"
)
print(f"   {'✅' if success else '❌'} {msg}")

# DELETE
print("\n4. DELETE - Removing the blog post")
success, msg = blog.delete_post(123)
print(f"   {'✅' if success else '❌'} {msg}")

print("\n" + "="*60)
print("All operations completed with professional error handling!")
print("="*60)
Output
============================================================
PRODUCTION-GRADE CRUD DEMONSTRATION
============================================================

1. CREATE - Making a new blog post
   ✅ Post 'Learning HTTP Methods' created successfully

2. READ - Retrieving the blog post
   ✅ Retrieved post 123

3. UPDATE - Modifying the blog post
   ✅ Post 123 updated successfully

4. DELETE - Removing the blog post
   ✅ Post 123 deleted successfully

============================================================
All operations completed with professional error handling!
============================================================
Professional Patterns Demonstrated
  • Input validation: Check data validity before making requests
  • Consistent timeouts: All requests use configurable timeout
  • Status code checking: Method-specific status codes handled appropriately
  • Content-Type validation: Shared validation method prevents duplicate code
  • Structured returns: Consistent tuple format for all operations
  • Network error handling: try/except blocks catch all network failures
  • Clear messages: Users get actionable feedback for every outcome

This complete example demonstrates how Chapter 4's defensive patterns scale to all HTTP methods. The code is more verbose than basic examples, but this verbosity prevents production bugs and provides clear feedback when things go wrong. This is the difference between code that works in demos and code that survives in production.

7. Chapter Summary

What You've Accomplished

You've expanded from read-only operations to full interactive capabilities with POST, PUT, and DELETE. More importantly, you've learned to apply Chapter 4's defensive programming patterns to operations that modify data—where errors carry higher stakes than simple read failures.

Through production-grade examples with httpbin and practical scenarios, you've seen how professional developers handle method-specific failure modes. You now understand that POST, PUT, and DELETE require more careful error handling than GET because they change server state and can leave data in inconsistent conditions if failures aren't detected and handled properly.

The CRUD blog manager example demonstrated how all four methods work together with consistent error handling, input validation, and status code checking. This isn't just tutorial code—it's the foundation for building reliable, user-friendly applications that handle real-world conditions gracefully.

Key Skills Mastered

1.

HTTP Method Selection

Choose the correct method for any operation using the decision framework: GET for reading, POST for creating, PUT for replacing, DELETE for removing.

2.

POST Request Implementation

Send both JSON and form data to create new resources, handle creation-specific status codes (201 Created, 409 Conflict), and validate input before transmission.

3.

PUT Request Mastery

Replace complete resources with new data, understand idempotency implications, handle concurrent modification conflicts (409), and distinguish from POST operations.

4.

DELETE Request Safety

Remove resources with proper confirmation workflows, treat 404 as success for deletion goals, handle dependency conflicts (409), and implement multi-step verification.

5.

Defensive Programming for All Methods

Apply Chapter 4's Make → Check → Extract pattern to data-modifying operations, validate at all four layers (network, HTTP, format, structure), and provide clear user feedback.

6.

Method-Specific Error Handling

Handle unique failure modes for each method: POST validation failures, PUT concurrent modifications, DELETE dependency conflicts, and provide actionable error messages.

Professional Habits Developed

Beyond technical skills, you've developed professional development habits:

  • Validate before sending: Check input data validity before making requests to save bandwidth and provide immediate feedback
  • Handle method-specific codes: Different methods return different status codes—check for all expected success codes (200, 201, 204)
  • Treat 404 contextually: For GET/PUT, 404 is an error. For DELETE, it means goal accomplished—handle accordingly
  • Return structured results: Functions return consistent tuples (success, data, message) for predictable error handling
  • Confirm destructive operations: Always implement confirmation workflows before DELETE operations

These habits separate amateur code from professional implementations. They prevent production bugs that plague applications without proper error handling and make debugging straightforward when problems do occur.

Checkpoint Quiz

Test your understanding with these questions. If you can answer confidently, you've mastered the material:

Select question to reveal the answer:
What's the fundamental difference between POST and PUT? When would you use each?

POST creates new resources (server assigns ID). PUT replaces existing resources (you specify which one). Use POST when you don't know the resource ID yet. Use PUT when updating a specific, existing resource.

Why should DELETE requests treat 404 status as success rather than failure?

For DELETE, the goal is ensuring the resource doesn't exist. If you get 404, the resource is already gone—goal accomplished. Treating 404 as failure would prevent idempotent DELETE operations and confuse users when deletion succeeds on retry.

What does "idempotent" mean, and which HTTP methods are idempotent?

Idempotent means calling an operation multiple times with the same data produces the same result. GET, PUT, and DELETE are idempotent. POST is not (multiple POSTs create multiple resources).

Why is input validation more important for POST/PUT/DELETE than for GET?

GET only reads data (safe to retry with bad input). POST/PUT/DELETE change server state—invalid input could create bad data, corrupt resources, or delete wrong items. Validation prevents these problems.

What's the difference between 200 OK and 201 Created for POST requests?

201 Created means a new resource was created and usually includes a Location header pointing to it. 200 OK means the request succeeded but might indicate processing rather than creation (resource could already exist).

How does Chapter 4's Make → Check → Extract pattern apply to PUT requests?

Make: Send PUT request with timeout. Check: Validate status (200/204 success, 404 not found, 409 conflict), Content-Type if expecting response. Extract: Parse response data if present (204 returns no content).

Why might a PUT request return 409 Conflict?

409 Conflict for PUT indicates concurrent modification—someone else changed the resource after you retrieved it. Your update would overwrite their changes. Some APIs use ETags or version numbers to detect this.

What validation layers should you check for all HTTP methods?

Four validation layers: (1) Network layer - try/except for timeouts and connection errors, (2) HTTP layer - status code checking, (3) Format layer - Content-Type validation, (4) Structure layer - validate response data has expected keys and types.

Looking Forward

With HTTP methods mastered and defensive programming internalized, you're ready for Chapter 6's deep dive into JSON parsing. You'll learn advanced techniques for extracting data from complex API responses, handling nested structures, and dealing with inconsistent data formats that real-world APIs return.

The methods and error handling patterns you've learned here will be the foundation for every API interaction going forward. Whether you're building social media applications, e-commerce systems, or content management tools, these four HTTP methods provide the complete set of operations needed for interactive, data-driven applications.

Before Moving On

Take time to practice the patterns from this chapter:

  • Build a simple todo list manager using POST (create), GET (list), PUT (update), DELETE (remove)
  • Add the complete error handling pattern from Section 6 to your Chapter 3 or 4 projects
  • Practice the safe deletion workflow with confirmation prompts
  • Experiment with httpbin's different endpoints to see various status codes

The more you apply these patterns now, the more natural defensive programming will feel. Professional developers don't think "I should add error handling"—they automatically write code that handles errors because the pattern is muscle memory.