Episode 3 — NodeJS MongoDB Backend Architecture / 3.2 — Creating Server
3.2.e — HTTP Status Codes
Status codes are three-digit numbers in every HTTP response that tell the client whether the request succeeded, failed, or needs further action.
Navigation: ← 3.2 Overview | Next → 3.2.f Nodemon for Development
1. What Status Codes Are and Why They Matter
Every HTTP response includes a status code. It is the first thing the client checks to understand what happened.
HTTP/1.1 200 OK ← Status code is 200, reason phrase is "OK"
Content-Type: application/json
{"message": "Success"}
Why they matter
- Clients depend on them. Frontend code uses status codes to decide what to show the user.
- Browsers use them. A 301 triggers a redirect. A 304 uses cached data. A 401 might trigger a login prompt.
- Search engines use them. Google treats 301 (permanent redirect) differently from 302 (temporary redirect).
- Monitoring tools use them. A spike in 500 errors triggers alerts. A flood of 404s suggests broken links.
- They are a contract. Other developers consuming your API trust your status codes.
The five categories
| Range | Category | Meaning |
|---|---|---|
| 1xx | Informational | Request received, processing continues |
| 2xx | Success | Request was successfully received, understood, and accepted |
| 3xx | Redirection | Further action needed to complete the request |
| 4xx | Client Error | The request contains bad syntax or cannot be fulfilled |
| 5xx | Server Error | The server failed to fulfill a valid request |
A quick rule of thumb:
- 2xx = "All good."
- 3xx = "Go somewhere else."
- 4xx = "You (the client) did something wrong."
- 5xx = "I (the server) did something wrong."
2. 1xx Informational
These are rarely used directly in application code, but understanding them helps.
100 Continue
The server has received the request headers and the client should proceed to send the body. This is used for large uploads — the server confirms it is willing to accept the body before the client sends it.
Client: "I want to upload a 500MB file. Should I proceed?"
Server: "100 Continue — yes, send it."
Client: *sends the file*
101 Switching Protocols
The server is switching to a different protocol as requested by the client. This is how HTTP upgrades to WebSocket.
Client: "Can we switch to WebSocket?"
Server: "101 Switching Protocols — upgrading now."
102 Processing
The server has received the request and is processing it, but no response is available yet. Used to prevent the client from timing out during long operations.
3. 2xx Success
These indicate the request was successful.
200 OK
The most common status code. The request succeeded.
// GET request — returning data
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ users: ['Alice', 'Bob'] }));
When to use: Successful GET requests. Successful PUT/PATCH requests that return the updated resource.
201 Created
A new resource was successfully created. Always use this after a successful POST that creates something.
// POST request — creating a new user
const newUser = { id: 4, name: 'Diana', email: 'diana@example.com' };
res.writeHead(201, {
'Content-Type': 'application/json',
'Location': '/api/users/4' // URL of the newly created resource
});
res.end(JSON.stringify({ user: newUser }));
When to use: After POST requests that create new resources (users, posts, orders).
204 No Content
The request succeeded but there is no content to send back. The response body must be empty.
// DELETE request — resource deleted, nothing to return
res.writeHead(204);
res.end(); // No body!
When to use: Successful DELETE requests. Successful PUT/PATCH requests when you do not need to return the updated resource.
2xx Summary Table
| Code | Name | Use Case | Has Body |
|---|---|---|---|
| 200 | OK | General success, returning data | Yes |
| 201 | Created | Resource created (POST) | Yes (the created resource) |
| 202 | Accepted | Request accepted but not yet processed | Yes (acknowledgment) |
| 204 | No Content | Success with no body (DELETE, some PUT) | No |
4. 3xx Redirection
These tell the client to go to a different URL.
301 Moved Permanently
The resource has permanently moved to a new URL. Browsers and search engines update their records.
// Old URL permanently moved
res.writeHead(301, { 'Location': 'https://example.com/new-page' });
res.end();
When to use: The page or resource has a new permanent URL. Changing domain names. Old URL structure replaced by new one.
302 Found (Temporary Redirect)
The resource temporarily resides at a different URL. The client should continue using the original URL for future requests.
// Temporary redirect (e.g., during maintenance)
res.writeHead(302, { 'Location': '/maintenance' });
res.end();
When to use: Temporary redirects during maintenance. Redirecting after a form submission (Post-Redirect-Get pattern).
304 Not Modified
The resource has not changed since the client last requested it. The client should use its cached copy.
// Check the If-Modified-Since header
const lastModified = new Date('2025-01-15');
const ifModifiedSince = req.headers['if-modified-since'];
if (ifModifiedSince && new Date(ifModifiedSince) >= lastModified) {
res.writeHead(304);
res.end(); // No body — use your cache
} else {
res.writeHead(200, {
'Content-Type': 'application/json',
'Last-Modified': lastModified.toUTCString()
});
res.end(JSON.stringify({ data: 'fresh data' }));
}
301 vs 302 — When to Use Which
| Scenario | Use |
|---|---|
Domain change: old.com to new.com | 301 |
URL structure change: /blog/123 to /posts/123 | 301 |
| After form submission (PRG pattern) | 302 |
| During server maintenance | 302 |
| A/B testing with different URLs | 302 |
| SSL redirect: HTTP to HTTPS | 301 |
5. 4xx Client Errors
These mean the client sent a bad request. The problem is on the client's side.
400 Bad Request
The server cannot process the request due to client error — malformed syntax, invalid data, etc.
// Invalid JSON in request body
let body = '';
req.on('data', chunk => { body += chunk.toString(); });
req.on('end', () => {
try {
const data = JSON.parse(body);
// ... process data
} catch (e) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Bad Request',
message: 'Request body is not valid JSON'
}));
}
});
When to use: Invalid JSON. Missing required fields. Invalid data types (string where number expected).
401 Unauthorized
The client must authenticate. Despite the name, this is about authentication (who are you?), not authorization (what can you do?).
if (!req.headers['authorization']) {
res.writeHead(401, {
'Content-Type': 'application/json',
'WWW-Authenticate': 'Bearer' // Tell client what auth scheme to use
});
res.end(JSON.stringify({
error: 'Unauthorized',
message: 'Authentication token is required'
}));
return;
}
When to use: No credentials provided. Expired token. Invalid credentials.
403 Forbidden
The client is authenticated but does not have permission to access this resource.
// User is logged in but is not an admin
if (user.role !== 'admin') {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Forbidden',
message: 'Admin access required'
}));
return;
}
When to use: User is logged in but lacks permission. Trying to access another user's private data. Non-admin trying to access admin routes.
401 vs 403 — The Key Difference
| Scenario | Code | Reason |
|---|---|---|
| No token provided | 401 | We do not know who you are |
| Invalid/expired token | 401 | Your credentials are invalid |
| Valid token, but user is not admin | 403 | We know who you are, but you cannot do this |
| User trying to access another user's data | 403 | Authenticated but not authorized |
404 Not Found
The requested resource does not exist.
const user = users.find(u => u.id === id);
if (!user) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Not Found',
message: `User with ID ${id} does not exist`
}));
return;
}
When to use: URL path does not match any route. Resource ID does not exist in the database.
405 Method Not Allowed
The HTTP method is not supported for this URL.
if (req.url === '/api/users' && req.method !== 'GET' && req.method !== 'POST') {
res.writeHead(405, {
'Content-Type': 'application/json',
'Allow': 'GET, POST' // Tell client which methods ARE allowed
});
res.end(JSON.stringify({
error: 'Method Not Allowed',
message: `${req.method} is not supported on /api/users. Use GET or POST.`
}));
return;
}
409 Conflict
The request conflicts with the current state of the server.
// Trying to create a user with an email that already exists
const existingUser = users.find(u => u.email === newUser.email);
if (existingUser) {
res.writeHead(409, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Conflict',
message: 'A user with this email already exists'
}));
return;
}
422 Unprocessable Entity
The request is syntactically correct (valid JSON) but semantically invalid (bad data values).
// JSON is valid but the data does not make sense
if (user.age < 0 || user.age > 150) {
res.writeHead(422, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Unprocessable Entity',
message: 'Age must be between 0 and 150',
field: 'age'
}));
return;
}
400 vs 422 — When to Use Which
| Scenario | Code |
|---|---|
| Request body is not valid JSON | 400 |
| Missing Content-Type header | 400 |
| Completely garbled request | 400 |
| Valid JSON but email format is wrong | 422 |
| Valid JSON but age is negative | 422 |
| Valid JSON but required field is empty string | 422 |
429 Too Many Requests
The client has sent too many requests in a given time period (rate limiting).
// Simple rate limiter using a Map
const requestCounts = new Map();
const RATE_LIMIT = 100; // Max 100 requests
const RATE_WINDOW = 60 * 1000; // Per minute
function checkRateLimit(ip) {
const now = Date.now();
const record = requestCounts.get(ip);
if (!record || now - record.windowStart > RATE_WINDOW) {
requestCounts.set(ip, { count: 1, windowStart: now });
return true;
}
record.count++;
if (record.count > RATE_LIMIT) {
return false; // Rate limit exceeded
}
return true;
}
// In your server:
const clientIP = req.socket.remoteAddress;
if (!checkRateLimit(clientIP)) {
res.writeHead(429, {
'Content-Type': 'application/json',
'Retry-After': '60' // Tell client when to try again (seconds)
});
res.end(JSON.stringify({
error: 'Too Many Requests',
message: 'Rate limit exceeded. Try again in 60 seconds.'
}));
return;
}
4xx Summary Table
| Code | Name | Meaning |
|---|---|---|
| 400 | Bad Request | Malformed syntax, unparseable body |
| 401 | Unauthorized | Authentication required or failed |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource does not exist |
| 405 | Method Not Allowed | Wrong HTTP method for this route |
| 409 | Conflict | Duplicate or conflicting state |
| 422 | Unprocessable Entity | Valid syntax but invalid data |
| 429 | Too Many Requests | Rate limit exceeded |
6. 5xx Server Errors
These mean the server encountered an error. The client's request was valid, but the server could not fulfill it.
500 Internal Server Error
A generic catch-all for unexpected server errors.
try {
// Some operation that might fail
const result = riskyOperation();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
} catch (error) {
console.error('Server error:', error); // Log the full error internally
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Internal Server Error',
message: 'Something went wrong. Please try again later.'
// NEVER include: error.stack, error.message, database details
}));
}
When to use: Unhandled exceptions. Database connection failures. Unexpected null values. Any error that is the server's fault.
502 Bad Gateway
The server was acting as a gateway or proxy and received an invalid response from an upstream server.
Client → Your Server (reverse proxy) → Backend API → Database
↑
502 if this connection fails
When to use: Reverse proxy (like Nginx) cannot reach your Node.js app. Your Node.js app calls another API and that API is down.
503 Service Unavailable
The server is temporarily unable to handle requests — usually due to maintenance or overload.
const isUnderMaintenance = false; // Toggle this for maintenance mode
if (isUnderMaintenance) {
res.writeHead(503, {
'Content-Type': 'application/json',
'Retry-After': '3600' // Try again in 1 hour
});
res.end(JSON.stringify({
error: 'Service Unavailable',
message: 'We are performing scheduled maintenance. Please try again later.'
}));
return;
}
5xx Summary Table
| Code | Name | Typical Cause |
|---|---|---|
| 500 | Internal Server Error | Unhandled exception, bug in server code |
| 502 | Bad Gateway | Upstream server returned invalid response |
| 503 | Service Unavailable | Maintenance, overload, dependency down |
| 504 | Gateway Timeout | Upstream server did not respond in time |
7. Status Code Decision Guide
Use this table when you are unsure which status code to return.
| Situation | Status Code |
|---|---|
| Everything worked, returning data | 200 OK |
| New resource created | 201 Created |
| Success but nothing to return (e.g., DELETE) | 204 No Content |
| Resource moved permanently | 301 Moved Permanently |
| Temporary redirect | 302 Found |
| Client has a cached copy that is still valid | 304 Not Modified |
| Client sent unparseable data | 400 Bad Request |
| Client needs to log in | 401 Unauthorized |
| Client is logged in but lacks permission | 403 Forbidden |
| Resource does not exist | 404 Not Found |
| Wrong HTTP method for this endpoint | 405 Method Not Allowed |
| Data conflicts with existing state (e.g., duplicate email) | 409 Conflict |
| Data is parseable but semantically invalid | 422 Unprocessable Entity |
| Too many requests in a time period | 429 Too Many Requests |
| Bug in the server code | 500 Internal Server Error |
| Upstream dependency is down | 502 Bad Gateway |
| Server is in maintenance mode | 503 Service Unavailable |
8. Common Mistakes in Status Code Usage
Mistake 1: Using 200 for everything
// BAD — sending 200 even when there is an error
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'User not found' }));
// GOOD — using the correct status code
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'User not found' }));
Mistake 2: Using 403 instead of 401
// BAD — no token means we don't know who they are → 401, not 403
if (!token) {
res.writeHead(403); // Wrong! This is for "known but not permitted"
}
// GOOD
if (!token) {
res.writeHead(401); // Correct: "please authenticate first"
}
Mistake 3: Using 500 for client errors
// BAD — the client sent bad data; that is not a server error
if (!data.email) {
res.writeHead(500); // Wrong! This is the client's fault
}
// GOOD
if (!data.email) {
res.writeHead(400); // Correct: client sent a bad request
}
Mistake 4: Sending a body with 204
// BAD — 204 means "no content" but you are sending content
res.writeHead(204, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Deleted' })); // This body will be ignored
// GOOD
res.writeHead(204);
res.end(); // No body
Mistake 5: Forgetting Location header with redirects
// BAD — redirect without telling the client where to go
res.writeHead(301);
res.end(); // Client has no idea where to go
// GOOD
res.writeHead(301, { 'Location': '/new-url' });
res.end();
9. Setting Status Codes in Node.js
There are two ways to set status codes.
Method 1: res.writeHead(statusCode, headers)
Sets the status code and headers in one call. Must be called before res.write() or res.end().
res.writeHead(201, {
'Content-Type': 'application/json',
'Location': '/api/users/4'
});
res.end(JSON.stringify({ id: 4, name: 'Diana' }));
Method 2: res.statusCode property
Sets the status code separately. Headers are set with res.setHeader(). The status code is sent when res.end() or res.write() is first called.
res.statusCode = 201;
res.setHeader('Content-Type', 'application/json');
res.setHeader('Location', '/api/users/4');
res.end(JSON.stringify({ id: 4, name: 'Diana' }));
Which to use?
Use res.writeHead() when... | Use res.statusCode when... |
|---|---|
| You know all headers upfront | You are building headers incrementally |
| You want concise code | Middleware might add headers later |
| Simple request handlers | Complex handlers with conditional headers |
Checking status code
You can also read the status code that was set:
res.statusCode = 404;
console.log(res.statusCode); // 404
Key Takeaways
- Status codes are a three-digit contract between server and client — they communicate the outcome of every request.
- 2xx means success (200 OK, 201 Created, 204 No Content).
- 3xx means redirect (301 permanent, 302 temporary, 304 not modified).
- 4xx means the client made an error (400 bad data, 401 not authenticated, 403 not authorized, 404 not found).
- 5xx means the server made an error (500 bug, 502 upstream down, 503 maintenance).
- The difference between 401 and 403 is authentication vs authorization.
- The difference between 400 and 422 is unparseable vs semantically invalid.
- Never use 200 for errors, never use 500 for client mistakes, never send a body with 204.
- Use the decision guide table until status codes become second nature.
Explain-It Challenge
Scenario: You are building an API for a bookstore. For each of the following situations, write the correct status code and a brief JSON response body. Explain your reasoning.
- A client requests
GET /api/booksand there are 50 books in the database. - A client sends
POST /api/bookswith valid JSON and the book is created. - A client sends
POST /api/bookswith an empty body. - A client sends
GET /api/books/999but book 999 does not exist. - A client sends
DELETE /api/books/5and the book is deleted. - A client sends
PATCH /api/books/5but is not logged in. - A client sends
PATCH /api/books/5but is a "viewer" role, not an "editor". - A client sends
POST /api/bookswith an ISBN that already exists. - Your database connection suddenly drops during a
GET /api/booksrequest. - A client sends 200 requests in 1 minute but the limit is 100.
Then explain to a partner: why is it a bad practice to always return 200 with { success: false } instead of using proper status codes?
Navigation: ← 3.2 Overview | Next → 3.2.f Nodemon for Development