Link Search Menu Expand Document

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.OpenApi generates the OpenAPI document. It isn’t part of the ASP.NET Core shared framework, so AddOpenApi() and MapOpenApi() aren’t available until the package is referenced.
  • Scalar.AspNetCore serves 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 /api prefix 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 OpenAPI operationId. 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 (200 with 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 the 200 response with its Vehicle schema and the 404 response, with no extra .Produces<T>() calls. TypedResults (rather than Results) keeps the concrete types, which is what makes this possible.
  • The is { } vehicle property 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.

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 reference
  • http://localhost:5000/openapi/v1.json: the raw OpenAPI document
  • http://localhost:5000/api/vehicles/NOPE: now returns 404 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.