Product Attribute Module — Admin
HTTP surface for managing the platform-wide catalogue of product attributes (typed metadata fields — Material, Capacity, Heel Height, etc.) and attribute groups that bundle…
HTTP surface for managing the platform-wide catalogue of product attributes (typed metadata fields — Material, Capacity, Heel Height, etc.) and attribute groups that bundle attributes together for application to specific product families.
Source:
api-modules/product-attribute/src/controllers/admin-product-attribute.controller.ts,api-modules/product-attribute/src/controllers/admin-product-attribute-group.controller.ts.Attributes are catalog-wide definitions. Vendors apply them (or a group of them) to their products via the vendor-side endpoints. The admin surface owns the universe of attributes / groups; vendors only consume.
Conventions
Authentication
All endpoints require a Better-Auth admin session and a role granting the matching productAttribute:* permission.
| Endpoint group | Permission |
|---|---|
GET /admin/product-attributes, GET /admin/product-attributes/:id, GET /admin/product-attribute-groups, GET /admin/product-attribute-groups/:id | productAttribute: read |
POST /admin/product-attributes, POST /admin/product-attribute-groups | productAttribute: create |
PUT /admin/product-attributes/:id, POST /admin/product-attributes/:id/restore, PUT /admin/product-attribute-groups/:id, POST /admin/product-attribute-groups/:id/restore | productAttribute: update |
DELETE /admin/product-attributes/:id, DELETE /admin/product-attribute-groups/:id | productAttribute: 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 |
| 409 | UNIQUE_VIOLATION (code already in use) |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Lifecycle
Attributes and groups soft-delete via deletedAt. DELETE flips it; POST /:id/restore clears it.
Domain types
ProductAttributeResponse
type ProductAttributeType = "multi_select" | "text" | "boolean" | "number" | "select";
type ProductAttributeValueResponse = {
id: string;
attributeId: string;
value: string; // 1..255
sortOrder: number;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
};
type ProductAttributeResponse = {
id: string;
title: string; // 1..255
code: string; // lowercase alnum + hyphens
type: ProductAttributeType;
isRequired: boolean;
isUnique: boolean;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
values: ProductAttributeValueResponse[]; // present for "select"/"multi_select"
};ProductAttributeGroupResponse
type ProductAttributeGroupResponse = {
id: string;
title: string;
code: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
attributes: Array<ProductAttributeResponse & { sortOrder: number }>;
};Product attributes
Base path: /admin/product-attributes.
GET /admin/product-attributes — List attributes
Required permission: productAttribute: read. Standard QueryDto (page / limit / search / filters[]).
Response 200 — paginated envelope of ProductAttributeResponse[].
GET /admin/product-attributes/:id — Get an attribute
Required permission: productAttribute: read.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
POST /admin/product-attributes — Create an attribute
Required permission: productAttribute: create.
Body
{
"title": "Material",
"code": "material",
"type": "multi_select",
"isRequired": false,
"isUnique": false,
"values": [
{ "value": "Cotton", "sortOrder": 0 },
{ "value": "Linen", "sortOrder": 1 },
{ "value": "Polyester", "sortOrder": 2 }
]
}| Field | Type | Constraints |
|---|---|---|
title | string | 1..255 |
code | string | 1..255, lowercase alnum + hyphens |
type | enum | multi_select / text / boolean / number / select |
isRequired | boolean | Default false |
isUnique | boolean | Default false |
values | Array<{value, sortOrder}>? | value: 1..255; sortOrder: >= 0. Relevant for select / multi_select |
Response 201 — ProductAttributeResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Body fails zod |
| 409 | UNIQUE_VIOLATION | code already taken |
PUT /admin/product-attributes/:id — Update an attribute
Required permission: productAttribute: update. Partial — every field optional.
Response 200 — updated ProductAttributeResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
DELETE /admin/product-attributes/:id — Soft-delete
Required permission: productAttribute: delete.
Response 200 — deleted ProductAttributeResponse.
POST /admin/product-attributes/:id/restore — Restore
Required permission: productAttribute: update.
Response 200 — restored ProductAttributeResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
Product attribute groups
Base path: /admin/product-attribute-groups.
GET /admin/product-attribute-groups — List groups
Required permission: productAttribute: read. Standard QueryDto.
Response 200 — paginated envelope of ProductAttributeGroupResponse[].
GET /admin/product-attribute-groups/:id — Get a group
Required permission: productAttribute: read.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
POST /admin/product-attribute-groups — Create a group
Required permission: productAttribute: create.
Body
{
"title": "Apparel essentials",
"code": "apparel-essentials",
"attributes": [
{ "attributeId": "01J9...", "sortOrder": 0 },
{ "attributeId": "01J9...", "sortOrder": 1 }
]
}| Field | Type | Constraints |
|---|---|---|
title | string | 1..255 |
code | string | 1..255, lowercase alnum + hyphens |
attributes | Array<{attributeId, sortOrder}>? | attributeId required; sortOrder >= 0 |
Response 201 — ProductAttributeGroupResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Body fails zod |
| 409 | UNIQUE_VIOLATION | code already taken |
PUT /admin/product-attribute-groups/:id — Update a group
Required permission: productAttribute: update. Partial. When attributes is sent it replaces the entire member set (whole-set semantics, not deep-merge).
Response 200 — updated ProductAttributeGroupResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
DELETE /admin/product-attribute-groups/:id — Soft-delete
Required permission: productAttribute: delete.
Response 200 — deleted ProductAttributeGroupResponse.
POST /admin/product-attribute-groups/:id/restore — Restore
Required permission: productAttribute: update.
Response 200 — restored ProductAttributeGroupResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
Related modules
admin-rbac— gates every endpoint viaproductAttribute:*. Seeadmin-rbac.md.catalog— vendor-side product create/update endpoints apply attributes (or an entire group) to a product.
Order Module — Admin
HTTP surface for platform-admin oversight of orders, returns, and vendor payouts. Read every order on the platform; perform ops actions (cancel on behalf of the customer, manually…
Reviews Module — Admin
HTTP surface for platform-admin moderation of product reviews: cross-vendor list with filters, full-field edit, lifecycle transitions (approve / reject / mark-spam), admin-create…