# Chapter 4: POST, PUT, DELETE - The Full CRUD

*"Reading data is nice, but real apps need to create, update, and delete things too."*

---

## From Consumer to Creator

Sarah's Weather Buddy app was a hit at the office. But her boss had a new request: "This is great for checking weather, but can we build something that lets people save their favorite cities and share their dashboard with others?"

"That means I need to store data, not just read it," Sarah realized.

Marcus overheard. "Time to learn about POST, PUT, and DELETE. The other three-quarters of CRUD."

"CRUD?" Sarah asked.

"Create, Read, Update, Delete. You've mastered Read with GET. Now let's complete your arsenal."

## Understanding CRUD Operations

Remember our restaurant metaphor? If GET is like reading the menu, then:

- **POST** is like placing a new order
- **PUT** is like changing your order completely  
- **PATCH** is like modifying part of your order
- **DELETE** is like canceling your order

Each has a specific purpose in the API world:

```javascript
// GET - Read data (You know this!)
const users = await tf.get('/api/users')

// POST - Create new data
const newUser = await tf.post('/api/users', {
  data: { name: 'Sarah Chen', role: 'developer' }
})

// PUT - Replace entire resource
const updated = await tf.put('/api/users/123', {
  data: { name: 'Sarah Chen', role: 'senior developer' }
})

// PATCH - Update part of resource
const patched = await tf.patch('/api/users/123', {
  data: { role: 'tech lead' }
})

// DELETE - Remove resource
await tf.delete('/api/users/123')
```

## POST: Creating New Things

POST is how you add new data to an API. It's like filling out a form and hitting submit.

### Basic POST Request

```javascript
// Creating a new todo item
const { data: newTodo } = await tf.post('https://jsonplaceholder.typicode.com/todos', {
  data: {
    title: 'Learn TypedFetch POST requests',
    completed: false,
    userId: 1
  }
})

console.log('Created todo:', newTodo)
// Output: { id: 201, title: 'Learn TypedFetch POST requests', completed: false, userId: 1 }
```

Notice what TypedFetch handles automatically:
- Sets `Content-Type: application/json`
- Converts your data to JSON
- Parses the response
- Handles errors

### Real Example: User Registration

Let's build a user registration system:

```javascript
async function registerUser(email, password, name) {
  try {
    const { data } = await tf.post('https://api.myapp.com/auth/register', {
      data: {
        email,
        password,
        name,
        acceptedTerms: true,
        signupSource: 'web'
      }
    })
    
    // Save the auth token
    localStorage.setItem('authToken', data.token)
    localStorage.setItem('userId', data.user.id)
    
    return {
      success: true,
      user: data.user
    }
    
  } catch (error) {
    // TypedFetch provides detailed error info
    if (error.response?.status === 409) {
      return {
        success: false,
        error: 'Email already registered'
      }
    }
    
    return {
      success: false,
      error: error.message,
      suggestions: error.suggestions
    }
  }
}

// Usage
const result = await registerUser('sarah@example.com', 'secure123', 'Sarah Chen')
if (result.success) {
  console.log('Welcome,', result.user.name)
} else {
  console.error('Registration failed:', result.error)
}
```

### POST with Different Content Types

Not everything is JSON. Here's how to handle other formats:

```javascript
// Form data (like traditional HTML forms)
const formData = new FormData()
formData.append('username', 'sarah_chen')
formData.append('avatar', fileInput.files[0])

const { data } = await tf.post('/api/upload', {
  data: formData
  // TypedFetch detects FormData and sets the right Content-Type
})

// URL-encoded data (for legacy APIs)
const { data: token } = await tf.post('/oauth/token', {
  data: new URLSearchParams({
    grant_type: 'password',
    username: 'sarah@example.com',
    password: 'secure123',
    client_id: 'my-app'
  })
})

// Plain text
const { data: result } = await tf.post('/api/parse', {
  data: 'Plain text content here',
  headers: {
    'Content-Type': 'text/plain'
  }
})
```

## PUT: Complete Replacement

PUT replaces an entire resource. It's like saying "forget what you had, here's the new version."

