Skip to content

REST HTTP Verbs and Status Codes

Introduction

REST (Representational State Transfer) is an architectural style for distributed hypermedia systems, defined by Roy Fielding in his 2000 doctoral dissertation. It leverages the semantics of HTTP to create stateless, cacheable, and uniform interfaces between clients and servers. Understanding HTTP methods and status codes is foundational to building interoperable, well-behaved web APIs.


HTTP Methods: Semantics and Idempotency

HTTP defines a set of request methods that indicate the desired action for a given resource. Each method carries specific semantic meaning and idempotency guarantees.

MethodSemanticIdempotentSafeBody
GETRetrieve resourceYesYesNo
HEADRetrieve headersYesYesNo
POSTCreate/processNoNoYes
PUTReplace resourceYesNoYes
PATCHPartial updateNo*NoYes
DELETERemove resourceYesNoNo
OPTIONSDescribe optionsYesYesNo

*PATCH can be made idempotent by using conditional requests (e.g., If-Match).

HTTP Methods Flowchart


Status Codes

HTTP status codes communicate the result of a request. They are grouped into five classes.

Status Code Categories

Status Code Decision Tree


RESTful Principles and Constraints

Fielding defined six architectural constraints that together constitute REST:


Request/Response Structure

Request/Response Lifecycle Sequence Diagram

CRUD to HTTP Verb Mapping


Implementation

Java HttpServer with RequestHandler Interface

java
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;

/**
 * Minimal REST framework using Java's built-in HttpServer.
 */
public interface RequestHandler {
    void handle(HttpExchange exchange) throws IOException;
}

// Router that dispatches by method
class MethodRouter implements HttpHandler {
    private final Map<String, RequestHandler> methodHandlers = new HashMap<>();

    public MethodRouter on(String method, RequestHandler handler) {
        methodHandlers.put(method.toUpperCase(), handler);
        return this;
    }

    @Override
    public void handle(HttpExchange exchange) throws IOException {
        String method = exchange.getRequestMethod().toUpperCase();
        RequestHandler handler = methodHandlers.get(method);
        if (handler != null) {
            handler.handle(exchange);
        } else {
            sendResponse(exchange, 405, "{\"error\":\"Method Not Allowed\"}");
        }
    }

    public static void sendResponse(HttpExchange exchange, int status, String body) throws IOException {
        byte[] bytes = body.getBytes();
        exchange.getResponseHeaders().add("Content-Type", "application/json");
        exchange.sendResponseHeaders(status, bytes.length);
        try (OutputStream os = exchange.getResponseBody()) {
            os.write(bytes);
        }
    }
}

UserHandler — GET, POST, PUT, DELETE for /users

java
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Handles CRUD operations for the /users endpoint.
 */
public class UserHandler {

    private final Map<Long, String> store = new ConcurrentHashMap<>();
    private final AtomicLong idSeq = new AtomicLong(1);

    public RequestHandler getAll() {
        return exchange -> {
            String json = store.entrySet().stream()
                    .map(e -> "{\"id\":" + e.getKey() + ",\"name\":\"" + e.getValue() + "\"}")
                    .collect(java.util.stream.Collectors.joining(",", "[", "]"));
            MethodRouter.sendResponse(exchange, 200, json);
        };
    }

    public RequestHandler create() {
        return exchange -> {
            String body = readBody(exchange);
            // Minimal parse: expects {"name":"Alice"}
            String name = extractField(body, "name");
            if (name == null || name.isBlank()) {
                MethodRouter.sendResponse(exchange, 400,
                        "{\"error\":\"'name' field is required\"}");
                return;
            }
            long id = idSeq.getAndIncrement();
            store.put(id, name);
            exchange.getResponseHeaders().add(
                    "Location", "/users/" + id);
            MethodRouter.sendResponse(exchange, 201,
                    "{\"id\":" + id + ",\"name\":\"" + name + "\"}");
        };
    }

    public RequestHandler replace(long id) {
        return exchange -> {
            String body = readBody(exchange);
            String name = extractField(body, "name");
            if (!store.containsKey(id)) {
                MethodRouter.sendResponse(exchange, 404,
                        "{\"error\":\"User not found\"}");
                return;
            }
            store.put(id, name);
            MethodRouter.sendResponse(exchange, 200,
                    "{\"id\":" + id + ",\"name\":\"" + name + "\"}");
        };
    }

    public RequestHandler delete(long id) {
        return exchange -> {
            if (store.remove(id) == null) {
                MethodRouter.sendResponse(exchange, 404,
                        "{\"error\":\"User not found\"}");
            } else {
                exchange.sendResponseHeaders(204, -1);
            }
        };
    }

    private String readBody(HttpExchange exchange) throws IOException {
        try (InputStream is = exchange.getRequestBody()) {
            return new String(is.readAllBytes(), StandardCharsets.UTF_8);
        }
    }

