# 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 Weather Buddy 4.0 - Save Your Cities

Weather Buddy 4.0 - Your Personal Weather Dashboard 🌍

Login or Register

``` ## 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 ( ) } ``` ### 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.