I remember the exact moment the production database alarm went off in 2019. It wasn’t a server crash or a memory leak; it was a simple ORDER_BY query returning 4,000 identical records instead of the expected 400. A junior developer had written a retry logic for a payment service using a POST request to create orders. When the network timed out, the client retried the request, but because POST is non-idempotent, the server executed the creation logic again, and again, until the timeout window closed. That single afternoon of duplicate data cost the company three days of manual data cleansing and a painful post-mortem.
This story highlights a core confusion that plagues many developers: the subtle but critical difference between post vs put in REST principles. While they both send data over HTTP, they carry fundamentally different semantic weights regarding state change and resource replacement. Choosing the wrong verb isn't just a stylistic preference; it directly impacts your API’s reliability, caching efficiency, and scalability. If you are building a distributed system where network interruptions are a given, not an exception, understanding idempotence is the difference between a resilient architecture and a fragile one.
Core Semantics: Idempotent vs Non-Idempotent Operations
To design an API that survives real-world chaos, you must first distinguish between operations that are safe to repeat and those that are not. This is where the concept of idempotence enters the equation.
Defining Idempotence in HTTP Requests
According to RFC 7231, an idempotent method is one where an arbitrary number of identical requests have the same effect on the server as a single request. PUT is idempotent by definition. If you send a PUT /users/123 with a specific JSON payload five times, the user record ends up in that exact state five times over. It’s like setting a thermostat to 70 degrees. Whether you press the "Set to 70" button once or ten times, the temperature goal remains 70.
POST, conversely, is not idempotent. If you send a POST /users five times, you typically create five distinct user accounts. It’s similar to clicking "Buy Now" on a shopping cart. Each click triggers a new transaction, potentially resulting in multiple orders. Idempotence does not mean "no effect"; it means "the same effect." A PUT request always converges to the state defined in the request body. A POST request diverges, adding new state with every invocation.
The Cost of Retries: Production Environment Impact
In a local development environment, you might never see a timeout. In production, however, network partitions, load balancer resets, and client-side flakiness are inevitable. When a client sends a request and doesn’t hear back immediately, standard HTTP client libraries (like fetch or axios) often retry the request automatically.
I’ve reviewed several post-mortems from fintech startups that lost thousands of dollars due to non-idempotent payment processing. The pattern was consistent: they used POST /payments without an idempotency key. When a user’s mobile connection dropped during the transaction, the client retried. The server processed the original payment, then processed the retry, resulting in double billing. In distributed systems, assuming a POST is safe to retry is a recipe for data corruption. By using PUT for updates or implementing application-level idempotency keys for POST, you ensure that repeated requests result in a predictable, single outcome rather than cascading errors.
Resource Control: Client-Defined vs Server-Generated Identifiers
The decision between the two methods also hinges on who controls the resource's identity: the client or the server. This distinction defines the URI structure of your restful api post vs put endpoints.
POST: The Server-Driven Creation Model
When you use POST, you are typically targeting a collection URI, such as /orders. The client provides the data, but the server decides where the new resource lives. The server generates the unique identifier—usually an auto-incrementing integer or a UUID—and returns it in the response. This model is ideal when the client does not have the authority or knowledge to define the ID.
Consider a scenario where a user places an order. The client sends:
POST /api/v1/orders
Content-Type: application/json
{
"item": "Widget",
"quantity": 2
}
The server processes this, creates the order, assigns it ID 452, and responds:
HTTP/1.1 201 Created
Location: /api/v1/orders/452
Content-Type: application/json
{
"id": 452,
"item": "Widget",
"quantity": 2,
"status": "pending"
}
The 201 Created status code signals that a new resource has been generated, and the Location header tells the client where to find it. This flexibility makes POST the default for creation, especially when complex business logic determines the final ID.
PUT: The Client-Driven Upsert Strategy
With PUT, the client specifies the full URI, including the ID. This shifts the burden of identity generation to the client. If the resource at PUT /api/v1/users/99 does not exist, the server creates it. If it does exist, the server replaces it entirely. This behavior is often referred to as an "upsert" (update or insert).
This approach is powerful for idempotent creation of resources where the client knows the ID in advance, such as importing data from a system that uses deterministic IDs. For example, a client might sync user profiles:
PUT /api/v1/users/123
Content-Type: application/json
{
"name": "Alice",
"email": "alice@example.com",
"role": "admin"
}
If user 123 doesn't exist, the server creates her. If she does, her profile is completely overwritten with the data in the payload. The response is typically 200 OK if the resource existed, or 201 Created if it was newly created by the PUT request. Note that because PUT implies a full state replacement, any fields omitted from the payload are usually reset to null or default values, which is a critical behavioral difference from partial updates.
Status Code Mapping: 201 Created vs 200 OK in Practice
Proper use of HTTP status codes reinforces the semantic difference between these methods. Clients rely on these codes to determine whether their actions succeeded, failed, or changed the resource state.
Interpreting Successful Responses
A successful POST request that results in a new resource should return 201 Created. This is not just a convention; it’s a requirement of the HTTP spec for creation. The response must include a Location header pointing to the new resource. If the POST does not create a new resource but performs some other processing (like triggering a webhook or calculating a sum), it may return 200 OK.
A successful PUT request usually returns 200 OK with the updated resource representation in the body, or 204 No Content if the response body is empty. If the PUT operation resulted in the creation of a new resource (because it didn't exist before), a 201 Created is also appropriate. The distinction matters for caching proxies: 200 responses from PUT can be cached, whereas 201 responses are typically not cached by default, reflecting the transient nature of the creation event.
Handling Errors: 409 Conflict and 405 Method Not Allowed
Errors reveal the mismatch between client intent and server expectation. A 409 Conflict is common with PUT when the server detects that the resource has been modified concurrently by another client. For instance, if the client sends a PUT with an ETag header that no longer matches the server's version, the server should return 409 to prevent overwriting newer data.
Conversely, 405 Method Not Allowed occurs when the client uses the wrong verb for the endpoint. If an API only accepts PUT for updating user profiles, sending a POST to the same URI will result in a 405. The error body should be explicit:
{
"error": "method_not_allowed",
"message": "Use PUT to update user 123. POST is reserved for creation at /users."
}
Clear error messaging guides developers to the correct method, reducing the cognitive load during integration.
The Missing Piece: Why You Need PATCH for Partial Updates
While POST and PUT cover creation and full replacement, they leave a gap for partial modifications. This is where PATCH becomes essential, and understanding its role clarifies why you should avoid using POST for updates.
PUT vs PATCH: Full Replacement vs Modification
PUT requires the client to send the entire resource representation. If you only want to change a user's email address, you must still send their name, phone number, and address. If you forget to include the phone number, PUT might wipe it out because it assumes you are defining the complete new state.
PATCH allows for granular changes. You can send a "delta" payload that specifies only what changed. For a JSON Merge Patch, the payload looks like:
{
"email": "new.email@example.com"
}
This is significantly more efficient over bandwidth-constrained networks and reduces the risk of data loss caused by incomplete payloads. PATCH is not always idempotent (depending on the patch logic, e.g., "increment counter" is not idempotent, but "set field to X" is), so care must be taken. However, for field-level updates, it is the semantically correct choice.
When to Avoid Using POST for Updates
A common anti-pattern in legacy systems is using POST /users/123/update or POST /users/123 to update an existing record. This violates REST semantics. POST is for creation. Using it for updates loses idempotence (retries cause errors or duplication), breaks caching mechanisms (which are optimized for PUT and GET), and confuses developers about the endpoint's purpose.
Here is a bad vs. good comparison:
Bad (Anti-pattern):
POST /api/users/123
{ "email": "updated@example.com" }
Result: Unclear semantics, non-idempotent, no caching benefits.
Good (Correct usage):
PATCH /api/users/123
{ "email": "updated@example.com" }
Result: Clear intent, efficient payload, appropriate status codes.
Framework Implementation: Post vs Put in Spring and Express
Theory is only useful if it translates to code. The http post vs put difference is most visible in how you structure your controllers in popular frameworks.
Spring Boot: @PostMapping vs @PutMapping
In Spring Boot, the mapping annotations define the method's behavior. When using @PutMapping, you are signaling that the incoming payload is the complete new state. This often pairs with strict validation annotations like @Validated to ensure no fields are missing if they are required for the full representation.
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody UserDto userDto) {
// Server generates ID, saves, returns 201
User user = userService.create(userDto);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody UserDto userDto) {
// Client defines ID, replaces entire record, returns 200 or 201
User user = userService.updateFull(id, userDto);
return ResponseEntity.ok(user);
}
}
The distinction in Spring is not just about the HTTP verb; it’s about the service layer logic. createUser might involve generating a UUID, while updateUser assumes the ID is known and performs a full replace-or-insert.
Node.js/Express: Router.post vs Router.put
In Express, the router middleware defines which method handles the request. The implication for body parsing is subtle but important. For PUT, you typically expect a complete JSON object. For PATCH, you might expect a partial object or a JSON Patch document.
const express = require('express');
const router = express.Router();
const { body, validationResult } = require('express-validator');
// POST: Create new resource
router.post('/orders',
body('item').notEmpty(),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
// Logic to create order, generate ID
const orderId = generateId();
res.status(201).json({ id: orderId, ...req.body });
}
);
// PUT: Replace entire resource
router.put('/users/:id',
body('name').notEmpty(),
body('email').isEmail(),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
// Logic to replace or upsert user
const result = replaceUser(req.params.id, req.body);
res.status(200).json(result);
}
);
In Node.js, you must be explicit about what the PUT handler does with missing fields. If the client sends a PUT without a phone field, does your replaceUser function nullify the existing phone number? If so, that is the correct behavior for PUT. If you want to preserve existing fields, you should be using PATCH.
Security Considerations: CSRF and CORS Implications
The choice between POST and PUT has significant security implications, particularly in web-based clients. This is a critical aspect of post vs put best practice that often gets overlooked until a vulnerability is discovered.
CSRF Protection Differences
Cross-Site Request Forgery (CSRF) attacks rely on the browser automatically including cookies when making requests to a trusted origin. Historically, only GET and POST requests were "simple" enough to be fired via an <img> tag or a form submission without a preflight check.
While POST is the primary target for CSRF, PUT is not automatically protected in all scenarios. Browsers do not send automatic cookies with PUT requests in a way that can be easily forged by a malicious HTML form (since HTML forms don't support PUT). However, if you are using a browser-based API client that uses XMLHttpRequest or fetch with credentials: 'include', you must implement CSRF protection for all state-changing methods, including PUT and PATCH.
The standard mitigation is to require a custom header (like X-CSRF-Token) that can only be set by JavaScript on the same origin. Since external malicious sites cannot set custom headers without a CORS preflight, this blocks the attack.
CORS Preflight Requests
When a client (especially a web browser) sends a PUT or PATCH request to a different origin, it triggers a CORS preflight OPTIONS request. This is because PUT and PATCH are not "simple" methods.
The preflight asks the server: "Do you allow PUT with Content-Type: application/json from this origin?" The server must respond with:
Access-Control-Allow-Methods: GET, PUT, PATCH
Access-Control-Allow-Headers: Content-Type
This adds an extra round-trip to the client's initial request, increasing latency. POST requests with simple content types (application/x-www-form-urlencoded, multipart/form-data) do not trigger a preflight. If you are building a high-frequency API used by browser clients, be aware that PUT/PATCH will always incur this preflight cost. For mobile or server-to-server communication, this is irrelevant. For web apps, it’s a trade-off between semantic correctness and performance.
FAQ
What is the main difference between POST and PUT?
The core difference is idempotence and resource identity. POST is non-idempotent and is used to create new resources where the server typically assigns the ID. PUT is idempotent and is used to replace an existing resource or create one at a client-specified URI. Think of POST as "add to the collection" and PUT as "set this specific slot to this value."
Can PUT be used to create a new resource?
Yes. If the client specifies a URI for a resource that does not yet exist (e.g., PUT /files/report-2023.pdf), the server can create it. This is known as an "upsert." If the resource is created, the response should be 201 Created; if it was replaced, it should be 200 OK.
Is POST idempotent or not?
No, POST is not idempotent. Sending the same POST request multiple times will typically result in multiple resources being created or multiple actions being triggered. To make a creation process safe against retries, you must implement an application-level idempotency key, such as a unique transaction ID in the payload.
When should I use PATCH instead of PUT?
Use PATCH when you only need to modify specific fields of a resource without sending the entire object. PUT requires the full representation of the resource. PATCH is more efficient for partial updates, saving bandwidth and reducing the risk of unintentionally nulling out fields that weren't included in the request.
Conclusion
Navigating the landscape of HTTP methods is less about memorizing rules and more about understanding intent. The decision matrix is straightforward: use POST when you are adding something new to a collection and the server determines its identity. Use PUT when you are replacing the entire state of a resource at a known URI. Use PATCH when you are making surgical changes to specific fields.
Choosing the right method improves API predictability, reduces the complexity of client-side retry logic, and aligns your service with the inherent caching and proxy mechanisms of the HTTP protocol. I encourage you to audit your current API endpoints. Are you using POST for updates? Are you missing 201 codes on creation? Tools like Postman's API Linter or **Swagger UI's schema validation