```javascript
// Get current user data
const { data: user } = await tf.get('/api/users/123')

// Update ALL fields (PUT requires complete data)
const { data: updated } = await tf.put('/api/users/123', {
  data: {
    id: 123,
    name: 'Sarah Chen',
    email: 'sarah.chen@example.com',
    role: 'Senior Developer',  // Changed this
    department: 'Engineering',
    startDate: '2022-01-15',
    active: true
  }
})
```

### PUT vs PATCH: When to Use Which?

```javascript
// ❌ Wrong: Using PUT with partial data
const { data } = await tf.put('/api/users/123', {
  data: { role: 'Tech Lead' }  // Missing other required fields!
})

// ✅ Right: Using PATCH for partial updates
const { data } = await tf.patch('/api/users/123', {
  data: { role: 'Tech Lead' }  // Only updates role
})

// ✅ Right: Using PUT with complete data
const { data: user } = await tf.get('/api/users/123')
const { data: updated } = await tf.put('/api/users/123', {
  data: {
    ...user,
    role: 'Tech Lead'  // Change what you need
  }
})
```

## PATCH: Surgical Updates

PATCH is for partial updates. You only send what changed.

```javascript
// Update just the fields that changed
const { data } = await tf.patch('/api/users/123', {
  data: {
    role: 'Tech Lead',
    salary: 120000
  }
})

// Using JSON Patch format (for APIs that support it)
const { data: patched } = await tf.patch('/api/users/123', {
  data: [
    { op: 'replace', path: '/role', value: 'Tech Lead' },
    { op: 'add', path: '/skills/-', value: 'Leadership' },
    { op: 'remove', path: '/temporaryAccess' }
  ],
  headers: {
    'Content-Type': 'application/json-patch+json'
  }
})
```

## DELETE: Removing Resources

DELETE is straightforward - it removes things. But there are nuances:

```javascript
// Simple delete
await tf.delete('/api/posts/456')

// Delete with confirmation
const { data } = await tf.delete('/api/users/123', {
  data: {
    confirmation: 'DELETE_USER_123',
    reason: 'User requested account deletion'
  }
})

// Soft delete (marking as deleted without removing)
const { data } = await tf.patch('/api/posts/789', {
  data: {
    deleted: true,
    deletedAt: new Date().toISOString()
  }
})
```

### Handling DELETE Responses

Different APIs handle DELETE differently:

```javascript
try {
  const response = await tf.delete('/api/items/123')
  
  // Some APIs return the deleted item
  if (response.data) {
    console.log('Deleted:', response.data)
  }
  
  // Some return 204 No Content
  if (response.response.status === 204) {
    console.log('Successfully deleted')
  }
  
  // Some return a confirmation
  if (response.data?.message) {
    console.log(response.data.message)
  }
  
} catch (error) {
  if (error.response?.status === 404) {
    console.log('Item already deleted')
  } else {
    console.error('Delete failed:', error.message)
  }
}
```

## Building Weather Buddy 4.0: Full CRUD

Let's add user preferences to Weather Buddy:

