# 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
Welcome, !
```
## 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.