Episode 3 — NodeJS MongoDB Backend Architecture / 3.4 — Express JS

3.4.e — HTTP Methods and Request Body

In one sentence: Express maps each HTTP verb to a dedicated method — app.get(), app.post(), app.put(), app.patch(), app.delete() — and combined with body parsing middleware (express.json()), these methods give you the complete toolkit for building CRUD APIs that create, read, update, and delete resources.

Navigation: <- 3.4.d — Query Parameters and URL Parameters | 3.4.f — Serving Static Files ->


1. HTTP methods in Express

Express provides a method for every standard HTTP verb:

app.get('/resource', handler);      // Read
app.post('/resource', handler);     // Create
app.put('/resource/:id', handler);  // Replace
app.patch('/resource/:id', handler); // Partial update
app.delete('/resource/:id', handler); // Remove

Each method registers a route that only responds to that specific HTTP verb:

// Only GET requests to /users trigger this handler
app.get('/users', (req, res) => {
  res.json({ method: 'GET', message: 'List all users' });
});

// Only POST requests to /users trigger this handler
app.post('/users', (req, res) => {
  res.status(201).json({ method: 'POST', message: 'Create a user' });
});

// A POST to /users will NOT trigger the GET handler, and vice versa

2. GET — retrieve data

GET requests read data. They should never change server state.

// List all users
app.get('/api/users', (req, res) => {
  res.json({ data: users });
});

// Get one user by ID
app.get('/api/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json({ data: user });
});

GET characteristics

PropertyValue
Has bodyNo (browsers ignore body in GET)
IdempotentYes — same request always returns same result (given no external changes)
CacheableYes — browsers and CDNs can cache GET responses
SafeYes — does not modify resources
Use forListing, searching, reading details

3. POST — create new resource

POST sends data to the server to create a new resource.

app.use(express.json()); // Required to parse JSON bodies

app.post('/api/users', (req, res) => {
  // req.body contains the parsed JSON from the request
  const { name, email } = req.body;

  // Validate
  if (!name || !email) {
    return res.status(400).json({
      error: 'Name and email are required',
    });
  }

  // Create
  const newUser = {
    id: users.length + 1,
    name,
    email,
    createdAt: new Date().toISOString(),
  };
  users.push(newUser);

  // Respond with 201 Created
  res.status(201).json({ data: newUser });
});

POST characteristics

PropertyValue
Has bodyYes — the data to create
IdempotentNo — calling twice creates two resources
CacheableNo (by default)
SafeNo — modifies server state
Use forCreating resources, submitting forms, uploading files

4. PUT — replace entire resource

PUT replaces a resource completely with the new data.

app.put('/api/users/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const index = users.findIndex(u => u.id === id);

  if (index === -1) {
    return res.status(404).json({ error: 'User not found' });
  }

  const { name, email, role } = req.body;

  // Validate — all fields required for PUT (full replacement)
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' });
  }

  // Replace entirely (only id is preserved)
  users[index] = {
    id,
    name,
    email,
    role: role || 'user',
    updatedAt: new Date().toISOString(),
  };

  res.json({ data: users[index] });
});

PUT characteristics

PropertyValue
Has bodyYes — the complete new resource
IdempotentYes — sending same PUT twice produces same result
Use forFull replacement of a resource
GotchaMissing fields are set to default/null (full replacement!)

5. PATCH — partial update

PATCH updates only the fields you send, leaving others unchanged.

app.patch('/api/users/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const user = users.find(u => u.id === id);

  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }

  // Only update fields that were sent
  const allowedFields = ['name', 'email', 'role'];
  const updates = {};

  for (const field of allowedFields) {
    if (req.body[field] !== undefined) {
      updates[field] = req.body[field];
    }
  }

  // Merge updates into existing user
  Object.assign(user, updates, { updatedAt: new Date().toISOString() });

  res.json({ data: user });
});

PUT vs PATCH — the critical difference

// Original user:
// { id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' }

// PUT /api/users/1 with body: { name: 'Alice Updated', email: 'new@example.com' }
// Result: { id: 1, name: 'Alice Updated', email: 'new@example.com', role: 'user' }
// ^ role RESET to default because it was missing from the PUT body!

