Node.js REST API · Foundations

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.

Beginner 7 min Lesson 3 of 25
Before you start:Express Setup
0/25 in Node.js REST API
On this page · 7 min · Beginner

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#

Text Illustrative
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#

JavaScriptmethods.js Illustrative
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();
});
Expected output
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#

  1. Two identical POSTs create two courses (ids 2 and 3): POST is not idempotent.
  2. PATCH changes only the price; the other fields stay.
  3. PUT without every field is rejected — a PUT body is the complete new representation.
  4. Repeating the full PUT leaves the course exactly the same: PUT is idempotent.
  5. 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#

Changing data in GET

GET /api/courses/7/publish can be triggered by link previews, prefetching or crawlers. Use POST or PATCH.

Using PUT for partial updates

Clients sending only { price } with PUT would wipe other fields if you followed PUT semantics. Use PATCH for partial changes.

Treating a repeated DELETE as an error to alert on

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?
Check courses.has(id) before setting, and pick the status from it.
Show solution
JavaScript Illustrative
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#

GET, POST, PUT, PATCH, DELETE quizMultiple choice Question 1 of 3

Which method is NOT idempotent?

Score so far: 0/0

Want more? Practise all Node.js REST API MCQs · Node.js REST API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.