Client πŸ’» GET Server πŸ–₯️ 200 OK Response GET POST PUT PATCH DELETE
Backend

Understanding HTTP Methods and Status Codes

Mayur Dabhi
Mayur Dabhi
March 22, 2026
20 min read

You call an API, the request "works", and the response is 200 OK with {"success": false} in the body. Your error handling never fires because it only checks response.ok. Or a form submit retries on a flaky connection and you end up with two orders in the database. Both bugs come from treating HTTP methods and status codes as details instead of a contract between client and server.

This post goes through the methods you'll actually use (GET, POST, PUT, PATCH, DELETE, plus HEAD and OPTIONS), what safe and idempotent mean in practice, the status codes worth knowing in each range, and a small Express API with a client that handles the responses properly.

What You'll Learn
  • 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

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.

HTTP Request-Response Cycle CLIENT 🌐 Browser / App Mobile / Server SERVER πŸ–₯️ Web Server API Endpoint REQUEST Method + URL + Headers + Body RESPONSE Status Code + Headers + Body GET /users 200 OK

The HTTP cycle: Client sends a request with a method, server returns a response with a status code

HTTP Methods Explained

The method (people often call it the "verb") says what you want done to the resource at that URL. HTTP defines a handful more, but these five cover nearly every API you'll build:

HTTP Methods & CRUD Operations GET Read / Retrieve πŸ“– Read data Safe & Cacheable POST Create / Submit βž• Create new Not Idempotent PUT Update / Replace πŸ”„ Replace all Idempotent PATCH Partial Update ✏️ Modify part Not Always Idempotent DELETE Remove / Delete πŸ—‘οΈ Delete data Idempotent CRUD Operations Mapping C POST R GET U PUT / PATCH D DELETE

HTTP methods map to CRUD operations: Create (POST), Read (GET), Update (PUT/PATCH), Delete (DELETE)

GET - Retrieve Data

GET Method Details

GET fetches a resource. It's the request your browser makes for every page load, and it should only ever read data. Never change state in a GET handler: crawlers, link prefetchers and browser preloading will happily call it for you.

  • 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)
GET Request Examples
// 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.)
POST Request Examples
// 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)
PUT Request Example
// 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
PATCH Request Example
// 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 Request Example
// 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

These three properties decide what caches, proxies and retry logic are allowed to do with a request, so they matter more than the names:

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
Understanding Idempotency

Idempotent means that making the same request multiple times leaves the server in the same state as making it once. That's what lets a client retry after a timeout without worrying about side effects:

  • 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

The first digit tells you who has to act. 2xx means it worked, 3xx means look elsewhere, 4xx means the client sent something wrong, and 5xx means the server broke. If you only remember one thing about status codes, make it that.

HTTP Status Code Categories 1xx Informational Continue Processing πŸ’­ "Hold on..." 2xx Success 200 OK 201 Created 204 No Content βœ… "All good!" 3xx Redirection 301 Moved 302 Found 304 Not Modified β†ͺ️ "Go here instead" 4xx Client Error 400 Bad Request 401 Unauthorized 404 Not Found πŸ™ˆ "Your bad" 5xx Server Error 500 Internal Error 502 Bad Gateway 503 Unavailable πŸ”₯ "Our bad"

The five categories of HTTP status codes and what they mean

2xx Success Codes

2xx Success - Request Succeeded

200 OK - The request succeeded. Response depends on the method used.
201 Created - A new resource was created. Usually returned after POST.
202 Accepted - Request accepted but processing not complete. Good for async operations.
204 No Content - Success but no content to return. Common for DELETE requests.

3xx Redirection Codes

3xx Redirection - Further Action Needed

301 Moved Permanently - Resource has permanently moved. Browsers cache this.
302 Found - Temporary redirect. Resource is temporarily at another URL.
304 Not Modified - Cached version is still valid. No body returned.
307 Temporary Redirect - Like 302 but preserves the HTTP method.
308 Permanent Redirect - Like 301 but preserves the HTTP method.

4xx Client Error Codes

4xx Client Error - Problem with Request

400 Bad Request - Invalid syntax, malformed JSON, missing required fields.
401 Unauthorized - Authentication required. No valid credentials provided.
403 Forbidden - Authenticated but not authorized. You don't have permission.
404 Not Found - Resource doesn't exist at this URL.
405 Method Not Allowed - HTTP method not supported for this resource.
409 Conflict - Request conflicts with current state (e.g., duplicate email).
422 Unprocessable Entity - Valid syntax but semantic errors (validation failed).
429 Too Many Requests - Rate limit exceeded. Slow down!
401 vs 403: Know the Difference
  • 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

500 Internal Server Error - Generic server error. Check your logs!
501 Not Implemented - Server doesn't support the requested functionality.
502 Bad Gateway - Server acting as gateway got invalid response upstream.
503 Service Unavailable - Server overloaded or down for maintenance.
504 Gateway Timeout - Upstream server didn't respond in time.

RESTful API Design Patterns

Here's how methods and codes line up for a typical users resource. Notice the URLs stay the same and only the method changes:

RESTful API Endpoints - Users Resource
// 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

Express.js REST API
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);
Client-Side API Calls
// 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' });
Retries and Error Handling
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

In practice, these twelve cover most of what a typical API sends back:

200
OK - Success
201
Created
204
No Content
301
Moved Permanently
304
Not Modified
400
Bad Request
401
Unauthorized
403
Forbidden
404
Not Found
422
Validation Error
500
Server Error
503
Unavailable

Common Mistakes to Avoid

Don't Do These
  • 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 (unless you're deliberately hiding that the resource exists)
  • Ignoring idempotency: POST should create; PUT should be repeatable
  • Generic 500 for everything: Use specific codes (400, 422, 409) for client errors
Best Practices
  • 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

Gotchas I'd Check Before Shipping an API

The tables above are the easy part. These are the things that tend to break once real clients, proxies and load balancers get involved:

I wouldn't chase "pure REST" too hard, though. If an operation doesn't map to a resource (sending a password reset email, running a report), a POST /api/users/123/reset-password style action endpoint is clearer than inventing a fake resource. And sometimes returning 404 instead of 403 is the right call: if confirming that a private resource exists leaks information, GitHub-style 404s are a reasonable choice. Pick a convention, document it, and make the status code agree with the body.

HTTP API REST Status Codes Web Development Backend
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building web applications with Laravel, React, and Node.js. Passionate about clean code and sharing knowledge.