```html
<!DOCTYPE html>
<html>
<head>
    <title>Weather Buddy 4.0 - Save Your Cities</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .city-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
        .city-card { border: 1px solid #ddd; padding: 15px; border-radius: 8px; position: relative; }
        .auth-section { background: #f0f0f0; padding: 20px; margin-bottom: 20px; border-radius: 8px; }
        .delete-btn { position: absolute; top: 10px; right: 10px; background: #ff4444; color: white; border: none; padding: 5px 10px; cursor: pointer; }
        .save-btn { background: #44ff44; color: black; border: none; padding: 10px 20px; cursor: pointer; margin: 10px 0; }
        .loading { opacity: 0.6; }
        .error { color: red; }
        .success { color: green; }
    </style>
    <script type="module">
        import { tf } from 'https://esm.sh/typedfetch'
        
        // API configuration
        const API_BASE = 'https://api.weatherbuddy.com'
        let currentUser = null
        
        // Create authenticated TypedFetch instance
        const api = tf.create({
            baseURL: API_BASE,
            headers: () => ({
                'Authorization': localStorage.getItem('authToken') 
                    ? `Bearer ${localStorage.getItem('authToken')}` 
                    : undefined
            })
        })
        
        // User authentication
        window.login = async function() {
            const email = document.getElementById('email').value
            const password = document.getElementById('password').value
            
            try {
                const { data } = await api.post('/auth/login', {
                    data: { email, password }
                })
                
                localStorage.setItem('authToken', data.token)
                localStorage.setItem('userId', data.user.id)
                currentUser = data.user
                
                showStatus('Logged in successfully!', 'success')
                loadUserCities()
                updateUI()
                
            } catch (error) {
                showStatus(error.message, 'error')
            }
        }
        
        window.register = async function() {
            const email = document.getElementById('email').value
            const password = document.getElementById('password').value
            const name = prompt('What\'s your name?')
            
            try {
                const { data } = await api.post('/auth/register', {
                    data: { email, password, name }
                })
                
                localStorage.setItem('authToken', data.token)
                localStorage.setItem('userId', data.user.id)
                currentUser = data.user
                
                showStatus('Account created!', 'success')
                updateUI()
                
            } catch (error) {
                if (error.response?.status === 409) {
                    showStatus('Email already registered', 'error')
                } else {
                    showStatus(error.message, 'error')
                }
            }
        }
        
        window.logout = function() {
            localStorage.clear()
            currentUser = null
            document.getElementById('cityGrid').innerHTML = ''
            updateUI()
            showStatus('Logged out', 'success')
        }
        
        // City management
        window.addCity = async function(cityName) {
            if (!currentUser) {
                showStatus('Please login first', 'error')
                return
            }
            
            try {
                // First, get weather to verify city exists
                const { data: weather } = await tf.get(`https://wttr.in/${cityName}?format=j1`)
                
                // Save to user's cities
                const { data: savedCity } = await api.post('/users/me/cities', {
                    data: {
                        name: cityName,
                        country: weather.nearest_area[0].country[0].value,
                        timezone: weather.timezone,
                        position: document.querySelectorAll('.city-card').length
                    }
                })
                
                addCityCard(savedCity, weather)
                showStatus(`Added ${cityName}`, 'success')
                
            } catch (error) {
                showStatus(`Failed to add ${cityName}: ${error.message}`, 'error')
            }
        }
        
        window.updateCityPosition = async function(cityId, newPosition) {
            try {
                await api.patch(`/users/me/cities/${cityId}`, {
                    data: { position: newPosition }
                })
            } catch (error) {
                console.error('Failed to update position:', error)
            }
        }
        
        window.deleteCity = async function(cityId, cityName) {
            if (!confirm(`Remove ${cityName} from your dashboard?`)) return
            
            try {
                await api.delete(`/users/me/cities/${cityId}`)
                
                document.getElementById(`city-${cityId}`).remove()
                showStatus(`Removed ${cityName}`, 'success')
                
            } catch (error) {
                showStatus(`Failed to remove ${cityName}`, 'error')
            }
        }
        
        window.shareDashboard = async function() {
            try {
                const { data } = await api.post('/share/dashboard', {
                    data: {
                        userId: currentUser.id,
                        cities: Array.from(document.querySelectorAll('.city-card'))
                            .map(card => card.dataset.cityName)
                    }
                })
                
                const shareUrl = `${window.location.origin}/shared/${data.shareId}`
                
                if (navigator.clipboard) {
                    await navigator.clipboard.writeText(shareUrl)
                    showStatus('Share link copied to clipboard!', 'success')
                } else {
                    prompt('Share this link:', shareUrl)
                }
                
            } catch (error) {
                showStatus('Failed to create share link', 'error')
            }
        }
        
        window.updatePreferences = async function() {
            const units = document.getElementById('units').value
            const refreshInterval = document.getElementById('refresh').value
            
            try {
                const { data } = await api.patch('/users/me/preferences', {
                    data: {
                        temperatureUnit: units,
                        refreshInterval: parseInt(refreshInterval)
                    }
                })
                
                currentUser.preferences = data
                showStatus('Preferences updated', 'success')
                
                // Refresh all city cards with new units
                document.querySelectorAll('.city-card').forEach(card => {
                    updateWeatherDisplay(card.id.replace('city-', ''))
                })
                
            } catch (error) {
                showStatus('Failed to update preferences', 'error')
            }
        }
        
        // Load user's saved cities
        async function loadUserCities() {
            try {
                const { data: cities } = await api.get('/users/me/cities')
                
                document.getElementById('cityGrid').innerHTML = ''
                
                // Load cities in parallel
                const weatherPromises = cities
                    .sort((a, b) => a.position - b.position)
                    .map(city => 
                        tf.get(`https://wttr.in/${city.name}?format=j1`)
                            .then(({ data }) => ({ city, weather: data }))
                            .catch(() => ({ city, weather: null }))
                    )
                
                const results = await Promise.all(weatherPromises)
                
                results.forEach(({ city, weather }) => {
                    if (weather) {
                        addCityCard(city, weather)
                    }
                })
                
            } catch (error) {
                console.error('Failed to load cities:', error)
            }
        }
        
        // Add city card to grid
        function addCityCard(city, weather) {
            const card = document.createElement('div')
            card.className = 'city-card'
            card.id = `city-${city.id}`
            card.dataset.cityName = city.name
            
            const units = currentUser?.preferences?.temperatureUnit || 'C'
            const temp = units === 'C' 
                ? weather.current_condition[0].temp_C 
                : weather.current_condition[0].temp_F
            
            card.innerHTML = `
                <button class="delete-btn" onclick="deleteCity('${city.id}', '${city.name}')">×</button>
                <h3>${city.name}</h3>
                <p>🌡️ ${temp}°${units}</p>
                <p>🌤️ ${weather.current_condition[0].weatherDesc[0].value}</p>
                <p>💨 ${weather.current_condition[0].windspeedKmph} km/h</p>
                <p>💧 ${weather.current_condition[0].humidity}%</p>
            `
            
            document.getElementById('cityGrid').appendChild(card)
        }
        
        // Utility functions
        function showStatus(message, type) {
            const status = document.getElementById('status')
            status.textContent = message
            status.className = type
            setTimeout(() => status.textContent = '', 3000)
        }
        
        function updateUI() {
            const authSection = document.getElementById('authSection')
            const mainSection = document.getElementById('mainSection')
            
            if (currentUser) {
                authSection.style.display = 'none'
                mainSection.style.display = 'block'
                document.getElementById('userName').textContent = currentUser.name
            } else {
                authSection.style.display = 'block'
                mainSection.style.display = 'none'
            }
        }
        
        // Check if already logged in
        window.addEventListener('load', async () => {
            if (localStorage.getItem('authToken')) {
                try {
                    const { data } = await api.get('/users/me')
                    currentUser = data
                    updateUI()
                    loadUserCities()
                } catch (error) {
                    // Token expired
                    localStorage.clear()
                    updateUI()
                }
            }
        })
    </script>
