Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close
    Cheat sheets: all cheat sheets

    HTTP and REST Essentials Cheat Sheet: Methods, Idempotency, Status Codes, Caching, Versioning

    Safe vs idempotent methods, the status codes interviewers expect, Cache-Control and ETag rules, and API versioning options, with a runnable demo of 201, 304, 412 and 428.

    Learning outcome

    By the end of this cheat sheet you should be able to answer the HTTP and REST questions that come up in almost every backend interview: which methods are safe and which are idempotent, which status code fits which situation, how conditional requests and caching headers work, and how to version an API. The definitions follow RFC 9110 (HTTP Semantics) and RFC 9111 (HTTP Caching), which replaced RFC 7231–7235 in 2022.

    Intuition

    • Safe means the client is not asking for any state change. A crawler can call it freely.
    • Idempotent means sending the same request once or ten times leaves the server in the same intended state. Responses can still differ: a second DELETE may return 404. Idempotency is what makes automatic retries safe.
    • Status codes are part of your contract. Clients, proxies and retry policies make decisions on the class (2xx, 4xx, 5xx) before they ever read the body.

    Quick reference: methods

    MethodSafeIdempotentRequest bodyTypical REST useTypical success
    GETYesYesNo defined semanticsRead a resource or collection200, 304
    HEADYesYesNo defined semanticsSame as GET, headers only200
    OPTIONSYesYesOptionalCapabilities; CORS preflight204/200
    POSTNoNoYesCreate a subordinate resource, or run a command201 + Location, 200, 202
    PUTNoYesYesReplace the resource at a known URI (create if absent)200, 204, 201
    PATCHNoNot guaranteedYesPartial update (RFC 5789). JSON Patch (RFC 6902) or JSON Merge Patch (RFC 7396)200, 204
    DELETENoYesNo defined semanticsRemove the resource204, 200, 202

    To make a POST safely retryable, accept an Idempotency-Key header and store the first outcome per key. See Idempotency Keys for Reliable HTTP APIs.

    Quick reference: status codes you must know

    CodeNameUse it when
    200OKSuccess with a body
    201CreatedResource created; include Location
    202AcceptedWork queued; return a status URL to poll
    204No ContentSuccess with no body (for example DELETE, PUT)
    301 / 308Moved Permanently / Permanent Redirect308 keeps the method and body; 301 may turn POST into GET
    302 / 307Found / Temporary Redirect307 keeps the method and body
    303See OtherAfter POST, redirect the client to GET a result
    304Not ModifiedConditional GET matched (If-None-Match / If-Modified-Since)
    400Bad RequestMalformed syntax or failed validation (ASP.NET Core's [ApiController] default)
    401UnauthorizedNot authenticated. Must send WWW-Authenticate
    403ForbiddenRequest understood but refused; refusal is not limited to authenticated callers
    404Not FoundNo such resource (or you hide its existence from this caller)
    405Method Not AllowedMethod not supported; send Allow
    406Not AcceptableCan't produce the format in Accept
    409ConflictState conflict: duplicate, version clash, invalid transition
    410GonePermanently removed
    412Precondition FailedIf-Match / If-Unmodified-Since failed (optimistic concurrency)
    415Unsupported Media TypeWrong Content-Type for the request body
    422Unprocessable ContentWell-formed but semantically invalid (RFC 9110 name)
    428Precondition RequiredYou require If-Match to prevent lost updates
    429Too Many RequestsRate limited; send Retry-After
    500Internal Server ErrorUnhandled server fault
    502 / 504Bad Gateway / Gateway TimeoutAn upstream failed or timed out
    503Service UnavailableOverloaded or in maintenance; Retry-After

    Errors in .NET APIs: return Problem Details (application/problem+json, RFC 9457, which obsoletes RFC 7807). In ASP.NET Core, call builder.Services.AddProblemDetails() and use Results.Problem(...) / ValidationProblem(...).

    Retry rule of thumb: retry idempotent requests on 408, 429, 502, 503, 504 and network failures, with backoff, jitter and a budget. Don't blindly retry 4xx.

    Caching and conditional requests

    HeaderMeaning
    Cache-Control: max-age=60Fresh for 60 s in any cache
    Cache-Control: privateOnly a private (single-user) cache may store it
    Cache-Control: publicShared caches may store it, even for authenticated requests
    Cache-Control: no-cacheMay be stored, but must be revalidated before every reuse
    Cache-Control: no-storeMust not be stored at all (use for sensitive data)
    s-maxageOverrides max-age for shared caches (CDN, proxy)
    ETag / If-None-MatchValidator for a representation. A match returns 304 for GET/HEAD, or 412 for other methods
    Last-Modified / If-Modified-SinceDate-based validator (one-second resolution)
    If-MatchOnly apply the write if the current ETag matches, otherwise 412. Prevents lost updates
    Vary: Accept-Encoding, AcceptThe cache key also includes these request headers

    Strong vs weak ETags: "abc" is strong (byte-identical). W/"abc" is weak (semantically equivalent) and can't be used with If-Match, which requires strong comparison.

    In ASP.NET Core, response caching middleware honours HTTP cache headers. Output caching (.NET 7+) is server-controlled and supports tag-based eviction.

    REST design rules interviewers expect

    • Resources are nouns, plural and hierarchical: /orders/42/lines. Actions are expressed with methods, not with /getOrder.
    • Use the query string for filtering, sorting and paging (?status=open&sort=-createdAt&limit=50&cursor=...). Prefer cursor (keyset) paging for large, changing sets.
    • POST /orders → 201 Created, Location: /orders/42, with the body as the created resource.
    • Use HTTP status codes for errors and Problem Details for the body. Don't return 200 with "success": false.
    • Content negotiation: the client sends Accept, and you describe the request body with Content-Type.
    • Use optimistic concurrency with an ETag on GET and require If-Match on PUT/PATCH/DELETE.

    Versioning options

    StrategyExampleProsCons
    URL path/v1/ordersVisible, cache-friendly, easy routingThe URI changes for the same resource
    Query string/orders?api-version=1.0Easy to defaultEasy to forget; some caches vary poorly
    Headerapi-version: 1.0 or X-Api-VersionClean URIsLess visible; needs Vary
    Media typeAccept: application/vnd.contoso.v1+jsonMost "RESTful"Hardest for clients and tooling

    In .NET, the Asp.Versioning.Http (minimal APIs) and Asp.Versioning.Mvc (controllers) packages support all four, plus api-supported-versions / api-deprecated-versions reporting. Version on breaking changes only. Adding optional fields is usually compatible with tolerant clients, but verify the existing serialization and client contracts.

    Failure modes

    • Returning 401 for "logged in but not allowed". That is 403.
    • Making POST /payments retryable without an idempotency key, so a timeout + retry charges twice.
    • Using no-cache when you meant no-store for personal data.
    • Cache-Control: public on per-user responses behind a CDN.
    • PUT that behaves like PATCH (missing fields left untouched) without documenting it.
    • Lost updates because PUT ignores ETags.

    Interview drill

    1. Safe vs idempotent: give a method that is idempotent but not safe.
    2. Is PATCH idempotent? When can it be?
    3. 401 vs 403, 400 vs 422, 409 vs 412: when do you use each?
    4. Walk through a conditional GET that returns 304.
    5. How would you stop a lost update between two editors?
    6. Pick a versioning strategy for a public API and defend it.

    Revision checklist

    • Can I fill in the methods table (safe / idempotent) from memory?
    • Can I name the status code for created, accepted, conflict, precondition failed and rate limited?
    • Do I know no-cache vs no-store, and private vs public?
    • Can I explain strong vs weak ETags and why If-Match needs strong ones?
    • Can I name the four versioning strategies and the .NET packages?

    Worked executable example

    A tiny in-memory minimal API returns 201 Created with a Location header, serves an ETag, answers a matching If-None-Match with 304, and rejects a stale If-Match with 412. It runs in memory with TestServer, so no port is opened.

    Run this example

    Use the .NET 10 SDK. In a new empty folder, save the project below as Example.csproj and the listing as Program.cs. Run dotnet run --project Example.csproj.

    XML
    <Project Sdk="Microsoft.NET.Sdk.Web">
      <PropertyGroup>
        <TargetFramework>net10.0</TargetFramework>
        <ImplicitUsings>disable</ImplicitUsings>
        <Nullable>enable</Nullable>
      </PropertyGroup>
      <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="10.0.0" />
      </ItemGroup>
    </Project>

    Program.cs

    C#
    using System;
    using System.Collections.Concurrent;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Net.Http.Json;
    using System.Threading;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.TestHost;
    
    var store = new ConcurrentDictionary<int, Item>();
    var nextId = 0;
    
    var builder = WebApplication.CreateBuilder(args);
    builder.WebHost.UseTestServer();
    var app = builder.Build();
    
    app.MapPost("/items", (ItemInput input) =>
    {
        var item = new Item(Interlocked.Increment(ref nextId), input.Name, Version: 1);
        store[item.Id] = item;
        return Results.Created($"/items/{item.Id}", item);
    });
    
    app.MapGet("/items/{id:int}", (int id, HttpContext http) =>
    {
        if (!store.TryGetValue(id, out var item)) return Results.NotFound();
        var etag = $"\"v{item.Version}\"";
        http.Response.Headers.ETag = etag;
        http.Response.Headers.CacheControl = "private, no-cache"; // store, but revalidate before reuse
        if (http.Request.Headers.IfNoneMatch.ToString() == etag) return Results.StatusCode(StatusCodes.Status304NotModified);
        return Results.Ok(item);
    });
    
    app.MapPut("/items/{id:int}", (int id, ItemInput input, HttpContext http) =>
    {
        if (!store.TryGetValue(id, out var current)) return Results.NotFound();
        var ifMatch = http.Request.Headers.IfMatch.ToString();
        if (string.IsNullOrEmpty(ifMatch)) return Results.StatusCode(StatusCodes.Status428PreconditionRequired);
        if (ifMatch != $"\"v{current.Version}\"") return Results.StatusCode(StatusCodes.Status412PreconditionFailed);
        var updated = current with { Name = input.Name, Version = current.Version + 1 };
        if (!store.TryUpdate(id, updated, current))
            return Results.StatusCode(StatusCodes.Status412PreconditionFailed);
        return Results.NoContent();
    });
    
    await app.StartAsync();
    var client = app.GetTestClient();
    
    var created = await client.PostAsJsonAsync("/items", new ItemInput("pen"));
    Console.WriteLine($"POST -> {(int)created.StatusCode}, Location: {created.Headers.Location}");
    
    var get = await client.GetAsync("/items/1");
    var etag = get.Headers.ETag!.Tag;
    Console.WriteLine($"GET -> {(int)get.StatusCode}, ETag: {etag}");
    
    var conditional = new HttpRequestMessage(HttpMethod.Get, "/items/1");
    conditional.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(etag));
    Console.WriteLine($"GET If-None-Match -> {(int)(await client.SendAsync(conditional)).StatusCode}");
    
    Console.WriteLine($"PUT without If-Match -> {(int)(await client.PutAsJsonAsync("/items/1", new ItemInput("x"))).StatusCode}");
    
    var put1 = new HttpRequestMessage(HttpMethod.Put, "/items/1") { Content = JsonContent.Create(new ItemInput("pencil")) };
    put1.Headers.IfMatch.Add(new EntityTagHeaderValue(etag));
    Console.WriteLine($"PUT If-Match current -> {(int)(await client.SendAsync(put1)).StatusCode}");
    
    var put2 = new HttpRequestMessage(HttpMethod.Put, "/items/1") { Content = JsonContent.Create(new ItemInput("marker")) };
    put2.Headers.IfMatch.Add(new EntityTagHeaderValue(etag)); // stale: the version moved on
    Console.WriteLine($"PUT If-Match stale -> {(int)(await client.SendAsync(put2)).StatusCode}");
    
    await app.StopAsync();
    
    record ItemInput(string Name);
    record Item(int Id, string Name, int Version);

    Expected output

    The host may also print its own startup log lines. The request lines are:

    Text
    POST -> 201, Location: /items/1
    GET -> 200, ETag: "v1"
    GET If-None-Match -> 304
    PUT without If-Match -> 428
    PUT If-Match current -> 204
    PUT If-Match stale -> 412

    The ETag comparison here is a simplified exact string match for a single strong ETag. Production code should parse lists and *, use weak comparison for If-None-Match, and use strong comparison for If-Match (see RFC 9110 §13.1).