Scalar API Docs — Replacing Swagger With a Modern Developer Experience
How to configure Scalar as the API documentation UI in a .NET Clean Architecture project, why it replaces Swagger/Swashbuckle, and how to annotate endpoints for meaningful API documentation.
Why Scalar Over Swagger UI
Scalar is a modern OpenAPI documentation UI that wraps the same OpenAPI spec that Swagger uses but provides a better developer experience:
Scalar vs Swagger UI:
✓ Modern, readable layout — collapsible, searchable, syntax-highlighted
✓ Built-in API client (send requests without copy-pasting curl)
✓ Multiple theming options out of the box
✓ First-class support for .NET's built-in OpenAPI (`Microsoft.AspNetCore.OpenApi`)
✓ No Swashbuckle dependency — lighter, actively maintained
✓ Works with the .NET 9+ `AddOpenApi()` built-inInstallation
<!-- Api.csproj -->
<PackageReference Include="Scalar.AspNetCore" Version="2.*" />.NET 9+ includes built-in OpenAPI generation — no Swashbuckle required.
Minimal Setup
// Api/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi(); // .NET 9+ built-in OpenAPI
// ... other services
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi(); // exposes /openapi/v1.json
app.MapScalarApiReference(); // exposes /scalar/v1 UI
}
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();With this in place, navigate to /scalar/v1 in development to see the full interactive API reference.
Customizing the Scalar UI
// Api/Program.cs
app.MapScalarApiReference(options =>
{
options.Title = "SystemForge API";
options.Theme = ScalarTheme.Purple;
options.DefaultHttpClient = new(ScalarTarget.CSharp, ScalarClient.HttpClient);
options.Authentication = new ScalarAuthenticationOptions
{
PreferredSecurityScheme = "Bearer",
};
});OpenAPI Metadata on Controllers
// Api/Controllers/PatientsController.cs
using Microsoft.AspNetCore.Http;
[ApiController]
[Route("api/patients")]
[Authorize]
[Tags("Patients")] // groups endpoints in Scalar sidebar
public sealed class PatientsController : ControllerBase
{
[HttpPost]
[Authorize(Roles = "Clinician,Admin")]
[ProducesResponseType<object>(StatusCodes.Status201Created)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status409Conflict)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status422UnprocessableEntity)]
[EndpointSummary("Register a new patient")]
[EndpointDescription("Creates a patient record. MRN must be unique across the system.")]
public async Task<IActionResult> Create(
CreatePatientRequest request, CancellationToken ct)
{ ... }
[HttpGet("{id:guid}")]
[ProducesResponseType<PatientResponse>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
[EndpointSummary("Get patient by ID")]
public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
{ ... }
[HttpGet]
[ProducesResponseType<PagedResult<PatientListItem>>(StatusCodes.Status200OK)]
[EndpointSummary("List active patients")]
[EndpointDescription("Returns paginated list. Supports optional search by name or MRN.")]
public async Task<IActionResult> List(
[FromQuery] string? search,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
CancellationToken ct = default)
{ ... }
}Configuring Authentication in the OpenAPI Spec
// Api/OpenApi/AddSecuritySchemeTransformer.cs
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models;
public sealed class AddSecuritySchemeTransformer : IOpenApiDocumentTransformer
{
public Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken ct)
{
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
Description = "Enter your JWT access token.",
};
// Apply globally so all endpoints require auth by default
document.SecurityRequirements.Add(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer",
}
},
[]
}
});
return Task.CompletedTask;
}
}
// Registration
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer<AddSecuritySchemeTransformer>();
});Versioning (Optional)
// If you add API versioning:
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1);
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
}).AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'V";
options.SubstituteApiVersionInUrl = true;
});
// Multiple Scalar UIs, one per version
app.MapScalarApiReference("v1", options => options.Title = "SystemForge API v1");
app.MapScalarApiReference("v2", options => options.Title = "SystemForge API v2");PRO TIP
Scalar respects the
[ProducesResponseType]attributes. Document every non-200 response explicitly — including 400, 404, 409, and 422 — so API consumers know what to handle. An API that only documents 200 responses forces the frontend team to discover failures through trial and error in production.
Development vs Production
// In production, Scalar should be disabled
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
// If you want it in staging for QA, use an explicit environment check or feature flagProduction issue I've seen: A team deployed Scalar to production with no authentication. The full API schema — including all endpoint paths, all request/response shapes, and all error codes — was publicly accessible. This is an information disclosure vulnerability. Disable it in production or protect it with authentication.
Key Takeaway
Scalar is a drop-in replacement for Swagger UI with a better developer experience and no Swashbuckle dependency. In .NET 9+,
AddOpenApi()is built-in.MapScalarApiReference()adds the UI in one line. Document every response type with[ProducesResponseType]and every endpoint with[EndpointSummary]— the documentation is the first thing a new developer sees when joining the team.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.