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

  1. Keep OpenAPI specs updated with accurate descriptions
  2. Provide meaningful examples for requests and responses
  3. Use consistent theming across all services
  4. Configure authentication for try-it-out functionality
  5. Include contact information for API support
  6. Version documentation alongside API versions
  7. Monitor usage to understand documentation effectiveness
  • 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)