Product Attribute Module
HTTP surface for product attributes (descriptive fields attached to products — material, country of origin, ingredient list, etc.) and attribute groups (reusable bundles of…
HTTP surface for product attributes (descriptive fields attached to products — material, country of origin, ingredient list, etc.) and attribute groups (reusable bundles of attributes). Admin owns full CRUD; vendor reads from a picker.
Source:
api-modules/product-attribute(registered viaProductAttributeModule.forRoot()inapps/api/src/app.module.ts).Attribute groups let the platform define a bundle once ("Skincare attributes": ingredients + skin type + finish + …) and reuse it across products. The vendor product form picks an attribute group, then fills the per-attribute values.
Conventions
Authentication
| Endpoint group | Auth | Permission |
|---|---|---|
GET /admin/product-attributes/** | required | productAttribute: read |
POST /admin/product-attributes | required | productAttribute: create |
PUT /admin/product-attributes/:id, POST /admin/product-attributes/:id/restore | required | productAttribute: update |
DELETE /admin/product-attributes/:id | required | productAttribute: delete |
/admin/product-attribute-groups/** | required | productAttribute: <action> (same productAttribute:* resource) |
GET /vendor/product-attributes/**, GET /vendor/product-attribute-groups/** | required (vendor session) | active vendor in session |
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 taken) |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Lifecycle / soft-delete
Both attributes and groups soft-delete via deletedAt. DELETE flips it, POST /:id/restore clears it. code is unique among non-deleted rows.
Domain types
ProductAttribute
type ProductAttributeType = "multi_select" | "text" | "boolean" | "number" | "select";
type ProductAttribute = {
id: string;
title: string; // 1..255 chars (display label)
code: string; // 1..255 chars; /^[a-z0-9]+(?:-[a-z0-9]+)*$/; unique
type: ProductAttributeType;
isRequired: boolean; // product form refuses to submit when missing
isUnique: boolean; // value must be unique across products that use this attribute
values: ProductAttributeValue[]; // populated for "select" / "multi_select" only
createdAt: string;
updatedAt: string;
deletedAt: string | null;
};
type ProductAttributeValue = {
id: string;
attributeId: string;
value: string; // 1..255 chars
sortOrder: number;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
};ProductAttributeGroup
type ProductAttributeGroup = {
id: string;
title: string;
code: string; // unique
createdAt: string;
updatedAt: string;
deletedAt: string | null;
attributes: Array<ProductAttribute & { sortOrder: number }>; // members in order
};Type semantics
type | Stored as | Notes |
|---|---|---|
text | single string | Free-form, no values[] |
number | numeric string | Free-form, no values[] |
boolean | "true" / "false" | No values[] |
select | one values[].id | Single-pick from a predefined list |
multi_select | array of values[].id | Multi-pick from a predefined list |
The values list is sortable via sortOrder and ignored for text / number / boolean.
Admin — attributes
Base path: /admin/product-attributes. Permissions on productAttribute:*.
GET /admin/product-attributes — List
Required permission: productAttribute: read. Standard QueryDto.
Response 200 — paginated envelope of ProductAttribute[].
GET /admin/product-attributes/:id
Required permission: productAttribute: read. Returns ProductAttribute with full values[].
POST /admin/product-attributes — Create
Required permission: productAttribute: create.
Body
{
"title": "Skin Type",
"code": "skin-type",
"type": "multi_select",
"isRequired": false,
"isUnique": false,
"values": [
{ "value": "Oily", "sortOrder": 0 },
{ "value": "Dry", "sortOrder": 1 },
{ "value": "Normal", "sortOrder": 2 }
]
}| Field | Constraints |
|---|---|
title | 1..255 chars |
code | 1..255 chars, regex ^[a-z0-9]+(?:-[a-z0-9]+)*$. Unique among non-deleted |
type | one of multi_select / text / boolean / number / select |
isRequired | default false |
isUnique | default false |
values | required for select / multi_select; ignored otherwise |
Response 201 — ProductAttribute.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Body fails zod |
| 409 | UNIQUE_VIOLATION | code collides with a non-deleted attribute |
PUT /admin/product-attributes/:id — Update
Required permission: productAttribute: update. Partial update. When values[] is sent, it replaces the value list wholesale (existing value ids are dropped — products referencing the old ids must be re-saved; the platform doesn't migrate them automatically).
DELETE /admin/product-attributes/:id — Soft delete
Required permission: productAttribute: delete. POST /:id/restore reverses.
Admin — attribute groups
Base path: /admin/product-attribute-groups. Same productAttribute:* permission resource.
GET /admin/product-attribute-groups — List
Required permission: productAttribute: read. Standard QueryDto. Returns groups with their member attributes resolved (including the sortOrder per member).
GET /admin/product-attribute-groups/:id — Detail
Required permission: productAttribute: read. Returns ProductAttributeGroup with full attributes[].
POST /admin/product-attribute-groups — Create
Required permission: productAttribute: create.
Body
{
"title": "Skincare attributes",
"code": "skincare",
"attributes": [
{ "attributeId": "01J9...", "sortOrder": 0 },
{ "attributeId": "01J9...", "sortOrder": 1 }
]
}| Field | Constraints |
|---|---|
title | 1..255 chars |
code | regex ^[a-z0-9]+(?:-[a-z0-9]+)*$. Unique among non-deleted groups |
attributes[].attributeId | must reference a non-deleted attribute |
attributes[].sortOrder | >= 0, default 0 |
Response 201 — ProductAttributeGroup.
PUT /admin/product-attribute-groups/:id — Update
Required permission: productAttribute: update. Partial update. attributes[] (when sent) replaces the membership wholesale.
DELETE /admin/product-attribute-groups/:id — Soft delete
Required permission: productAttribute: delete. POST /:id/restore reverses.
Vendor — read-only picker
Base path: /vendor/product-attributes and /vendor/product-attribute-groups. Auth: vendor session with active vendor. Read-only — vendors cannot create attributes (request the platform admin to create them).
| Method + path | Notes |
|---|---|
GET /vendor/product-attributes | Search attributes for the product form. Standard QueryDto |
GET /vendor/product-attributes/:id | Single attribute with values[] |
GET /vendor/product-attribute-groups | Search groups |
GET /vendor/product-attribute-groups/:id | Group with member attributes resolved |
Both reads filter out soft-deleted rows.
Related modules
catalog— products attach to one attribute group; the per-product attribute values live in catalog tables (product_attribute_value). Seecatalog.md.admin-rbac—productAttribute:*permission resource. Seeadmin-rbac.md.
Payment — Razorpay Provider
Razorpay payment provider that plugs into the platform-neutral payment module. Exposes the SDK-bootstrap flow at place-order (via the PaymentProvider port), the customer-side…
Reviews Module
HTTP surface for product reviews — customer submission + anonymous read, vendor moderation of reviews on their own products (gated by admin settings), and admin full-CRUD with…