How HTTP Works

Method · unsafe · idempotent

> PUT /users/42 HTTP/1.1>  Host: api.example.com>  Content-Type: application/json

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.

Reviewed 4 min readbeginner2 sourcesTry itMarkdown
Safe
No
Idempotent
Yes
Cacheable
No
Request body
Yes
Response body
Optional
Spec
RFC 9110 §9.3.4
On this page

TL;DR: PUT creates or replaces the state of the resource at the target URL. Repeating the same request has the same intended effect, but can return a different status. Use If-Match to avoid overwriting a concurrent edit.

What is PUT?

The client chooses the target URL and submits a replacement representation. A server can reject the representation if it violates the resource’s constraints. PUT does not prescribe database columns or require an API to erase every omitted field; a missing required field can be a validation error.

Illustrative request. The body is 35 UTF-8 bytes with no trailing newline:

PUT /documents/notes HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 35
If-Match: "notes-v7"

{"title":"Notes","text":"Reviewed"}

PUT vs POST vs PATCH

PUT supplies replacement state at a known URL. POST requests processing under that target’s own rules; creation with a server-assigned URL is one use. PATCH supplies a change document whose format defines how to apply it. An ORM’s partial update helper does not automatically implement PUT semantics.

Idempotency

A first PUT might create a resource and return 201; an identical retry might replace it and return 204. That remains idempotent because the intended stored state is the same. The server can log each attempt or keep revision history.

Idempotency permits retry after a connection failure, but does not prevent a retry from overwriting somebody else’s intervening update. Preconditions solve a different problem.

Implementation

This runnable Express example replaces an in-memory document. The storage is deliberately process-local and only demonstrates the method:

const express = require('express')
const app = express()
const documents = new Map()
app.use(express.json())
app.put('/documents/:id', (req, res) => {
  const { title, text } = req.body ?? {}
  if (typeof title !== 'string' || typeof text !== 'string') {
    return res.status(400).json({ error: 'title and text must be strings' })
  }
  const existed = documents.has(req.params.id)
  documents.set(req.params.id, { title, text })
  if (existed) return res.status(204).end()
  res.location(`/documents/${encodeURIComponent(req.params.id)}`)
    .status(201).json({ title, text })
})
app.listen(3000)

Run against it:

curl -i -X PUT 'http://localhost:3000/documents/notes' \
  -H 'Content-Type: application/json' \
  --data '{"title":"Notes","text":"Reviewed"}'

Run the same command again and compare the status.

A client must accept an empty success response as well as a JSON representation:

async function replaceDocument(id, document) {
  const response = await fetch(`/api/documents/${encodeURIComponent(id)}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(document)
  })
  if (!response.ok) throw new Error(`PUT failed: ${response.status}`)
  return response.status === 204 ? null : response.json()
}

This client assumes JSON on nonempty successful responses. Send all writable fields required by the endpoint, not server-controlled fields copied blindly from GET. The server still has to validate the submitted document; client validation cannot protect an API from another caller.

PUT for Create (Upsert)

Creation is permitted, not required: an update-only API can reject an unknown URL. If PUT creates the target’s representation, the server must send 201. Replacing an existing representation successfully requires 200 or 204. To make a creation conditional on absence, send If-None-Match: * and have the server evaluate it atomically with the write. See RFC 9110 §9.3.4.

curl -i -X PUT 'https://api.example.com/documents/new-notes' \
  -H 'If-None-Match: *' \
  -H 'Content-Type: application/json' \
  --data '{"title":"Notes","text":"Reviewed"}'

With that precondition enforced, a competing creator cannot silently turn this attempt into replacement. A failed precondition is normally 412. Decide whether the caller should pick a different URL or retrieve the existing document; do not automatically remove the condition and overwrite it.

Common Response Codes

201 means creation; 200 or 204 means a successful replacement. 415 can reject an unsupported media type. A failed If-Match normally returns 412, not 409; 409 describes a conflict with resource state more generally.

Getting PUT right

Define the writable representation and validate it before replacing state. Protect server-controlled fields such as owner and role. PUT responses are not cacheable; a successful unsafe request invalidates the cached target under RFC 9111 §4.4.

Optimistic Concurrency with ETags

Read the representation and its strong ETag, then submit it with the replacement:

curl -i 'https://api.example.com/documents/notes'
curl -i -X PUT 'https://api.example.com/documents/notes' \
  -H 'If-Match: "notes-v7"' \
  -H 'Content-Type: application/json' \
  --data '{"title":"Notes","text":"Reviewed"}'

The ETag above is illustrative; use the one your GET returned. If-Match uses strong comparison, so a weak tag such as W/"notes-v7" cannot satisfy it. The server must check the version and write as one atomic operation. A separate read, string comparison, and unconditional database update leaves a race. The header can contain a list of tags or *, so checking it as one arbitrary string is incomplete.

A successful PUT must not return a new validator if the request representation was transformed before storage; see the validator rule in §9.3.4.

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 requests creation or replacement of state at the target URL. It is idempotent: identical repetitions have the same intended effect, even when response statuses differ.

What is the difference between PUT and POST?

PUT supplies creation or replacement state at a client-selected URL. POST requests processing according to the target resource’s rules; creating a resource with a server-assigned URL is one use of POST.

What is the difference between PUT and PATCH?

PUT supplies replacement state. PATCH supplies a change document whose media type defines how modifications are applied. Neither method prescribes a database update function.

Is PUT idempotent?

PUT is idempotent in its intended effect on server state. The first attempt can return 201 and a repeat 200 or 204; identical responses are not required.

Sources

  1. MDN Web Docs: PUTdeveloper.mozilla.org
  2. RFC 9110: PUTrfc-editor.org

Keep going

Browse /search