HTTP Status Codes Cheat Sheet
A complete reference of 2xx, 3xx, 4xx, and 5xx HTTP status codes with correct meanings and example server-side usage.
Setting Status Codes (Express)
Returning the right code from a REST endpoint.
const express = require('express');const app = express();app.get('/users/:id', (req, res) => { const user = db.findUser(req.params.id); if (!user) return res.status(404).json({ error: 'User not found' }); res.status(200).json(user);});app.post('/users', (req, res) => { const user = db.createUser(req.body); res.status(201).location(`/users/${user.id}`).json(user);});app.delete('/users/:id', (req, res) => { db.deleteUser(req.params.id); res.status(204).send(); // no body});app.use((req, res) => res.status(404).json({ error: 'Route not found' }));
2xx Success & 3xx Redirection
Codes for successful and redirected requests.
- 200 OK- Standard success response with a response body
- 201 Created- Resource created; typically includes a Location header pointing to it
- 204 No Content- Success with no response body (common for DELETE)
- 301 Moved Permanently- Resource permanently relocated; clients should update stored links
- 302 Found- Temporary redirect
- 304 Not Modified- Cached response is still valid (used with ETag/If-None-Match)
- 307/308- Temporary/Permanent redirect that preserves the original method and body
4xx Client Errors
Codes indicating a problem with the request.
- 400 Bad Request- Malformed syntax or invalid request parameters
- 401 Unauthorized- Missing/invalid authentication credentials
- 403 Forbidden- Authenticated but not permitted to access this resource
- 404 Not Found- Resource doesn't exist at this URL
- 405 Method Not Allowed- The URL exists but doesn't support this HTTP method
- 409 Conflict- Request conflicts with current state (e.g. duplicate resource)
- 422 Unprocessable Entity- Syntactically valid but semantically invalid (e.g. failed validation)
- 429 Too Many Requests- Rate limit exceeded; often includes a Retry-After header
5xx Server Errors
Codes indicating the server failed to fulfill a valid request.
- 500 Internal Server Error- Generic catch-all for unhandled server-side exceptions
- 502 Bad Gateway- Upstream server (behind a reverse proxy) returned an invalid response
- 503 Service Unavailable- Server temporarily overloaded or down for maintenance
- 504 Gateway Timeout- Upstream server did not respond in time
- 507 Insufficient Storage- Server can't store the representation needed to complete the request (WebDAV)
Structured Errors with RFC 9457 (Problem Details)
Returning machine-readable error bodies instead of ad-hoc JSON shapes, using the standardized application/problem+json format.
app.use((err, req, res, next) => { if (err.name === 'ValidationError') { return res.status(422) .type('application/problem+json') .json({ type: 'https://api.example.com/errors/validation-failed', title: 'Your request parameters didn\'t validate', status: 422, detail: err.message, instance: req.originalUrl, errors: err.fieldErrors, // extension member — extra fields are allowed }); } res.status(500).type('application/problem+json').json({ type: 'about:blank', title: 'Internal Server Error', status: 500, });});// RFC 9457 standardizes the shape (type/title/status/detail/instance) so// API clients can parse errors generically instead of guessing each API's// bespoke error format.
Content Negotiation Failures (406 / 415)
Distinguishing 'can't produce what you asked for' from 'can't consume what you sent'.
app.get('/report', (req, res) => { // Client's Accept header requests a representation we don't support res.format({ 'application/json': () => res.json({ data: report }), 'text/csv': () => res.type('text/csv').send(toCsv(report)), default: () => res.status(406).json({ error: 'Not Acceptable' }), // 406 });});app.post('/import', (req, res) => { const contentType = req.headers['content-type'] || ''; if (!contentType.includes('application/json')) { // Server refuses the payload's media type, distinct from malformed JSON (400) return res.status(415).json({ error: 'Unsupported Media Type' }); // 415 } res.status(202).json({ status: 'queued' }); // 202: accepted, processed async});
Underused but Precise Status Codes
Codes that most APIs skip in favor of 400/500 but that carry more precise meaning.
- 202 Accepted- Request accepted for asynchronous processing; poll a status URL or wait for a webhook for the result
- 206 Partial Content- Response to a Range header, used for resumable downloads and video/audio seeking
- 226 IM Used- Response represents the result of applying one or more instance manipulations (delta encoding, rarely seen)
- 406 Not Acceptable- Server can't produce a representation matching the client's Accept header
- 409 Conflict- Also correct for optimistic-concurrency version mismatches, not just duplicate resources
- 410 Gone- Resource permanently removed and won't come back — stronger signal to crawlers than 404
- 412 Precondition Failed- If-Match/If-Unmodified-Since precondition didn't hold (optimistic locking on updates)
- 425 Too Early- Server unwilling to process a request that might be replayed (TLS 0-RTT early data)
- 451 Unavailable For Legal Reasons- Resource withheld due to a legal demand (e.g. government censorship notice)
Conditional Requests: ETag + If-Match for Optimistic Locking
Preventing lost updates by requiring the client to prove it saw the latest version before writing.
app.get('/documents/:id', (req, res) => { const doc = db.findDocument(req.params.id); const etag = `"${doc.version}"`; if (req.headers['if-none-match'] === etag) { return res.status(304).end(); // client's cached copy is still fresh } res.set('ETag', etag).status(200).json(doc);});app.put('/documents/:id', (req, res) => { const doc = db.findDocument(req.params.id); const currentEtag = `"${doc.version}"`; if (req.headers['if-match'] !== currentEtag) { // Someone else updated it since the client last read it return res.status(412).json({ error: 'Precondition Failed: stale version' }); } const updated = db.updateDocument(req.params.id, req.body, doc.version + 1); res.set('ETag', `"${updated.version}"`).status(200).json(updated);});
Retry-After on 429 / 503
Telling well-behaved clients exactly when to retry instead of making them guess with backoff alone.
const rateLimiter = require('./rateLimiter');app.use((req, res, next) => { const result = rateLimiter.check(req.ip); if (!result.allowed) { res.set('Retry-After', String(result.retryAfterSeconds)); // seconds, or an HTTP-date return res.status(429).json({ error: 'Too Many Requests', retryAfter: result.retryAfterSeconds, }); } next();});app.use((req, res, next) => { if (maintenanceMode.isActive) { res.set('Retry-After', '120'); // also valid on 503 for planned downtime return res.status(503).json({ error: 'Service temporarily unavailable' }); } next();});
Distinguish 401 vs 403 correctly: return 401 when the client isn't authenticated at all (or its token is invalid/expired), and 403 when it's authenticated but lacks permission — conflating them makes API errors far harder to handle.