Banner Module — Admin
HTTP surface for managing promotional banners attached to catalog entities (categories, brands, tags, ingredients). Banner targets a single entity via a polymorphic (entityType,…
HTTP surface for managing promotional banners attached to catalog entities (categories, brands, tags, ingredients). Banner targets a single entity via a polymorphic (entityType, entityId) pair and is consumed by an unauthenticated storefront endpoint owned by the same module.
Source:
api-modules/banner/src/controllers/admin-banner.controller.ts.Banner attaches to one of four catalog entity types. Catalog admin list endpoints surface a
bannerCountso admins see how many banners are attached before deleting a brand/category.
Conventions
Authentication
All endpoints require a Better-Auth admin session and a role granting the matching banner:* permission.
| Endpoint group | Permission |
|---|---|
GET /admin/banners, GET /admin/banners/:id | banner: read |
POST /admin/banners | banner: create |
PUT /admin/banners/:id, POST /admin/banners/:id/restore | banner: update |
DELETE /admin/banners/:id | banner: delete |
Response envelope
Successful responses are wrapped by ResponseInterceptor:
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": { /* optional, e.g. pagination */ }
}Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN |
| 404 | NOT_FOUND |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Lifecycle
Banners soft-delete via deletedAt. Hidden from default listings; POST /admin/banners/:id/restore reverses the deletion.
Domain types
BannerResponse
type BannerEntityType = "category" | "brand" | "tag" | "ingredient";
type Platform = "APP" | "WEB" | "BOTH";
type BannerResponse = {
id: string;
entityType: BannerEntityType;
entityId: string;
image: string; // S3 storage key
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.
Image storage
image is the S3 key returned by the storage module's presigned-upload flow. Banner module validates length only (1..1024 chars); the key format and existence are the storage module's contract.
Endpoints
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
| Name | Type | Notes |
|---|---|---|
entityType | BannerEntityType? | One of category / brand / tag / ingredient |
entityId | string? | Filter to one entity (typically combined with entityType) |
platform | Platform? | APP / WEB / BOTH |
isActive | boolean? | Coerced from true/false |
page, limit, search, filters[] | — | Standard QueryDto |
Response 200 — paginated envelope of BannerResponse[].
GET /admin/banners/:id — Get a banner
Required permission: banner: read.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
POST /admin/banners — Create a banner
Required permission: banner: create.
Body
{
"entityType": "brand",
"entityId": "01J9...",
"image": "banners/2026-05/festive.jpg",
"url": "/sale",
"platform": "BOTH",
"isActive": true,
"sortOrder": 0,
"metadata": { "campaign": "Q2" }
}| Field | Type | Constraints |
|---|---|---|
entityType | enum | category / brand / tag / ingredient |
entityId | string | Required; non-deleted row of the matching taxonomy table |
image | string | 1..1024 chars; storage key |
url | string | 1..2048 chars; https://... or relative /path |
platform | enum | APP / WEB / BOTH. Default BOTH |
isActive | boolean? | Default true |
sortOrder | int? | >= 0. Default 0 |
metadata | Record<string, unknown> | null? | Free-form |
Response 201 — BannerResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Body fails zod (bad URL, unknown entity type, etc.) |
| 404 | NOT_FOUND | entityId does not resolve to a non-deleted row of entityType |
PUT /admin/banners/:id — Update a banner
Required permission: banner: update. entityType and entityId are immutable — to move a banner to a different entity, soft-delete and re-create.
Body — partial CreateBannerInput minus entityType / entityId.
Response 200 — BannerResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
| 400 | VALIDATION_ERROR | Body fails zod |
DELETE /admin/banners/:id — Soft-delete
Required permission: banner: delete. Stamps deletedAt. Hidden from storefront and default admin listings. Reversible via POST /:id/restore.
Response 200 — the deleted BannerResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
POST /admin/banners/:id/restore — Restore
Required permission: banner: update. Clears deletedAt.
Response 200 — BannerResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
Domain events
Emitted via EventEmitter2 after writes.
| Event | Fired when |
|---|---|
banner.created | POST /admin/banners |
banner.updated | PUT /admin/banners/:id, POST /admin/banners/:id/restore |
banner.deleted | DELETE /admin/banners/:id |
Related modules
admin-rbac— gates every endpoint viabanner:*permissions. Seeadmin-rbac.md.catalog— owns the four entity tables (category,brand,tag,ingredient) that banners reference. Catalog admin lists surface abannerCountaggregated from this table. Seecatalog.md.storage— issues presigned-upload URLs forimagekeys and resolves them to CDN URLs on the storefront.
Affiliate Module — Admin
HTTP surface for platform staff managing the affiliate plugin: review applications, manage affiliates (incl. suspend/resume + per-affiliate commission overrides), configure…
Cart Module — Admin
HTTP surface for platform-admin oversight of carts: paginated browse + detail view (priced, vendor-allocated), force-discard, manual reservation release, and a suite of analytics…