Method · unsafe · idempotent
> PUT /users/42 HTTP/1.1> Host: api.example.com> Content-Type: application/jsonHTTP 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.
- 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-Matchto 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.
Related
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
Related
PUT vs PATCH
Understand the difference between PUT and PATCH HTTP methods. Learn when to use full replacement vs partial update, and how each affects idempotency and API design.
PUT vs POST: Create or Replace a Resource
PUT vs POST in REST APIs: who picks the URL, idempotency, retries, upsert, and raw requests showing when to create with POST and when to replace with PUT.
HTTP DELETE Method: Remove Resources
Learn how the HTTP DELETE method works, when to use it, and best practices for deleting resources in REST APIs.
HTTP HEAD Method
Learn how HTTP HEAD requests retrieve resource metadata (headers) without downloading the body. Useful for checking existence, size, and modification dates.
HTTP OPTIONS Method
Learn how HTTP OPTIONS requests discover server capabilities, supported methods, and handle CORS preflight checks for cross-origin requests.
HTTP PATCH Method
Learn how HTTP PATCH requests apply partial modifications to resources. Understand JSON Patch, merge patch formats, and when to use PATCH vs PUT.