Documentation
adrs/022-scalar-api-documentation.md
ADR-022: Scalar API Documentation
Status
Accepted
Context
API documentation is critical for developer experience, both for internal team members and external API consumers. While Swagger UI has been the de facto standard for OpenAPI documentation, newer alternatives like Scalar provide enhanced user experience, better design, and additional features.
Scalar offers:
- Modern, clean interface design
- Built-in API client for testing
- Multiple language examples for each endpoint
- Dark mode support
- Better search and navigation
- Request history tracking
- Environment variable support
Decision
We will use Scalar as the standard API documentation interface for all Dynaplex services, replacing Swagger UI while maintaining OpenAPI specification compatibility.
Implementation:
app.MapScalarApiReference(options =>
{
options.Title = "Catalog API";
options.Theme = ScalarTheme.Saturn;
options.DarkMode = true;
});
Consequences
Positive
- Better UX: Modern, intuitive interface for API exploration
- Integrated Testing: Built-in API client without external tools
- Code Examples: Auto-generated examples in multiple languages
- Developer Productivity: Faster API discovery and testing
- Professional Appearance: Polished documentation interface
- Features: Request history, environments, authentication management
Negative
- New Dependency: Additional package to maintain
- Learning Curve: Team familiar with Swagger needs to adapt
- Customization: Less community resources than Swagger UI
- Maturity: Newer tool with potential bugs
Neutral
- OpenAPI Based: Still uses standard OpenAPI specifications
- Side-by-Side: Can run alongside Swagger during transition
- Performance: Similar performance characteristics to Swagger UI
Implementation Notes
Basic Configuration
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add OpenAPI services
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{
config.Title = "Catalog API";
config.Version = "v5.0";
config.Description = "Asset catalog management service";
});
var app = builder.Build();
// Add OpenAPI endpoint
app.MapOpenApi();
// Add Scalar UI
app.MapScalarApiReference(options =>
{
options.Title = "Catalog API Documentation";
options.Theme = ScalarTheme.Saturn;
options.DarkMode = true;
options.ShowSidebar = true;
options.RoutePrefix = "/api-docs"; // Access at /api-docs
options.EndpointPath = "/openapi/v1.json"; // OpenAPI spec location
});
// Optionally keep Swagger for transition
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/openapi/v1.json", "Catalog API v5.0");
c.RoutePrefix = "swagger"; // Access at /swagger
});
}
Advanced Configuration
app.MapScalarApiReference(options =>
{
// Branding
options.Title = "Dynaplex Catalog API";
options.Theme = ScalarTheme.Saturn;
options.DarkMode = true;
options.Favicon = "/assets/favicon.ico";
// Custom CSS
options.CustomCss = @"
.scalar-api-reference {
--scalar-primary-color: #007ACC;
--scalar-background-1: #1e1e1e;
}
";
// Authentication
options.Authentication = new ScalarAuthenticationOptions
{
PreferredSecurityScheme = "Bearer",
ApiKey = new ApiKeyOptions
{
Token = "{{apiKey}}" // Placeholder for user input
}
};
// Servers/Environments
options.Servers = new[]
{
new ScalarServer
{
Url = "https://api.dynaplex.com",
Description = "Production"
},
new ScalarServer
{
Url = "https://staging-api.dynaplex.com",
Description = "Staging"
},
new ScalarServer
{
Url = "http://localhost:5001",
Description = "Local Development"
}
};
// Hide internal endpoints
options.HideEndpoints = new[]
{
"/health",
"/metrics"
};
// Default expanded sections
options.DefaultOpenLevel = 2;
options.ShowSidebar = true;
options.SortEndpointsBy = "path"; // or "method"
});
Environment-Specific Documentation
if (EnvironmentDetector.IsRunningInContainer)
{
app.MapScalarApiReference(options =>
{
options.Title = $"Catalog API - {app.Environment.EnvironmentName}";
options.Theme = app.Environment.IsProduction()
? ScalarTheme.Mars // Red theme for production
: ScalarTheme.Saturn; // Blue theme for non-prod
// Production: Disable try-it-out feature
if (app.Environment.IsProduction())
{
options.HideTryIt = true;
}
});
}
else
{
// Development: Full features
app.MapScalarApiReference(options =>
{
options.Title = "Catalog API - Development";
options.Theme = ScalarTheme.Neptune; // Green for dev
options.ShowRequestSamples = true;
options.DefaultOpenLevel = 3; // Expand more by default
});
}
Custom Metadata for Scalar
// Enhance OpenAPI spec for better Scalar experience
builder.Services.AddOpenApiDocument(config =>
{
config.Title = "Catalog API";
config.Version = "v5.0";
// Contact information
config.Contact = new OpenApiContact
{
Name = "Dynaplex Team",
Email = "api-support@dynaplex.com",
Url = new Uri("https://dynaplex.com/support")
};
// License
config.License = new OpenApiLicense
{
Name = "MIT",
Url = new Uri("https://opensource.org/licenses/MIT")
};
// External docs
config.ExternalDocs = new OpenApiExternalDocs
{
Description = "Full API Documentation",
Url = new Uri("https://docs.dynaplex.com/api/catalog")
};
// Tags for grouping
config.Tags = new List<OpenApiTag>
{
new() { Name = "Assets", Description = "Asset management operations" },
new() { Name = "Types", Description = "Asset type configuration" },
new() { Name = "Categories", Description = "Category management" }
};
// Security definitions
config.AddSecurity("Bearer", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
Description = "JWT Authorization header using the Bearer scheme"
});
});
Request Examples in Documentation
group.MapPost("/assets", CreateAsset)
.WithOpenApi(operation =>
{
// Add examples that Scalar will display
operation.RequestBody.Content["application/json"].Examples =
new Dictionary<string, OpenApiExample>
{
["Laptop"] = new()
{
Summary = "Create a laptop asset",
Description = "Example of creating an IT equipment asset",
Value = new
{
name = "Dell Latitude 5520",
typeId = 1,
serialNumber = "DL123456",
purchaseDate = "2024-01-15",
locationId = 42
}
},
["Furniture"] = new()
{
Summary = "Create office furniture",
Description = "Example of creating a furniture asset",
Value = new
{
name = "Standing Desk",
typeId = 5,
serialNumber = "DESK-001",
purchaseDate = "2024-02-01",
locationId = 15
}
}
};
return operation;
});
Scalar-Specific Extensions
// Add custom extensions that Scalar understands
operation.Extensions["x-scalar-examples"] = new
{
curl = @"curl -X POST https://api.dynaplex.com/assets \
-H 'Authorization: Bearer {{token}}' \
-H 'Content-Type: application/json' \
-d '{""name"":""Asset Name""}'",
javascript = @"
const response = await fetch('https://api.dynaplex.com/assets', {
method: 'POST',
headers: {
'Authorization': 'Bearer {{token}}',
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'Asset Name' })
});",
python = @"
import requests
response = requests.post(
'https://api.dynaplex.com/assets',
headers={'Authorization': 'Bearer {{token}}'},
json={'name': 'Asset Name'}
)"
};
Integration with Development Workflow
// Development helper endpoints for Scalar
if (app.Environment.IsDevelopment())
{
// Provide sample JWT for testing
app.MapGet("/dev/token", () =>
{
var token = GenerateDevToken();
return new { token, expiresIn = 3600 };
})
.ExcludeFromDescription() // Don't show in API docs
.AllowAnonymous();
// Reset database to known state for testing
app.MapPost("/dev/reset", async (CatalogDb db) =>
{
await db.Database.EnsureDeletedAsync();
await db.Database.EnsureCreatedAsync();
await SeedTestData(db);
return Results.Ok("Database reset complete");
})
.ExcludeFromDescription()
.RequireAuthorization("DevOnly");
}
Monitoring Documentation Usage
// Track API documentation usage
app.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments("/api-docs"))
{
// Log documentation access
logger.LogInformation("API documentation accessed from {IP} at {Time}",
context.Connection.RemoteIpAddress,
DateTime.UtcNow);
// Track metrics
DocumentationAccessCounter.Add(1,
new("service", "catalog"),
new("environment", app.Environment.EnvironmentName));
}
await next();
});
Best Practices
- Keep OpenAPI specs updated with accurate descriptions
- Provide meaningful examples for requests and responses
- Use consistent theming across all services
- Configure authentication for try-it-out functionality
- Include contact information for API support
- Version documentation alongside API versions
- Monitor usage to understand documentation effectiveness
Related ADRs
- ADR-016: Endpoint Grouping and Tagging Strategy (organization in docs)
- ADR-017: OpenAPI Metadata Enrichment (source for documentation)
- ADR-021: Microsoft Kiota for API Client Generation (uses same OpenAPI spec)