OpenAPI and Scalar ā API Documentation in Minimal APIs
Generate OpenAPI documentation for Minimal APIs: .NET 9 built-in OpenAPI, Scalar UI, describing endpoints with WithSummary and WithOpenApi, request/response schemas, and producing documentation CI can validate.
OpenAPI in .NET 9
.NET 9 includes built-in OpenAPI document generation ā no Swashbuckle needed. Microsoft.AspNetCore.OpenApi generates the OpenAPI spec, and Scalar provides the documentation UI.
// NuGet packages
// Microsoft.AspNetCore.OpenApi (included in .NET 9 web projects)
// Scalar.AspNetCore
// Program.cs
builder.Services.AddOpenApi(); // generates /openapi/v1.json
var app = builder.Build();
app.MapOpenApi(); // serves the OpenAPI JSON document
// Add Scalar UI
app.MapScalarApiReference(options =>
{
options.Title = "SystemForge API";
options.Theme = ScalarTheme.Moon;
options.DefaultHttpClient = (ScalarTarget.CSharp, ScalarClient.HttpClient);
options.Authentication = new ScalarAuthenticationOptions
{
PreferredSecurityScheme = "Bearer"
};
});Navigate to /scalar/v1 in development for the interactive documentation UI.
Describing Endpoints
// Add metadata to endpoints for documentation
app.MapPost("/patients", CreatePatient)
.WithName("CreatePatient") // operation ID
.WithSummary("Create a new patient")
.WithDescription("""
Creates a new patient record. The MRN must be unique across the hospital.
Returns 201 Created with the new patient's ID.
Returns 409 Conflict if the MRN already exists.
""")
.WithTags("Patients") // Scalar UI section
.Produces<PatientCreatedResponse>(StatusCodes.Status201Created)
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest)
.Produces<ProblemDetails>(StatusCodes.Status409Conflict)
.RequireAuthorization()
.WithOpenApi();Request and Response Schemas
// Mark DTO properties with descriptions
public sealed record CreatePatientDto(
[property: Description("Patient's full name (2-100 characters)")]
string Name,
[property: Description("Date of birth (ISO 8601 date: yyyy-MM-dd)")]
DateOnly DateOfBirth,
[property: Description("Medical Record Number ā must be unique")]
string MRN);
// Or configure schema in AddOpenApi
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((document, context, ct) =>
{
document.Info.Title = "SystemForge Clinical API";
document.Info.Version = "v1";
document.Info.Description = "API for the SystemForge clinical management platform";
document.Info.Contact = new OpenApiContact
{
Name = "SystemForge Engineering",
Email = "api@systemforge.com"
};
return Task.CompletedTask;
});
});Security Scheme Configuration
// Add JWT Bearer scheme to OpenAPI docs
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});
// BearerSecuritySchemeTransformer.cs
public sealed class BearerSecuritySchemeTransformer
: IOpenApiDocumentTransformer
{
public Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken ct)
{
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes = new Dictionary<string, OpenApiSecurityScheme>
{
["Bearer"] = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
Description = "Enter your JWT token. Example: 'eyJhbGci...'"
}
};
return Task.CompletedTask;
}
}Grouping with Tags
// Tags group endpoints in Scalar's sidebar
var patientGroup = app.MapGroup("/api/patients")
.WithTags("Patients")
.RequireAuthorization();
var prescriptionGroup = app.MapGroup("/api/prescriptions")
.WithTags("Prescriptions")
.RequireAuthorization("DoctorsOnly");
var adminGroup = app.MapGroup("/api/admin")
.WithTags("Administration")
.RequireAuthorization("AdminOnly");Versioned OpenAPI Documents
// Multiple OpenAPI versions
builder.Services.AddOpenApi("v1");
builder.Services.AddOpenApi("v2");
app.MapOpenApi("/openapi/{documentName}.json");
app.MapScalarApiReference(options =>
{
options.Servers = [];
// Configure which version the UI shows
});
// Map endpoints to a specific version
var v1Patients = app.MapGroup("/api/v1/patients").WithGroupName("v1");
var v2Patients = app.MapGroup("/api/v2/patients").WithGroupName("v2");Documenting Error Responses
// Consistent error responses documented in all endpoints
app.MapGet("/patients/{id:guid}", GetPatient)
.WithName("GetPatient")
.WithSummary("Get patient by ID")
.Produces<PatientDto>(StatusCodes.Status200OK)
.Produces<ProblemDetails>(StatusCodes.Status401Unauthorized,
"application/problem+json")
.Produces<ProblemDetails>(StatusCodes.Status403Forbidden,
"application/problem+json")
.Produces<ProblemDetails>(StatusCodes.Status404NotFound,
"application/problem+json")
.RequireAuthorization()
.WithOpenApi(operation =>
{
operation.Parameters[0].Description = "The patient's unique identifier (UUID)";
return operation;
});CI Validation of OpenAPI Schema
# .github/workflows/ci.yml ā catch schema regressions
- name: Generate OpenAPI schema
run: dotnet run --project src/Api -- --generate-openapi-schema
- name: Compare with baseline
run: diff openapi-baseline.json openapi-current.json
# Fails the build if the schema changed without updating the baseline
# Prevents accidental breaking changes to the public API contractDevelopment Only
// Only expose documentation in development
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
// Production: optionally serve docs behind auth
// app.MapOpenApi().RequireAuthorization("InternalDocsOnly");Production issue I've seen: A team exposed their OpenAPI JSON at
/openapi/v1.jsonin production without authentication. A security researcher found it and extracted the full API surface, endpoint names, request schemas, and error codes. This information significantly accelerated reconnaissance during a later pen test. OpenAPI docs should be behind authentication in production or not served at all.
Key Takeaway
.NET 9 generates OpenAPI documents natively ā add
AddOpenApi()andMapOpenApi(). Scalar provides a modern documentation UI with an integrated API client. Describe endpoints withWithSummary,WithDescription,Produces<T>(), andWithTags. Add Bearer security scheme for JWT-authenticated endpoints. Restrict OpenAPI docs to development or authenticated users in production ā the schema is reconnaissance information for attackers.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.