Every HTTP response has two essential parts: a status code that tells the client whether the request succeeded and why, and a body that carries the data. In a MERN REST API the body is always JSON. Getting both right โ choosing the correct status code and structuring a consistent JSON response shape โ is what separates a professional API from one that leaves your React frontend guessing. In this lesson you will master the Express response object, learn which HTTP status codes belong in which situations, and build a consistent response pattern you will use throughout the series.
HTTP Status Code Groups
| Range | Category | Common Codes |
|---|---|---|
| 2xx | Success | 200 OK, 201 Created, 204 No Content |
| 3xx | Redirection | 301 Moved Permanently, 302 Found |
| 4xx | Client Error | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable |
| 5xx | Server Error | 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable |
{ success: boolean, data: any, message: string }. Your React frontend can then always check response.data.success and read from response.data.data, regardless of which endpoint it called. Inconsistent response shapes are one of the most frustrating things to work with on the frontend.res.status(200).json({ error: 'Not found' }). This breaks HTTP semantics and forces your React code to inspect the body to determine success instead of using the standard HTTP status code. Axios and fetch both have built-in error handling that triggers on 4xx/5xx status codes โ use them correctly.The Most Important Status Codes for MERN APIs
| Code | Name | When to use in MERN |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH, DELETE responses |
| 201 | Created | Successful POST that creates a new resource |
| 204 | No Content | Successful DELETE with no body to return |
| 400 | Bad Request | Validation failure โ missing or invalid fields in request body |
| 401 | Unauthorized | Missing, invalid, or expired JWT token |
| 403 | Forbidden | Valid token but user lacks permission (not owner, not admin) |
| 404 | Not Found | Requested resource does not exist in MongoDB |
| 409 | Conflict | Duplicate โ e.g. email already registered |
| 422 | Unprocessable Entity | Well-formed request but semantic validation failed |
| 500 | Internal Server Error | Unexpected server error โ database down, unhandled exception |
Express Response Methods
// โโ res.json() โ send a JSON response โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res.json({ success: true, data: post });
// Automatically sets Content-Type: application/json
// Accepts objects, arrays, strings, numbers, booleans, null
// โโ res.status() โ set the HTTP status code โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res.status(201).json({ success: true, data: newPost });
res.status(404).json({ success: false, message: 'Post not found' });
res.status(500).json({ success: false, message: 'Internal server error' });
// โโ res.send() โ send any response โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res.send('Hello World'); // text/html by default
res.send(Buffer.from('binary')); // binary data
// For APIs: always use res.json() instead of res.send()
// โโ res.sendStatus() โ send status code only, body is the status text โโโโโโโโโ
res.sendStatus(204); // 204 No Content โ body is "No Content"
// Useful for DELETE responses where you have nothing meaningful to return
// โโ res.redirect() โ send a redirect response โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res.redirect(301, 'https://newurl.com');
res.redirect('/api/v2/posts');
// โโ res.set() โ manually set headers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res.set('X-Request-ID', '12345');
res.set('Cache-Control', 'no-store');
// โโ res.cookie() โ set a cookie โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res.cookie('refreshToken', token, {
httpOnly: true, // not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in milliseconds
});
Consistent API Response Pattern
// server/src/utils/apiResponse.js
// A helper to keep all responses consistent across the API
const sendSuccess = (res, data, statusCode = 200, message = '') => {
const response = { success: true };
if (message) response.message = message;
if (Array.isArray(data)) response.count = data.length;
response.data = data;
return res.status(statusCode).json(response);
};
const sendError = (res, message, statusCode = 500) => {
return res.status(statusCode).json({ success: false, message });
};
module.exports = { sendSuccess, sendError };
// โโ Using the helpers in route handlers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const { sendSuccess, sendError } = require('../utils/apiResponse');
app.get('/api/posts', (req, res) => {
const posts = getAllPosts();
sendSuccess(res, posts);
// Response: { success: true, count: 3, data: [...] }
});
app.get('/api/posts/:id', (req, res) => {
const post = getPostById(req.params.id);
if (!post) return sendError(res, 'Post not found', 404);
sendSuccess(res, post);
});
app.post('/api/posts', (req, res) => {
const { title, body } = req.body;
if (!title || !body) return sendError(res, 'Title and body are required', 400);
const newPost = createPost({ title, body });
sendSuccess(res, newPost, 201, 'Post created successfully');
});
Response Examples โ All Scenarios
// โโ 200 โ successful GET โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res.status(200).json({ success: true, data: posts });
// โโ 201 โ resource created โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res.status(201).json({ success: true, data: newPost, message: 'Post created' });
// โโ 204 โ deleted, no content โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res.status(204).end(); // no body โ do not call res.json() with 204
// โโ 400 โ validation failure โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res.status(400).json({ success: false, message: 'Title is required', field: 'title' });
// โโ 401 โ not authenticated โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res.status(401).json({ success: false, message: 'No token provided โ please log in' });
// โโ 403 โ authenticated but not authorised โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res.status(403).json({ success: false, message: 'You do not own this post' });
// โโ 404 โ resource not found โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res.status(404).json({ success: false, message: `Post with id ${id} not found` });
// โโ 409 โ conflict โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res.status(409).json({ success: false, message: 'Email already registered' });
// โโ 500 โ unexpected error โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res.status(500).json({ success: false, message: 'An unexpected error occurred' });
// In practice: caught by global error middleware โ not returned from route handlers
Common Mistakes
Mistake 1 โ Always returning 200 regardless of outcome
โ Wrong โ React has no way to know if something failed:
app.post('/api/auth/login', (req, res) => {
if (!userFound) return res.json({ success: false, message: 'User not found' }); // status 200!
// Axios treats this as a success โ React must check success flag manually
});
โ Correct โ use the appropriate 4xx status code so Axios catches it automatically:
if (!userFound) return res.status(404).json({ success: false, message: 'User not found' });
// Axios throws an error โ React .catch() block handles it โ
Mistake 2 โ Sending a body with 204 No Content
โ Wrong โ 204 responses must have no body:
res.status(204).json({ success: true, message: 'Deleted' });
// The json body is silently discarded by the browser โ React receives nothing
โ
Correct โ use res.status(204).end() or return 200 with a confirmation body:
res.status(200).json({ success: true, message: 'Post deleted successfully' }); // โ
Mistake 3 โ Leaking internal error details to the client in production
โ Wrong โ sending raw error objects or stack traces to the client:
res.status(500).json({ error: err }); // may expose DB credentials, file paths, stack traces
โ Correct โ log the full error server-side, send a generic message to the client:
console.error(err);
res.status(500).json({ success: false, message: 'An unexpected error occurred' });
Quick Reference
| Situation | Status | Response |
|---|---|---|
| GET list or GET one | 200 | res.json({ success: true, data: ... }) |
| POST โ created | 201 | res.status(201).json({ data: newItem }) |
| DELETE โ no body | 204 | res.status(204).end() |
| Validation failed | 400 | res.status(400).json({ message: '...' }) |
| No / bad token | 401 | res.status(401).json({ message: '...' }) |
| No permission | 403 | res.status(403).json({ message: '...' }) |
| Resource not found | 404 | res.status(404).json({ message: '...' }) |
| Duplicate / conflict | 409 | res.status(409).json({ message: '...' }) |
| Unexpected server error | 500 | Global error middleware handles this |