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

RangeCategoryMeaning
1xxInformationalRequest received, processing continues
2xxSuccessRequest was successfully received, understood, and accepted
3xxRedirectionFurther action needed to complete the request
4xxClient ErrorThe request contains bad syntax or cannot be fulfilled
5xxServer ErrorThe 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

CodeNameUse CaseHas Body
200OKGeneral success, returning dataYes
201CreatedResource created (POST)Yes (the created resource)
202AcceptedRequest accepted but not yet processedYes (acknowledgment)
204No ContentSuccess 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

ScenarioUse
Domain change: old.com to new.com301
URL structure change: /blog/123 to /posts/123301
After form submission (PRG pattern)302
During server maintenance302
A/B testing with different URLs302
SSL redirect: HTTP to HTTPS301

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

ScenarioCodeReason
No token provided401We do not know who you are
Invalid/expired token401Your credentials are invalid
Valid token, but user is not admin403We know who you are, but you cannot do this
User trying to access another user's data403Authenticated 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

ScenarioCode
Request body is not valid JSON400
Missing Content-Type header400
Completely garbled request400
Valid JSON but email format is wrong422
Valid JSON but age is negative422
Valid JSON but required field is empty string422

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

CodeNameMeaning
400Bad RequestMalformed syntax, unparseable body
401UnauthorizedAuthentication required or failed
403ForbiddenAuthenticated but not authorized
404Not FoundResource does not exist
405Method Not AllowedWrong HTTP method for this route
409ConflictDuplicate or conflicting state
422Unprocessable EntityValid syntax but invalid data
429Too Many RequestsRate 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

CodeNameTypical Cause
500Internal Server ErrorUnhandled exception, bug in server code
502Bad GatewayUpstream server returned invalid response
503Service UnavailableMaintenance, overload, dependency down
504Gateway TimeoutUpstream server did not respond in time

7. Status Code Decision Guide

Use this table when you are unsure which status code to return.

SituationStatus Code
Everything worked, returning data200 OK
New resource created201 Created
Success but nothing to return (e.g., DELETE)204 No Content
Resource moved permanently301 Moved Permanently
Temporary redirect302 Found
Client has a cached copy that is still valid304 Not Modified
Client sent unparseable data400 Bad Request
Client needs to log in401 Unauthorized
Client is logged in but lacks permission403 Forbidden
Resource does not exist404 Not Found
Wrong HTTP method for this endpoint405 Method Not Allowed
Data conflicts with existing state (e.g., duplicate email)409 Conflict
Data is parseable but semantically invalid422 Unprocessable Entity
Too many requests in a time period429 Too Many Requests
Bug in the server code500 Internal Server Error
Upstream dependency is down502 Bad Gateway
Server is in maintenance mode503 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 upfrontYou are building headers incrementally
You want concise codeMiddleware might add headers later
Simple request handlersComplex 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

  1. Status codes are a three-digit contract between server and client — they communicate the outcome of every request.
  2. 2xx means success (200 OK, 201 Created, 204 No Content).
  3. 3xx means redirect (301 permanent, 302 temporary, 304 not modified).
  4. 4xx means the client made an error (400 bad data, 401 not authenticated, 403 not authorized, 404 not found).
  5. 5xx means the server made an error (500 bug, 502 upstream down, 503 maintenance).
  6. The difference between 401 and 403 is authentication vs authorization.
  7. The difference between 400 and 422 is unparseable vs semantically invalid.
  8. Never use 200 for errors, never use 500 for client mistakes, never send a body with 204.
  9. 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.

  1. A client requests GET /api/books and there are 50 books in the database.
  2. A client sends POST /api/books with valid JSON and the book is created.
  3. A client sends POST /api/books with an empty body.
  4. A client sends GET /api/books/999 but book 999 does not exist.
  5. A client sends DELETE /api/books/5 and the book is deleted.
  6. A client sends PATCH /api/books/5 but is not logged in.
  7. A client sends PATCH /api/books/5 but is a "viewer" role, not an "editor".
  8. A client sends POST /api/books with an ISBN that already exists.
  9. Your database connection suddenly drops during a GET /api/books request.
  10. 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