HTTP Methods Explained — GET, POST, PUT, PATCH, DELETE and Everything In Between

SeriesTopicLevelRead Time
Web & HTTP Foundations — Part 2 of 12HTTP Methods, Idempotency & SafetyBeginner 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

MethodPurposeHas Body?Safe?Idempotent?Status Codes
GETRead / fetch a resourceNoYESYES200, 404
POSTCreate a resource / trigger actionYesNONO201, 400, 409
PUTReplace entire resourceYesNOYES200, 201, 404
PATCHPartially update a resourceYesNONO*200, 404
DELETERemove a resourceNoNOYES204, 404
HEADGET without response bodyNoYESYES200, 404
OPTIONSAsk what methods are allowedNoYESYES204
💡  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 safetyWhat they do with safe methodsWhat breaks if you get it wrong
BrowsersFreely prefetch GET links in the background for speedBrowser prefetches a ‘GET /delete-account’ — account gets deleted
CDNs & CachesCache GET responses automaticallyPOST response gets cached — users see stale creation responses
Web crawlersFollow GET links to index your siteGooglebot crawls ‘GET /logout’ — logs everyone out
Load balancersCan retry safe requests on failure without asking clientRetried GET modifies data — double side effects
API clientsOptimistically retry safe requests on timeoutRetried 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.

ScenarioMethod UsedWhat Happens on RetryOutcome
POST /orders (non-idempotent)POSTServer creates a SECOND orderUser charged twice. Support nightmare.
PUT /orders/123 (idempotent)PUTServer replaces order 123 with same dataSame 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:

PropertyMeansMethods
SafeDoes not modify server stateGET, HEAD, OPTIONS
IdempotentCalling N times = same result as calling onceGET, PUT, DELETE, HEAD, OPTIONS
Safe + IdempotentRead-only AND repeatableGET, HEAD, OPTIONS
Idempotent onlyModifies state BUT repeating it is harmlessPUT, DELETE
NeitherModifies state AND repeating it causes different/additional side effectsPOST, 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.

PropertyValue
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 Codes200 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.

PropertyValue
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 Codes201 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.

PropertyValue
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 Codes200 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.

PropertyValue
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 Codes200 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:

ScenarioPUT ResultPATCH Result
Send only {name: ‘Rahul’}name=Rahul, email=null, role=null (others wiped)name=Rahul, email unchanged, role unchanged
Send all fieldsAll fields replaced — same outcome as PATCHOnly sent fields updated — same outcome as PUT
Retry same requestIdempotent — same state every timeUsually idempotent, but not guaranteed
Best forReplacing entire config, settings objectsUpdating 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).

PropertyValue
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 Codes204 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.

PropertyValue
Safe?YES — read only
Idempotent?YES
Has Body?NO request body, NO response body
Use casesCheck 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:

EndpointMethodActionResponse
GET    /api/usersGETList all users (with pagination)200 OK + array
GET    /api/users/42GETGet user with id 42200 OK or 404
POST   /api/usersPOSTCreate a new user201 Created or 409
PUT    /api/users/42PUTReplace user 42 completely200 OK or 404
PATCH  /api/users/42PATCHUpdate specific fields of user 42200 OK or 404
DELETE /api/users/42DELETEDelete user 42204 No Content or 404
HEAD   /api/users/42HEADCheck if user 42 exists200 or 404 (no body)
OPTIONS /api/usersOPTIONSCORS preflight — what methods allowed204 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

MethodSafeIdempotentBodyUse forAvoid
GETYESYESNoFetching data, listing resourcesAny data modification
POSTNONOYesCreating resources, triggering actionsUpdates (use PUT/PATCH)
PUTNOYESYesFull resource replacementPartial updates (use PATCH)
PATCHNONO*YesPartial field updatesFull replacement (use PUT)
DELETENOYESNoRemoving a resourceBulk delete without ID
HEADYESYESNoExistence checks, getting metadataWhen you need the body
OPTIONSYESYESNoCORS preflight, capability discoveryRegular 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.

Leave a Comment