Skip to main content

Catalog V2 DTO Reuse Refactor Report

Summary

This report documents the current v2 catalog DTO duplication in services/amm/agent-web-api and proposes a refactor so that catalog shape changes propagate consistently across endpoints that are intended to share the same contract.

Goal: if we change the canonical shape of CatalogApp, CatalogVendor, or CatalogCategory, those changes should automatically be reflected everywhere those same DTOs are used.

For this report, the docs are the source of truth for which endpoints are supposed to share the same DTO contract.

Scope Reviewed

  • services/amm/agent-web-api/src/api/v2/apps/apps.controller.ts#L60
  • services/amm/agent-web-api/src/api/v2/apps/apps-dtos.ts#L159
  • services/amm/agent-web-api/src/api/v2/catalog-apps/catalog-apps.controller.ts#L49
  • services/amm/agent-web-api/src/api/v2/catalog-apps/catalog-apps-dtos.ts#L89
  • services/amm/agent-web-api/src/api/v2/catalog-vendors/catalog-vendors.controller.ts#L98
  • services/amm/agent-web-api/src/api/v2/catalog-vendors/catalog-vendors-dtos.ts#L65
  • services/amm/agent-web-api/src/api/v2/catalog-categories/catalog-categories.controller.ts#L33
  • services/amm/agent-web-api/src/api/v2/catalog-categories/catalog-categories-dtos.ts#L60
  • services/amm/agent-web-api/src/api/v2/analytics-apps/app-utilization/app-utilization-dtos.ts#L69

Findings

1. There is no canonical shared CatalogApp DTO class in the backend

The docs treat CatalogApp as a shared contract used across domains, especially when embedded under App.catalog_app.

The backend does not currently implement that as one reusable class. Instead it has separate DTOs with overlapping responsibility:

  • CatalogAppListItemDto in services/amm/agent-web-api/src/api/v2/catalog-apps/catalog-apps-dtos.ts#L123
  • CatalogAppDetailDto in services/amm/agent-web-api/src/api/v2/catalog-apps/catalog-apps-dtos.ts#L161
  • CatalogAppEmbedDto in services/amm/agent-web-api/src/api/v2/apps/apps-dtos.ts#L184
  • UtilizationCatalogAppDto in services/amm/agent-web-api/src/api/v2/analytics-apps/app-utilization/app-utilization-dtos.ts#L88

As a result, changing one class does not update the others.

2. Vendor DTOs are duplicated across modules

The same vendor identity shape appears in multiple places with different class names:

  • CatalogVendorDto in services/amm/agent-web-api/src/api/v2/catalog-vendors/catalog-vendors-dtos.ts#L65
  • CatalogVendorEmbedDto in services/amm/agent-web-api/src/api/v2/apps/apps-dtos.ts#L159
  • UtilizationCatalogVendorDto in services/amm/agent-web-api/src/api/v2/analytics-apps/app-utilization/app-utilization-dtos.ts#L69

These are all variants of:

{
"id": "uuid",
"name": "Vendor name",
"url": "https://example.com"
}

Any change to vendor identity fields currently has to be repeated manually.

3. Category DTOs are duplicated across modules

Category identity is also modeled multiple times:

  • CatalogCategoryDto in services/amm/agent-web-api/src/api/v2/catalog-categories/catalog-categories-dtos.ts#L60
  • CatalogCategoryEmbedDto in services/amm/agent-web-api/src/api/v2/apps/apps-dtos.ts#L174
  • UtilizationCatalogCategoryDto in services/amm/agent-web-api/src/api/v2/analytics-apps/app-utilization/app-utilization-dtos.ts#L80
  • CatalogAppCategoryRefDto in services/amm/agent-web-api/src/api/v2/catalog-apps/catalog-apps-dtos.ts#L89

These shapes are not even normalized:

  • some use id
  • one uses category_id
  • some include timestamps
  • some omit timestamps

That makes category changes especially brittle.

4. The apps domain does not reuse catalog DTO classes

AppDetailDto.catalog_app uses CatalogAppEmbedDto, not a catalog-apps DTO:

  • services/amm/agent-web-api/src/api/v2/apps/apps-dtos.ts#L233

The apps controller also builds that embed manually:

  • services/amm/agent-web-api/src/api/v2/apps/apps.controller.ts#L69
catalog_app: {
id: app.catalogAppId,
canonical_name: app.catalogAppName,
description: app.catalogAppDescription,
display_colour: app.catalogAppDisplayColour,
logo: app.catalogAppLogo,
type: app.catalogAppType ?? '',
vendor: app.vendorId
? { id: app.vendorId, name: app.vendorName!, url: app.vendorUrl }
: null,
categories: app.catalogCategories,
created_at: app.catalogAppCreatedAt,
updated_at: app.catalogAppUpdatedAt,
}

That means apps can drift from catalog-apps even when both are supposed to describe the same resource.

5. Catalog controllers each own their own manual mapping logic

The controllers currently map raw rows into module-local DTOs:

  • catalog apps list/detail mapping in services/amm/agent-web-api/src/api/v2/catalog-apps/catalog-apps.controller.ts#L49
  • catalog vendors mapping in services/amm/agent-web-api/src/api/v2/catalog-vendors/catalog-vendors.controller.ts#L98
  • catalog categories mapping in services/amm/agent-web-api/src/api/v2/catalog-categories/catalog-categories.controller.ts#L33
  • apps embedded catalog mapping in services/amm/agent-web-api/src/api/v2/apps/apps.controller.ts#L69

There is no shared mapper layer for canonical catalog DTOs.

Current Duplication Map

Vendor

PurposeClass
Catalog vendors endpointCatalogVendorDto
Apps embedded catalog appCatalogVendorEmbedDto
Analytics app utilizationUtilizationCatalogVendorDto

