Catalog API Reference
Complete API contracts for the Catalog module, including routes, auth, DTOs, responses, errors, examples, and integration notes.
Catalog - API Reference
Audience: Frontend engineers, mobile engineers, backend engineers, QA, and API consumers. Scope: Admin, storefront and job-facing APIs owned by the Catalog module (categories, brands, brand series, tags, jobs, search).
1. Documentation Evidence
| Area | Files Inspected | What Was Verified |
|---|---|---|
| Controllers | admin/{category,brand,brand-series,job}/*.controller.ts, catalog-category-bulk.controller.ts, catalog-brand-bulk.controller.ts, catalog-brand-series-bulk.controller.ts, customer/**/*.controller.ts, catalog/{admin,customer}/tag/*.controller.ts | Routes, methods, guards, permissions, status codes, registration order |
| DTOs | dto/*.ts under each leaf | Request/query/response validation and defaults |
| Services | all catalog services | Behavior, side effects, error codes |
| Schema | packages/db/src/schema/catalog/*.ts | Constraints, enums, indexes |
| Jobs/cache | packages/jobs/src/index.ts, cache-invalidation.tags.ts | Queue names, payloads, domains |
| Error registry | apps/api/src/common/types/error-codes.ts | CATALOG_*, CATEGORY_*, BRAND_*, BRAND_SERIES_*, TAG_* |
2. Module Summary
| Field | Value |
|---|---|
| Module name | catalog |
| Module slug | catalog |
| Primary actors | guest, admin, worker |
| API surfaces | admin, mobile |
| Base route prefixes | /api/admin/catalog/{categories,brands,brand-series,jobs,tags}, /api/mobile/catalog/{categories,category-tree,brands,series,search,tags} |
| Auth model | @Public() storefront; JwtAuthGuard + RoleGuard admin |
| Persistence | PostgreSQL, Redis (cache), BullMQ (CATALOG queue) |
| Runtime source of truth | PostgreSQL tables |
| Sibling docs | Backend, Features and flows |
3. Concepts and Terminology
| Term | Meaning | Source File | Used By |
|---|---|---|---|
publicId | Exposed UUID v7 identifier; integer PK never leaves the service | schema | All routes |
slug | Route-safe URL key; owned by per-entity slug tables, current + retired under one unique | catalog-slug.service.ts | Storefront :slug routes |
canonicalSlug | Current slug returned when a retired slug was requested | slug service | Storefront detail |
effectiveVisible | Entity is_visible AND every ancestor's, AND non-deleted | catalog-category-tree.service.ts | Admin responses |
depth | Tree depth, root = 0, cap 6 | schema | Category routes |
retired slug | Former URL of an entity; never re-hands to another entity | slug tables | Storefront |
isStalled | Derived flag: job in processing past the 15-min claim TTL | catalog-job-admin.service.ts | Job detail |
4. API Surface Map
4.1 Admin categories — /api/admin/catalog/categories
| Method | Path | Permission | Rate limit | Controller | Purpose |
|---|---|---|---|---|---|
GET | / | Categories_READ | ADMIN_READ 30/min | CatalogCategoryAdminController | Sibling list at one tree level |
GET | /tree | Categories_READ | ADMIN_READ 30/min | same | Full non-deleted tree |
GET | /:publicId | Categories_READ | ADMIN_READ 30/min | same | Detail |
GET | /:publicId/breadcrumbs | Categories_READ | ADMIN_READ 30/min | same | Ancestor chain |
POST | / | Categories_CREATE | ADMIN_WRITE 10/min | same | Create (201) |
PATCH | /:publicId | Categories_UPDATE | ADMIN_WRITE 10/min | same | General update |
PATCH | /:publicId/move | Categories_UPDATE | ADMIN_WRITE 10/min | same | Move to new parent/root |
PATCH | /:publicId/visibility | Categories_UPDATE | ADMIN_WRITE 10/min | same | Visibility |
DELETE | /:publicId | Categories_DELETE | ADMIN_WRITE 10/min | same | Soft delete (200, message-only) |
POST | /:publicId/restore | Categories_RESTORE | ADMIN_WRITE 10/min | same | Restore (200) |
POST | /bulk/delete | Categories_DELETE | ADMIN_BULK_WRITE 5/min | CatalogCategoryBulkController | Bulk soft delete (200) |
POST | /bulk/restore | Categories_RESTORE | ADMIN_BULK_WRITE 5/min | same | Bulk restore (200) |
POST | /bulk/visibility | Categories_UPDATE | ADMIN_BULK_WRITE 5/min | same | Bulk visibility (200) |
PATCH | /reorder | Categories_UPDATE | ADMIN_REORDER 30/min | same | Reorder siblings (200, message-only) |
4.2 Admin brands — /api/admin/catalog/brands
| Method | Path | Permission | Rate limit | Controller | Purpose |
|---|---|---|---|---|---|
GET | / | Brands_READ | ADMIN_READ 30/min | CatalogBrandAdminController | Paginated list |
GET | /:publicId | Brands_READ | ADMIN_READ 30/min | same | Detail |
POST | / | Brands_CREATE | ADMIN_WRITE 10/min | same | Create (201) |
PATCH | /:publicId | Brands_UPDATE | ADMIN_WRITE 10/min | same | Update |
PATCH | /:publicId/visibility | Brands_UPDATE | ADMIN_WRITE 10/min | same | Visibility |
DELETE | /:publicId | Brands_DELETE | ADMIN_WRITE 10/min | same | Soft delete (200, message-only) |
POST | /:publicId/restore | Brands_RESTORE | ADMIN_WRITE 10/min | same | Restore (200) |
POST | /bulk/delete | Brands_DELETE | ADMIN_BULK_WRITE 5/min | CatalogBrandBulkController | Bulk delete (200) |
POST | /bulk/restore | Brands_RESTORE | ADMIN_BULK_WRITE 5/min | same | Bulk restore (200) |
POST | /bulk/visibility | Brands_UPDATE | ADMIN_BULK_WRITE 5/min | same | Bulk visibility (200) |
PATCH | /reorder | Brands_UPDATE | ADMIN_REORDER 30/min | same | Reorder all brands (200, message-only) |
4.3 Admin brand series — /api/admin/catalog/brand-series
Same 11-route shape as brands with BrandSeries_* permissions; create requires an owning brand (404 BRAND_SERIES_BRAND_NOT_FOUND, 409 BRAND_SERIES_BRAND_DELETED); reorder is sibling-scoped per brand (CATALOG_REORDER_MIXED_PARENT on mixed brands).
4.4 Admin jobs — /api/admin/catalog/jobs
| Method | Path | Permission | Rate limit | Purpose |
|---|---|---|---|---|
POST | /import | Catalog_CREATE + entity-specific | ADMIN_ASYNC_JOB_SUBMIT 10/hour | Import job (multipart; Idempotency-Key) |
POST | /export | Catalog_READ | ADMIN_ASYNC_JOB_SUBMIT 10/hour | Export job (Idempotency-Key) |
POST | /:publicId/cancel | Catalog_UPDATE | ADMIN_WRITE 10/min | Cancel queued/processing |
GET | / | Catalog_READ | ADMIN_READ 30/min | Paginated list |
GET | /:publicId | Catalog_READ | ADMIN_READ 30/min | Detail incl. errors, isStalled |
4.5 Storefront — /api/mobile/catalog/*
| Method | Path | Rate limit | Purpose |
|---|---|---|---|
GET | /categories | PUBLIC_READ 60/min | Root visible categories |
GET | /category-tree | PUBLIC_HIGH_FREQUENCY 300/min | Visible roots with their visible direct children, depth 1 |
GET | /categories/:slug/breadcrumbs | PUBLIC_READ 60/min | Ancestor chain by slug |
GET | /categories/:slug | PUBLIC_READ 60/min | Detail (current or retired slug) |
GET | /brands | PUBLIC_READ 60/min | Paginated visible brands |
GET | /brands/:slug/series | PUBLIC_READ 60/min | Visible series of a brand |
GET | /brands/:slug | PUBLIC_READ 60/min | Brand detail |
GET | /series | PUBLIC_READ 60/min | Paginated visible series |
GET | /series/:slug | PUBLIC_READ 60/min | Series detail |
GET | /search | PUBLIC_SEARCH 60/min | Search all three entities |
GET | /search/suggestions | PUBLIC_SUGGEST 300/min | Autocomplete (≤10) |
GET | /tags | PUBLIC_READ 60/min | Visible tags |
GET | /tags/:slug | PUBLIC_READ 60/min | Tag by slug |
Storefront series base is /series (not brand-series); the admin path is /brand-series.
5. Auth, Identity, and Permissions
| Surface | Guard/Decorator | Identity Shape | Permission | Guest Allowed | Notes |
|---|---|---|---|---|---|
| Storefront | @Public() + IpThrottlerGuard | None | N/A | Yes | Rate limits per §4.5 |
| Admin | JwtAuthGuard, RoleGuard | req.user | Categories_* / Brands_* / BrandSeries_* / Catalog_* / Tags_* | No | superadmin bypasses |
| Import submit | route Catalog_CREATE + service re-check | req.user | entity-specific create (Categories_CREATE etc.) | No | 403 CATALOG_JOB_ENTITY_PERMISSION_DENIED |
Idempotency: Idempotency-Key required on import/export submit (scopes catalog-job-import / catalog-job-export). Missing → 400 IDEMPOTENCY_KEY_REQUIRED; replay with different payload → 409 IDEMPOTENCY_KEY_CONFLICT; in-flight → 409 IDEMPOTENCY_REQUEST_IN_PROGRESS.
6. DTO and Model Reference
6.1 CreateCategoryDto / UpdateCategoryDto
| Field | Type | Required | Default | Validation | Example | Source |
|---|---|---|---|---|---|---|
name | string | Yes | N/A | 2..150, trimmed | "Living Room Furniture" | create-category.dto.ts |
description | string | No | NULL | — | "Sofas and armchairs" | |
parentId | UUID v7 | No (create) | NULL | @IsUUID("7") | 018f4e2a-… | Not accepted on PATCH — use /move |
slug | string | No | generated | ^[a-z0-9]+(-[a-z0-9]+)*$, max 160 | "living-room-furniture" | |
displayOrder | number | No | 0 | int >= 0 | 0 | |
isVisible | boolean | No | true | — | true | |
imageUrl / imageAlt | string | No | NULL | max 2048 / 255; alt requires url | ||
seoTitle / seoDescription | string | No | NULL | max 160 / 320 |
MoveCategoryDto { parentId: UUID v7 \| null } — required but nullable (null = root). CategoryVisibilityDto { isVisible: boolean }.
6.2 CreateCatalogBrandDto / CreateCatalogBrandSeriesDto
Brand: name (2..150, required), description (max 2000), slug, logoUrl, logoAlt (requires url), seoTitle, seoDescription. Series adds required brandPublicId (UUID v7). No displayOrder in create payloads — ordering is set via reorder.
6.3 Bulk DTOs
CatalogBulkIdsDto { publicIds: string[] } — non-empty UUID v7 array; duplicates deduped before the 100-item cap (409 CATALOG_BULK_LIMIT_EXCEEDED). CatalogBulkVisibilityDto adds isVisible. CatalogReorderDto { publicIds: string[] } — index position becomes displayOrder.
6.4 Query DTOs
QueryDto base: pagination (true), page (1), size (20, max 100), sort (updatedAt), order (desc). Category adds parentId, deletedOnly; brand adds includeDeleted; series adds includeDeleted, brandPublicId; jobs add kind, entity, status. Sortable fields: name, displayOrder, createdAt, updatedAt (jobs: createdAt, updatedAt, finishedAt).
7. Enum Reference
| Enum | Value | Meaning | Runtime Effect | Source |
|---|---|---|---|---|
catalog_job_kind | import / export | Job kind | — | enums.ts |
catalog_job_entity | category / brand / brand_series | Import/export target | Entity-specific permission check | |
catalog_job_status | queued / processing / completed / failed / cancelled | Job lifecycle | Lease claim, cooperative cancel |
8. Endpoint Reference
8.1 GET /api/mobile/catalog/categories/:slug
Purpose
Category detail for a storefront category page. Accepts a current or retired slug; returns canonicalSlug so the frontend can redirect.
Auth and Permissions
@Public(), PUBLIC_READ 60/min, no permission.
Response
200 — { publicId, name, canonicalSlug, description, parentId, imageUrl, imageAlt, seoTitle, seoDescription }. parentId is null when the parent is soft-deleted or hidden — a deleted parent's id is never returned.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 404 | CATEGORY_NOT_FOUND | Missing, hidden, deleted, or under a hidden/deleted ancestor — both slug branches |
8.2 POST /api/admin/catalog/categories
Purpose
Create a category (optionally under a parent). Guarded by the tree invariants: parent liveness, depth cap.
Auth and Permissions
JwtAuthGuard, RoleGuard, IpThrottlerGuard; Categories_CREATE; ADMIN_WRITE 10/min; no idempotency header (create is not idempotent-guarded).
Request
Body per §6.1.
Response
201 — { publicId, name, slug, description, parentId, depth, displayOrder, isVisible, effectiveVisible, imageUrl, imageAlt, seoTitle, seoDescription, createdAt, updatedAt, deletedAt }.
Side Effects
category + category_slug rows in one transaction (advisory lock first when parented); activity record; cache invalidation.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 400 | VALIDATION_FAILED | imageAlt without imageUrl |
| 404 | CATEGORY_PARENT_NOT_FOUND | Unknown parent |
| 409 | CATEGORY_PARENT_DELETED / CATEGORY_MAX_DEPTH_EXCEEDED / CATALOG_SLUG_GENERATION_FAILED | Deleted parent / depth / slug exhaustion |
8.3 PATCH /api/admin/catalog/categories/:publicId/move
The only parent-mutating route. Body { parentId: null } moves to root. 404 CATEGORY_NOT_FOUND/CATEGORY_PARENT_NOT_FOUND; 409 CATEGORY_PARENT_DELETED, CATEGORY_CIRCULAR_HIERARCHY, CATEGORY_MAX_DEPTH_EXCEEDED. Side effect: one-statement subtree rewrite.
8.4 DELETE /api/admin/catalog/:entity/:publicId (categories, brands, brand-series)
200 message-only body. 404 entity not found; 409 already deleted / has active children (category) / has active series (brand). Restore: POST /:publicId/restore, 200 with the entity payload; 409 not-deleted.
8.5 Bulk and reorder
POST /bulk/{delete,restore,visibility} — 200 with { succeeded: string[], failures: [{ publicId, errorCode }] }; per-item failures never roll back the batch; duplicates count once. PATCH /reorder — 200 message-only; the deduplicated set must exactly equal the alive sibling set (409 CATALOG_REORDER_INVALID_ITEM / CATALOG_REORDER_MIXED_PARENT).
8.6 POST /api/admin/catalog/jobs/import
multipart/form-data (file, entity). CSV/XLSX, 25 MB, 50,000 rows. Entity-specific permission re-check. Returns 200 with the queued job.
8.7 POST /api/admin/catalog/jobs/export
JSON { entity, includeDeleted?, search? }; 200 with the queued job. > 50,000 matching rows fails the job (CATALOG_EXPORT_TOO_LARGE).
8.8 POST /api/admin/catalog/jobs/:publicId/cancel
200 with the job row; 404 CATALOG_JOB_NOT_FOUND; 409 CATALOG_JOB_NOT_CANCELLABLE from a terminal state. Cooperative: a processing import aborts and rolls back.
8.9 GET /api/admin/catalog/jobs(/:publicId)
List (paginated, filters, ordered by createdAt; page cap → 400 PAGINATION_LIMIT_TOO_LARGE) and detail (adds startedAt, isStalled, retained errors).
8.10 GET /api/admin/catalog/jobs/import-template
Downloads the column template for an import file — header row only, no example row, because a downloadable import file is eventually uploaded unmodified and a data row would create junk. Zero rows is a no-op.
entity is one of category, brand, brand_series.
Permission Catalog_READ — the file carries no data, only column names. Rate limit ADMIN_READ 30/min. Returns 200 as text/csv; charset=utf-8 with Content-Disposition: attachment. An unknown entity fails DTO validation with 400.
Columns: category -> name, slug, description, parentSlug, displayOrder, isVisible, imageUrl, imageAlt, seoTitle, seoDescription; brand swaps the image pair for logoUrl, logoAlt and drops parentSlug; brand_series adds a required brandSlug.
The list is generated from the same constant the row builder's fields are typed against, so a column cannot silently diverge from what the parser reads: satisfies rejects a column that is not a row field, an Exclude assertion rejects a row field with no column, and a round-trip spec parses the emitted template back through the real builder.
Route ordering is load-bearing — this literal segment is declared above @Get(":publicId") in the controller. Declared after it, the wildcard swallows the path and the request fails as "publicId must be a UUID".
8.11 Tag endpoints
Admin /api/admin/catalog/tags (list/detail/create(201)/update/visibility/delete(200)/restore(200); Tags_*; errors TAG_NOT_FOUND, TAG_SLUG_ALREADY_EXISTS, TAG_ALREADY_DELETED, TAG_NOT_DELETED, TAG_HAS_LINKED_PRODUCTS). Storefront /api/mobile/catalog/tags (list/detail by slug; 404 TAG_NOT_FOUND).
8.12 GET /api/mobile/catalog/category-tree
Purpose
The whole storefront navigation in one request: every visible root category, each with its visible direct children.
It exists because no other customer response exposed a category's parent. GET /categories
filters parentId IS NULL and its DTO carries no parent field, so a consumer could only recover the
hierarchy by calling GET /categories/{slug}/breadcrumbs once per descendant — up to ~95 requests to
render one menu, against a shared per-IP budget. That is what this endpoint replaces.
Depth is 1. A child carries no children of its own. A grandchild is not returned.
Source Evidence
| File | What it establishes |
|---|---|
catalog/customer/category-tree/catalog-category-tree-customer.controller.ts | Route, @Public(), rate limit |
catalog/customer/category-tree/catalog-category-tree-customer.service.ts | The single SQL statement, the caps, the drop-on-unresolved-slug rule |
catalog/customer/category-tree/dto/category-tree-node.dto.ts | Response shape |
catalog/shared/catalog.constants.ts | CATALOG_CUSTOMER_ROOT_CATEGORY_LIST_MAX, CATALOG_CUSTOMER_TREE_CHILDREN_PER_ROOT_MAX, CATALOG_CUSTOMER_TREE_CHILD_ROWS_MAX |
mobile/mobile.module.ts | Registered as a concrete leaf in MOBILE_CHILDREN |
Auth and Permissions
@Public(), IpThrottlerGuard + PUBLIC_HIGH_FREQUENCY 300/min, no permission.
The budget differs from the rest of the storefront catalog surface on purpose: this renders in the
shared navigation on every page view, which is what PUBLIC_HIGH_FREQUENCY names, while
/categories is an ordinary content read at PUBLIC_READ 60/min.
Request
No parameters. No query string, no body. Unknown query parameters are rejected by the global
ValidationPipe's forbidNonWhitelisted.
Response
200 — an array of nodes in the standard envelope:
{
"message": "Category tree fetched successfully",
"data": [
{
"publicId": "01a0192f-31bf-71ee-8bc3-f3e0cbf7e5e3",
"name": "Smartphones",
"slug": "smartphones",
"description": "Discover the latest smartphones…",
"imageUrl": "http://localhost:5002/public/thumbnail/KAHfnL.webp",
"imageAlt": "Latest smartphones and mobile phones",
"displayOrder": 1,
"children": [
{
"publicId": "01a01935-d765-7003-974f-ffbb32477c56",
"name": "Android Phones",
"slug": "android-phones",
"description": "Explore Android smartphones…",
"imageUrl": "https://cdn.example.com/categories/android-phones.jpg",
"imageAlt": "Android smartphones",
"displayOrder": 1
}
]
}
]
}children is always present, [] for a childless root. A consumer may treat it as required.
Ordering is roots by displayOrder then id, and children by displayOrder then id within
their own root — the same ordering GET /categories uses. The database does the ordering; a
consumer that re-sorts is stating a second opinion about the same question.
Side Effects
None. One read, no writes, no cache population, no queue job. There is deliberately no server-side cache: the sole consumer caches the response for an hour on its own side.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 429 | — | PUBLIC_HIGH_FREQUENCY budget exhausted for the caller's key |
There is no 404 and no 400. An empty catalogue is 200 with data: [], because "no categories" is a
valid state of the store rather than a missing resource.
Edge Cases
| Case | Behaviour |
|---|---|
| Root hidden or soft-deleted | Root and all its children are absent |
| Child hidden or soft-deleted | That child is absent; its root and siblings are unaffected |
| Node has no current slug | Dropped, and a WARN names its publicId. A row with no current slug has no reachable URL, so emitting slug: "" would list a category that links to the collection root |
| Category renamed | The current slug is returned; retired slugs are not |
| More than 200 visible roots | Roots past 200 by displayOrder are absent, and a WARN says so |
| A root with more than 200 visible children | Children past 200 within that root are absent, and a WARN names the parent |
| More than 4,800 child rows in total | Roots ordered last render childless, and a WARN says so. 4,800 ÷ 200 = 24 roots can each hold a full 200; the two ceilings are nested, not independent |
| Grandchildren exist | Not returned. The endpoint is depth 1 |
The caps exist because SlugOwnershipService.getCurrentSlugs uses an unchunked inArray, which has
a hard ceiling at Postgres's 65,535 bind parameters. Truncation is deterministic — by displayOrder,
never arbitrary — and never silent.
Example Requests
curl -s http://localhost:5002/api/mobile/catalog/category-treeConsistency
One statement, one snapshot. Roots and children are read by a single UNION ALL rather than two
queries, so a category demoted from root to child between two reads cannot appear twice in one
response, and a promoted one cannot vanish. That race is absent rather than mitigated — which is
also why there is no transaction here.
Slug resolution runs as two further batched statements (one per level, never one per node), so a request issues three statements in total. The claim is that the tree rows come from one snapshot, not that the request makes one round trip.
8.13 GET /api/mobile/catalog/categories
Purpose
Visible root categories, flat. parentId IS NULL — it does not describe the hierarchy; use
8.12 for that.
Auth and Permissions
@Public(), PUBLIC_READ 60/min, no permission.
Response
200 — [{ publicId, name, slug, description, imageUrl, imageAlt, displayOrder }], ordered by
displayOrder then id, capped at CATALOG_CUSTOMER_ROOT_CATEGORY_LIST_MAX (200).
Edge Cases
| Case | Behaviour |
|---|---|
| Root has no current slug | Dropped, with a WARN naming its publicId |
That last row is a behaviour change. This endpoint previously emitted slug: "" for such a row,
which put a category in the list whose link resolved to the collection root and whose own detail
route 404'd. It now matches 8.12, because two public
endpoints disagreeing about which categories exist is worse than either answer: the home page would
show a category the navigation does not.
9. Flow Diagrams
9.1 Route Ownership
9.2 Request Sequence (move)
9.3 Error Branch (delete category)
10. Pagination, Sorting, Filtering, and Search
| Endpoint | Pagination Type | Default Size | Max Size | Sort Fields | Filters | Result Cap |
|---|---|---|---|---|---|---|
| Admin lists (category/brand/series/jobs) | offset page/size | 20 | 100 | name/displayOrder/createdAt/updatedAt (jobs: createdAt/updatedAt/finishedAt) | per §6.4 | offset depth 10,000 (400 PAGINATION_LIMIT_TOO_LARGE) |
GET /mobile/catalog/categories | none (bounded) | — | — | displayOrder | root+visible | 200 |
GET /mobile/catalog/brands / series | offset page/size | 20 | 100 | displayOrder | visible | 200 (series per brand) |
GET /mobile/catalog/search | offset page/size | 20 | 100 | similarity rank, displayOrder, id | entity, term | — |
| Suggestions | none | — | — | rank | entity, term | 10 |
Search ranking is fixed: similarity DESC, display_order ASC, id ASC; term < 2 chars = no search; terms > 64 chars skip the Redis cache.
11. Caching, Jobs, and External Integrations
| Integration | Used? | Details | Source |
|---|---|---|---|
| Redis cache | Yes | Three catalog domains (category/brand/series) + search prefix; CACHE_TTL.STANDARD 300s; cross-entity patterns | cache-invalidation.tags.ts |
| BullMQ | Yes | CATALOG queue: catalog.import_entities, catalog.export_entities; lease claim; registered by its own module (not REGISTERED_QUEUES); sweep on MAINTENANCE | packages/jobs/src/index.ts |
| External API | No | — | — |
13. Mandatory Deep API Documentation Pack
13.1 Route-by-Route Completeness Matrix
| Route | Controller Method | DTOs | Service Method | Guards | Permissions | Cache | Jobs | DB Touches | Errors | Documented? |
|---|---|---|---|---|---|---|---|---|---|---|
GET /api/admin/catalog/categories | findAll | FetchCategoryDto | CatalogCategoryAdminService.findAll | JWT+Role+IpThrottle | Categories_READ | — | — | category, slugs | 400/404/422→400 | Yes |
GET /api/admin/catalog/categories/tree | findTree | — | …findTree | same | Categories_READ | — | — | category | 400 CATALOG_TREE_TOO_LARGE | Yes |
GET /api/admin/catalog/categories/:publicId | findById | CategoryParamsDto | …findById | same | Categories_READ | — | — | category | 404 | Yes |
GET /api/admin/catalog/categories/:publicId/breadcrumbs | findBreadcrumbs | CategoryParamsDto | …findBreadcrumbs | same | Categories_READ | — | — | category | 404 | Yes |
POST /api/admin/catalog/categories | create | CreateCategoryDto | CatalogCategoryWriteService.create | same | Categories_CREATE | invalidate | — | category, slug | 400/404/409 | Yes |
PATCH /api/admin/catalog/categories/:publicId | update | UpdateCategoryDto | …update | same | Categories_UPDATE | invalidate | — | category, slug | 400/404/409 | Yes |
PATCH /:publicId/move | move | MoveCategoryDto | …move | same | Categories_UPDATE | invalidate | — | category subtree | 404/409 | Yes |
PATCH /:publicId/visibility | setVisibility | CategoryVisibilityDto | …setVisibility | same | Categories_UPDATE | invalidate | — | category | 404/409 | Yes |
DELETE /:publicId | delete | CategoryParamsDto | …delete | same | Categories_DELETE | invalidate | — | category | 404/409 | Yes |
POST /:publicId/restore | restore | CategoryParamsDto | …restore | same | Categories_RESTORE | invalidate | — | category | 404/409 | Yes |
POST /bulk/* + PATCH /reorder (category) | 4 methods | Bulk DTOs | CatalogCategoryBulkService.* | same | per route | invalidate | — | category | 409 | Yes |
| Brand routes (11) | 11 methods | Brand DTOs | CatalogBrandAdminService.* + bulk | same | Brands_* | invalidate (admin list cached) | — | brand, slugs, series | 404/409 | Yes |
| Series routes (11) | 11 methods | Series DTOs | CatalogBrandSeriesAdminService.* + bulk | same | BrandSeries_* | invalidate | — | series, brand, slugs | 404/409 | Yes |
| Job routes (5) | 5 methods | Job DTOs | CatalogJobAdminService.* | same | Catalog_* | — | outbox/queue | catalog_job, outbox_events | 400/403/404/409 | Yes |
| Storefront category (3) | 3 methods | slug params | CatalogCategoryCustomerService.* | Public+IpThrottle | — | domain | — | category, slugs | 404 | Yes |
| Storefront brand (3) | 3 methods | QueryDto | CatalogBrandCustomerService.* | Public+IpThrottle | — | domain | — | brand, series, slugs | 404 | Yes |
| Storefront series (2) | 2 methods | QueryDto | CatalogBrandSeriesCustomerService.* | Public+IpThrottle | — | domain | — | series, brand, slugs | 404 | Yes |
| Storefront search (2) | 2 methods | Search DTOs | CatalogSearchCustomerService.* | Public+IpThrottle | — | search | — | 3 tables | — | Yes |
| Tag routes (9) | 9 methods | Tag DTOs | CatalogTagAdminService.* + customer | JWT+Role / Public | Tags_* | invalidate | — | tag, links | 404/409 | Yes |
13.2 Request/Response Exhaustiveness
Covered in §8: minimal create payloads (§6.1), success responses (§8.1, §8.2), empty-list behavior (admin lists return data: [] + pagination metadata; storefront bounded lists return []), validation error (400 VALIDATION_FAILED representative), domain errors per endpoint (§8 error tables), rate-limit behavior (throttler 429), admin permission errors (403).
13.3 API Diagram Pack
Route ownership (§9.1), sequence per endpoint family (§9.2, backend §7), activity diagrams per mutation family (§9.3, feature §6), error decision trees (§9.3), async/job flow (backend §7.2), cache flow (backend §8).
13.4 Consumer Integration Notes
| Consumer | Required Knowledge | Failure Handling | Contract Stability |
|---|---|---|---|
| Web frontend | Storefront slugs, canonicalSlug redirect pattern, effectiveVisible | 404 on hidden/deleted → remove from UI; redirect on canonicalSlug | Stable |
| Mobile app | PUBLIC_SEARCH/PUBLIC_SUGGEST rate limits (ip+device) | 429 → back off | Stable |
| Admin panel | Tree invariants, bulk per-item failures, reorder exact-set rule | 409 codes map to specific admin UI messages | Stable |
| QA | Retired slug behavior, depth cap, delete-blocked cases | Reproduce via exact error codes | Stable |
| Internal services | Import entity permission, idempotency keys on jobs | 403 CATALOG_JOB_ENTITY_PERMISSION_DENIED → token fix | Stable |
13.5 API Tradeoffs and Rationale
| Decision | Chosen Behavior | Alternatives Considered | Why This Tradeoff | Risk | Mitigation |
|---|---|---|---|---|---|
| Slug ownership tables | Current + retired under one unique | Slug column + separate history | Re-pointing is unrepresentable | More tables | Schema probes |
| Bulk per-item results | succeeded/failures in data | Single errorCode | Envelope carries one errorCode | Partial failures look like success | Documented |
| Reorder exact-set | Strict equality with siblings | Partial reorder | No silently-stale items | 409 churn | Exact error codes |
| Retired slugs resolve | 200 + canonicalSlug | 3xx redirect | Frontend owns routing | Duplicate redirect logic | Documented |
| One error code per status | PAGINATION_LIMIT_TOO_LARGE = 400 everywhere | Mixed 400/422 | Clients branch on status | — | Review-enforced |
13.6 API Change Impact
| Change | Affected Consumers | Backend Impact | Data Impact | Migration Needed? | Compatibility Plan |
|---|---|---|---|---|---|
Reorder rate limit → ADMIN_REORDER | Admin panel | Constant swap | None | No | Higher budget (30/min) |
| Import entity permission | Admin panel | Service re-check | None | No | 403 with exact code |
14. Zero-Omission API Checklist
- Every controller route is documented (§4, §8, §13.1).
- Every parent route prefix and runtime URL is documented (§2, §4).
- Every DTO field, enum, default, transform and validator is documented (§6, §7).
- Every response field, nullable field, generated field and omitted raw entity field is documented (§8).
- Every auth, guard, permission, public decorator and guest identity branch is documented (§5).
- Every success, validation, auth, permission, not-found, conflict, rate-limit and server-error branch is documented (§8).
- Every DB read/write, cache hit/miss/invalidation, queue job, audit log and external call is documented (§11, backend §8/§9).
- Every route has examples for minimal request, success response and representative failures (§8).
- Every endpoint family has route, sequence, activity and error diagrams (§9, backend §7).
- Every tradeoff and compatibility risk is documented (§13.5, §13.6).
- The API doc links to backend and features/flows (§1, See Also).
15. Integration Checklist
- Every route from controllers is documented.
- Every DTO field is documented.
- Every enum value is documented.
- Every response envelope is documented.
- Every error code is documented.
- Every auth guard and permission is documented.
- Every cache key, queue job and external call is documented.
- Every diagram matches the current code.
- The API doc links to backend and features/flows.
See Also
- Backend doc: /docs/developer/catalog/backend
- Features and flows doc: /docs/developer/catalog/feature
- TDD: not yet published