GET, POST, PUT, PATCH, DELETE
Each HTTP method makes a promise to the client. GET never changes data; PUT and DELETE can be safely retried; POST cannot. Keeping those promises makes your API predictable and retry-safe.
On this page · 7 min · Beginner
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 7 min
- Difficulty
- Beginner
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Use each method for its intended purpose
- Explain safe and idempotent methods
- Implement PUT (replace) and PATCH (partial update) correctly
- Return the right status code for each outcome
Explanation#
GET reads (200). POST creates or triggers processing (201 + Location). PUT replaces the whole resource (200 or 204). PATCH changes some fields (200). DELETE removes (204).
Safe methods do not change server state: GET, HEAD, OPTIONS. Crawlers, prefetchers and caches may call them at any time — never change data in a GET handler.
Idempotent methods have the same effect whether called once or ten times: GET, PUT, DELETE (and HEAD/OPTIONS). The responses may differ (the second DELETE returns 404), but the server state is the same. POST and PATCH are not guaranteed to be idempotent.
Idempotency matters for retries: if a network error hides the response, a client can safely repeat a PUT or DELETE. For POST (payments, orders), use an Idempotency-Key header to make retries safe.
PUT requires the full representation — missing fields mean “remove/reset”, so validate that all are present. PATCH sends only what changes.
Syntax#
Method Safe Idempotent Typical success
GET yes yes 200 OK
POST no no 201 Created + Location
PUT no yes 200 OK / 204 No Content
PATCH no no* 200 OK
DELETE no yes 204 No Content
* PATCH can be idempotent (e.g. "set price to 399") but is not required to be.
Code example: All five methods on the courses resource#
import express from 'express';
const FIELDS = ['title', 'level', 'price'];
const courses = new Map([[1, { id: 1, title: 'Node.js APIs', level: 'beginner', price: 499 }]]);
let nextId = 2;
const pick = (body = {}) => Object.fromEntries(FIELDS.filter(f => body[f] !== undefined).map(f => [f, body[f]]));
const app = express();
app.use(express.json());
app.get('/api/courses/:id', (req, res) => {
const course = courses.get(Number(req.params.id));
course ? res.json(course) : res.status(404).json({ error: 'Not found' });
});
app.post('/api/courses', (req, res) => { // NOT idempotent: every call creates a new course
const course = { id: nextId++, ...pick(req.body) };
courses.set(course.id, course);
res.status(201).location(`/api/courses/${course.id}`).json(course);
});
app.put('/api/courses/:id', (req, res) => { // replace the whole resource
const missing = FIELDS.filter(f => req.body?.[f] === undefined);
if (missing.length) return res.status(400).json({ error: `PUT needs every field; missing: ${missing.join(', ')}` });
const id = Number(req.params.id);
if (!courses.has(id)) return res.status(404).json({ error: 'Not found' });
courses.set(id, { id, ...pick(req.body) });
res.json(courses.get(id));
});
app.patch('/api/courses/:id', (req, res) => { // change only the fields sent
const course = courses.get(Number(req.params.id));
if (!course) return res.status(404).json({ error: 'Not found' });
Object.assign(course, pick(req.body));
res.json(course);
});
app.delete('/api/courses/:id', (req, res) => {
courses.delete(Number(req.params.id)) ? res.sendStatus(204) : res.status(404).json({ error: 'Not found' });
});
const server = app.listen(3101, async () => {
const call = async (method, path, body) => {
const r = await fetch('http://localhost:3101/api/courses' + path, {
method, headers: { 'Content-Type': 'application/json' }, body: body && JSON.stringify(body),
});
console.log(`${method.padEnd(6)} ${(path || '/').padEnd(3)} → ${r.status} ${r.status === 204 ? '(no body)' : await r.text()}`);
};
const react = { title: 'React', level: 'beginner', price: 599 };
const full = { title: 'Node.js REST APIs', level: 'intermediate', price: 699 };
await call('POST', '', react);
await call('POST', '', react); // twice → two courses
await call('PATCH', '/1', { price: 399 });
await call('PUT', '/1', { title: 'Node.js REST APIs' });
await call('PUT', '/1', full);
await call('PUT', '/1', full); // twice → same state
await call('DELETE', '/3');
await call('DELETE', '/3'); // twice → same state, different response
server.close();
});
POST / → 201 {"id":2,"title":"React","level":"beginner","price":599}
POST / → 201 {"id":3,"title":"React","level":"beginner","price":599}
PATCH /1 → 200 {"id":1,"title":"Node.js APIs","level":"beginner","price":399}
PUT /1 → 400 {"error":"PUT needs every field; missing: level, price"}
PUT /1 → 200 {"id":1,"title":"Node.js REST APIs","level":"intermediate","price":699}
PUT /1 → 200 {"id":1,"title":"Node.js REST APIs","level":"intermediate","price":699}
DELETE /3 → 204 (no body)
DELETE /3 → 404 {"error":"Not found"}This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- Two identical POSTs create two courses (ids 2 and 3): POST is not idempotent.
- PATCH changes only the price; the other fields stay.
- PUT without every field is rejected — a PUT body is the complete new representation.
- Repeating the full PUT leaves the course exactly the same: PUT is idempotent.
- The second DELETE returns 404, but the state (course 3 gone) is unchanged: DELETE is idempotent.
Real-world use case#
A mobile app on a flaky network retries requests automatically. Because LearnHub uses PUT for “save my profile” and DELETE for “unenrol”, retries are harmless; “place order” uses POST with an Idempotency-Key so a retry never charges twice.
Common mistakes#
GET /api/courses/7/publish can be triggered by link previews, prefetching or crawlers. Use POST or PATCH.
Clients sending only { price } with PUT would wipe other fields if you followed PUT semantics. Use PATCH for partial changes.
A 404 on a retried DELETE is normal. Clients can treat 404 on delete as “already gone”.
Best practices#
- GET never changes state
- PATCH for partial updates, PUT for full replacement
- 201 + Location for creates, 204 for deletes
- Idempotency keys for retry-sensitive POSTs
Remember this#
Safe: GET. Idempotent: GET, PUT, DELETE.
PUT = replace everything. PATCH = change some.
POST twice = two things.
Practice exercise#
Your task: Change PUT /api/courses/:id into an “upsert”: if the course does not exist, create it with that id and return 201; otherwise replace it and return 200.
Need a hint?
Show solution
import express from 'express';
const courses = new Map();
const app = express();
app.use(express.json());
app.put('/api/courses/:id', (req, res) => {
const { title, level, price } = req.body ?? {};
if (title === undefined || level === undefined || price === undefined) return res.status(400).json({ error: 'title, level and price are required' });
const id = Number(req.params.id);
const existed = courses.has(id);
courses.set(id, { id, title, level, price });
res.status(existed ? 200 : 201).json(courses.get(id));
});
const server = app.listen(3151, async () => {
const put = () => fetch('http://localhost:3151/api/courses/9', {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: 'Vue', level: 'beginner', price: 299 }),
});
console.log((await put()).status, (await put()).status); // 201 200
server.close();
});
Mini quiz#
Which method is NOT idempotent?
Want more? Practise all Node.js REST API MCQs · Node.js REST API interview questions
My notes#
Saved in this browser.