Category

PurposeClass
Catalog categories endpointCatalogCategoryDto
Apps embedded catalog appCatalogCategoryEmbedDto
Catalog apps list/detail category child shapeCatalogAppCategoryRefDto
Analytics app utilizationUtilizationCatalogCategoryDto

Catalog App

PurposeClass
Catalog apps list endpointCatalogAppListItemDto
Catalog apps detail endpointCatalogAppDetailDto
Apps embedded catalog appCatalogAppEmbedDto
Analytics app utilizationUtilizationCatalogAppDto

Refactor Goal

Introduce canonical shared DTO classes for catalog resources and use them anywhere the docs say the same resource shape should be returned or embedded.

Important constraint: not every endpoint should necessarily use the full CatalogApp shape.

The rule should be:

  • if the docs say an endpoint returns CatalogApp, use the same shared CatalogAppDto
  • if the docs say an endpoint returns CatalogVendor, use the same shared CatalogVendorDto
  • if the docs say an endpoint returns CatalogCategory, use the same shared CatalogCategoryDto
  • if an endpoint intentionally returns a lighter projection, give it a distinct Ref or ListItem DTO name and do not pretend it is the canonical resource

Proposed Target Design

1. Create shared canonical DTOs in one module

Create one shared DTO file for reusable catalog resource contracts, for example:

src/api/v2/catalog/shared/catalog-resource-dtos.ts

Recommended canonical classes:

  • CatalogVendorDto
  • CatalogCategoryDto
  • CatalogAppDto
  • CatalogAppUrlPatternDto
  • CatalogAppDesktopAliasDto

Recommended supporting projections only when truly needed:

  • CatalogVendorRefDto
  • CatalogCategoryRefDto
  • CatalogAppRefDto

2. Reuse canonical DTOs in endpoint-specific DTOs

Examples:

  • catalog vendors list response should use shared CatalogVendorDto
  • catalog categories list response should use shared CatalogCategoryDto
  • apps AppDetailDto.catalog_app should use shared CatalogAppDto
  • any endpoint documented as embedding vendor/category identity should reuse shared CatalogVendorDto and CatalogCategoryDto, or explicit Ref types if the docs call for a smaller contract

3. Move mapping into shared catalog mappers

Create shared mapper functions, for example:

toCatalogVendorDto(...)
toCatalogCategoryDto(...)
toCatalogAppDto(...)

Then reuse those mappers from:

  • catalog-apps controller
  • catalog-vendors controller
  • catalog-categories controller
  • apps controller
  • any analytics controller that intentionally uses the same canonical shapes

4. Normalize shape names and field names

The refactor should remove near-duplicate DTO names for the same contract:

  • replace CatalogVendorEmbedDto with shared CatalogVendorDto
  • replace CatalogCategoryEmbedDto with shared CatalogCategoryDto or CatalogCategoryRefDto
  • replace CatalogAppEmbedDto with shared CatalogAppDto

Field naming should also be normalized. For example, category identity should not use category_id in one place and id in another if the intent is the same DTO.

Phase 1. Establish canonical resource DTOs

  • add the shared catalog DTO module
  • add shared catalog mapper helpers
  • keep existing endpoint-specific DTOs temporarily for compatibility while migrating imports

Phase 2. Migrate endpoints that should share canonical contracts

  • switch AppDetailDto.catalog_app to the shared CatalogAppDto
  • switch embedded vendor/category types in apps to shared canonical types
  • switch catalog vendors/categories endpoints to the shared canonical types if they are not already using them directly

Phase 3. Decide which list endpoints are canonical vs projection endpoints

  • if docs say list returns CatalogApp[], make the list endpoint use CatalogAppDto[]
  • if a list should remain lightweight, rename the DTO to CatalogAppRefDto or CatalogAppListItemDto in the docs and code consistently

Phase 4. Audit analytics endpoints

  • if analytics endpoints are documented as embedding canonical catalog shapes, switch them to shared DTOs
  • if analytics endpoints intentionally use lighter analytics-specific shapes, keep them separate but name them as such and do not treat them as canonical catalog DTOs

Phase 5. Regenerate and verify the contract

  • regenerate services/amm/agent-web-api/openapi.json
  • rerun the contract validation script
  • rerun apps and catalog contract tests
  • confirm that duplicate OpenAPI schemas for the same conceptual resource have been eliminated where intended

Acceptance Criteria

The refactor is complete when all of the following are true:

  • changing the shared CatalogVendorDto updates every endpoint that is documented to return CatalogVendor
  • changing the shared CatalogCategoryDto updates every endpoint that is documented to return CatalogCategory
  • changing the shared CatalogAppDto updates every endpoint that is documented to return or embed CatalogApp
  • apps no longer define CatalogVendorEmbedDto, CatalogCategoryEmbedDto, or CatalogAppEmbedDto for canonical shapes
  • shared mapper functions are used instead of repeated controller-local catalog mapping blocks
  • OpenAPI no longer emits multiple schema names for the same intended catalog contract unless the shapes are intentionally different

Risks

Over-sharing partial DTOs

Some endpoints intentionally need smaller projections. Reusing a full canonical DTO there can over-expand the contract and create backwards-incompatible changes.

Preserving intentional differences

If analytics wants a purpose-built shape, that is fine. The important part is to make that explicit in both docs and code and not duplicate canonical DTOs accidentally.

Breaking generated clients

Consolidating DTOs may rename schemas in OpenAPI. That can affect generated SDKs even when the wire shape stays the same.

Requested Action

Implement a shared catalog DTO layer for v2, migrate apps and catalog endpoints to reuse those canonical DTOs where the docs require shared contracts, and keep separate Ref or analytics-specific DTOs only where the response is intentionally not the canonical catalog resource.