// PATCH /api/users/1 with body: { name: 'Alice Updated' }
// Result: { id: 1, name: 'Alice Updated', email: 'alice@example.com', role: 'admin' }
// ^ email and role UNCHANGED — only name was updated
AspectPUTPATCH
SendsComplete resourceOnly changed fields
Missing fieldsReset to defaultsLeft unchanged
IdempotentYesUsually yes (depends on implementation)
Common in practiceLess common for APIsMore common for APIs

6. DELETE — remove resource

DELETE removes a resource from the server.

app.delete('/api/users/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const index = users.findIndex(u => u.id === id);

  if (index === -1) {
    return res.status(404).json({ error: 'User not found' });
  }

  const deleted = users.splice(index, 1)[0];

  // Option 1: 200 with the deleted resource
  res.json({ message: 'User deleted', data: deleted });

  // Option 2: 204 No Content (no body)
  // res.status(204).end();
});

DELETE characteristics

PropertyValue
Has bodyUsually no (some APIs accept a body)
IdempotentYes — deleting the same resource twice has the same effect
Use forRemoving resources
Common responses200 with data, 204 with no body, or 404 if not found

7. app.all() and app.use() — matching all methods

app.all() — matches any HTTP method for a specific path

// Runs for GET, POST, PUT, DELETE, etc. to /api/secret
app.all('/api/secret', (req, res, next) => {
  console.log(`${req.method} /api/secret accessed`);

  // Check auth for all methods
  if (!req.headers.authorization) {
    return res.status(401).json({ error: 'Authentication required' });
  }

  next(); // Pass to the specific method handler
});

app.get('/api/secret', (req, res) => res.json({ secret: 'data' }));
app.post('/api/secret', (req, res) => res.json({ created: true }));

app.use() — middleware for all methods and sub-paths

// Runs for ALL methods and ALL paths starting with /api/
app.use('/api', (req, res, next) => {
  console.log(`API request: ${req.method} ${req.originalUrl}`);
  next();
});

