| Series | Topic | Level | Read Time |
| Web & HTTP Foundations — Part 2 of 12 | HTTP Methods, Idempotency & Safety | Beginner to Intermediate | ~14 minutes |
| 📌 This is Part 2 of the Web & HTTP Foundations series. Part 1 covered the full HTTP Request Lifecycle — read that first if you haven’t already. |
Every API call you make — whether from a browser, a mobile app, or another service — uses an HTTP method.
Most developers know the basics: GET fetches data, POST creates something. But stop there and you’ll make mistakes that real-world systems punish hard — duplicate orders placed on retry, partial updates wiping out fields, caches returning stale data because you used the wrong method.
In this post we go deep. Not just what each method does — but WHY the rules exist, what idempotency and safety really mean, and how to design your API methods correctly so they behave predictably under network failures, retries, and caching.
This is the stuff that separates a developer who can write API endpoints from one who can design them well.
Quick Reference — All HTTP Methods
| Method | Purpose | Has Body? | Safe? | Idempotent? | Status Codes |
| GET | Read / fetch a resource | No | YES | YES | 200, 404 |
| POST | Create a resource / trigger action | Yes | NO | NO | 201, 400, 409 |
| PUT | Replace entire resource | Yes | NO | YES | 200, 201, 404 |
| PATCH | Partially update a resource | Yes | NO | NO* | 200, 404 |
| DELETE | Remove a resource | No | NO | YES | 204, 404 |
| HEAD | GET without response body | No | YES | YES | 200, 404 |
| OPTIONS | Ask what methods are allowed | No | YES | YES | 204 |
| 💡 The two columns that matter most in API design are Safe and Idempotent. These properties determine how browsers, CDNs, load balancers, and API clients treat your endpoints. Get them wrong and you get caching bugs, duplicate writes, and broken retries. |
Safe Methods vs Unsafe Methods
This is the first concept to understand because it affects caching, browser behaviour, and how users interact with your API.
What Does ‘Safe’ Mean?
| 🔑 A method is SAFE if calling it does not change anything on the server. It is purely read-only. The server state is identical before and after a safe request. |
Safe methods: GET, HEAD, OPTIONS
Unsafe methods: POST, PUT, PATCH, DELETE
Why Does Safety Matter in Practice?
| Who relies on safety | What they do with safe methods | What breaks if you get it wrong |
| Browsers | Freely prefetch GET links in the background for speed | Browser prefetches a ‘GET /delete-account’ — account gets deleted |
| CDNs & Caches | Cache GET responses automatically | POST response gets cached — users see stale creation responses |
| Web crawlers | Follow GET links to index your site | Googlebot crawls ‘GET /logout’ — logs everyone out |
| Load balancers | Can retry safe requests on failure without asking client | Retried GET modifies data — double side effects |
| API clients | Optimistically retry safe requests on timeout | Retried unsafe request causes duplicate order / payment |
| ❌ Never put data-modifying logic behind a GET endpoint. Never. Even if it’s ‘easier’. The HTTP spec and every tool that speaks HTTP will treat GET as safe — and your mutation will fire at the worst possible time. |
Idempotency — The Most Misunderstood HTTP Concept
If safety is about whether a method reads or writes, idempotency is about what happens when you call the same method multiple times.
The Formal Definition
| 🔑 A method is IDEMPOTENT if making the exact same request N times produces the same server state as making it once. The response might differ (e.g. second DELETE returns 404), but the server’s data is the same. |
A Real-World Example — Why Idempotency Matters
Imagine a user clicks ‘Place Order’ and the network is slow. The request reaches your server, the order is created, but the response gets lost. The client times out and retries.
| Scenario | Method Used | What Happens on Retry | Outcome |
| POST /orders (non-idempotent) | POST | Server creates a SECOND order | User charged twice. Support nightmare. |
| PUT /orders/123 (idempotent) | PUT | Server replaces order 123 with same data | Same order. No duplicate. Safe. |
| 🌍 This is not theoretical. Payment services, e-commerce platforms, and booking systems lose real money because of non-idempotent retries. Stripe’s API uses idempotency keys to handle this — they allow clients to safely retry POST requests. We’ll cover that pattern below. |
Idempotency vs Safety — They Are NOT the Same
People often confuse these. Here’s the clear distinction:
| Property | Means | Methods |
| Safe | Does not modify server state | GET, HEAD, OPTIONS |
| Idempotent | Calling N times = same result as calling once | GET, PUT, DELETE, HEAD, OPTIONS |
| Safe + Idempotent | Read-only AND repeatable | GET, HEAD, OPTIONS |
| Idempotent only | Modifies state BUT repeating it is harmless | PUT, DELETE |
| Neither | Modifies state AND repeating it causes different/additional side effects | POST, PATCH* |
| 💡 All safe methods are idempotent. But not all idempotent methods are safe. DELETE is idempotent (deleting the same resource twice ends up with it deleted) but definitely not safe (it modifies data). |
HTTP Methods — Deep Dive
| HTTP GET |
GET is for fetching a resource. It is safe, idempotent, and should NEVER modify data.
| Property | Value |
| Safe? | YES — read only, no server state changes |
| Idempotent? | YES — same request, same response every time |
| Has Body? | NO — GET requests must not have a body (some servers ignore it, others reject it) |
| Cacheable? | YES — browsers, CDNs, and proxies cache GET responses by default |
| Status Codes | 200 OK (found), 404 Not Found (doesn’t exist), 304 Not Modified (use cached version) |
HTTP Request
GET /api/users/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Accept: application/json
Node JS
// GET /api/users/:id
app.get('/api/users/:id', authenticate, async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
return res.status(200).json({ data: user });
// never modify data here — GET is read-only
});
Common GET Mistakes
| ❌ GET /api/users/42/delete — Never use GET for deletion. Browsers prefetch links, web crawlers follow them. Your data will disappear. |
| ❌ GET /api/users?filter={json object as string} — Don’t put complex JSON in query params. Use POST with a body for complex search queries, or design proper filter params like ?status=active&role=admin. |
| ✅ GET /api/users?status=active&role=admin&page=1&limit=20 — Clean, cacheable, bookmarkable query parameters for filtering. |
| HTTP POST |
POST creates a new resource or triggers an action. It is neither safe nor idempotent — each call can produce a new side effect.
| Property | Value |
| Safe? | NO — creates new data or triggers side effects |
| Idempotent? | NO — calling twice creates two resources (or triggers action twice) |
| Has Body? | YES — the resource data goes in the request body |
| Cacheable? | NO — not cacheable by default |
| Status Codes | 201 Created (success), 400 Bad Request (invalid data), 409 Conflict (already exists) |
HTTP REQUEST
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
{
"name": "Rahul Sharma",
"email": "rahul@example.com",
"role": "developer"
}
Node JS
// POST /api/users
app.post('/api/users', authenticate, async (req, res) => {
const { name, email, role } = req.body;
// validate required fields
if (!name || !email) {
return res.status(400).json({
error: 'name and email are required'
});
}
// check for duplicate
const existing = await User.findOne({ email });
if (existing) {
return res.status(409).json({
error: 'A user with this email already exists'
});
}
// create user
const user = await User.create({ name, email, role });
// 201 Created — not 200
return res.status(201).json({
message: 'User created successfully',
data: { id: user._id, name, email, role }
});
});
Handling POST Idempotency — Idempotency Keys
Since POST is not idempotent by nature, how do payment systems and booking APIs handle retries safely? They use Idempotency Keys.
The client generates a unique key for each logical operation and sends it as a header. The server stores the result against that key. If the same key comes again (retry), the server returns the stored result without processing again.
HTTP REQUEST
POST /api/payments HTTP/1.1
Host: api.example.com
Content-Type: application/json
Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890
{
"amount": 4999,
"currency": "INR",
"userId": "user_42"
}
Node JS
// POST /api/payments with idempotency key support
app.post('/api/payments', authenticate, async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
if (!idempotencyKey) {
return res.status(400).json({
error: 'Idempotency-Key header is required'
});
}
// check if we already processed this key
const cached = await redis.get(`idem:${idempotencyKey}`);
if (cached) {
// return the exact same response as the first time
return res.status(200).json(JSON.parse(cached));
}
// process payment (first time only)
const payment = await PaymentService.charge(req.body);
const response = { success: true, paymentId: payment.id };
// cache result for 24 hours against the idempotency key
await redis.setex(`idem:${idempotencyKey}`, 86400, JSON.stringify(response));
return res.status(201).json(response);
});
| 🚀 Idempotency keys are used by Stripe, Razorpay, Braintree, and every serious payment API. If you’re building any API that handles money or critical operations, implement this pattern. It is not optional. |
| HTTP PUT |
PUT replaces an entire resource. Whatever you send in the body becomes the new complete state of the resource. Fields not included in the body are removed or reset.
| Property | Value |
| Safe? | NO — modifies server data |
| Idempotent? | YES — sending the same PUT twice results in the same resource state |
| Has Body? | YES — the complete new representation of the resource |
| Cacheable? | NO — modifies data |
| Status Codes | 200 OK (updated), 201 Created (if resource didn’t exist), 404 Not Found |
HTTP REQUEST
PUT /api/users/42 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
{
"name": "Rahul Kumar",
"email": "rahul.kumar@example.com",
"role": "senior-developer"
}
| ⚠️ PUT requires the COMPLETE resource in the body. If the user has a ‘createdAt’ field and you don’t include it in the PUT body, it gets overwritten or lost. Always send all fields when using PUT. |
Node JS
// PUT /api/users/:id — REPLACE entire resource
app.put('/api/users/:id', authenticate, async (req, res) => {
const { name, email, role } = req.body;
// validate all required fields are present
if (!name || !email || !role) {
return res.status(400).json({
error: 'PUT requires all fields: name, email, role'
});
}
// findByIdAndUpdate with overwrite semantics
const user = await User.findByIdAndUpdate(
req.params.id,
{ name, email, role }, // replace these fields completely
{
new: true, // return updated document
overwrite: true, // true overwrite — not merge
runValidators: true
}
);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
return res.status(200).json({ data: user });
});
| HTTP PATCH |
PATCH partially updates a resource. Only the fields you send are changed — everything else stays exactly as it is.
| Property | Value |
| Safe? | NO — modifies server data |
| Idempotent? | NOT GUARANTEED — depends on implementation (e.g. PATCH with increment is not idempotent) |
| Has Body? | YES — only the fields you want to change |
| Cacheable? | NO — modifies data |
| Status Codes | 200 OK (updated with body), 204 No Content (updated, no body), 404 Not Found |
HTTP REQUEST
PATCH /api/users/42 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
{
"name": "Rahul Kumar"
}
In this PATCH request, ONLY the name is changed. Email, role, createdAt, and every other field stays untouched. This is the key difference from PUT.
Node JS
// PATCH /api/users/:id — partial update
app.patch('/api/users/:id', authenticate, async (req, res) => {
// only update the fields that were actually sent
const updates = req.body;
// prevent overwriting protected fields
const forbidden = ['_id', 'createdAt', 'passwordHash'];
forbidden.forEach(field => delete updates[field]);
if (Object.keys(updates).length === 0) {
return res.status(400).json({
error: 'No valid fields to update'
});
}
const user = await User.findByIdAndUpdate(
req.params.id,
{ $set: updates }, // $set merges — does NOT overwrite entire doc
{ new: true, runValidators: true }
);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
return res.status(200).json({ data: user });
});
PUT vs PATCH — Side by Side
This is one of the most asked interview questions. Here’s the definitive comparison:
| Scenario | PUT Result | PATCH Result |
| Send only {name: ‘Rahul’} | name=Rahul, email=null, role=null (others wiped) | name=Rahul, email unchanged, role unchanged |
| Send all fields | All fields replaced — same outcome as PATCH | Only sent fields updated — same outcome as PUT |
| Retry same request | Idempotent — same state every time | Usually idempotent, but not guaranteed |
| Best for | Replacing entire config, settings objects | Updating name, status, single field changes |
| 💡 In most real-world APIs, PATCH is the right choice for update operations. It is safer (no accidental data loss), more bandwidth-efficient (smaller payload), and more intuitive for clients. Use PUT only when you truly want full-resource replacement semantics. |
| HTTP DELETE |
DELETE removes a resource. It is idempotent — deleting something that is already deleted should not throw a server error (it is already gone).
| Property | Value |
| Safe? | NO — permanently removes data |
| Idempotent? | YES — first call deletes, subsequent calls find it already gone |
| Has Body? | NO — the resource is identified by the URL |
| Cacheable? | NO |
| Status Codes | 204 No Content (deleted, no body returned), 404 Not Found (already gone) |
HTTP REQUEST
DELETE /api/users/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Node JS
// DELETE /api/users/:id
app.delete('/api/users/:id', authenticate, authorize('admin'), async (req, res) => {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
// idempotent: resource already gone — still a valid outcome
// you can return 404 or 204 — both are acceptable
// 404 is more informative; 204 is more strictly idempotent
return res.status(404).json({ error: 'User not found' });
}
// 204 No Content — deleted successfully, nothing to return
return res.status(204).send();
});
Soft Delete vs Hard Delete
In production systems, you often don’t actually delete data — you mark it as deleted. This is called a soft delete.
Node JS
// Soft delete pattern — mark as deleted, don't actually remove
app.delete('/api/users/:id', authenticate, async (req, res) => {
const user = await User.findByIdAndUpdate(
req.params.id,
{
deletedAt: new Date(), // mark when it was deleted
isDeleted: true // flag for filtering
},
{ new: true }
);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
return res.status(204).send();
});
// then in all other queries, filter out soft-deleted records
const activeUsers = await User.find({ isDeleted: { $ne: true } });
| 🌍 Soft delete is standard in production because: (1) data recovery is possible, (2) audit trails are preserved, (3) foreign key relationships don’t break, (4) GDPR compliance — you can truly delete when legally required. Almost every production system uses soft delete. |
| HTTP HEAD |
HEAD is exactly like GET — same request, same headers in response — but the server does NOT send a response body. Just the headers.
| Property | Value |
| Safe? | YES — read only |
| Idempotent? | YES |
| Has Body? | NO request body, NO response body |
| Use cases | Check if resource exists, get content-length before downloading, check if cache is fresh |
Curl
# Check if a large file exists and its size before downloading
curl -I https://example.com/large-video.mp4
# Response:
HTTP/1.1 200 OK
Content-Length: 1073741824 # 1GB file
Content-Type: video/mp4
Last-Modified: Mon, 30 Mar 2026 12:00:00 GMT
| 💡 HEAD is useful for lightweight checks: does this resource exist? (no need to download the whole body), what’s the file size before download? Is the cache still valid? It’s significantly more efficient than GET when you don’t need the data. |
| HTTP OPTIONS |
OPTIONS asks the server what HTTP methods are allowed on a specific endpoint. You’ll mostly encounter it as the CORS preflight request — the browser’s way of asking ‘is this cross-origin request allowed?’
HTTP REQUEST
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://myfrontend.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
HTTP RESPONSE
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myfrontend.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
| 📌 CORS preflight and all CORS headers are covered in full depth in Part 5 of this series. For now, just know that OPTIONS is the method browsers use to check cross-origin permissions before sending actual requests. |
Putting It All Together — Correct Method Design for a REST API
Here’s how a well-designed API uses HTTP methods for a Users resource:
| Endpoint | Method | Action | Response |
| GET /api/users | GET | List all users (with pagination) | 200 OK + array |
| GET /api/users/42 | GET | Get user with id 42 | 200 OK or 404 |
| POST /api/users | POST | Create a new user | 201 Created or 409 |
| PUT /api/users/42 | PUT | Replace user 42 completely | 200 OK or 404 |
| PATCH /api/users/42 | PATCH | Update specific fields of user 42 | 200 OK or 404 |
| DELETE /api/users/42 | DELETE | Delete user 42 | 204 No Content or 404 |
| HEAD /api/users/42 | HEAD | Check if user 42 exists | 200 or 404 (no body) |
| OPTIONS /api/users | OPTIONS | CORS preflight — what methods allowed | 204 with Allow header |
Common Method Design Mistakes in Production APIs
| ❌ GET /api/deleteUser?id=42 — GET must never delete. Use DELETE /api/users/42 |
| ❌ POST /api/users/42/update — Don’t use POST for updates. Use PUT or PATCH /api/users/42 |
| ❌ DELETE /api/users (delete all!) — Without an ID in the URL, this deletes everything. Catastrophic bug waiting to happen. |
| ❌ POST /api/users/42 — POST on a specific resource ID is confusing. POST means create (no ID yet). Use PUT or PATCH for existing resources. |
| ✅ PUT /api/users/42 with full body — Replace entire user |
| ✅ PATCH /api/users/42 with {status: ‘inactive’} — Update only status |
| ✅ DELETE /api/users/42 — Delete specific user, return 204 |
Interview Questions on HTTP Methods
| 🎯 Q: What is idempotency and which HTTP methods are idempotent? |
A method is idempotent if making it N times produces the same server state as making it once. Idempotent methods: GET, PUT, DELETE, HEAD, OPTIONS. Non-idempotent: POST (creates new each time), PATCH (can be non-idempotent depending on implementation, e.g. PATCH with increment operations).
| 🎯 Q: What is the difference between PUT and PATCH? |
PUT replaces the entire resource — you must send all fields, missing fields get overwritten or lost. PATCH partially updates — only the sent fields change, everything else remains. PUT is idempotent, PATCH is not guaranteed to be. In practice, use PATCH for most update operations to avoid accidental data loss.
| 🎯 Q: Can GET have a request body? |
Technically the HTTP spec doesn’t forbid it, but practically no. Most servers, proxies, and CDNs ignore or reject GET bodies. If you need to send complex data to filter/search, use POST with a body for search operations, or properly designed query parameters for GET. Never rely on GET request bodies.
| 🎯 Q: How would you handle duplicate POST requests (retries)? |
Use idempotency keys. The client generates a unique UUID per operation and sends it as an Idempotency-Key header. The server stores the result against that key. On retry, the server recognises the key and returns the cached result without processing again. This is the standard pattern used by Stripe, Razorpay, and all serious payment APIs.
| 🎯 Q: What status code should DELETE return? |
204 No Content when the resource was successfully deleted — no body needed. 404 Not Found if the resource didn’t exist. Some teams return 200 OK with a success message in the body — this is also acceptable but less RESTful. Never return 200 with an empty body for DELETE — use 204 for that.
Quick Reference — Everything in One Table
| Method | Safe | Idempotent | Body | Use for | Avoid |
| GET | YES | YES | No | Fetching data, listing resources | Any data modification |
| POST | NO | NO | Yes | Creating resources, triggering actions | Updates (use PUT/PATCH) |
| PUT | NO | YES | Yes | Full resource replacement | Partial updates (use PATCH) |
| PATCH | NO | NO* | Yes | Partial field updates | Full replacement (use PUT) |
| DELETE | NO | YES | No | Removing a resource | Bulk delete without ID |
| HEAD | YES | YES | No | Existence checks, getting metadata | When you need the body |
| OPTIONS | YES | YES | No | CORS preflight, capability discovery | Regular data operations |
Wrapping Up
HTTP methods are not just labels you slap on a route. They carry specific semantics — about safety, idempotency, caching, and client behaviour — that the entire web infrastructure relies on.
The three rules to always remember:
- GET must never modify data — ever
- Use PATCH for partial updates, PUT only for full replacement
- POST is not idempotent — handle retries with idempotency keys for critical operations
Get these right and your API becomes predictable, debuggable, and safe to use from any client. Get them wrong and you’ll spend hours debugging mysterious duplicate records, wiped fields, and cache inconsistencies.
| 📬 This is Part 2 of the Web & HTTP Foundations series. Next: Part 3 — HTTP Status Codes (correct production usage, error handling patterns). Subscribe so you don’t miss it. |
What is idempotency in HTTP methods?
An HTTP method is idempotent if making the same request multiple times produces the same result as making it once. GET, PUT, DELETE, HEAD, and OPTIONS are idempotent. POST and PATCH are not. Idempotency matters because networks are unreliable — if a request times out and the client retries, an idempotent method is safe to retry without fear of duplicate side effects.
What is the difference between PUT and PATCH in HTTP?
PUT replaces the entire resource with the new data you provide. If you send a PUT request with only a name field, all other fields (email, age, etc.) get replaced or removed. PATCH partially updates the resource — only the fields you send are changed, everything else stays the same. PATCH is better for most update operations where you only want to change specific fields.
What is the difference between safe and unsafe HTTP methods?
A safe HTTP method is one that does not modify any data on the server — it is read-only. GET, HEAD, and OPTIONS are safe. POST, PUT, PATCH, and DELETE are unsafe because they change server state. Safe methods can be cached, prefetched, and called freely by browsers. Unsafe methods should not be called without explicit user intent.
Got a question about a specific method or pattern? Drop it in the comments — I reply to every one.