HTTP Methods: GET, POST, PUT, PATCH, DELETE Explained
HTTP methods define the action you want to perform on a resource. They're the verbs of the web, and using them correctly isn't just about following conventions—it directly impacts your application's...
Key Insights
- GET and DELETE are idempotent (multiple identical requests produce the same result), while POST is not—understanding this distinction prevents data inconsistencies in your APIs
- PATCH sends only changed fields while PUT replaces the entire resource, making PATCH more efficient for large objects but PUT simpler to implement correctly
- Choosing the wrong HTTP method breaks REST conventions and causes real problems: using GET for state changes bypasses CSRF protection, and using POST for everything makes caching impossible
Understanding HTTP Methods in Modern Web Development
HTTP methods define the action you want to perform on a resource. They’re the verbs of the web, and using them correctly isn’t just about following conventions—it directly impacts your application’s performance, security, and maintainability.
When you build a RESTful API, HTTP methods map to CRUD operations: GET for reading, POST for creating, PUT and PATCH for updating, and DELETE for removing. But the differences go deeper than simple CRUD mappings. Two critical concepts separate these methods: safety and idempotency.
A safe method doesn’t modify resources. Only GET qualifies as safe. An idempotent method produces the same result when called multiple times with identical parameters. GET, PUT, PATCH, and DELETE are idempotent; POST is not. These properties matter when requests fail and clients retry, when users refresh pages, or when network issues cause duplicate requests.
GET - Retrieving Data
GET requests fetch resources without causing side effects. They’re safe and idempotent, meaning you can call them repeatedly without changing server state. This makes GET requests cacheable by browsers and CDNs, dramatically improving performance.
Never use GET for operations that modify data. I’ve seen developers create endpoints like /api/users/delete?id=123 using GET requests. This breaks the web. Browser prefetching, link crawlers, and security scanners will trigger these URLs, accidentally deleting data.
Query parameters belong in the URL for GET requests:
// Fetch API GET request
async function getUser(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
// GET with query parameters using axios
import axios from 'axios';
async function searchUsers(filters) {
const response = await axios.get('https://api.example.com/users', {
params: {
role: filters.role,
active: filters.active,
page: filters.page,
limit: 20
}
});
return response.data;
}
The server should return 200 OK for successful requests, 404 Not Found when resources don’t exist, and 304 Not Modified for cached responses.
POST - Creating New Resources
POST creates new resources. It’s the only non-idempotent method we’re discussing. Sending the same POST request twice creates two resources (or should—more on this later).
POST requests carry data in the request body, not the URL. Always set the Content-Type header to tell the server how to parse the body. For JSON APIs, use application/json.
async function createUser(userData) {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getAuthToken()}`
},
body: JSON.stringify({
email: userData.email,
name: userData.name,
role: userData.role
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return await response.json();
}
The server should return 201 Created for successful creation, typically with a Location header pointing to the new resource. Return the created resource in the response body to avoid forcing clients to make a follow-up GET request.
POST’s non-idempotent nature creates challenges. If a request times out, the client doesn’t know if it succeeded. Did the server create the resource before the connection dropped? Implement idempotency keys for critical operations—clients send a unique key with each request, and servers track these keys to prevent duplicate processing.
PUT - Full Resource Updates
PUT replaces an entire resource. Send the complete resource representation, and the server overwrites what exists. PUT is idempotent—sending the same PUT request ten times produces the same result as sending it once.
async function updateUserProfile(userId, profileData) {
const response = await fetch(`https://api.example.com/users/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getAuthToken()}`
},
body: JSON.stringify({
email: profileData.email,
name: profileData.name,
role: profileData.role,
bio: profileData.bio,
preferences: profileData.preferences,
avatarUrl: profileData.avatarUrl
})
});
if (!response.ok) {
throw new Error(`Update failed: ${response.status}`);
}
return await response.json();
}
Some APIs use PUT for both creation and updates. If the client controls resource IDs, PUT to a non-existent resource creates it. This works but isn’t common. Most APIs reserve POST for creation and PUT for updates only.
Return 200 OK with the updated resource, or 204 No Content if you don’t return the resource body. Never return 201 Created for updates—that status code specifically indicates creation.
PATCH - Partial Resource Updates
PATCH modifies specific fields without touching the rest. This is more efficient than PUT when resources are large or when clients don’t have the complete resource representation.
The key difference: PUT requires sending the entire resource, while PATCH sends only the changes.
// PATCH - only send what changed
async function updateUserEmail(userId, newEmail) {
const response = await fetch(`https://api.example.com/users/${userId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getAuthToken()}`
},
body: JSON.stringify({
email: newEmail
})
});
return await response.json();
}
// Compare payload sizes: PATCH vs PUT
const patchPayload = {
email: 'newemail@example.com'
};
const putPayload = {
email: 'newemail@example.com',
name: 'John Doe',
role: 'admin',
bio: 'Long biography text...',
preferences: {
theme: 'dark',
notifications: true,
language: 'en'
},
avatarUrl: 'https://example.com/avatar.jpg'
};
PATCH is idempotent when you send absolute values ({"count": 5}), but not when you send operations ({"count": "+1"}). Stick with absolute values for predictable behavior.
PATCH implementations vary. Some servers expect JSON Patch format (RFC 6902), which uses an array of operations. Most modern APIs accept simple JSON objects with the fields to update. Document your API’s expectations clearly.
DELETE - Removing Resources
DELETE removes resources. It’s idempotent—deleting the same resource twice has the same effect as deleting it once. The second DELETE should return 404 Not Found (the resource is already gone) or 204 No Content (treating the absence of the resource as success).
async function deleteUser(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${getAuthToken()}`
}
});
// 204 No Content is standard for successful deletion
if (response.status === 204) {
return { success: true };
}
// Some APIs return 200 with a response body
if (response.status === 200) {
return await response.json();
}
throw new Error(`Delete failed: ${response.status}`);
}
Consider soft deletes for user-facing features. Instead of removing database records, set a deleted_at timestamp. This enables undo functionality and preserves data for auditing. Your DELETE endpoint sets the flag, while GET requests filter out soft-deleted resources.
// Server-side soft delete example (Express.js)
app.delete('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user || user.deleted_at) {
return res.status(404).json({ error: 'User not found' });
}
user.deleted_at = new Date();
await user.save();
res.status(204).send();
});
Best Practices and Common Pitfalls
Choose methods based on semantics, not convenience. Using POST for everything works technically, but you lose caching, idempotency, and semantic clarity. Using GET for deletions creates security vulnerabilities.
Return appropriate status codes. 200 OK means success with a response body. 201 Created indicates resource creation. 204 No Content signals success without a response body. 400 Bad Request means client error. 404 Not Found indicates the resource doesn’t exist. 409 Conflict suggests the operation violates constraints.
Here’s a complete Express.js example demonstrating all five methods for a user resource:
import express from 'express';
const app = express();
app.use(express.json());
const users = new Map();
let nextId = 1;
// GET - Retrieve all users or a specific user
app.get('/api/users', (req, res) => {
res.json(Array.from(users.values()));
});
app.get('/api/users/:id', (req, res) => {
const user = users.get(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
// POST - Create new user
app.post('/api/users', (req, res) => {
const user = {
id: String(nextId++),
...req.body,
createdAt: new Date()
};
users.set(user.id, user);
res.status(201)
.location(`/api/users/${user.id}`)
.json(user);
});
// PUT - Replace entire user
app.put('/api/users/:id', (req, res) => {
if (!users.has(req.params.id)) {
return res.status(404).json({ error: 'User not found' });
}
const user = {
id: req.params.id,
...req.body,
updatedAt: new Date()
};
users.set(req.params.id, user);
res.json(user);
});
// PATCH - Update specific fields
app.patch('/api/users/:id', (req, res) => {
const user = users.get(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
Object.assign(user, req.body, { updatedAt: new Date() });
res.json(user);
});
// DELETE - Remove user
app.delete('/api/users/:id', (req, res) => {
if (!users.has(req.params.id)) {
return res.status(404).json({ error: 'User not found' });
}
users.delete(req.params.id);
res.status(204).send();
});
app.listen(3000);
Protect state-changing methods (POST, PUT, PATCH, DELETE) with CSRF tokens in browser-based applications. Require authentication for all methods except public GET endpoints. Validate input thoroughly—never trust client data.
Understanding HTTP methods isn’t academic. It’s practical knowledge that affects your API’s usability, performance, and security. Use GET for reads, POST for creation, PUT for full updates, PATCH for partial updates, and DELETE for removal. Follow these conventions, and your APIs will be predictable, efficient, and correct.