Supercommerce API Docs
Full Module Docs

Banner Module

HTTP surface for promotional banners attached to catalog entities (categories, brands, tags, ingredients) — admin CRUD + unauthenticated storefront read by entity slug.

HTTP surface for promotional banners attached to catalog entities (categories, brands, tags, ingredients) — admin CRUD + unauthenticated storefront read by entity slug.

Source: api-modules/banner (registered via BannerModule.forRoot() in apps/api/src/app.module.ts).

Banner attaches to one catalog entity via a polymorphic (entity_type, entity_id) pair. The catalog module's admin list endpoints surface a bannerCount so admins can see how many banners are attached before deleting a brand/category.


Conventions

Authentication

Endpoint groupAuthPermission
GET /store/banners/:entityType/slug/:slugnone
GET /admin/banners, GET /admin/banners/:idrequiredbanner: read
POST /admin/bannersrequiredbanner: create
PUT /admin/banners/:id, POST /admin/banners/:id/restorerequiredbanner: update
DELETE /admin/banners/:idrequiredbanner: delete

Response envelope

Successful responses are wrapped by ResponseInterceptor:

{
  "data": <payload>,
  "message": "Success",
  "statusCode": 200,
  "metadata": { /* optional, e.g. pagination */ }
}

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED
403FORBIDDEN
404NOT_FOUND
409CONFLICT, FOREIGN_KEY_VIOLATION (when entityId doesn't resolve)
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Lifecycle

Banners soft-delete via deletedAt. Hidden from default listings and from the storefront read, but GET /admin/banners?includeDeleted=true (via the generic filters field) returns them.


Domain types

BannerResponse

type BannerEntityType = "category" | "brand" | "tag" | "ingredient";
type Platform = "APP" | "WEB" | "BOTH";

type BannerResponse = {
  id: string;
  entityType: BannerEntityType;
  entityId: string;
  image: string;                       // storage key (S3) — resolve to URL client-side
  url: string;                         // click destination
  platform: Platform;
  isActive: boolean;
  sortOrder: number;                   // ascending; ties broken by createdAt desc
  metadata: Record<string, unknown> | null;
  createdAt: string;                   // ISO
  updatedAt: string;
  deletedAt: string | null;
};

URL rules

url is validated to be either an absolute https:// URL or a relative path that begins with /. Plain http://, javascript:, mailto:, etc. are rejected. The storefront uses banner URLs verbatim — keep them sanitized.

Image storage

image is the S3 key returned by the storage module's presigned-upload flow. The banner module does not validate the key format or that the object exists — it's a length-only check (1..1024 chars). The storefront resolves keys to public URLs via the storage CDN.


Admin

Base path: /admin/banners.

GET /admin/banners — List banners

Required permission: banner: read. Standard QueryDto (page / limit / search / filters) plus first-class banner filters.

Query

NameTypeNotes
entityTypeBannerEntityType?filter to one of category / brand / tag / ingredient
entityIdstring?filter to one entity (combine with entityType)
platformPlatform?filter to APP / WEB / BOTH
isActiveboolean?coerced from true/false
page, limit, search, filters[]standard QueryDto

Response 200 — paginated envelope of BannerResponse[].


GET /admin/banners/:id

Required permission: banner: read. 404 on unknown id.


POST /admin/banners — Create

Required permission: banner: create.

Body

{
  "entityType": "brand",
  "entityId": "01J9...",            // must reference a non-deleted entity
  "image": "banners/2026-05/festive.jpg",
  "url": "/sale",                   // or "https://example.com/sale"
  "platform": "BOTH",               // default "BOTH"
  "isActive": true,                 // default true
  "sortOrder": 0,                   // default 0
  "metadata": { "campaign": "Q2" }  // optional
}
FieldTypeConstraints
entityTypeenumcategory / brand / tag / ingredient
entityIdstringRequired; non-deleted row of the matching taxonomy table
imagestring1..1024 chars; storage key
urlstring1..2048 chars; https://... or relative /path
platformenumAPP / WEB / BOTH. Default BOTH
isActiveboolean?Default true
sortOrderint?>= 0. Default 0
metadataRecord<string, unknown> | null?free-form

Response 201BannerResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod (bad URL, unknown entity type, etc.)
404NOT_FOUNDentityId doesn't resolve to a non-deleted row of entityType

PUT /admin/banners/:id — Update

Required permission: banner: update. entityType and entityId are immutable — to move a banner to a different entity, soft-delete and re-create. Keeps service-layer validation simple and makes the intent explicit.

Body — partial CreateBannerInput minus entityType / entityId.

Response 200BannerResponse.


DELETE /admin/banners/:id — Soft delete

Required permission: banner: delete. Stamps deletedAt. Reversible via POST /:id/restore.


POST /admin/banners/:id/restore — Restore

Required permission: banner: update. Clears deletedAt.


Storefront

GET /store/banners/:entityType/slug/:slug — Active banners for an entity

Unauthenticated. Returns active (isActive=true, deletedAt IS NULL) banners attached to the resolved entity, sorted by sortOrder ASC, createdAt DESC.

Path params

NameNotes
entityTypeOne of category / brand / tag / ingredient. Anything else → 400 BAD_REQUEST
slugSlug of the catalog entity

Query

NameTypeNotes
platformPlatform?When set to APP or WEB, filters out banners targeted at the other platform. Absent or BOTH returns every active banner regardless of platform

When the entity slug doesn't resolve, the response is an empty array (not 404 — distinguishes "no banners" from "no such category" the same way for the consumer).

Response 200 — array of BannerResponse.

{
  "data": [
    {
      "id": "01J9...",
      "entityType": "brand",
      "entityId": "01J9...",
      "image": "banners/2026-05/festive.jpg",
      "url": "/sale",
      "platform": "BOTH",
      "isActive": true,
      "sortOrder": 0,
      "metadata": null,
      "createdAt": "2026-05-01T08:00:00.000Z",
      "updatedAt": "2026-05-01T08:00:00.000Z",
      "deletedAt": null
    }
  ],
  "message": "Success",
  "statusCode": 200
}

Domain events

Emitted via EventEmitter2 after writes — consumed by the storefront cache invalidator.

EventFired when
banner.createdPOST /admin/banners
banner.updatedPUT /admin/banners/:id, POST /admin/banners/:id/restore
banner.deletedDELETE /admin/banners/:id

  • catalog — owns the four entity tables (category, brand, tag, ingredient) that banners reference. Admin lists in catalog surface a bannerCount aggregated from this table. See catalog.md.
  • storage — provides presigned-upload URLs for banner images and resolves keys to CDN URLs on the storefront.

On this page