A good endpoint is a small agreement: what comes in, what can happen, what comes out and how failure is represented.
Work through the path
Open a card when you need it. Marking a lesson complete is optional and stays only in this browser.
1Let HTTP status codes carry meaning
A predictable status code and response shape help clients handle success, absence, validation and conflict without guessing.
Use the status code for the broad outcome and the body for useful details. A successful create commonly returns 201 with a location or representation.
Consistency matters more than picking the theoretically perfect code for every edge case. Document the choice in OpenAPI and test it.
Try it
Write a table of outcomes for one endpoint: valid, missing, invalid, conflict and unexpected failure.
app.MapGet("/orders/{id:guid}", async (
Guid id, IOrderQueries queries, CancellationToken token) =>
{
var order = await queries.FindAsync(id, token);
return order is null ? Results.NotFound() : Results.Ok(order);
});2Validate at the boundary, enforce in the domain
Transport validation checks shape and basic rules; domain logic protects business invariants wherever the object is used.
A request DTO can reject an empty name or malformed identifier before the handler does work. The domain still needs rules such as a submitted order requiring at least one line.
Return a stable problem-details shape so clients can show field errors without parsing prose.
Try it
Separate a malformed request from a valid request that violates a business rule. Give each a different test and response reason.
public sealed record CreateLine(
Guid ProductId,
[property: Range(1, 10000)] int Quantity);
public sealed record CreateOrderRequest(
Guid CustomerId,
[property: MinLength(1)] IReadOnlyList<CreateLine> Lines);3Return errors clients can act on
Problem Details gives HTTP APIs a consistent error envelope without leaking stack traces or internal secrets.
Clients need a status, stable type or code and human-readable title. Validation errors also need field-level details.
Keep internal exception messages and connection details in server logs, not in responses. A correlation ID connects the two safely.
Try it
Take one exception response and remove details a consumer does not need. Put them in structured logs instead.
return Results.Problem(
statusCode: StatusCodes.Status409Conflict,
title: "Order cannot be submitted",
detail: "The order changed. Reload and try again.",
type: "https://developerha.com/problems/order-conflict");4Make retries safe for commands
A client may retry after a network failure even when the server already processed the command, so define an idempotency strategy.
GET is intended to be safe to repeat; payment or order submission may not be. An idempotency key associates repeats with the same intended operation and original result.
Store the key with a request fingerprint and result. Decide how long it is valid and what happens when the same key is reused with different input.
Try it
Simulate a client timeout after sending a command. Decide what the second request receives and how the server recognizes it.
POST /payments HTTP/1.1
Idempotency-Key: 4a8a0a8d-demo
Content-Type: application/json
{ "amount": 1200, "currency": "EUR" }No lesson matches that phrase.
Related tools
Turn the concept into a working example with the browser-based tools.