4. OpenAPI and Scalar
The Module 03 API works, but the only way to find out what it does is to read the source. This module makes the API describe itself. ASP.NET Core generates an OpenAPI document from the endpoint definitions, and Scalar turns that document into an interactive API reference at /scalar.
Getting a useful document means changing the endpoints themselves. They get names, summaries and descriptions, and they declare their possible responses with typed results so that the document includes 404 Not Found alongside 200 OK.
What’s in this module
04-openapi-and-scalar/
├── .gitignore (new)
├── README.md (new)
└── Autobarn.Website/
├── Autobarn.Website.csproj (changed)
├── Program.cs (changed)
├── Api/AutobarnApiEndpointRouteBuilderExtensions.cs (changed)
├── Views/Shared/_Layout.cshtml (changed)
└── wwwroot/css/site.css (changed)
Changes from Module 03
| File | Change |
|---|---|
Autobarn.Website/Autobarn.Website.csproj | Modified. Adds the Microsoft.AspNetCore.OpenApi and Scalar.AspNetCore packages |
Autobarn.Website/Program.cs | Modified. Registers OpenAPI document generation, and maps the document and the Scalar UI |
Autobarn.Website/Api/AutobarnApiEndpointRouteBuilderExtensions.cs | Modified. Adds a route group, OpenAPI metadata, typed results and 404 handling |
Autobarn.Website/Views/Shared/_Layout.cshtml | Modified. Adds an “Autobarn API” link to the nav bar |
Autobarn.Website/wwwroot/css/site.css | Modified. Moves that link to the right-hand end of the nav bar |
.gitignore, README.md | Added. Standard Visual Studio .gitignore and a placeholder readme |
Step 1: Add the package references
File: Autobarn.Website/Autobarn.Website.csproj
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Scalar.AspNetCore" />
</ItemGroup>
There are no versions here because Directory.Packages.props has pinned both packages since Module 01.
Why:
Microsoft.AspNetCore.OpenApigenerates the OpenAPI document. It isn’t part of the ASP.NET Core shared framework, soAddOpenApi()andMapOpenApi()aren’t available until the package is referenced.Scalar.AspNetCoreserves the Scalar API reference UI.
In .NET 10, Microsoft.AspNetCore.OpenApi also includes a source generator that reads XML doc comments. The projects have set <GenerateDocumentationFile>true</GenerateDocumentationFile> since Module 01, so the ///<summary> and ///<example> comments on Vehicle, VehicleMake and VehicleModel now show up as schema descriptions and examples in the document.
Step 2: Register OpenAPI and map the OpenAPI and Scalar endpoints
File: Autobarn.Website/Program.cs
using Scalar.AspNetCore; // new
builder.Services.AddDbContext<AutobarnDbContext>(options => options.UseSqlite(connectionString));
builder.Services.AddControllersWithViews(options => options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));
builder.Services.AddOpenApi(); // new: registers the OpenAPI document generator
builder.Services.AddValidation();
app.MapStaticAssets();
app.MapOpenApi(); // new: serves /openapi/v1.json
app.MapScalarApiReference(); // new: serves the UI at /scalar
app.MapAutobarnApi("/api");
AddOpenApi()registers the services that build the OpenAPI document from the app’s endpoint metadata.MapOpenApi()serves the generated document at/openapi/v1.json.MapScalarApiReference()serves an interactive reference at/scalar. It reads the OpenAPI document and lets you send requests to the API from the browser.
Step 3: Group the endpoints and add OpenAPI metadata
File: Autobarn.Website/Api/AutobarnApiEndpointRouteBuilderExtensions.cs
Module 03 built each route by string concatenation (pattern + "/vehicles"). This module creates a route group:
var api = endpoints.MapGroup(pattern).WithTags("Autobarn");
api.MapGet("/vehicles",
async (AutobarnDbContext db) => await db.Vehicles.ToListAsync())
.WithName("GetVehicles")
.WithSummary("List vehicles")
.WithDescription("Returns every vehicle currently listed for sale at Autobarn.");
MapGroup(pattern)applies the/apiprefix to every endpoint in the group, so there’s no string concatenation..WithTags("Autobarn")is set once on the group and applies to all its endpoints. Scalar uses tags to group operations in the sidebar..WithName(...)sets the OpenAPIoperationId. It also makes the endpoint addressable by name, which Module 05 relies on to generate links..WithSummary(...)and.WithDescription(...)give the human-readable text shown in Scalar.
The using Microsoft.AspNetCore.Builder; and using Microsoft.AspNetCore.Routing; directives were removed. The Web SDK already includes both namespaces as implicit usings.
Step 4: Return typed results and handle “not found”
File: Autobarn.Website/Api/AutobarnApiEndpointRouteBuilderExtensions.cs
using Autobarn.Data.Entities; // new
using Microsoft.AspNetCore.Http.HttpResults; // new
Before (Module 03):
endpoints.MapGet(pattern + "/vehicles/{registration}",
async (AutobarnDbContext db, string registration) => await db.Vehicles.FindAsync(registration));
After (Module 04):
api.MapGet("/vehicles/{registration}",
async Task<Results<Ok<Vehicle>, NotFound>> (AutobarnDbContext db, string registration)
=> await db.Vehicles.FindAsync(registration) is { } vehicle
? TypedResults.Ok(vehicle)
: TypedResults.NotFound())
.WithName("GetVehicle")
.WithSummary("Find a vehicle")
.WithDescription("Returns the vehicle with the given registration plate, or 404 if no such vehicle is listed.");
Models by make, before:
endpoints.MapGet(pattern + "/makes/{code}/models",
async (AutobarnDbContext db, string code) => await db.Models.Where(m => m.MakeCode == code).ToListAsync());
After:
api.MapGet("/makes/{code}/models",
async Task<Results<Ok<List<VehicleModel>>, NotFound>> (AutobarnDbContext db, string code)
=> await db.Makes.FindAsync(code) is null
? TypedResults.NotFound()
: TypedResults.Ok(await db.Models.Where(m => m.MakeCode == code).ToListAsync()))
.WithName("GetModelsByMake")
.WithSummary("List vehicle models by make")
.WithDescription("Returns every model built by the manufacturer with the given code, or 404 if no such manufacturer exists.");
Why:
- Correct HTTP semantics. A missing vehicle or make now returns
404 Not Found. For/makes/{code}/models, the code first checks that the make exists, so an unknown make (404) is no longer confused with a make that has no models (200with an empty list). - Self-describing responses. The lambda’s return type is
Results<Ok<Vehicle>, NotFound>, a union of the possible outcomes. The OpenAPI generator reads this type at startup and documents both the200response with itsVehicleschema and the404response, with no extra.Produces<T>()calls.TypedResults(rather thanResults) keeps the concrete types, which is what makes this possible. - The
is { } vehicleproperty pattern combines a null check with a variable declaration.
The two list endpoints (/vehicles and /makes) only gain metadata. They still return every row, without paging.
Step 5: Link to the API reference from the website
Files: Views/Shared/_Layout.cshtml, wwwroot/css/site.css
<li class="nav-item">
<a class="nav-link text-dark" href="~/scalar">Autobarn API</a>
</li>
nav ul li:has(a[href='/scalar']) {
margin-left: auto;
}
The :has() selector finds the nav item containing the Scalar link and pushes it to the far right of the flex nav bar, with no extra class in the markup.
Trying it out
dotnet run --project Autobarn.Website
http://localhost:5000/scalar: the interactive API referencehttp://localhost:5000/openapi/v1.json: the raw OpenAPI documenthttp://localhost:5000/api/vehicles/NOPE: now returns404 Not Found
What’s next
The API is documented, but it still returns raw entities, has no paging, and gives clients no way to move from one resource to a related one. Module 05 fixes all three with resource types, pagination and hypermedia links.