HTTP

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.

4 min read beginner Try in Playground

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

MethodPurposeIdempotentBody
POSTCreate new resourceNoNew resource data
PUTReplace entire resourceYesComplete resource
PATCHPartial updateNo*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...
})

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.

Keep Learning