OpenAPI Documentation
By the end of this lesson
Produce accurate machine-readable documentation from your code.
OpenAPI is a standard format for describing an HTTP API: which paths exist, which methods each supports, what the request and response bodies look like, which status codes can come back, and what authentication is needed. The description is a JSON or YAML document, written to be read by tools rather than only by people.
That distinction is the point. A page of prose describing your endpoints helps a developer read; a machine-readable description lets software act. Tools generate a browsable reference, build client libraries, stand up mock servers, configure gateways and check that responses match what you promised.
The document can be written by hand or generated from your code, and the difference decides whether anyone can trust it. A hand-maintained document is accurate on the day it is written and wrong the first time someone ships an endpoint without updating it. Nothing warns you: the code compiles, the tests pass, and the documentation quietly becomes fiction.
Generated documentation cannot drift on the parts it derives from the code. Paths, methods, parameter names and types, and the shape of your request and response models all come from the types you actually compiled. Rename a field and the document renames it too.
Generation is not a complete answer, though, and it is worth being clear about the limit. The generator can see your types; it cannot see your intentions. It does not know that this endpoint can return 409, or what the error body looks like, or that startDate must fall inside the department's lifetime. Everything in that category has to be stated, and the annotations below are how you state it.
builder.Services.AddOpenApi();
var app = builder.Build();
// Serves the document at /openapi/v1.json
app.MapOpenApi();
app.MapGet("/api/employees/{id:int}", async (int id, EmployeeService employees) =>
await employees.FindAsync(id) is { } employee
? Results.Ok(employee)
: Results.NotFound())
.WithSummary("Read one employee")
.WithDescription("Readable by the employee, their manager, or an HR role.")
.Produces<EmployeeResponse>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status401Unauthorized)
.ProducesProblem(StatusCodes.Status403Forbidden)
.ProducesProblem(StatusCodes.Status404NotFound);
app.MapPost("/api/employees", EmployeeEndpoints.Create)
.WithSummary("Create an employee")
.Produces<EmployeeResponse>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.ProducesProblem(StatusCodes.Status409Conflict);- AddOpenApi and MapOpenApi come with ASP.NET Core in current versions and produce the document from your endpoints and types. Projects created a few years ago typically use the Swashbuckle package for the same job, and the annotations below apply either way.
- MapOpenApi serves the raw document. A browsable interface is a separate component you add if you want one — the document itself is what tools consume.
- Produces<EmployeeResponse> names the type behind the 200. Without it the generator knows an IResult comes back and can say nothing about its shape, so a caller reading the document learns nothing about the response.
- The three ProducesProblem lines are the part most often left out. They tell a caller that 401, 403 and 404 are expected outcomes with a problem document in the body, which is what lets a client handle them deliberately rather than by discovery.
- ProducesValidationProblem describes the 400 shape with its field errors, matching the format from the validation lesson.
- WithSummary and WithDescription carry the intent no signature can express. Keep them short and factual; a stale paragraph is worse than no paragraph.
What to document beyond the successful case. Every item here is something a caller has to know and cannot infer from your types:
- Every status code the endpoint can return, not only the 2xx
- The error body shape, so a caller can write one handler for all of them
- Which fields are required, and any length, range or format rules
- Allowed values for enums, in the string form the API actually sends
- The authentication needed, and the role or scope for each endpoint
- Paging, filtering and sorting parameters, including the maximum page size and the sortable fields
- Anything eventually consistent, such as a search index that lags behind a write
Summary
- OpenAPI describes an API in a form tools can act on, not only read
- Generating it from code removes drift in paths, types and model shapes
- A generator cannot infer status codes, error shapes or business rules, so those must be annotated
- Document every failure code and the error body, because that is what callers have to handle
- Short, factual summaries age better than paragraphs describing behaviour
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Find the gaps in your own document
Generate the OpenAPI document for an API you work on and open it. Pick one endpoint that can fail in more than one way.
Check whether the document lists every status code it can return, describes the error body, and states the authentication required. Note each gap.
Show solution
The common result is that 200 and its type are described and the failures are not, because the successful shape is the part the generator can infer.
Closing the gaps is mostly annotation: ProducesProblem for each failure status, ProducesValidationProblem for the 400, and a short summary saying who may call it.
The gap worth thinking about hardest is the error shape. If your endpoints do not all fail the same way, the document will show that plainly — which makes it a useful review of the work from the error format lesson.
Think about it
Why drift is worse than absence
Argue that documentation which is confidently wrong causes more harm than no documentation at all. Then describe what stops a generated document from being confidently wrong about behaviour.
Show solution
With no documentation, a developer reads the API cautiously: they call an endpoint, inspect what comes back, and build on what they observed. With wrong documentation, they build on a stated contract and only find out at the point of failure — often after shipping.
Generation removes drift from anything derived from code: paths, methods, parameter types, model shapes. Those cannot disagree with the implementation because they are read from it.
It does not protect the parts a human wrote. Summaries, descriptions and examples still age, so they need reviewing whenever the endpoint changes. Keeping them short is the practical defence: a one-line summary is easy to correct, and a paragraph describing behaviour is where the fiction accumulates.
Saved in this browser only.