Documentation

adrs/016-endpoint-grouping-tagging.md

ADR-016: Endpoint Grouping and Tagging Strategy

Status

Accepted

Context

With multiple components exposing numerous endpoints, we need a consistent strategy for organizing and categorizing APIs. This affects API documentation, client generation, routing, and the overall developer experience. Proper grouping and tagging makes APIs more discoverable and maintainable.

Key requirements:

  • Logical organization of related endpoints
  • Clear categorization in API documentation (Swagger/Scalar)
  • Consistent URL structure across services
  • Support for applying common middleware to groups
  • Ability to version groups of endpoints together

Decision

We will use MapGroup() to create logical endpoint groups and WithTags() to categorize endpoints for API documentation. Each component will organize its endpoints into logical groups with consistent naming and tagging.

Pattern:

public static void MapCoreDataEndpoints(this IEndpointRouteBuilder endpoints)
{
    var group = endpoints.MapGroup("/api/core-data")
        .WithTags("Core Data");
        
    var utilsGroup = group.MapGroup("/utilities")
        .WithTags("Utilities");
        
    var languagesGroup = group.MapGroup("/languages")
        .WithTags("Languages");
}

Consequences

Positive

  • Organization: Clear hierarchical structure of endpoints
  • Documentation: Automatic grouping in Swagger/Scalar UI
  • Middleware: Apply authentication, validation, etc. to groups
  • URL Consistency: Enforced URL structure through groups
  • Maintenance: Related endpoints are colocated
  • Discoverability: Easy to find related functionality

Negative

  • Nesting Complexity: Deep nesting can become confusing
  • Tag Proliferation: Too many tags clutters documentation
  • Rigidity: Changing groups affects multiple endpoints
  • Cross-Cutting Concerns: Some endpoints don't fit neat categories

Neutral

  • Documentation Structure: Tags directly affect API documentation layout
  • Client Generation: Affects generated client code organization
  • Route Templates: Groups define URL hierarchy

Implementation Notes

Basic Grouping Pattern

public static void MapCatalogEndpoints(this IEndpointRouteBuilder endpoints)
{
    // Main component group
    var catalogGroup = endpoints.MapGroup("/api/catalog")
        .WithTags("Catalog")
        .RequireAuthorization();
    
    // Asset endpoints
    var assetsGroup = catalogGroup.MapGroup("/assets")
        .WithTags("Assets");
        
    assetsGroup.MapGet("/", GetAssets)
        .WithDisplayName("Get all assets");
    assetsGroup.MapGet("/{id}", GetAssetById)
        .WithDisplayName("Get asset by ID");
    assetsGroup.MapPost("/", CreateAsset)
        .WithDisplayName("Create asset");
    assetsGroup.MapPut("/{id}", UpdateAsset)
        .WithDisplayName("Update asset");
    assetsGroup.MapDelete("/{id}", DeleteAsset)
        .WithDisplayName("Delete asset");
    
    // Asset Type endpoints
    var typesGroup = catalogGroup.MapGroup("/types")
        .WithTags("Asset Types");
        
    typesGroup.MapGet("/", GetAssetTypes)
        .WithDisplayName("Get all asset types");
    typesGroup.MapGet("/{id}", GetAssetTypeById)
        .WithDisplayName("Get asset type by ID");
}

Hierarchical Tagging

public static void MapIdentityEndpoints(this IEndpointRouteBuilder endpoints)
{
    var identityGroup = endpoints.MapGroup("/api/identity")
        .WithTags("Identity");
    
    // Authentication endpoints
    var authGroup = identityGroup.MapGroup("/auth")
        .WithTags("Authentication");  // Sub-tag
        
    authGroup.MapPost("/login", Login);
    authGroup.MapPost("/logout", Logout);
    authGroup.MapPost("/refresh", RefreshToken);
    
    // User management endpoints
    var usersGroup = identityGroup.MapGroup("/users")
        .WithTags("User Management");  // Sub-tag
        
    usersGroup.MapGet("/", GetUsers);
    usersGroup.MapPost("/", CreateUser);
    
    // Permission endpoints
    var permissionsGroup = identityGroup.MapGroup("/permissions")
        .WithTags("Permissions");  // Sub-tag
        
    permissionsGroup.MapGet("/", GetPermissions);
    permissionsGroup.MapPost("/assign", AssignPermission);
}