</head>
<body>
    <h1>Weather Buddy 4.0 - Your Personal Weather Dashboard 🌍</h1>
    
    <div id="status"></div>
    
    <div id="authSection" class="auth-section">
        <h2>Login or Register</h2>
        <input type="email" id="email" placeholder="Email" />
        <input type="password" id="password" placeholder="Password" />
        <button onclick="login()">Login</button>
        <button onclick="register()">Register</button>
    </div>
    
    <div id="mainSection" style="display: none;">
        <div class="auth-section">
            <p>Welcome, <span id="userName"></span>!</p>
            <button onclick="logout()">Logout</button>
            <button onclick="shareDatabase()">Share Dashboard</button>
            
            <div style="margin-top: 10px;">
                <label>Temperature Unit:
                    <select id="units" onchange="updatePreferences()">
                        <option value="C">Celsius</option>
                        <option value="F">Fahrenheit</option>
                    </select>
                </label>
                
                <label>Refresh Every:
                    <select id="refresh" onchange="updatePreferences()">
                        <option value="60">1 minute</option>
                        <option value="300">5 minutes</option>
                        <option value="600">10 minutes</option>
                    </select>
                </label>
            </div>
        </div>
        
        <div class="search-box">
            <input 
                type="text" 
                id="citySearch" 
                placeholder="Add a city..."
                onkeypress="if(event.key==='Enter') addCity(this.value)"
            />
            <button onclick="addCity(document.getElementById('citySearch').value)">Add City</button>
        </div>
        
        <div id="cityGrid" class="city-grid"></div>
    </div>
