Learnixo
Back to blog
AI Systemsintermediate

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.

Asma Hafeez KhanMay 16, 20264 min read
Minimal APIsOpenAPIScalarASP.NET Core.NET 9
Share:š•

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.

C#
// 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

C#
// 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

C#
// 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

C#
// 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

C#
// 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

C#
// 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

C#
// 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

YAML
# .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 contract

Development Only

C#
// 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.json in 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() and MapOpenApi(). Scalar provides a modern documentation UI with an integrated API client. Describe endpoints with WithSummary, WithDescription, Produces<T>(), and WithTags. 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.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.