Applying Group Middleware

public static void MapAdminEndpoints(this IEndpointRouteBuilder endpoints)
{
    var adminGroup = endpoints.MapGroup("/api/admin")
        .WithTags("Administration")
        .RequireAuthorization("AdminPolicy")
        .RequireRateLimiting("strict")
        .WithOpenApi()
        .AddEndpointFilter<AuditLogFilter>();
        
    adminGroup.MapGet("/system-info", GetSystemInfo);
    adminGroup.MapPost("/maintenance-mode", SetMaintenanceMode);
}

Cross-Component Groups

public static void MapReportingEndpoints(this IEndpointRouteBuilder endpoints)
{
    var reportsGroup = endpoints.MapGroup("/api/reports")
        .WithTags("Reporting");
    
    // Asset reports (from Catalog component)
    var assetReports = reportsGroup.MapGroup("/assets")
        .WithTags("Asset Reports");
        
    // User reports (from Identity component)  
    var userReports = reportsGroup.MapGroup("/users")
        .WithTags("User Reports");
        
    // System reports (from multiple components)
    var systemReports = reportsGroup.MapGroup("/system")
        .WithTags("System Reports");
}

Versioned Groups

public static void MapVersionedEndpoints(this IEndpointRouteBuilder endpoints)
{
    var v5 = endpoints.NewApiVersionSet()
        .HasApiVersion(new ApiVersion(5, 0))
        .Build();
        
    var catalogGroup = endpoints.MapGroup("/api/catalog")
        .WithTags("Catalog")
        .WithApiVersionSet(v5);
        
    // All endpoints in group inherit version
    catalogGroup.MapGet("/assets", GetAssetsV5)
        .MapToApiVersion(5, 0);
}

Tag Naming Conventions

Component-Level Tags

  • Use component name: "Core Data", "Identity", "Catalog"
  • PascalCase with spaces
  • Singular form

Feature-Level Tags

  • Describe the feature: "Authentication", "User Management"
  • Action-oriented when appropriate
  • Avoid redundancy with component tag

Utility Tags

  • "Health", "Diagnostics", "Utilities"
  • For cross-cutting concerns

OpenAPI Result

The tagging strategy produces organized documentation:

tags:
  - name: Catalog
    description: Catalog component operations
  - name: Assets
    description: Asset management operations
  - name: Asset Types
    description: Asset type configuration

paths:
  /api/catalog/assets:
    get:
      tags:
        - Assets
      summary: Get all assets

Best Practices for Groups

  1. Limit Nesting Depth: Maximum 3 levels deep
// Good
/api/catalog/assets/{id}

// Too deep
/api/catalog/assets/types/attributes/values/{id}
  1. Consistent Naming: Use plural for collections
/api/catalog/assets     // ✓ Plural
/api/catalog/asset      // ✗ Singular
  1. Logical Grouping: Group by business domain
// By domain
catalogGroup.MapGroup("/assets")
catalogGroup.MapGroup("/types")

// Not by HTTP method
catalogGroup.MapGroup("/get")  // ✗
catalogGroup.MapGroup("/post") // ✗
  1. Tag Sparingly: One primary tag per endpoint
// Good
assetsGroup.WithTags("Assets");

// Too many
assetsGroup.WithTags("Assets", "Catalog", "Management", "CRUD"); // ✗
  1. Group Configuration: Configure once at group level
var group = endpoints.MapGroup("/api/secure")
    .RequireAuthorization()     // Applied to all
    .RequireRateLimiting()      // Applied to all
    .WithTags("Secure Operations");
  • ADR-013: Minimal APIs with Static Classes (where groups are defined)
  • ADR-015: Extension Methods for Endpoint Registration (how groups are registered)
  • ADR-017: OpenAPI Metadata Enrichment (documentation of groups)
  • ADR-022: Scalar API Documentation (how tags appear in UI)