// Without a path — runs for EVERY request
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url} at ${new Date().toISOString()}`);
  next();
});

app.all() vs app.use()

Featureapp.all(path, handler)app.use(path, handler)
MethodsAll HTTP methodsAll HTTP methods
Path matchingExact matchPrefix match
Exampleapp.all('/api') matches only /apiapp.use('/api') matches /api, /api/users, /api/posts/1, etc.

8. Request body parsing

The problem

HTTP request bodies arrive as raw bytes in a stream. Express does not parse them by default:

// WITHOUT body parsing middleware
app.post('/api/users', (req, res) => {
  console.log(req.body); // undefined!
  res.json({ body: req.body });
});

express.json() — parse JSON bodies

// Add BEFORE your routes
app.use(express.json());

app.post('/api/users', (req, res) => {
  console.log(req.body); // { name: 'Alice', email: 'alice@example.com' }
  res.json({ received: req.body });
});

express.json() parses requests with Content-Type: application/json and populates req.body.

Options for express.json()

app.use(express.json({
  limit: '10mb',        // Max body size (default: '100kb')
  strict: true,         // Only accept arrays and objects (default: true)
  type: 'application/json', // Content-Type to parse (default)
}));

express.urlencoded() — parse form data

HTML forms send data as application/x-www-form-urlencoded:

<form method="POST" action="/register">
  <input name="username" value="alice" />
  <input name="password" value="secret" />
  <button type="submit">Register</button>
</form>
// Parse URL-encoded form bodies
app.use(express.urlencoded({ extended: true }));

app.post('/register', (req, res) => {
  console.log(req.body); // { username: 'alice', password: 'secret' }
  res.json({ registered: req.body.username });
});
Optionextended: falseextended: true
Parserquerystring (built-in)qs library
Nested objectsNot supportedSupported (user[name]=Alice)
ArraysBasic supportRich support (colors[]=red&colors[]=blue)
RecommendationSimple formsMost cases (use extended: true)

Standard setup — use both

const express = require('express');
const app = express();

// Parse both JSON and form data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Now req.body works for both content types

9. req.body — accessing parsed body

After parsing middleware is set up, req.body contains the parsed request body:

app.post('/api/products', (req, res) => {
  // Client sends: { "name": "Laptop", "price": 999, "tags": ["electronics"] }
  const { name, price, tags } = req.body;

  console.log(name);   // 'Laptop'
  console.log(price);  // 999 (already a number from JSON)
  console.log(tags);   // ['electronics']

  // Validate
  if (!name || typeof price !== 'number') {
    return res.status(400).json({ error: 'Invalid product data' });
  }

  // Create product...
  res.status(201).json({ data: { id: 1, name, price, tags } });
});

Important: Unlike req.params and req.query (always strings), req.body preserves JSON types — numbers stay numbers, arrays stay arrays, booleans stay booleans.


10. req.headers — accessing request headers

app.get('/api/data', (req, res) => {
  // All headers are lowercased by Node.js
  console.log(req.headers);
  // {
  //   'host': 'localhost:3000',
  //   'user-agent': 'Mozilla/5.0 ...',
  //   'accept': 'application/json',
  //   'authorization': 'Bearer eyJ...',
  //   'content-type': 'application/json',
  // }

  // Access specific headers
  const authHeader = req.headers['authorization'];  // or req.get('Authorization')
  const contentType = req.headers['content-type'];
  const userAgent = req.headers['user-agent'];

  // Express shorthand
  const auth = req.get('Authorization');    // same as req.headers['authorization']
  const type = req.get('Content-Type');

  res.json({ authHeader, contentType, userAgent });
});

Common headers to check

HeaderPurposeExample
AuthorizationAuth tokenBearer eyJhbGci...
Content-TypeBody formatapplication/json
AcceptDesired response formatapplication/json
User-AgentClient infoMozilla/5.0 ...
X-Request-IdTrace IDuuid-here
OriginRequest origin (CORS)http://localhost:3000

11. req.method, req.path, req.originalUrl

app.use((req, res, next) => {
  // GET /api/users/42?fields=name,email

  console.log(req.method);      // 'GET'
  console.log(req.path);        // '/api/users/42'      (no query string)
  console.log(req.originalUrl); // '/api/users/42?fields=name,email' (full URL)
  console.log(req.url);         // '/api/users/42?fields=name,email' (may change in sub-routers)
  console.log(req.baseUrl);     // '' (or mount path if using Router)
  console.log(req.hostname);    // 'localhost'
  console.log(req.ip);          // '::1' or '127.0.0.1'
  console.log(req.protocol);    // 'http'
  console.log(req.secure);      // false (true if HTTPS)

  next();
});

Request property reference

PropertyReturnsExample
req.methodHTTP method'GET', 'POST'
req.pathPath without query'/api/users/42'
req.originalUrlFull URL with query'/api/users/42?page=1'
req.queryParsed query params{ page: '1' }
req.paramsRoute parameters{ id: '42' }
req.bodyParsed body{ name: 'Alice' }
req.headersRequest headers{ host: 'localhost' }
req.hostnameHost name'localhost'
req.ipClient IP'127.0.0.1'

12. Idempotency explained

Idempotent means: calling the operation multiple times produces the same result as calling it once.

MethodIdempotent?Why
GETYesReading data does not change it
PUTYesReplacing with the same data gives the same result
DELETEYesDeleting something already deleted = still deleted
PATCHUsually yesSetting name = 'Alice' twice = same result
POSTNoCreating a resource twice = two resources

Why idempotency matters

  1. Retries — If a network request fails and the client retries, idempotent methods are safe to retry
  2. Caching — GET requests can be cached by browsers and CDNs because they are safe and idempotent
  3. API design — Clients expect POST to create, and they expect repeating a DELETE to not fail
// POST is NOT idempotent — calling twice creates two orders
app.post('/api/orders', (req, res) => {
  const order = createOrder(req.body); // New order each time
  res.status(201).json(order);
});

// PUT IS idempotent — calling twice with same data = same result
app.put('/api/users/1', (req, res) => {
  const user = replaceUser(1, req.body); // Same result each time
  res.json(user);
});

// DELETE IS idempotent — deleting twice = same state (resource gone)
app.delete('/api/users/1', (req, res) => {
  deleteUser(1); // Already deleted? Still deleted.
  res.status(204).end();
});

13. CRUD mapping

OperationHTTP MethodExpressTypical URLStatus
CreatePOSTapp.post()/api/users201
Read (all)GETapp.get()/api/users200
Read (one)GETapp.get()/api/users/:id200
Update (full)PUTapp.put()/api/users/:id200
Update (partial)PATCHapp.patch()/api/users/:id200
DeleteDELETEapp.delete()/api/users/:id200 or 204

14. Real example: full CRUD for a resource

const express = require('express');
const app = express();

app.use(express.json());

// In-memory "database"
let books = [
  { id: 1, title: '1984', author: 'George Orwell', year: 1949 },
  { id: 2, title: 'Brave New World', author: 'Aldous Huxley', year: 1932 },
];
let nextId = 3;

// CREATE — POST /api/books
app.post('/api/books', (req, res) => {
  const { title, author, year } = req.body;

  if (!title || !author) {
    return res.status(400).json({ error: 'Title and author are required' });
  }

  const book = { id: nextId++, title, author, year: year || null };
  books.push(book);

  res.status(201).json({ data: book });
});

// READ ALL — GET /api/books
app.get('/api/books', (req, res) => {
  // Support optional filtering
  let results = [...books];

  if (req.query.author) {
    results = results.filter(b =>
      b.author.toLowerCase().includes(req.query.author.toLowerCase())
    );
  }

  res.json({ data: results, count: results.length });
});

// READ ONE — GET /api/books/:id
app.get('/api/books/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const book = books.find(b => b.id === id);

  if (!book) {
    return res.status(404).json({ error: 'Book not found' });
  }

  res.json({ data: book });
});

// UPDATE (full replace) — PUT /api/books/:id
app.put('/api/books/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const index = books.findIndex(b => b.id === id);

  if (index === -1) {
    return res.status(404).json({ error: 'Book not found' });
  }

  const { title, author, year } = req.body;

  if (!title || !author) {
    return res.status(400).json({ error: 'Title and author are required' });
  }

  books[index] = { id, title, author, year: year || null };

  res.json({ data: books[index] });
});

// UPDATE (partial) — PATCH /api/books/:id
app.patch('/api/books/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const book = books.find(b => b.id === id);

  if (!book) {
    return res.status(404).json({ error: 'Book not found' });
  }

  // Only update provided fields
  const allowedFields = ['title', 'author', 'year'];
  for (const field of allowedFields) {
    if (req.body[field] !== undefined) {
      book[field] = req.body[field];
    }
  }

  res.json({ data: book });
});

// DELETE — DELETE /api/books/:id
app.delete('/api/books/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const index = books.findIndex(b => b.id === id);

  if (index === -1) {
    return res.status(404).json({ error: 'Book not found' });
  }

  const deleted = books.splice(index, 1)[0];

  res.json({ message: 'Book deleted', data: deleted });
});

// Start server
app.listen(3000, () => {
  console.log('Books API running on http://localhost:3000');
});

Testing the CRUD API

# Create
curl -X POST http://localhost:3000/api/books \
  -H "Content-Type: application/json" \
  -d '{"title":"Dune","author":"Frank Herbert","year":1965}'

# Read all
curl http://localhost:3000/api/books

# Read one
curl http://localhost:3000/api/books/1

# Update (full)
curl -X PUT http://localhost:3000/api/books/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"1984 (Updated)","author":"George Orwell","year":1949}'

# Update (partial)
curl -X PATCH http://localhost:3000/api/books/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"Nineteen Eighty-Four"}'

# Delete
curl -X DELETE http://localhost:3000/api/books/1

15. Key takeaways

  1. Express has a method for every HTTP verb: app.get(), app.post(), app.put(), app.patch(), app.delete().
  2. GET reads, POST creates, PUT replaces, PATCH updates partially, DELETE removes.
  3. Body parsing requires middleware: express.json() for JSON, express.urlencoded() for forms.
  4. req.body preserves JSON types (unlike req.params/req.query which are always strings).
  5. Idempotent = safe to retry. GET, PUT, DELETE are idempotent; POST is not.
  6. CRUD maps directly to HTTP methods: Create=POST, Read=GET, Update=PUT/PATCH, Delete=DELETE.
  7. Always validate req.body before using it — never trust client data.

Explain-It Challenge

Explain without notes:

  1. What is the difference between PUT and PATCH? Give a concrete example where each would be used.
  2. Why is POST not idempotent, and what problem does this create for clients with unreliable networks?
  3. Why must express.json() be added before your route definitions?
  4. Walk through the full CRUD lifecycle of a "book" resource — list the URL, method, and expected status code for each operation.

Navigation: <- 3.4.d — Query Parameters and URL Parameters | 3.4.f — Serving Static Files ->