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#L60services/amm/agent-web-api/src/api/v2/apps/apps-dtos.ts#L159services/amm/agent-web-api/src/api/v2/catalog-apps/catalog-apps.controller.ts#L49services/amm/agent-web-api/src/api/v2/catalog-apps/catalog-apps-dtos.ts#L89services/amm/agent-web-api/src/api/v2/catalog-vendors/catalog-vendors.controller.ts#L98services/amm/agent-web-api/src/api/v2/catalog-vendors/catalog-vendors-dtos.ts#L65services/amm/agent-web-api/src/api/v2/catalog-categories/catalog-categories.controller.ts#L33services/amm/agent-web-api/src/api/v2/catalog-categories/catalog-categories-dtos.ts#L60services/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:
CatalogAppListItemDtoinservices/amm/agent-web-api/src/api/v2/catalog-apps/catalog-apps-dtos.ts#L123CatalogAppDetailDtoinservices/amm/agent-web-api/src/api/v2/catalog-apps/catalog-apps-dtos.ts#L161CatalogAppEmbedDtoinservices/amm/agent-web-api/src/api/v2/apps/apps-dtos.ts#L184UtilizationCatalogAppDtoinservices/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:
CatalogVendorDtoinservices/amm/agent-web-api/src/api/v2/catalog-vendors/catalog-vendors-dtos.ts#L65CatalogVendorEmbedDtoinservices/amm/agent-web-api/src/api/v2/apps/apps-dtos.ts#L159UtilizationCatalogVendorDtoinservices/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:
CatalogCategoryDtoinservices/amm/agent-web-api/src/api/v2/catalog-categories/catalog-categories-dtos.ts#L60CatalogCategoryEmbedDtoinservices/amm/agent-web-api/src/api/v2/apps/apps-dtos.ts#L174UtilizationCatalogCategoryDtoinservices/amm/agent-web-api/src/api/v2/analytics-apps/app-utilization/app-utilization-dtos.ts#L80CatalogAppCategoryRefDtoinservices/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
| Purpose | Class |
|---|---|
| Catalog vendors endpoint | CatalogVendorDto |
| Apps embedded catalog app | CatalogVendorEmbedDto |
| Analytics app utilization | UtilizationCatalogVendorDto |
Category
| Purpose | Class |
|---|---|
| Catalog categories endpoint | CatalogCategoryDto |
| Apps embedded catalog app | CatalogCategoryEmbedDto |
| Catalog apps list/detail category child shape | CatalogAppCategoryRefDto |
| Analytics app utilization | UtilizationCatalogCategoryDto |
Catalog App
| Purpose | Class |
|---|---|
| Catalog apps list endpoint | CatalogAppListItemDto |
| Catalog apps detail endpoint | CatalogAppDetailDto |
| Apps embedded catalog app | CatalogAppEmbedDto |
| Analytics app utilization | UtilizationCatalogAppDto |
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 sharedCatalogAppDto - if the docs say an endpoint returns
CatalogVendor, use the same sharedCatalogVendorDto - if the docs say an endpoint returns
CatalogCategory, use the same sharedCatalogCategoryDto - if an endpoint intentionally returns a lighter projection, give it a distinct
ReforListItemDTO 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:
CatalogVendorDtoCatalogCategoryDtoCatalogAppDtoCatalogAppUrlPatternDtoCatalogAppDesktopAliasDto
Recommended supporting projections only when truly needed:
CatalogVendorRefDtoCatalogCategoryRefDtoCatalogAppRefDto
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_appshould use sharedCatalogAppDto - any endpoint documented as embedding vendor/category identity should reuse shared
CatalogVendorDtoandCatalogCategoryDto, or explicitReftypes 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
CatalogVendorEmbedDtowith sharedCatalogVendorDto - replace
CatalogCategoryEmbedDtowith sharedCatalogCategoryDtoorCatalogCategoryRefDto - replace
CatalogAppEmbedDtowith sharedCatalogAppDto
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.
Recommended Rollout
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_appto the sharedCatalogAppDto - 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 useCatalogAppDto[] - if a list should remain lightweight, rename the DTO to
CatalogAppRefDtoorCatalogAppListItemDtoin 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
CatalogVendorDtoupdates every endpoint that is documented to returnCatalogVendor - changing the shared
CatalogCategoryDtoupdates every endpoint that is documented to returnCatalogCategory - changing the shared
CatalogAppDtoupdates every endpoint that is documented to return or embedCatalogApp - apps no longer define
CatalogVendorEmbedDto,CatalogCategoryEmbedDto, orCatalogAppEmbedDtofor 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.