    private String extractField(String json, String field) {
        // Naive extraction — use a proper JSON library in production
        String marker = "\"" + field + "\"";
        int idx = json.indexOf(marker);
        if (idx < 0) return null;
        int colon = json.indexOf(':', idx);
        int start = json.indexOf('"', colon) + 1;
        int end   = json.indexOf('"', start);
        return json.substring(start, end);
    }
}

Main Class — Wiring the Server

java
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

/**
 * Entry point: creates an HTTP server and registers /users routes.
 */
public class RestServer {

    public static void main(String[] args) throws Exception {
        HttpServer server = HttpServer.create(
                new InetSocketAddress(8080), 0);

        UserHandler users = new UserHandler();

        // Collection endpoint: GET all, POST create
        server.createContext("/users", new MethodRouter()
                .on("GET",  users.getAll())
                .on("POST", users.create()));

        // Item endpoints: PUT replace, DELETE remove (id parsed from path)
        server.createContext("/users/", exchange -> {
            String path = exchange.getRequestURI().getPath();
            long id = Long.parseLong(path.substring("/users/".length()));
            new MethodRouter()
                    .on("PUT",    users.replace(id))
                    .on("DELETE", users.delete(id))
                    .handle(exchange);
        });

        server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
        server.start();
        System.out.println("REST server started on :8080");
    }
}

Status Code Helper Utility

java
/**
 * Utility for selecting appropriate HTTP status codes.
 */
public final class HttpStatus {

    private HttpStatus() {}

    // 2xx
    public static final int OK                    = 200;
    public static final int CREATED               = 201;
    public static final int ACCEPTED              = 202;
    public static final int NO_CONTENT            = 204;
    public static final int PARTIAL_CONTENT       = 206;

    // 3xx
    public static final int MOVED_PERMANENTLY     = 301;
    public static final int NOT_MODIFIED          = 304;

    // 4xx
    public static final int BAD_REQUEST           = 400;
    public static final int UNAUTHORIZED          = 401;
    public static final int FORBIDDEN             = 403;
    public static final int NOT_FOUND             = 404;
    public static final int METHOD_NOT_ALLOWED    = 405;
    public static final int CONFLICT              = 409;
    public static final int GONE                  = 410;
    public static final int UNPROCESSABLE_ENTITY  = 422;
    public static final int TOO_MANY_REQUESTS     = 429;

    // 5xx
    public static final int INTERNAL_SERVER_ERROR = 500;
    public static final int BAD_GATEWAY           = 502;
    public static final int SERVICE_UNAVAILABLE   = 503;
    public static final int GATEWAY_TIMEOUT       = 504;

    /** Returns true for any 2xx status. */
    public static boolean isSuccess(int status) {
        return status >= 200 && status < 300;
    }

    /** Returns true for any 4xx status. */
    public static boolean isClientError(int status) {
        return status >= 400 && status < 500;
    }

    /** Returns true for any 5xx status. */
    public static boolean isServerError(int status) {
        return status >= 500 && status < 600;
    }

    /** Maps CRUD operation to its canonical HTTP method. */
    public static String crudToMethod(String operation) {
        return switch (operation.toUpperCase()) {
            case "CREATE" -> "POST";
            case "READ"   -> "GET";
            case "UPDATE" -> "PUT";
            case "PARTIAL_UPDATE" -> "PATCH";
            case "DELETE" -> "DELETE";
            default -> throw new IllegalArgumentException(
                    "Unknown operation: " + operation);
        };
    }
}

Best Practices

  1. Resource naming — Use plural nouns for collections (/users, /orders). Avoid verbs in URIs; let the HTTP method carry the action semantics (GET /users not GET /getUsers).

  2. Idempotency by design — Make PUT and DELETE unconditionally idempotent. For PATCH, use If-Match with ETags to avoid lost-update races.

  3. Caching headers — Return ETag on all GET responses. Set Cache-Control: no-store for sensitive data, Cache-Control: max-age=3600, must-revalidate for public resources. Use Last-Modified as a fallback when ETags are expensive.

  4. Versioning strategies — Prefer URI versioning (/v1/users) for clear lifecycle management. Consider Accept: application/vnd.api+json;version=2 (vendor media type) or the Accept-Version header for header-based versioning when URIs must remain stable.

  5. Consistent error bodies — Always return a structured error object (e.g., {"type": "...", "title": "...", "status": 404, "detail": "..."} per RFC 9457 / Problem Details).

  6. Use 201 + Location on POST — Always set the Location header pointing to the newly created resource so clients can retrieve it without a second lookup.

  7. Pagination — Use Link headers (RFC 5988) or X-Total-Count for collection pagination. Prefer cursor-based pagination for large, frequently-updated datasets.

  8. Rate limiting — Return 429 Too Many Requests with Retry-After and X-RateLimit-* headers to help clients back off gracefully.