Understanding HTTP Methods and Status Codes
Every time you browse a website, submit a form, or fetch data from an API, HTTP (Hypertext Transfer Protocol) is working behind the scenes. Understanding HTTP methods and status codes is fundamental to web developmentβwhether you're building REST APIs, debugging network issues, or optimizing your application's communication with servers.
In this comprehensive guide, we'll explore all the HTTP methods you need to know, decode status codes like a pro, and provide practical examples that you can use in your projects today. By the end, you'll speak HTTP fluently and handle any request-response scenario with confidence.
- All HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS) with real examples
- Status codes decoded: 1xx, 2xx, 3xx, 4xx, 5xx and when to use each
- RESTful API design patterns and best practices
- Idempotency, safety, and cacheability of methods
- Practical code examples in JavaScript, Node.js, and cURL
- Common mistakes and how to avoid them
The HTTP Request-Response Cycle
Before diving into methods and codes, let's understand the fundamental cycle. Every HTTP interaction involves a request from the client and a response from the server. The method tells the server what action to perform, and the status code tells the client what happened.
The HTTP cycle: Client sends a request with a method, server returns a response with a status code
HTTP Methods Explained
HTTP methods (also called "verbs") tell the server what operation to perform on the specified resource. While HTTP defines many methods, these are the most commonly used in web development:
HTTP methods map to CRUD operations: Create (POST), Read (GET), Update (PUT/PATCH), Delete (DELETE)
GET - Retrieve Data
GET Method Details
The GET method requests data from a specified resource. It's the most common HTTP method and should only be used to retrieve dataβnever to modify it.
- Safe: Does not modify server state
- Idempotent: Multiple identical requests produce the same result
- Cacheable: Responses can be cached by browsers and CDNs
- No body: Data sent via URL parameters (query strings)
// JavaScript Fetch API
const response = await fetch('/api/users');
const users = await response.json();
// With query parameters
const response = await fetch('/api/users?page=1&limit=10');
// Get single resource
const user = await fetch('/api/users/123');
// cURL
curl -X GET "https://api.example.com/users" -H "Accept: application/json"
POST - Create Data
POST Method Details
The POST method submits data to be processed by the server, typically resulting in a new resource being created or a state change on the server.
- Not Safe: Modifies server state
- Not Idempotent: Multiple requests may create multiple resources
- Not Cacheable: By default (can be made cacheable with headers)
- Has body: Data sent in request body (JSON, form data, etc.)
// JavaScript Fetch API - Create a user
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'John Doe',
email: 'john@example.com'
})
});
const newUser = await response.json();
// Response: { id: 124, name: 'John Doe', email: 'john@example.com' }
// cURL
curl -X POST "https://api.example.com/users" \
-H "Content-Type: application/json" \
-d '{"name":"John Doe","email":"john@example.com"}'
PUT - Replace Data
PUT Method Details
The PUT method replaces the entire resource with the provided data. If the resource doesn't exist, PUT may create it (though this depends on server implementation).
- Not Safe: Modifies server state
- Idempotent: Multiple identical requests produce the same result
- Replaces entire resource: All fields must be provided
- Can create: If resource doesn't exist (server-dependent)
// Replace entire user resource
const response = await fetch('/api/users/123', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: 123,
name: 'John Updated',
email: 'john.updated@example.com',
role: 'admin' // Must include ALL fields
})
});
PATCH - Partial Update
PATCH Method Details
The PATCH method applies partial modifications to a resource. Unlike PUT, you only send the fields that need to be updated.
- Not Safe: Modifies server state
- Not Always Idempotent: Depends on implementation
- Partial update: Only changed fields are sent
- More efficient: Smaller payloads than PUT
// Update only the email field
const response = await fetch('/api/users/123', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: 'new.email@example.com' // Only send what changes
})
});
DELETE - Remove Data
DELETE Method Details
The DELETE method removes the specified resource from the server.
- Not Safe: Modifies server state
- Idempotent: Deleting twice has same effect as once
- Usually no body: Resource identified by URL
- Returns: 200 OK, 202 Accepted, or 204 No Content
// Delete a user
const response = await fetch('/api/users/123', {
method: 'DELETE'
});
if (response.status === 204) {
console.log('User deleted successfully');
}
// cURL
curl -X DELETE "https://api.example.com/users/123"
Other HTTP Methods
HEAD, OPTIONS, TRACE, CONNECT
HEAD - Same as GET but returns only headers, not the body. Useful for checking if a resource exists or getting metadata.
curl -I "https://api.example.com/users/123"
OPTIONS - Returns the allowed HTTP methods for a resource. Used in CORS preflight requests.
curl -X OPTIONS "https://api.example.com/users" -i
# Response Header: Allow: GET, POST, PUT, DELETE, OPTIONS
TRACE - Echoes the received request for debugging. Usually disabled for security reasons.
CONNECT - Establishes a tunnel to the server, typically used for HTTPS through HTTP proxies.
Method Properties Comparison
Understanding the properties of each method helps you choose the right one for your use case:
| Method | Safe | Idempotent | Cacheable | Request Body |
|---|---|---|---|---|
| GET | β Yes | β Yes | β Yes | β No |
| POST | β No | β No | β οΈ Conditional | β Yes |
| PUT | β No | β Yes | β No | β Yes |
| PATCH | β No | β οΈ Not guaranteed | β No | β Yes |
| DELETE | β No | β Yes | β No | β οΈ Optional |
Idempotent means that making the same request multiple times produces the same result as making it once. This is crucial for handling network retries safely:
- GET /users/123 β Always returns the same user (idempotent)
- DELETE /users/123 β First deletes, subsequent calls return 404 (same final state = idempotent)
- POST /users β Each call creates a new user (NOT idempotent)
HTTP Status Codes Decoded
Status codes are three-digit numbers that tell you exactly what happened with your request. They're grouped into five categories:
The five categories of HTTP status codes and what they mean
2xx Success Codes
2xx Success - Request Succeeded
3xx Redirection Codes
3xx Redirection - Further Action Needed
4xx Client Error Codes
4xx Client Error - Problem with Request
- 401 Unauthorized: "Who are you?" - No authentication provided or invalid credentials
- 403 Forbidden: "I know who you are, but no." - Authenticated but lacking permission
5xx Server Error Codes
5xx Server Error - Something Went Wrong
RESTful API Design Patterns
Now that you understand HTTP methods and status codes, let's see how they come together in RESTful API design:
// Standard REST endpoints for a Users resource
GET /api/users β 200 OK (list all users)
GET /api/users/123 β 200 OK (get single user) | 404 Not Found
POST /api/users β 201 Created (create user) | 400/422 (validation error)
PUT /api/users/123 β 200 OK (replace user) | 404 Not Found
PATCH /api/users/123 β 200 OK (update fields) | 404 Not Found
DELETE /api/users/123 β 204 No Content (deleted) | 404 Not Found
// Nested resources
GET /api/users/123/posts β 200 OK (user's posts)
POST /api/users/123/posts β 201 Created (create post for user)
// Actions (non-CRUD operations)
POST /api/users/123/activate β 200 OK (activate user)
POST /api/users/123/deactivate β 200 OK (deactivate user)
Complete API Example
const express = require('express');
const app = express();
app.use(express.json());
let users = [
{ id: 1, name: 'John', email: 'john@example.com' }
];
// GET all users
app.get('/api/users', (req, res) => {
res.status(200).json({ data: users });
});
// GET single user
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.status(200).json({ data: user });
});
// POST create user
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email required' });
}
const newUser = { id: users.length + 1, name, email };
users.push(newUser);
res.status(201).json({ data: newUser });
});
// PATCH update user
app.patch('/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' });
}
Object.assign(user, req.body);
res.status(200).json({ data: user });
});
// DELETE user
app.delete('/api/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'User not found' });
}
users.splice(index, 1);
res.status(204).send();
});
app.listen(3000);
// API wrapper with proper error handling
class ApiClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async request(method, endpoint, data = null) {
const options = {
method,
headers: { 'Content-Type': 'application/json' }
};
if (data && ['POST', 'PUT', 'PATCH'].includes(method)) {
options.body = JSON.stringify(data);
}
const response = await fetch(`${this.baseUrl}${endpoint}`, options);
// Handle different status codes
if (response.status === 204) return null;
const json = await response.json();
if (!response.ok) {
throw new Error(json.error || `HTTP ${response.status}`);
}
return json.data;
}
getUsers() { return this.request('GET', '/users'); }
getUser(id) { return this.request('GET', `/users/${id}`); }
createUser(data) { return this.request('POST', '/users', data); }
updateUser(id, data) { return this.request('PATCH', `/users/${id}`, data); }
deleteUser(id) { return this.request('DELETE', `/users/${id}`); }
}
// Usage
const api = new ApiClient('/api');
const users = await api.getUsers();
const newUser = await api.createUser({ name: 'Jane', email: 'jane@example.com' });
async function fetchWithRetry(url, options, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
// Handle different status code categories
switch (true) {
case response.status >= 200 && response.status < 300:
return response; // Success!
case response.status === 401:
// Refresh token and retry
await refreshAuthToken();
continue;
case response.status === 429:
// Rate limited - wait and retry
const retryAfter = response.headers.get('Retry-After') || 5;
await delay(retryAfter * 1000);
continue;
case response.status >= 500:
// Server error - retry with backoff
if (i < retries - 1) {
await delay(Math.pow(2, i) * 1000);
continue;
}
throw new Error(`Server error: ${response.status}`);
default:
// Client errors (4xx) - don't retry
const error = await response.json();
throw new Error(error.message || `HTTP ${response.status}`);
}
} catch (err) {
if (i === retries - 1) throw err;
}
}
}
const delay = ms => new Promise(r => setTimeout(r, ms));
Quick Reference Chart
Here's a handy reference for the most common status codes you'll use:
Common Mistakes to Avoid
- Using GET for mutations: GET /api/users/123/delete β Use DELETE instead
- Returning 200 for errors: Always use appropriate error codes (4xx/5xx)
- Using 404 for authorization: Use 403 Forbidden for permission issues
- Ignoring idempotency: POST should create; PUT should be repeatable
- Generic 500 for everything: Use specific codes (400, 422, 409) for client errors
- Use nouns, not verbs: /api/users not /api/getUsers
- Consistent error format: { "error": "message", "code": "ERROR_CODE" }
- Include Location header: With 201 Created, point to the new resource
- Use 202 Accepted: For async operations that will complete later
- Document your API: Use OpenAPI/Swagger for clear documentation
Conclusion
HTTP methods and status codes are the foundation of web communication. By mastering them, you can:
- Build RESTful APIs that are intuitive and follow industry standards
- Debug faster by understanding exactly what status codes mean
- Create robust client applications that handle all response scenarios
- Communicate effectively with other developers using standard terminology
Remember: methods tell the server what to do, status codes tell you what happened. Use them correctly, and your APIs will be a joy to work with.
Key Takeaways
- GET for reading, POST for creating, PUT/PATCH for updating, DELETE for removing
- 2xx = Success, 3xx = Redirect, 4xx = Client error, 5xx = Server error
- Idempotent methods (GET, PUT, DELETE) are safe to retry
- Always return appropriate status codesβdon't hide errors behind 200 OK
- Use 401 for "who are you?" and 403 for "you can't do that"
