Method · unsafe · not idempotent
> POST /orders HTTP/1.1> Host: api.example.com> Content-Type: application/jsonHTTP POST Method: Complete Guide with Examples
Learn how the HTTP POST method works. Understand when to use POST requests, request bodies, form submissions, and API calls with practical examples.
- Safe
- No
- Idempotent
- No
- Cacheable
- Only with explicit freshness
- Request body
- Yes
- Response body
- Yes
- Spec
- RFC 9110 §9.3.3
On this page
TL;DR: POST sends data in the request body for the server to process: create a record, submit a form, upload a file, or trigger an action. Return
201 Createdwith aLocationheader when it creates something. POST isn’t idempotent, so retrying after a dropped connection can create a duplicate, and putting data in the body doesn’t encrypt it. Use HTTPS for that.
What is POST?
RFC 9110 §9.3.3 defines POST loosely: the target resource processes the request body according to its own rules. Creating a record is one common use, but not the definition. A POST can just as well compute and return a result without creating anything.
This curl command sends JSON to an example API (requests and responses on this page are trimmed examples):
curl -i 'https://api.example.com/documents' \
-H 'Content-Type: application/json' \
--data '{"title":"Notes","text":"Reviewed"}'
A creation response, headers only:
HTTP/1.1 201 Created
Location: /documents/notes
Content-Type: application/json
Cache-Control: no-store
Location points to the main resource that was created. On a 201 it’s information for the client, not a redirect the browser follows.
Key Characteristics
POST is neither safe nor idempotent. Picture a connection that drops after the server has committed an order but before the response arrives. If the client sends the request again, it may create a second order. Applications can add their own deduplication, but the method doesn’t give you that.
POST responses are cacheable in a narrow case: they need explicit freshness information and a Content-Location that matches the target URI. A cache can then use that stored response for later GET or HEAD requests, never for another POST. In practice, many caches only cache GET and HEAD. If a response must never be stored, say so with Cache-Control: no-store instead of counting on the method to keep it private.
Content Types
For JSON, set Content-Type: application/json. For URL-encoded forms, let curl do the encoding:
curl -i 'https://api.example.com/search' \
--data-urlencode 'q=red shoes' \
--data-urlencode 'category=books'
For uploads, point at a real local file and let the client generate the multipart boundary:
curl -i 'https://api.example.com/uploads' \
-F 'file=@./report.pdf;type=application/pdf' \
-F 'description=Quarterly report'
In the browser, FormData does the same job. Leave Content-Type unset and let it write the header. A bare multipart/form-data you set by hand has no boundary, so the server can’t parse the body.
The server’s body parser has to match the request’s media type. In Express, JSON and URL-encoded forms each get their own middleware:
app.use(express.json({ limit: '100kb' }))
app.use(express.urlencoded({ extended: false, limit: '100kb' }))
The documented JSON parser limit defaults to 100kb; this example just makes it explicit. Neither middleware handles multipart uploads, so add a multipart parser with its own limits if you accept files. A reverse proxy might reject a large upload before Express sees it, so when you get a 413, find out which layer sent it.
For plain text, --data-binary sends a local file’s contents unchanged:
curl -i 'https://api.example.com/import-text' \
-H 'Content-Type: text/plain; charset=utf-8' \
--data-binary @./notes.txt
You could assemble a multipart body by hand, but -F generates matching boundary delimiters and part headers for you. In browser code, FormData does the same.
Real-World Examples
After handling an HTML form, the server can reply with a 303 pointing at a result page. The browser then loads that page with GET, instead of leaving the form POST as the page on screen:
HTTP/1.1 303 See Other
Date: Mon, 05 Oct 2026 12:00:00 GMT
Location: /orders/42
Content-Length: 0
This is the Post/Redirect/Get pattern. Refreshing the result page reloads the GET instead of resubmitting the form. It only helps with refreshes, though: if two submissions already reached the server, you still have two.
Creating a document in Express
This runnable example assigns a UUID, keeps the document in process memory, and returns its URL:
const express = require('express')
const { randomUUID } = require('node:crypto')
const app = express()
const documents = new Map()
app.use(express.json({ limit: '100kb' }))
app.post('/documents', (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 id = randomUUID()
const document = { id, title, text }
documents.set(id, document)
res.location(`/documents/${id}`).status(201).json(document)
})
app.get('/documents/:id', (req, res) => {
const document = documents.get(req.params.id)
if (!document) return res.status(404).end()
res.json(document)
})
app.listen(3000)
Send the same JSON twice and compare the two Location values: you get two documents. That’s POST working as designed. The example skips authentication and durable storage, which a real application needs. The 201 tells the client something was created, and following its Location retrieves it.
Response Status Codes
Use 201 when something was created, 200 to return a processing result, 202 when the work was accepted but isn’t finished, and 204 for success with no body. A content type the endpoint doesn’t support gets 415, and a body that’s too large gets 413.
POST vs GET
GET retrieves a representation; POST submits content for processing. POST URLs can carry query parameters too. Moving values into the body keeps them out of the URL, but application logs and monitoring tools may still record the body. Send credentials over HTTPS and keep their values out of your logs.
Neither method has unlimited size. nginx’s documented default, for example, is client_max_body_size 1m, and a request body over that limit gets 413. Other proxies and applications set their own limits.
POST vs PUT
With POST, the target decides what processing means. PUT creates or replaces the state at a URL the client chooses, and it’s idempotent. POST /documents with a server-assigned URL is a common design, but nothing requires POST to target a collection.
Common Headers
Content-Type describes the body you’re sending, and Accept asks for a response format. Clients normally handle framing headers like Content-Length themselves, and browser Fetch won’t let you set it. Location can point to a new resource. For a cross-origin response, JavaScript can only read it if the server sends Access-Control-Expose-Headers: Location.
Getting POST right
If clients retry after an uncertain failure, give them an idempotency-key contract and document it. The header on its own prevents nothing. The server has to store each key with its request and result, and stop concurrent attempts with the same key from running the action twice.
In browsers, a cross-origin POST with a JSON body or an Authorization header normally triggers a CORS preflight. The actual response needs CORS headers as well, not just the preflight.
JavaScript Examples
const response = await fetch('/api/documents', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Notes', text: 'Reviewed' })
})
if (!response.ok) throw new Error(`POST failed: ${response.status}`)
if (response.status !== 204) {
const result = await response.json() // This endpoint promises JSON when it has content.
console.log(result)
}
Fetch only rejects on network failures, not HTTP error statuses. Check the status, and know what the endpoint returns, before parsing the body.
Submitting a form with a file
This handler expects an existing <form id="upload"> with a file input named file. Each form field name becomes a multipart part name:
const form = document.querySelector('#upload')
form.addEventListener('submit', async (event) => {
event.preventDefault()
const body = new FormData(form)
const response = await fetch('/api/uploads', { method: 'POST', body })
if (!response.ok) throw new Error(`Upload failed: ${response.status}`)
console.log(response.status)
})
For a form without files, URLSearchParams works too:
const body = new URLSearchParams({ q: 'red shoes', category: 'books' })
const response = await fetch('/api/search', { method: 'POST', body })
if (!response.ok) throw new Error(`Search failed: ${response.status}`)
Fetch sets the right form media type for URLSearchParams automatically. With FormData, setting Content-Type yourself can leave the boundary missing or mismatched. Either way, the endpoint has to accept that format. Switching a request from JSON to form encoding changes what the server’s parser receives.
Try It Yourself
Open the create preset. Compare the request Content-Type, the response status and any Location header. It’s an exercise in reading the request and response, not a test of retry deduplication.
If you repeat a creation request, use throwaway example data. Two 201 responses with different Location values mean two resources were created. A 303 redirect to a result page won’t merge them.
Related Methods
Related Concepts
Check your understanding
Create a resource with POST
Use the playground to send a JSON body, then compare the request and response with a GET flow so the creation semantics are concrete.
- Open the create preset and inspect the `Content-Type` header and JSON body before sending.
- Send the request and note the response status and any Location-style creation hints.
- Switch back to a GET preset and compare how the request shape changes when you are only retrieving data.
Look for: POST should include a body and represent a state-changing action, unlike GET.
Why it matters: The fastest way to internalize POST is to contrast a creation request with a read-only request side by side.
Pick the better POST use case
Which scenario is the strongest fit for POST rather than GET?
In practice
Express.js
// POST /users — create a new user
app.post('/users', async (req, res) => {
const { name, email, password } = req.body
const user = await db.users.create({ name, email, password })
res.status(201)
.location(`/users/${user.id}`)
.json(user)
})
// POST /sessions — login
app.post('/sessions', async (req, res) => {
const user = await db.users.findByEmail(req.body.email)
if (!user || !user.verifyPassword(req.body.password)) {
return res.status(401).json({ error: 'Invalid credentials' })
}
const token = generateToken(user)
res.json({ token })
})Next.js App Router
// app/api/users/route.ts
export async function POST(request: Request) {
const body = await request.json()
const user = await db.users.create(body)
return Response.json(user, {
status: 201,
headers: { Location: `/api/users/${user.id}` }
})
}
// app/api/sessions/route.ts
export async function POST(request: Request) {
const { email, password } = await request.json()
const user = await db.users.findByEmail(email)
if (!user?.verifyPassword(password)) {
return Response.json({ error: 'Invalid credentials' }, { status: 401 })
}
return Response.json({ token: generateToken(user) })
}Django
# views.py
import json
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
@require_POST
def create_user(request):
data = json.loads(request.body)
user = User.objects.create(
name=data['name'],
email=data['email'],
)
response = JsonResponse(user.to_dict(), status=201)
response['Location'] = f'/users/{user.id}'
return responseFrequently asked questions
What is the HTTP POST method?
POST submits content for the target resource to process under its own rules. It can create a resource, submit a form, or perform an action; it can also return a processing result without creating anything.
When should I use POST instead of GET?
Use POST when submitting content for processing, creating resources, uploading files, or performing actions. Body size limits still apply, and POST responses are not categorically uncacheable.
Is POST secure?
POST does not encrypt submitted data. A body avoids putting values in the URL, but application logs can still capture it. Use HTTPS and avoid logging credentials.
What Content-Type should I use with POST?
Use a media type the endpoint supports, such as application/json or application/x-www-form-urlencoded. For browser FormData or curl multipart uploads, let the client generate the multipart Content-Type with its boundary.
Is POST idempotent?
POST has no general idempotency guarantee. A particular endpoint can implement deduplication or idempotent processing, but clients must follow its documented contract before retrying an uncertain result.
What status code should a POST request return?
Use 201 for creation, 200 for a processing result, 202 for accepted but incomplete work, or 204 for success without content. Location can identify the primary resource created by a 201 response.
Sources
Related
GET vs POST
Understand the fundamental differences between GET and POST HTTP methods. Learn about safety, idempotency, caching, request bodies, and when to use each.
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 QUERY Method: Safe Requests With a Body (RFC 10008)
HTTP QUERY is a safe, idempotent method with the query in the body. RFC 10008 status, an example exchange, caching rules, and why GET with a body fails.
Idempotency Key (Safe POST Retries)
An Idempotency-Key header lets a client safely retry a POST: the server stores the first result and replays it. Learn the protocol, edge cases and conflicts.
REST API Design with HTTP Semantics
Choose HTTP methods and status codes for a REST API: 201, 202, 204, 400 vs 422, 409, ETag with If-Match, pagination, problem+json and idempotency keys.
Webhooks: Delivery, Retries and Signature Verification
Build a reliable webhook receiver: respond 2xx fast, verify HMAC over the raw body, reject replays, dedupe by event ID. Covers Stripe and GitHub.