Skip to main content

OpenAPI: Describing and Testing SchoolApp.Api – Web API | Lesson 71

Published / updated

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 .http files.

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​

UserUses it to
Front-end and mobile developersLearn routes and fields without reading C#
Code generatorsCreate a typed client in TypeScript, Kotlin, Swift, or C#
Testers and test toolsSee every status code to check, and send requests
API gatewaysImport routes and rules
You, six months laterRemember 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​

src/SchoolApp.Api/SchoolApp.Api.csproj
<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​

src/SchoolApp.Api/Controllers/StudentsController.cs
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​

src/SchoolApp.Api/Program.cs
// 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:

Output (part of /openapi/v1.json)
"/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​

src/SchoolApp.Api/SchoolApp.Api.http
# 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:

ChangeBreaks existing clients?
Add a new endpointNo
Add an optional field to a responseNo, for well-written clients
Add a required request fieldYes: old requests now fail with 400
Rename or remove a response fieldYes: clients read a field that is gone
Change a field's type, such as number to textYes

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​

  1. Declaring only error responses. The 200 disappears from the document. Fix: declare every response, success included.
  2. Comments without GenerateDocumentationFile. Summaries never reach the document. Fix: turn it on in the project file.
  3. A document out of step with the code. Hand-written documents drift. Fix: generate it from the code, and review it in pull requests.
  4. Publishing development endpoints. Diagnostic routes appear in public documents. Fix: IgnoreApi, and real protection on the endpoint itself.
  5. Showing the documentation page in production by accident. Fix: map it in Development only, unless publishing is a deliberate choice.

In real projects​

From 16 years of .NET 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 .http file 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; IgnoreApi hides an endpoint.
  • Scalar shows the document as a page with a Send button; .http files 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):

Output
"/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-assignment/071-library-openapi.cs
// 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​

  1. What is an OpenAPI document?

    1. A test report
    2. A standard JSON description of an API's paths, parameters, and responses
    3. A database schema
    4. A login page
    Show answer

    Answer: B. A standard JSON description of an API's paths, parameters, and responses SchoolApp.Api serves it at /openapi/v1.json.

  2. After adding only [ProducesResponseType(404)], what happened to the 200?

    1. It stayed
    2. It disappeared from the document
    3. It became 201
    4. The build failed
    Show answer

    Answer: B. It disappeared from the document Declare every response once you declare any.

  3. What makes /// comments appear in the document?

    1. Nothing; they always appear
    2. GenerateDocumentationFile in the project file
    3. A database setting
    4. The Scalar package
    Show answer

    Answer: B. GenerateDocumentationFile in the project file The build writes them to an XML file the generator reads.

  4. Does [ApiExplorerSettings(IgnoreApi = true)] protect an endpoint?

    1. Yes, it blocks requests
    2. No, it only hides it from the document
    3. Only in Production
    4. Only for POST
    Show answer

    Answer: B. No, it only hides it from the document DiagnosticsController is protected by its own Development check.

  5. Why keep a .http file when the Scalar page can send requests?

    1. Scalar cannot send requests
    2. The file lives in the repository, so everyone can repeat the same checks
    3. It is faster
    4. It is required by ASP.NET Core
    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.

See the example · Permalink

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.

See the example · Permalink

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.

See the example · Permalink

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.

See the example · Permalink

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.

See the example · Permalink

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.

See the example · Permalink

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).

See the example · Permalink

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.

See the example · Permalink

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.

See the example · Permalink

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.