</body>
</html>
```

## Advanced CRUD Patterns

### 1. Optimistic Updates

Update the UI immediately, then sync with server:

```javascript
async function toggleTodoOptimistic(todoId, currentState) {
  // Update UI immediately
  const todoElement = document.getElementById(`todo-${todoId}`)
  todoElement.classList.toggle('completed')
  
  try {
    // Sync with server
    await tf.patch(`/api/todos/${todoId}`, {
      data: { completed: !currentState }
    })
  } catch (error) {
    // Revert on failure
    todoElement.classList.toggle('completed')
    showError('Failed to update todo')
  }
}
```

### 2. Bulk Operations

Handle multiple items efficiently:

```javascript
// Delete multiple items
async function deleteSelectedTodos(todoIds) {
  try {
    // Some APIs support bulk delete
    await tf.post('/api/todos/bulk-delete', {
      data: { ids: todoIds }
    })
  } catch (error) {
    // Fallback to individual deletes
    const results = await Promise.allSettled(
      todoIds.map(id => tf.delete(`/api/todos/${id}`))
    )
    
    const failed = results.filter(r => r.status === 'rejected')
    if (failed.length > 0) {
      showError(`Failed to delete ${failed.length} items`)
    }
  }
}

// Bulk create
async function importTodos(todos) {
  const { data } = await tf.post('/api/todos/bulk', {
    data: { todos }
  })
  
  return data.created
}
```

### 3. Idempotent Requests

Make requests safe to retry:

```javascript
// Using idempotency keys
async function createPayment(amount, currency) {
  const idempotencyKey = crypto.randomUUID()
  
  try {
    const { data } = await tf.post('/api/payments', {
      data: { amount, currency },
      headers: {
        'Idempotency-Key': idempotencyKey
      }
    })
    
    return data
  } catch (error) {
    // Safe to retry with same idempotency key
    if (error.code === 'NETWORK_ERROR') {
      return tf.post('/api/payments', {
        data: { amount, currency },
        headers: {
          'Idempotency-Key': idempotencyKey
        }
      })
    }
    throw error
  }
}
```

### 4. Conditional Updates

Only update if resource hasn't changed:

```javascript
// Get resource with ETag
const { data: user, response } = await tf.get('/api/users/123')
const etag = response.headers.get('etag')

// Update only if unchanged
try {
  const { data: updated } = await tf.put('/api/users/123', {
    data: {
      ...user,
      role: 'Tech Lead'
    },
    headers: {
      'If-Match': etag
    }
  })
} catch (error) {
  if (error.response?.status === 412) {
    console.error('User was modified by someone else!')
    // Reload and try again
  }
}
```

## Error Handling in CRUD Operations

Each CRUD operation can fail differently:

```javascript
async function handleCrudErrors() {
  try {
    await tf.post('/api/resources', { data: {} })
  } catch (error) {
    switch (error.response?.status) {
      case 400:
        console.error('Bad Request:', error.data?.errors)
        break
      case 401:
        console.error('Not authenticated')
        // Redirect to login
        break
      case 403:
        console.error('Not authorized')
        break
      case 409:
        console.error('Conflict - resource already exists')
        break
      case 422:
        console.error('Validation failed:', error.data?.errors)
        break
      case 429:
        console.error('Too many requests')
        // Implement backoff
        break
      default:
        console.error('Unexpected error:', error.message)
    }
  }
}
```

## CRUD Best Practices

### 1. Use the Right Method
```javascript
// ✅ Correct
await tf.post('/api/users', { data: newUser })      // Create
await tf.patch('/api/users/123', { data: changes }) // Partial update
await tf.put('/api/users/123', { data: fullUser })  // Full replace
await tf.delete('/api/users/123')                   // Delete

