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
DELETEmay 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
| Method | Safe | Idempotent | Request body | Typical REST use | Typical success |
|---|---|---|---|---|---|
| GET | Yes | Yes | No defined semantics | Read a resource or collection | 200, 304 |
| HEAD | Yes | Yes | No defined semantics | Same as GET, headers only | 200 |
| OPTIONS | Yes | Yes | Optional | Capabilities; CORS preflight | 204/200 |
| POST | No | No | Yes | Create a subordinate resource, or run a command | 201 + Location, 200, 202 |
| PUT | No | Yes | Yes | Replace the resource at a known URI (create if absent) | 200, 204, 201 |
| PATCH | No | Not guaranteed | Yes | Partial update (RFC 5789). JSON Patch (RFC 6902) or JSON Merge Patch (RFC 7396) | 200, 204 |
| DELETE | No | Yes | No defined semantics | Remove the resource | 204, 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
| Code | Name | Use it when |
|---|---|---|
| 200 | OK | Success with a body |
| 201 | Created | Resource created; include Location |
| 202 | Accepted | Work queued; return a status URL to poll |
| 204 | No Content | Success with no body (for example DELETE, PUT) |
| 301 / 308 | Moved Permanently / Permanent Redirect | 308 keeps the method and body; 301 may turn POST into GET |
| 302 / 307 | Found / Temporary Redirect | 307 keeps the method and body |
| 303 | See Other | After POST, redirect the client to GET a result |
| 304 | Not Modified | Conditional GET matched (If-None-Match / If-Modified-Since) |
| 400 | Bad Request | Malformed syntax or failed validation (ASP.NET Core's [ApiController] default) |
| 401 | Unauthorized | Not authenticated. Must send WWW-Authenticate |
| 403 | Forbidden | Request understood but refused; refusal is not limited to authenticated callers |
| 404 | Not Found | No such resource (or you hide its existence from this caller) |
| 405 | Method Not Allowed | Method not supported; send Allow |
| 406 | Not Acceptable | Can't produce the format in Accept |
| 409 | Conflict | State conflict: duplicate, version clash, invalid transition |
| 410 | Gone | Permanently removed |
| 412 | Precondition Failed | If-Match / If-Unmodified-Since failed (optimistic concurrency) |
| 415 | Unsupported Media Type | Wrong Content-Type for the request body |
| 422 | Unprocessable Content | Well-formed but semantically invalid (RFC 9110 name) |
| 428 | Precondition Required | You require If-Match to prevent lost updates |
| 429 | Too Many Requests | Rate limited; send Retry-After |
| 500 | Internal Server Error | Unhandled server fault |
| 502 / 504 | Bad Gateway / Gateway Timeout | An upstream failed or timed out |
| 503 | Service Unavailable | Overloaded 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
| Header | Meaning |
|---|---|
Cache-Control: max-age=60 | Fresh for 60 s in any cache |
Cache-Control: private | Only a private (single-user) cache may store it |
Cache-Control: public | Shared caches may store it, even for authenticated requests |
Cache-Control: no-cache | May be stored, but must be revalidated before every reuse |
Cache-Control: no-store | Must not be stored at all (use for sensitive data) |
s-maxage | Overrides max-age for shared caches (CDN, proxy) |
ETag / If-None-Match | Validator for a representation. A match returns 304 for GET/HEAD, or 412 for other methods |
Last-Modified / If-Modified-Since | Date-based validator (one-second resolution) |
If-Match | Only apply the write if the current ETag matches, otherwise 412. Prevents lost updates |
Vary: Accept-Encoding, Accept | The 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 withContent-Type. - Use optimistic concurrency with an ETag on GET and require
If-Matchon PUT/PATCH/DELETE.
Versioning options
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL path | /v1/orders | Visible, cache-friendly, easy routing | The URI changes for the same resource |
| Query string | /orders?api-version=1.0 | Easy to default | Easy to forget; some caches vary poorly |
| Header | api-version: 1.0 or X-Api-Version | Clean URIs | Less visible; needs Vary |
| Media type | Accept: application/vnd.contoso.v1+json | Most "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 /paymentsretryable without an idempotency key, so a timeout + retry charges twice. - Using
no-cachewhen you meantno-storefor personal data. Cache-Control: publicon 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
- Safe vs idempotent: give a method that is idempotent but not safe.
- Is PATCH idempotent? When can it be?
- 401 vs 403, 400 vs 422, 409 vs 412: when do you use each?
- Walk through a conditional GET that returns 304.
- How would you stop a lost update between two editors?
- 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-cachevsno-store, andprivatevspublic? - Can I explain strong vs weak ETags and why
If-Matchneeds 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.
<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
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:
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).
Related on TechInterviewNotes
- Idempotency Keys for Reliable HTTP APIs: Semantics, Storage, and Failure Recovery
- ASP.NET Core Validation and Problem Details Companion
- Designing Retry Budgets and Backpressure for Resilient HTTP APIs
- Stable Pagination in .NET APIs
- .NET Networking Interview Checklist and the Networking for .NET Developers Field Guide (see HTTP/1.1, HTTP/2 and HTTP/3)
- Idempotency, Retries, and Transaction Boundaries
Sources
RFC 9110: HTTP Semantics (methods §9, status codes §15, conditional requests §13)
RFC 5789: PATCH Method for HTTP
RFC 6585: Additional HTTP Status Codes (428, 429)
RFC 9457: Problem Details for HTTP APIs
Microsoft Learn: Handle errors in ASP.NET Core APIs (Problem Details)
Microsoft Learn: Output caching middleware in ASP.NET Core
Microsoft Learn: RESTful web API design (Azure Architecture Center)
GitHub: dotnet/aspnet-api-versioning (Asp.Versioning packages)