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
| Property | Value |
|---|---|
| Has body | No (browsers ignore body in GET) |
| Idempotent | Yes — same request always returns same result (given no external changes) |
| Cacheable | Yes — browsers and CDNs can cache GET responses |
| Safe | Yes — does not modify resources |
| Use for | Listing, 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
| Property | Value |
|---|---|
| Has body | Yes — the data to create |
| Idempotent | No — calling twice creates two resources |
| Cacheable | No (by default) |
| Safe | No — modifies server state |
| Use for | Creating 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
| Property | Value |
|---|---|
| Has body | Yes — the complete new resource |
| Idempotent | Yes — sending same PUT twice produces same result |
| Use for | Full replacement of a resource |
| Gotcha | Missing 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
| Aspect | PUT | PATCH |
|---|---|---|
| Sends | Complete resource | Only changed fields |
| Missing fields | Reset to defaults | Left unchanged |
| Idempotent | Yes | Usually yes (depends on implementation) |
| Common in practice | Less common for APIs | More 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
| Property | Value |
|---|---|
| Has body | Usually no (some APIs accept a body) |
| Idempotent | Yes — deleting the same resource twice has the same effect |
| Use for | Removing resources |
| Common responses | 200 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()
| Feature | app.all(path, handler) | app.use(path, handler) |
|---|---|---|
| Methods | All HTTP methods | All HTTP methods |
| Path matching | Exact match | Prefix match |
| Example | app.all('/api') matches only /api | app.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 });
});
| Option | extended: false | extended: true |
|---|---|---|
| Parser | querystring (built-in) | qs library |
| Nested objects | Not supported | Supported (user[name]=Alice) |
| Arrays | Basic support | Rich support (colors[]=red&colors[]=blue) |
| Recommendation | Simple forms | Most 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
| Header | Purpose | Example |
|---|---|---|
Authorization | Auth token | Bearer eyJhbGci... |
Content-Type | Body format | application/json |
Accept | Desired response format | application/json |
User-Agent | Client info | Mozilla/5.0 ... |
X-Request-Id | Trace ID | uuid-here |
Origin | Request 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
| Property | Returns | Example |
|---|---|---|
req.method | HTTP method | 'GET', 'POST' |
req.path | Path without query | '/api/users/42' |
req.originalUrl | Full URL with query | '/api/users/42?page=1' |
req.query | Parsed query params | { page: '1' } |
req.params | Route parameters | { id: '42' } |
req.body | Parsed body | { name: 'Alice' } |
req.headers | Request headers | { host: 'localhost' } |
req.hostname | Host name | 'localhost' |
req.ip | Client IP | '127.0.0.1' |
12. Idempotency explained
Idempotent means: calling the operation multiple times produces the same result as calling it once.
| Method | Idempotent? | Why |
|---|---|---|
| GET | Yes | Reading data does not change it |
| PUT | Yes | Replacing with the same data gives the same result |
| DELETE | Yes | Deleting something already deleted = still deleted |
| PATCH | Usually yes | Setting name = 'Alice' twice = same result |
| POST | No | Creating a resource twice = two resources |
Why idempotency matters
- Retries — If a network request fails and the client retries, idempotent methods are safe to retry
- Caching — GET requests can be cached by browsers and CDNs because they are safe and idempotent
- 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
| Operation | HTTP Method | Express | Typical URL | Status |
|---|---|---|---|---|
| Create | POST | app.post() | /api/users | 201 |
| Read (all) | GET | app.get() | /api/users | 200 |
| Read (one) | GET | app.get() | /api/users/:id | 200 |
| Update (full) | PUT | app.put() | /api/users/:id | 200 |
| Update (partial) | PATCH | app.patch() | /api/users/:id | 200 |
| Delete | DELETE | app.delete() | /api/users/:id | 200 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
- Express has a method for every HTTP verb:
app.get(),app.post(),app.put(),app.patch(),app.delete(). - GET reads, POST creates, PUT replaces, PATCH updates partially, DELETE removes.
- Body parsing requires middleware:
express.json()for JSON,express.urlencoded()for forms. req.bodypreserves JSON types (unlikereq.params/req.querywhich are always strings).- Idempotent = safe to retry. GET, PUT, DELETE are idempotent; POST is not.
- CRUD maps directly to HTTP methods: Create=POST, Read=GET, Update=PUT/PATCH, Delete=DELETE.
- Always validate
req.bodybefore using it — never trust client data.
Explain-It Challenge
Explain without notes:
- What is the difference between PUT and PATCH? Give a concrete example where each would be used.
- Why is POST not idempotent, and what problem does this create for clients with unreliable networks?
- Why must
express.json()be added before your route definitions? - 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 ->