// ❌ Wrong
await tf.post('/api/users/123', { data: updates })  // POST shouldn't update
await tf.get('/api/users/delete/123')               // GET shouldn't change data
```

### 2. Handle Loading States
```javascript
function CrudButton({ action, endpoint, data }) {
  const [loading, setLoading] = useState(false)
  
  async function handleClick() {
    setLoading(true)
    try {
      await tf[action](endpoint, { data })
      showSuccess(`${action} successful`)
    } catch (error) {
      showError(error.message)
    } finally {
      setLoading(false)
    }
  }
  
  return (
    <button onClick={handleClick} disabled={loading}>
      {loading ? 'Loading...' : action.toUpperCase()}
    </button>
  )
}
```

### 3. Validate Before Sending
```javascript
async function createUser(userData) {
  // Client-side validation
  const errors = validateUserData(userData)
  if (errors.length > 0) {
    return { success: false, errors }
  }
  
  try {
    const { data } = await tf.post('/api/users', { data: userData })
    return { success: true, user: data }
  } catch (error) {
    // Server-side validation errors
    if (error.response?.status === 422) {
      return { 
        success: false, 
        errors: error.data.errors 
      }
    }
    throw error
  }
}
```

## Practice Time! 🏋️

### Exercise 1: Todo App CRUD
Build a complete todo app with all CRUD operations:

```javascript
// Your code here:
// 1. Create todo (POST)
// 2. List todos (GET) 
// 3. Update todo (PATCH)
// 4. Delete todo (DELETE)
// 5. Bulk operations
```

### Exercise 2: Resource Versioning
Implement optimistic locking with version numbers:

```javascript
// Your code here:
// Track resource versions and handle conflicts
```

### Exercise 3: Retry Logic
Build smart retry for failed mutations:

```javascript
// Your code here:
// Retry with exponential backoff for safe operations
```

## Key Takeaways 🎯

1. **POST creates, PUT replaces, PATCH updates, DELETE removes**
2. **TypedFetch handles JSON automatically** for all methods
3. **Use PATCH for partial updates** instead of PUT
4. **Handle errors specifically** - each status code means something
5. **Optimistic updates** improve perceived performance
6. **Idempotency keys** make retries safe
7. **Validate client-side first** but always handle server validation
8. **Loading states** are crucial for user experience

## Common Pitfalls 🚨

1. **Using GET for state changes** - Never modify data with GET
2. **Forgetting error handling** - Mutations fail more than reads
3. **Not showing loading states** - Users need feedback
4. **Ignoring HTTP status codes** - They convey important info
5. **PUT with partial data** - Use PATCH instead
6. **Not handling conflicts** - Multiple users = conflicts

## What's Next?

You've mastered CRUD operations! But what happens when things go wrong? In Chapter 5, we'll dive deep into error handling:

- Understanding every HTTP status code
- Building resilient retry strategies
- Creating helpful error messages
- Implementing circuit breakers
- Handling network failures gracefully

We'll make Weather Buddy bulletproof - able to handle any failure and recover gracefully.

Ready to become an error-handling ninja? See you in Chapter 5! 🥷

---

## Chapter Summary

- CRUD = Create (POST), Read (GET), Update (PUT/PATCH), Delete (DELETE)
- POST creates new resources and returns the created item
- PUT replaces entire resources, PATCH updates parts
- DELETE removes resources, may return the deleted item or 204
- TypedFetch handles JSON serialization/parsing automatically
- Always handle specific error cases for better UX
- Optimistic updates make apps feel faster
- Use proper HTTP methods - don't use GET for mutations
- Weather Buddy now saves user preferences and syncs across devices

**Next Chapter Preview**: Error Handling Like a Pro - turning failures into features with smart retry logic, circuit breakers, and user-friendly error messages.