How do CRUD operations map to HTTP verbs, and what does it mean for a verb to be idempotent?
Show answer — try answering out loud first
Create = POST, Read = GET, Update = PUT or PATCH, Delete = DELETE. GET, PUT and DELETE are idempotent (repeating them leaves the system in the same state); POST is not (repeating it creates new resources). PUT replaces the whole resource, whereas PATCH updates only a part.
CRUD are the four basic operations on data: Create, Read, Update and Delete. In a REST API each one has its HTTP verb:
| CRUD | HTTP verb | Example | Idempotent |
|---|---|---|---|
| Create | POST | POST /usuarios |
No |
| Read | GET | GET /usuarios/5 |
Yes |
| Update | PUT / PATCH | PUT /usuarios/5 |
Yes |
| Delete | DELETE | DELETE /usuarios/5 |
Yes |
Idempotency
An operation is idempotent if running it once or many times leaves the system in the same final state.
- GET is idempotent: reading 100 times changes nothing.
- PUT is idempotent: if I send the same complete resource 5 times, the final result is the same as sending it once.
- DELETE is idempotent: deleting a resource once or several times leaves the system the same (the resource is no longer there).
- POST is NOT idempotent: each POST to a collection usually creates a new resource. Doing it 3 times creates 3 resources.
This matters a lot for retries: if a request fails due to the network, a client can safely retry a GET, PUT or DELETE, but repeating a POST can duplicate data.
PUT vs PATCH
- PUT replaces the whole resource. You must send the entire object; fields you don't send are considered absent.
- PATCH updates partially. You send only the fields you want to change; the rest stays as it was.
Example: a user { id: 5, nombre: "Ana", email: "ana@mail.com" }.
- With
PATCH /usuarios/5and body{ "email": "nuevo@mail.com" }, the name is still "Ana". - With
PUT /usuarios/5and that same body withoutnombre, you risk deleting the name, because PUT represents the complete resource.
const express = require('express');
const app = express();
app.use(express.json());
let usuarios = [{ id: 5, nombre: 'Ana', email: 'ana@mail.com' }];
// READ (GET) - idempotent
app.get('/usuarios/:id', (req, res) => {
const usuario = usuarios.find((u) => u.id === Number(req.params.id));
if (!usuario) return res.status(404).json({ error: 'No encontrado' });
res.json(usuario);
});
// CREATE (POST) - NOT idempotent: each call creates a new one
app.post('/usuarios', (req, res) => {
const nuevo = { id: Date.now(), ...req.body };
usuarios.push(nuevo);
res.status(201).json(nuevo);
});
// Full UPDATE (PUT) - replaces the whole resource
app.put('/usuarios/:id', (req, res) => {
const id = Number(req.params.id);
const indice = usuarios.findIndex((u) => u.id === id);
if (indice === -1) return res.status(404).json({ error: 'No encontrado' });
// We replace the complete resource (keeping the id)
usuarios[indice] = { id, nombre: req.body.nombre, email: req.body.email };
res.json(usuarios[indice]);
});
// Partial UPDATE (PATCH) - only changes the fields sent
app.patch('/usuarios/:id', (req, res) => {
const usuario = usuarios.find((u) => u.id === Number(req.params.id));
if (!usuario) return res.status(404).json({ error: 'No encontrado' });
// We merge the existing with the new; whatever isn't sent is kept
Object.assign(usuario, req.body);
res.json(usuario);
});
// DELETE - idempotent
app.delete('/usuarios/:id', (req, res) => {
const indice = usuarios.findIndex((u) => u.id === Number(req.params.id));
if (indice === -1) return res.status(404).json({ error: 'No encontrado' });
usuarios.splice(indice, 1);
res.status(204).send();
});
app.listen(3000);
# PATCH: I only change the email, the name is kept
curl -X PATCH http://localhost:3000/usuarios/5 \
-H "Content-Type: application/json" \
-d '{"email":"nuevo@mail.com"}'
# PUT: I send the complete resource
curl -X PUT http://localhost:3000/usuarios/5 \
-H "Content-Type: application/json" \
-d '{"nombre":"Ana","email":"ana@mail.com"}'
Using PUT as if it were PATCH by sending only one field. Since PUT represents the complete resource, absent fields can end up deleted or set to null. If you only want to change one piece of data, use PATCH.
Another mistake is using GET to modify data (for example GET /usuarios/5/borrar). GET must be safe and idempotent: browsers, caches and crawlers can repeat it freely, and that would delete data by accident.
"The typical mapping is Create with POST, Read with GET, Update with PUT or PATCH and Delete with DELETE. A key concept is idempotency: an operation is idempotent if repeating it leaves the system in the same state. GET, PUT and DELETE are, but POST is not, because each POST usually creates a new resource. That matters to me above all for retries: I can retry a GET or a DELETE without fear, but repeating a POST could duplicate data. About PUT and PATCH: PUT replaces the complete resource, so I must send the entire object; PATCH updates only the fields I send and leaves the rest intact. That's why, if I only want to change one field, I use PATCH."
A client makes DELETE /pedidos/42 and the response is lost due to a network problem, so it retries the same request. The second time the order no longer exists. Is this behavior correct with respect to idempotency? What code would you return?
See answer
Yes, it is correct and consistent with idempotency: the final state of the system is the same whether the DELETE runs once or is retried (order 42 does not exist).
In practice, the first call might return 204 No Content (successfully deleted) and the second 404 Not Found (it no longer exists). The code may differ, but the system state does not change with retries, which is what defines idempotency.