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
- Limit Nesting Depth: Maximum 3 levels deep
// Good
/api/catalog/assets/{id}
// Too deep
/api/catalog/assets/types/attributes/values/{id}
- Consistent Naming: Use plural for collections
/api/catalog/assets // ✓ Plural
/api/catalog/asset // ✗ Singular
- Logical Grouping: Group by business domain
// By domain
catalogGroup.MapGroup("/assets")
catalogGroup.MapGroup("/types")
// Not by HTTP method
catalogGroup.MapGroup("/get") // ✗
catalogGroup.MapGroup("/post") // ✗
- Tag Sparingly: One primary tag per endpoint
// Good
assetsGroup.WithTags("Assets");
// Too many
assetsGroup.WithTags("Assets", "Catalog", "Management", "CRUD"); // ✗
- Group Configuration: Configure once at group level
var group = endpoints.MapGroup("/api/secure")
.RequireAuthorization() // Applied to all
.RequireRateLimiting() // Applied to all
.WithTags("Secure Operations");
Related ADRs
- 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)