- Home
- HTTP Methods
- HTTP PUT Method: Update Resources
Method
HTTP PUT Method: Update Resources
Learn how the HTTP PUT method works, when to use PUT vs POST vs PATCH, and best practices for updating resources in REST APIs.
TL;DR: PUT replaces a resource entirely. It’s idempotent—multiple identical requests have the same effect as one.
What is PUT?
PUT requests the server to replace the resource at the target URL with the request body. If the resource doesn’t exist, PUT may create it.
PUT /api/users/123 HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
"id": 123,
"name": "Jane Doe",
"email": "jane@example.com",
"role": "admin"
}
```text
```http
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 123,
"name": "Jane Doe",
"email": "jane@example.com",
"role": "admin"
}
PUT vs POST vs PATCH
| Method | Purpose | Idempotent | Body |
|---|---|---|---|
| POST | Create new resource | No | New resource data |
| PUT | Replace entire resource | Yes | Complete resource |
| PATCH | Partial update | No* | Only changed fields |
*PATCH can be idempotent depending on implementation.
Example Comparison
# POST: Create (server assigns ID)
POST /api/users
{"name": "Jane"}
→ 201 Created, Location: /api/users/123
# PUT: Replace entire resource
PUT /api/users/123
{"name": "Jane", "email": "jane@example.com", "role": "user"}
→ 200 OK
# PATCH: Update specific fields
PATCH /api/users/123
{"role": "admin"}
→ 200 OK
```javascript
## Idempotency
PUT is idempotent—calling it multiple times produces the same result:
```javascript
// All three calls result in the same state
await fetch('/api/users/123', { method: 'PUT', body: userData })
await fetch('/api/users/123', { method: 'PUT', body: userData })
await fetch('/api/users/123', { method: 'PUT', body: userData })
// User 123 has the same data after all calls
This makes PUT safe to retry on network failures.
Implementation
Express.js
app.put('/api/users/:id', async (req, res) => {
const { id } = req.params
const userData = req.body
// Validate complete resource
if (!userData.name || !userData.email) {
return res.status(400).json({ error: 'Missing required fields' })
}
// Replace entire resource
const user = await User.findByIdAndUpdate(
id,
userData,
{ new: true, overwrite: true } // overwrite: true for full replacement
)
if (!user) {
return res.status(404).json({ error: 'User not found' })
}
res.json(user)
})
```javascript
### Client-Side
```javascript
async function updateUser(id, userData) {
const response = await fetch(`/api/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
})
if (!response.ok) {
throw new Error(`Update failed: ${response.status}`)
}
return response.json()
}
// Usage: Send complete resource
await updateUser(123, {
name: 'Jane Doe',
email: 'jane@example.com',
role: 'admin'
})
PUT for Create (Upsert)
PUT can create resources when the client controls the ID:
PUT /api/documents/my-custom-id HTTP/1.1
Content-Type: application/json
{"title": "My Document", "content": "..."}
```text
```http
HTTP/1.1 201 Created
Location: /api/documents/my-custom-id
// Express upsert pattern
app.put('/api/documents/:id', async (req, res) => {
const result = await Document.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, upsert: true } // Create if doesn't exist
)
res.status(result.isNew ? 201 : 200).json(result)
})
```javascript
## Common Response Codes
| Code | Meaning |
| ---- | ---------------------------- |
| 200 | Updated successfully |
| 201 | Created (if PUT creates) |
| 204 | Updated, no content returned |
| 400 | Invalid request body |
| 404 | Resource not found |
| 409 | Conflict (version mismatch) |
## Best Practices
1. **Send complete resource** - PUT replaces everything; missing fields may be deleted
2. **Use for known URLs** - Client should know the resource URL
3. **Validate completely** - Ensure all required fields present
4. **Consider versioning** - Use ETags to prevent lost updates
### Preventing Lost Updates
```javascript
app.put('/api/users/:id', async (req, res) => {
const ifMatch = req.headers['if-match']
const user = await User.findById(req.params.id)
if (ifMatch && ifMatch !== user.etag) {
return res.status(412).json({ error: 'Resource modified' })
}
// Proceed with update...
})
Related
- POST method - Create resources
- PATCH method - Partial updates
- GET method - Retrieve resources
- DELETE method - Remove resources
Optimistic Concurrency with ETags
PUT requests are vulnerable to lost updates when multiple clients modify the same resource concurrently. Client A reads a resource, Client B reads the same resource, Client A writes an update, then Client B writes an update that overwrites Client A’s changes without knowing they existed.
The solution is optimistic concurrency control using ETags and the If-Match header. When the client reads a resource, the server includes an ETag in the response. When the client sends a PUT to update the resource, it includes If-Match: <etag> with the ETag it received. The server checks whether the current ETag matches. If it does, the resource has not changed since the client read it, and the update proceeds. If it does not match, another client has modified the resource, and the server returns 412 Precondition Failed.
// Express.js: ETag-based optimistic locking
app.put('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id)
if (!user) return res.status(404).end()
const currentETag = `"${user.version}"`
const ifMatch = req.headers['if-match']
if (ifMatch && ifMatch !== currentETag) {
return res.status(412).set('ETag', currentETag).json({ error: 'Conflict' })
}
const updated = await user.update(req.body)
res.set('ETag', `"${updated.version}"`).json(updated)
})
This pattern is essential for collaborative editing, inventory management, and any scenario where concurrent writes to the same resource must be detected and handled gracefully.
In Practice
Express.js
// PUT /users/:id — full replacement
app.put('/users/:id', async (req, res) => {
const { name, email, role } = req.body
// All fields required — missing fields are cleared
const user = await db.users.replace(req.params.id, { name, email, role })
if (!user) return res.status(404).json({ error: 'Not found' })
res.json(user)
})Next.js App Router
// app/api/users/[id]/route.ts
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
const body = await request.json()
// Full replacement — body must contain all required fields
const user = await db.users.replace(params.id, body)
if (!user) return new Response(null, { status: 404 })
return Response.json(user)
}Django
# views.py
import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def user_detail(request, user_id):
if request.method == 'PUT':
data = json.loads(request.body)
# Full replacement — update all fields
updated = User.objects.filter(pk=user_id).update(**data)
if not updated:
return JsonResponse({'error': 'Not found'}, status=404)
return JsonResponse(User.objects.get(pk=user_id).to_dict())Frequently Asked Questions
What is the HTTP PUT method?
PUT replaces a resource entirely with the request body. It's idempotent—calling it multiple times has the same effect as calling it once.
What is the difference between PUT and POST?
PUT replaces a resource at a specific URL (idempotent). POST creates a new resource, with the server deciding the URL (not idempotent).
What is the difference between PUT and PATCH?
PUT replaces the entire resource. PATCH applies partial modifications. Use PUT for full updates, PATCH for partial updates.
Is PUT idempotent?
Yes. Sending the same PUT request multiple times produces the same result. The resource ends up in the same state regardless of how many times you call it.