OpenAPI: Describing and Testing SchoolApp.Api – Web API | Lesson 71
Introduction
A mobile app developer, a website developer, and a tester all need to know what SchoolApp.Api offers: which routes exist, what each one expects, and what each one returns. Writing that in a document by hand goes out of date the day the code changes.
OpenAPI is a standard format for describing an HTTP API, generated from the code itself. This lesson improves SchoolApp.Api's OpenAPI document and adds a page where anyone can read it and try every endpoint.
By the end of this lesson, you will be able to:
- Explain what an OpenAPI document is and who uses it.
- Add summaries and descriptions from
///XML comments. - Declare every possible response with
[ProducesResponseType], and avoid losing the 200. - Change the document's title with a document transformer, and hide endpoints from it.
- Browse and try the API with Scalar, and test it with
.httpfiles.
Why this matters
An API is only as useful as its documentation. With an accurate OpenAPI document, other teams can build clients without asking you questions, tools can generate client code in many languages, and testers can see every status code to check.
In interviews, "how do you document an API?" and "how do clients know what changed?" are common questions, and OpenAPI is the expected answer.
The concept
Example: what OpenAPI is
An OpenAPI document is a JSON (or YAML) file that lists every path, method, parameter, request body, response, and data shape of an API. Think of a school prospectus: every course, its timings, and its rules, in a standard layout that any parent can read.
OpenAPI used to be called Swagger; the name now belongs to a family of tools. Since .NET 9, the
Microsoft.AspNetCore.OpenApi package that the template added in Lesson 66 generates the document. In
.NET 10 it produces OpenAPI version 3.1.
Example: who uses the document
| User | Uses it to |
|---|---|
| Front-end and mobile developers | Learn routes and fields without reading C# |
| Code generators | Create a typed client in TypeScript, Kotlin, Swift, or C# |
| Testers and test tools | See every status code to check, and send requests |
| API gateways | Import routes and rules |
| You, six months later | Remember what an endpoint promises |
Example: what the generator can and cannot see
The generator reads routes, parameters, and return types from the code. It cannot read your intentions:
it does not know that an action may return 404, or what a parameter means in school terms. To the
generator, admissionNumber is just a string.
You add that knowledge in two ways: /// XML comments for words, and attributes such as
[ProducesResponseType] for responses. Both live next to the code, so they change with it, and a
reviewer sees both in the same pull request.
Apply it to the SMS
Step 1: the project file
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>a13055ff-100b-4e7f-bd1b-2cecb205a99a</UserSecretsId>
<!-- Lesson 71: write the /// comments to an XML file, so the OpenAPI document can include them.
1591 ("missing XML comment") is turned off: not every public type needs one. -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<!-- Lesson 66: the OpenAPI document. Lesson 71: Scalar, a page for reading and trying the API. -->
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.11" />
<PackageReference Include="Scalar.AspNetCore" Version="2.17.14" />
</ItemGroup>
<!-- Lesson 68: the rules (Core) and the data access (Data) from Modules 7 to 9. -->
<ItemGroup>
<ProjectReference Include="..\SchoolApp.Core\SchoolApp.Core.csproj" />
<ProjectReference Include="..\SchoolApp.Data\SchoolApp.Data.csproj" />
</ItemGroup>
</Project>
GenerateDocumentationFile writes the /// comments into an XML file during the build. The OpenAPI
package in .NET 10 picks them up automatically. Warning 1591 would complain about every public type
without a comment, so it is switched off.
Scalar.AspNetCore adds a documentation page. It is an open-source package, one of the options listed
in Microsoft's documentation for showing an OpenAPI document; Swagger UI is another.
Step 2: document the actions
using Microsoft.AspNetCore.Mvc;
using SchoolApp.Core.Dtos;
using SchoolApp.Core.Services;
namespace SchoolApp.Api.Controllers;
// Lesson 68: the first SMS endpoint backed by SchoolDb. The controller only translates HTTP:
// the route value goes into StudentService, and the result comes back as 200 or 404.
// Lesson 71: /// comments and Produces... attributes describe the endpoint in the OpenAPI document.
[ApiController]
[Route("api/students")]
[Produces("application/json")]
public class StudentsController : ControllerBase
{
private readonly StudentService _students;
// ASP.NET Core passes in a StudentService for each request (dependency injection, Lesson 69).
public StudentsController(StudentService students)
{
_students = students;
}
// GET /api/students/ADM-2017-0003
// ActionResult<StudentDto>: either a StudentDto (200 OK) or another result, such as 404.
/// <summary>Gets one student by admission number.</summary>
/// <param name="admissionNumber">The student's admission number, such as ADM-2017-0003.</param>
/// <param name="cancellationToken">Cancelled when the client gives up.</param>
/// <response code="200">The student.</response>
/// <response code="404">No student has this admission number.</response>
[HttpGet("{admissionNumber}")]
[ProducesResponseType(StatusCodes.Status200OK)] // the body type comes from ActionResult<StudentDto>
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<StudentDto>> GetByAdmissionNumber(string admissionNumber, CancellationToken cancellationToken)
{
StudentDto? student = await _students.FindAsync(admissionNumber, cancellationToken);
if (student is null)
{
return NotFound();
}
return student; // converted to Ok(student)
}
}
<summary> describes the action, <param> each parameter, and <response> each status code. The
attributes tell the generator which status codes exist, and [Produces("application/json")] says the
API answers only in JSON.
Example: the missing 200
The first try had only [ProducesResponseType(StatusCodes.Status404NotFound)]. The document then showed
the 404, and no 200 at all: once you declare any response, the generator stops guessing the success
one from ActionResult<StudentDto>.
Adding [ProducesResponseType(StatusCodes.Status200OK)] brought it back, with StudentDto as the body,
still taken from ActionResult<StudentDto>. Declare every response, success included.
Step 3: the document's title, and hidden endpoints
// Lesson 66: the start of SchoolApp.Api. Program.cs has two halves:
// 1. Register services (the "builder" part), 2. build the app and describe how requests are handled.
using SchoolApp.Api.Diagnostics;
using SchoolApp.Api.Middleware;
using SchoolApp.Api.Options;
using Scalar.AspNetCore;
using SchoolApp.Core.Services;
using SchoolApp.Data;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Services: things the app can use. AddControllers finds every controller class in this project.
builder.Services.AddControllers();
// Lesson 71: the OpenAPI document, with a title and description of our own.
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
document.Info.Title = "SchoolApp API";
document.Info.Description = "The School Management System API from the NexCoding SMS course.";
return Task.CompletedTask;
});
});
// Lesson 70: settings come from configuration: appsettings.json, then appsettings.{Environment}.json,
// then user secrets (Development only), then environment variables, then the command line.
// A later source overrides an earlier one.
string? connectionString = builder.Configuration.GetConnectionString("SchoolDb");
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new InvalidOperationException("The connection string 'SchoolDb' is missing or empty.");
}
// The "School" section as a SchoolOptions object, checked when the app starts.
builder.Services.AddOptions<SchoolOptions>()
.BindConfiguration(SchoolOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
// Lesson 69: the dependency injection container. Each line says "when something asks for X, give it Y,
// and keep it for this long". SchoolApp.Data registers its own classes.
builder.Services.AddSchoolData(connectionString);
builder.Services.AddScoped<StudentService>();
builder.Services.AddScoped<FeePaymentService>();
// Lesson 69: the three lifetimes, side by side (see DiagnosticsController).
builder.Services.AddSingleton<SingletonExample>(); // one object for the whole app
builder.Services.AddScoped<ScopedExample>(); // one object per request
builder.Services.AddTransient<TransientExample>(); // a new object every time one is asked for
builder.Services.AddScoped<LifetimeReporter>();
WebApplication app = builder.Build();
// The request pipeline: what happens to every request, in this order (Lesson 67).
// Each Use... adds one middleware; a request goes down the list, and the response comes back up.
app.UseRequestTiming(); // first, so its time includes everything below it
if (app.Environment.IsDevelopment())
{
app.MapOpenApi(); // /openapi/v1.json, only while developing
app.MapScalarApiReference(); // Lesson 71: /scalar, a page to read and try every endpoint
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers(); // send each request to the matching controller action
app.Run(); // start the web server and wait for requests
A document transformer changes the finished document. Here it replaces the default title, "SchoolApp.Api | v1", with "SchoolApp API" and adds a description.
[ApiExplorerSettings(IgnoreApi = true)] on DiagnosticsController keeps the teaching endpoint from
Lesson 69 out of the document. Hiding it from the document does not protect it; its Development check
does that.
Step 4: read the document
Run the API and open http://localhost:5080/openapi/v1.json. The student endpoint now reads:
"/api/students/{admissionNumber}": {
"get": {
"tags": [
"Students"
],
"summary": "Gets one student by admission number.",
"parameters": [
{
"name": "admissionNumber",
"in": "path",
"description": "The student's admission number, such as ADM-2017-0003.",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "The student.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StudentDto"
}
}
}
},
"404": {
"description": "No student has this admission number.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
}
Every sentence came from the XML comments, and the 404 body is the ProblemDetails shape from Lesson 68.
StudentDto is described once, under components, and each response points to it with $ref. The
tags entry, Students, comes from the controller's name; pages such as Scalar group endpoints by tag.
Example: the error shape is documented too
The ProblemDetails schema under components lists its five standard fields: type, title,
status, detail, and instance. A client developer reading the document now knows that every error
from this endpoint has the same shape, and can write one piece of error-handling code for all of them.
Lesson 74 extends ProblemDetails with SMS error numbers, such as 50013, and the document will show those responses in the same way.
Example: numbers that may be strings
In the StudentDto schema, id has the type ["integer", "string"], not just integer. ASP.NET Core's
JSON settings for web apps accept numbers written as strings, such as "3", and the document honestly
says so.
Most clients send plain numbers anyway. If a client generator handles this badly, it can be changed in the JSON options, but it is worth knowing why it looks that way before anyone "fixes" it.
Step 5: the Scalar page
Open http://localhost:5080/scalar in a browser; it redirects to /scalar/v1. The page lists every
endpoint, its parameters, and its responses, and each endpoint has a button to send a real request.
The page and the document are mapped only in Development, so a production server shows neither. Many
companies publish their documents on purpose, but that should be a decision, not an accident.
The page reads the same /openapi/v1.json you just opened. It adds nothing of its own: if the document
is missing a response, the page is missing it too. Improving the document improves every tool that
reads it.
Step 6: .http files for repeatable checks
# Lesson 66: requests you can send from Visual Studio or VS Code (REST Client):
# click "Send Request" above each one. ### separates requests.
@SchoolApp.Api_HostAddress = http://localhost:5080
GET {{SchoolApp.Api_HostAddress}}/api/school
Accept: application/json
###
# Lesson 71: the OpenAPI document. Open http://localhost:5080/scalar in a browser to read it as a page.
GET {{SchoolApp.Api_HostAddress}}/openapi/v1.json
###
# Lesson 68: a student from SchoolDb, from the controller and from the minimal API.
GET {{SchoolApp.Api_HostAddress}}/api/students/ADM-2017-0003
###
GET {{SchoolApp.Api_HostAddress}}/api/students/ADM-1999-0001
###
# Lesson 69: which objects dependency injection created. Send it twice and compare the ids.
GET {{SchoolApp.Api_HostAddress}}/api/diagnostics/lifetimes
###
The Scalar page is good for exploring; the .http file is good for repeating. It lives in the repository,
so every developer can run the same requests after every change, and variables such as
{{SchoolApp.Api_HostAddress}} keep the address in one place.
Example: the document as a contract
Once other teams build on the document, it is a contract: a promise about what the API accepts and returns. Some changes keep the promise, and some break it:
| Change | Breaks existing clients? |
|---|---|
| Add a new endpoint | No |
| Add an optional field to a response | No, for well-written clients |
| Add a required request field | Yes: old requests now fail with 400 |
| Rename or remove a response field | Yes: clients read a field that is gone |
| Change a field's type, such as number to text | Yes |
Comparing the document before and after a change, in every pull request, catches breaking changes before users do. Module 12 adds API versioning for the changes that must break.
Common mistakes
- Declaring only error responses. The
200disappears from the document. Fix: declare every response, success included. - Comments without
GenerateDocumentationFile. Summaries never reach the document. Fix: turn it on in the project file. - A document out of step with the code. Hand-written documents drift. Fix: generate it from the code, and review it in pull requests.
- Publishing development endpoints. Diagnostic routes appear in public documents. Fix:
IgnoreApi, and real protection on the endpoint itself. - Showing the documentation page in production by accident. Fix: map it in
Developmentonly, unless publishing is a deliberate choice.
In real projects
A school's mobile team kept asking the API team the same questions: which fields are optional, what the error looks like, which status means "already paid". Every answer was an email, and half of them went out of date.
We generated the client code for the mobile app from the OpenAPI document instead. A change in the API now showed up as a compile error in the app the same day, not as a crash in front of a parent.
That only worked because the document was complete, every response declared. The attributes took an afternoon to add; the emails stopped for good.
Best practices
- Generate the OpenAPI document from code; never write it by hand.
- Give every action a summary, and declare every status code it can return.
- Keep DTOs as response types, so the document shows exactly what clients get.
- Hide internal endpoints, and protect them separately.
- Keep a
.httpfile with a request for every endpoint.
Recap
- OpenAPI describes an API's paths, parameters, bodies, and responses in a standard JSON document.
- .NET 10 generates it with
Microsoft.AspNetCore.OpenApi, as OpenAPI 3.1. ///comments add descriptions;[ProducesResponseType]adds status codes, and every one must be declared.- A document transformer sets the title;
IgnoreApihides an endpoint. - Scalar shows the document as a page with a Send button;
.httpfiles keep repeatable requests.
Homework
Library module task: In a single-file library API, add GET /api/books/{isbn} with XML comments for
the summary, the parameter, and both responses, plus [ProducesResponseType] for 200 and 404. Set the
document's title to "Library API", then open /openapi/v1.json and find your descriptions.
A correct result looks like (part of the document):
"/api/books/{isbn}": {
"get": {
"summary": "Gets one book by its ISBN, with the number of copies on the shelf.",
"parameters": [
{
"name": "isbn",
"in": "path",
"description": "The 13-digit ISBN.",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "The book."
},
"404": {
"description": "The library has no book with this ISBN."
}
}
}
}
Show the solution
// Library homework, Lesson 71: describe the library API in an OpenAPI document, with summaries from
// /// comments and both possible responses of each action.
// Run with: dotnet run library-assignment/071-library-openapi.cs
// Then open http://localhost:5071/openapi/v1.json
#:sdk Microsoft.NET.Sdk.Web
#:package Microsoft.AspNetCore.OpenApi@10.0.11
#:property PublishAot=false
#:property GenerateDocumentationFile=true
#:property NoWarn=$(NoWarn);1591
using Microsoft.AspNetCore.Mvc;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddOpenApi(options => options.AddDocumentTransformer((document, context, cancellationToken) =>
{
document.Info.Title = "Library API";
return Task.CompletedTask;
}));
WebApplication app = builder.Build();
app.MapOpenApi();
app.MapControllers();
app.Run("http://localhost:5071");
public record Book(string Isbn, string Title, int AvailableCopies);
[ApiController]
[Route("api/books")]
[Produces("application/json")]
public class BooksController : ControllerBase
{
private static readonly Book[] Books =
[
new Book("9788173711466", "Wings of Fire", 2),
new Book("9780141325293", "The Jungle Book", 0),
new Book("9780143039655", "Malgudi Days", 1),
];
/// <summary>Gets one book by its ISBN, with the number of copies on the shelf.</summary>
/// <param name="isbn">The 13-digit ISBN.</param>
/// <response code="200">The book.</response>
/// <response code="404">The library has no book with this ISBN.</response>
[HttpGet("{isbn}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<Book> GetByIsbn(string isbn)
{
Book? book = Books.FirstOrDefault(b => b.Isbn == isbn);
return book is null ? NotFound() : book;
}
}
The #:package and #:property lines do in a single-file app what the project file does in
SchoolApp.Api. The output above is shortened: the real document also shows each response's content type.
Quiz
What is an OpenAPI document?
Show answer
Answer: B. A standard JSON description of an API's paths, parameters, and responses SchoolApp.Api serves it at /openapi/v1.json.
After adding only [ProducesResponseType(404)], what happened to the 200?
Show answer
Answer: B. It disappeared from the document Declare every response once you declare any.
What makes /// comments appear in the document?
Show answer
Answer: B. GenerateDocumentationFile in the project file The build writes them to an XML file the generator reads.
Does [ApiExplorerSettings(IgnoreApi = true)] protect an endpoint?
Show answer
Answer: B. No, it only hides it from the document DiagnosticsController is protected by its own Development check.
Why keep a .http file when the Scalar page can send requests?
Show answer
Answer: B. The file lives in the repository, so everyone can repeat the same checks Scalar is for exploring; the .http file is for repeating.
Interview questions
Questions are grouped by experience level. Click a question to see a model answer. All questions: Web API interview questions.
Fresher (0-2 yrs)
F What is OpenAPI, and how is it related to Swagger? concept
OpenAPI is a standard format, in JSON or YAML, that describes an HTTP API: paths, methods, parameters, bodies, responses, and data shapes. It was originally called the Swagger specification; Swagger is now the name of a family of tools around it, such as Swagger UI.
F Who uses an API's OpenAPI document? concept
Front-end and mobile developers to learn routes and fields, code generators to create typed clients, testers and test tools to know every status code, API gateways to import routes, and the API team itself as a reference and contract.
F How do you add descriptions to an ASP.NET Core API's OpenAPI document? coding
Write /// XML comments on actions, with <summary>, <param>, and <response>, and set
GenerateDocumentationFile to true in the project file. In .NET 10, the OpenAPI package reads the
generated XML file and adds the text to the document.
Mid (3-5 yrs)
M What does [ProducesResponseType] do, and what is the common mistake with it? coding
It declares a status code, and optionally a body type, that an action can return, so the OpenAPI document lists it. Once any response is declared, the success response is no longer inferred: declaring only 404 removed the 200. Declare every response, including the success one.
M How do you change the document's title, or hide an endpoint from it? coding
Register a document transformer in AddOpenApi(options => options.AddDocumentTransformer(...)) and set
document.Info.Title. Hide a controller or action with [ApiExplorerSettings(IgnoreApi = true)];
hiding does not protect it, so secure the endpoint separately.
M Why does an integer field appear as type integer or string in an ASP.NET Core OpenAPI document? concept
ASP.NET Core's web JSON defaults allow reading numbers from strings, such as "3", so the schema honestly lists both types with a number pattern. Clients normally send numbers; change the JSON number handling if a client generator copes badly with it.
Senior (6-9 yrs)
S Which API changes are breaking for clients, and how do you catch them before release? design
Breaking: removing or renaming a response field, changing a field's type, adding a required request field, removing an endpoint or changing its meaning. Usually safe: adding endpoints or optional fields. Generate the OpenAPI document in the build and compare it with the previous version in every pull request, failing the build on breaking differences.
How to answer: Give examples on both sides, then describe automated contract comparison and what you do when a breaking change is unavoidable (versioning, deprecation period).
S Should the OpenAPI document and a documentation page be available in production? design
Only as a deliberate decision. Public APIs often publish them, sometimes behind authentication; internal APIs may expose them only inside the network or not at all. Never expose them by accident, and never rely on hiding endpoints from the document as security.
How to answer: Weigh discoverability for partners against exposing the attack surface, and describe controls: environment checks, authentication, or publishing a static copy of the document instead.
Architect / Lead (10+ yrs)
A How would you use OpenAPI to keep a mobile app and an API in step across several teams? design
Treat the document as the contract: generate it from the API in CI, publish each version, generate the mobile client from it, and compare versions to detect breaking changes. Require complete response declarations and descriptions in review, and version the API when a break is necessary.
How to answer: Cover generation, publication, client generation, contract checks, and versioning, and the team agreements that make it work, such as reviewing document diffs.
Resources
- Code for this lesson: git tag
lesson-71-openapi(code repository coming soon) - Files:
src/SchoolApp.Api/SchoolApp.Api.csproj,src/SchoolApp.Api/Controllers/StudentsController.cs,src/SchoolApp.Api/Program.cs,src/SchoolApp.Api/SchoolApp.Api.http,library-assignment/071-library-openapi.cs - Video: coming soon
What's next
The document shows two paths so far. Next: routing in depth, with route templates, constraints, lowercase URLs, nested resources, and query strings for paging the 10,012 students. Lesson 72: Routing
Want live sessions, code reviews, and interview preparation? Join the next batch at Sahasra Technologies.