509 lines
12 KiB
Markdown
509 lines
12 KiB
Markdown
# API Documentation
|
||
|
||
Endpoint reference for the Marketplace Backoffice.
|
||
Base URL: `https://your-api-domain.com/api`
|
||
|
||
> 🇷🇺 Документация на русском языке: [API.ru.md](./API.ru.md)
|
||
|
||
---
|
||
|
||
## Projects
|
||
|
||
### Get All Projects
|
||
```
|
||
GET /api/projects
|
||
|
||
Response 200:
|
||
[
|
||
{
|
||
"id": "dexar",
|
||
"name": "dexar",
|
||
"displayName": "Dexar Marketplace",
|
||
"active": true,
|
||
"logoUrl": "https://..."
|
||
}
|
||
]
|
||
```
|
||
|
||
---
|
||
|
||
## Categories
|
||
|
||
### Get Categories for Project
|
||
```
|
||
GET /api/projects/:projectId/categories
|
||
|
||
Response 200:
|
||
[
|
||
{
|
||
"id": "cat1",
|
||
"name": "Electronics",
|
||
"visible": true,
|
||
"priority": 1,
|
||
"img": "https://...",
|
||
"projectId": "dexar",
|
||
"subcategories": [ ...Subcategory[] ]
|
||
}
|
||
]
|
||
```
|
||
|
||
### Get Single Category
|
||
```
|
||
GET /api/categories/:categoryId
|
||
|
||
Response 200:
|
||
{
|
||
"id": "cat1",
|
||
"name": "Electronics",
|
||
"visible": true,
|
||
"priority": 1,
|
||
"img": "https://...",
|
||
"projectId": "dexar",
|
||
"subcategories": [ ...Subcategory[] ]
|
||
}
|
||
```
|
||
|
||
### Create Category
|
||
```
|
||
POST /api/projects/:projectId/categories
|
||
|
||
Body:
|
||
{
|
||
"name": "New Category", // required
|
||
"visible": true,
|
||
"priority": 10,
|
||
"img": "https://..."
|
||
}
|
||
|
||
Response 201: (created category object)
|
||
```
|
||
|
||
### Update Category
|
||
```
|
||
PATCH /api/categories/:categoryId
|
||
|
||
Body: (any subset of fields)
|
||
{
|
||
"name": "Updated Name",
|
||
"visible": false,
|
||
"priority": 5
|
||
}
|
||
|
||
Response 200: (updated category object)
|
||
```
|
||
|
||
### Delete Category
|
||
```
|
||
DELETE /api/categories/:categoryId
|
||
|
||
Response 204 No Content
|
||
|
||
Note: Cascades and deletes all subcategories and items under this category
|
||
```
|
||
|
||
---
|
||
|
||
## Subcategories
|
||
|
||
Subcategories are **recursive** - they can be nested under a root `Category` or under
|
||
another `Subcategory`. The nesting depth is unlimited, with one constraint:
|
||
**a subcategory that already contains items cannot have child subcategories** (and vice versa).
|
||
|
||
### Subcategory Object
|
||
```json
|
||
{
|
||
"id": "sub1",
|
||
"name": "Smartphones",
|
||
"visible": true,
|
||
"priority": 1,
|
||
"img": "https://...",
|
||
"categoryId": "cat1", // always the ROOT category ID
|
||
"parentId": "cat1", // direct parent ID (category OR subcategory)
|
||
"itemCount": 15,
|
||
"hasItems": true,
|
||
"subcategories": [] // nested children (empty when hasItems = true)
|
||
}
|
||
```
|
||
|
||
> `categoryId` is always the **root-level category** this subtree belongs to.
|
||
> `parentId` is the **direct parent** - it can be either a category ID or a subcategory ID.
|
||
|
||
---
|
||
|
||
### Get Subcategories Under a Category
|
||
```
|
||
GET /api/categories/:categoryId/subcategories
|
||
|
||
Response 200: Subcategory[] (nested subcategories populated recursively)
|
||
```
|
||
|
||
### Get Single Subcategory
|
||
```
|
||
GET /api/subcategories/:subcategoryId
|
||
|
||
Response 200: Subcategory object (with nested subcategories if any)
|
||
```
|
||
|
||
### Create Subcategory Under a Category (level 1)
|
||
```
|
||
POST /api/categories/:categoryId/subcategories
|
||
|
||
Body:
|
||
{
|
||
"id": "custom-id", // optional, auto-generated if omitted (used in URL routing)
|
||
"name": "Smartphones", // required
|
||
"visible": true,
|
||
"priority": 10
|
||
}
|
||
|
||
Response 201: (created subcategory object)
|
||
Error 400: if category does not exist
|
||
```
|
||
|
||
### Create Subcategory Under a Parent Subcategory (level 2+, nested)
|
||
```
|
||
POST /api/subcategories/:parentSubcategoryId/subcategories
|
||
|
||
Body:
|
||
{
|
||
"id": "custom-id", // optional, auto-generated if omitted (used in URL routing)
|
||
"name": "Apple", // required
|
||
"visible": true,
|
||
"priority": 10
|
||
}
|
||
|
||
Response 201: (created subcategory object)
|
||
Error 400: if parent subcategory has items (hasItems = true)
|
||
Error 404: if parent subcategory does not exist
|
||
```
|
||
|
||
### Update Subcategory
|
||
```
|
||
PATCH /api/subcategories/:subcategoryId
|
||
|
||
Body: (any subset of fields)
|
||
{
|
||
"id": "new-slug", // ID is editable - used for marketplace URL routing
|
||
"name": "Updated Name",
|
||
"visible": false,
|
||
"priority": 3
|
||
}
|
||
|
||
Response 200: (updated subcategory object)
|
||
```
|
||
|
||
### Delete Subcategory
|
||
```
|
||
DELETE /api/subcategories/:subcategoryId
|
||
|
||
Response 204 No Content
|
||
|
||
Note: Cascades and deletes all nested subcategories and items under this subcategory
|
||
```
|
||
|
||
---
|
||
|
||
## Items
|
||
|
||
Items always belong to the **deepest subcategory** in the hierarchy (a leaf node).
|
||
A subcategory with at least one item has `hasItems: true` and cannot have child subcategories.
|
||
|
||
### Get Items (Paginated)
|
||
```
|
||
GET /api/subcategories/:subcategoryId/items
|
||
|
||
Query Params:
|
||
page number (default: 1)
|
||
limit number (default: 20)
|
||
search string optional - filters by name (case-insensitive)
|
||
visible boolean optional - filter by visibility
|
||
tags string optional - comma-separated tag list
|
||
|
||
Response 200:
|
||
{
|
||
"items": [
|
||
{
|
||
"id": "item1",
|
||
"name": "iPhone 15 Pro",
|
||
"visible": true,
|
||
"priority": 1,
|
||
"quantity": 50,
|
||
"price": 1299,
|
||
"discount": 10,
|
||
"currency": "USD",
|
||
"imgs": ["https://...", "https://..."],
|
||
"tags": ["new", "featured"],
|
||
"badges": ["new", "exclusive"],
|
||
"simpleDescription": "Latest iPhone...",
|
||
"description": [
|
||
{ "key": "Color", "value": "Black" },
|
||
{ "key": "Storage", "value": "256GB" }
|
||
],
|
||
"subcategoryId": "sub1",
|
||
"translations": {
|
||
"ru": {
|
||
"name": "iPhone 15 Про",
|
||
"simpleDescription": "Последний iPhone...",
|
||
"description": [
|
||
{ "key": "Цвет", "value": "Чёрный" },
|
||
{ "key": "Память", "value": "256 ГБ" }
|
||
]
|
||
}
|
||
},
|
||
"comments": [
|
||
{
|
||
"id": "c1",
|
||
"text": "Great product!",
|
||
"author": "John Doe",
|
||
"stars": 5,
|
||
"createdAt": "2024-01-10T10:30:00Z"
|
||
}
|
||
]
|
||
}
|
||
],
|
||
"total": 150,
|
||
"page": 1,
|
||
"limit": 20,
|
||
"hasMore": true
|
||
}
|
||
```
|
||
|
||
### Get Single Item
|
||
```
|
||
GET /api/items/:itemId
|
||
|
||
Response 200: (full item object)
|
||
```
|
||
|
||
### Create Item
|
||
```
|
||
POST /api/subcategories/:subcategoryId/items
|
||
|
||
Body:
|
||
{
|
||
"name": "New Product", // required
|
||
"visible": true,
|
||
"priority": 10,
|
||
"quantity": 100,
|
||
"price": 999,
|
||
"discount": 0, // 0–100 (percentage off price)
|
||
"currency": "USD", // USD | EUR | RUB | GBP | UAH
|
||
"imgs": ["https://..."],
|
||
"tags": ["new"],
|
||
"badges": ["new", "exclusive"], // optional - predefined or custom badge labels
|
||
"simpleDescription": "Short description",
|
||
"description": [
|
||
{ "key": "Size", "value": "Large" }
|
||
],
|
||
"translations": { // optional - localized content for marketplace
|
||
"ru": {
|
||
"name": "Новый товар",
|
||
"simpleDescription": "Краткое описание",
|
||
"description": [
|
||
{ "key": "Размер", "value": "Большой" }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
|
||
Response 201: (created item object)
|
||
|
||
Side effect: Sets hasItems = true on the subcategory.
|
||
The subcategory can no longer receive child subcategories.
|
||
```
|
||
|
||
### Update Item
|
||
```
|
||
PATCH /api/items/:itemId
|
||
|
||
Body: (any subset of fields)
|
||
{
|
||
"name": "Updated Name",
|
||
"price": 899,
|
||
"discount": 15,
|
||
"quantity": 80,
|
||
"visible": false
|
||
}
|
||
|
||
Response 200: (updated item object)
|
||
```
|
||
|
||
**Updating images - always send the full array:**
|
||
```
|
||
PATCH /api/items/:itemId
|
||
|
||
Body:
|
||
{
|
||
"imgs": ["https://new1.jpg", "https://new2.jpg"]
|
||
}
|
||
|
||
Response 200: (updated item object)
|
||
```
|
||
|
||
### Delete Item
|
||
```
|
||
DELETE /api/items/:itemId
|
||
|
||
Response 204 No Content
|
||
|
||
Side effect: If no items remain in the subcategory, sets hasItems = false.
|
||
The subcategory can then receive child subcategories again.
|
||
```
|
||
|
||
### Bulk Update Items
|
||
```
|
||
PATCH /api/items/bulk
|
||
|
||
Body:
|
||
{
|
||
"itemIds": ["item1", "item2", "item3"],
|
||
"data": {
|
||
"visible": true
|
||
}
|
||
}
|
||
|
||
Response 204 No Content
|
||
```
|
||
|
||
---
|
||
|
||
## Upload
|
||
|
||
### Upload Image
|
||
```
|
||
POST /api/upload
|
||
|
||
Body: multipart/form-data
|
||
image: File
|
||
|
||
Response 201:
|
||
{
|
||
"url": "https://cdn.example.com/uploads/abc123.jpg"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Notes
|
||
|
||
- All responses are JSON.
|
||
- Use `PATCH` for partial updates - send only the fields you want to change.
|
||
- `priority`: lower number = appears first in the list.
|
||
- `currency` supported values: `USD`, `EUR`, `RUB`, `GBP`, `UAH`.
|
||
- `badges`: optional string array. Predefined values with UI colors: `new`, `sale`, `exclusive`, `hot`, `limited`, `bestseller`, `featured`. Custom strings are also allowed.
|
||
- `imgs`: always send the **complete** array on update, not individual images.
|
||
- `discount`: integer `0`–`100` representing a percentage discount. `0` means no discount. The discounted price is calculated as `price * (1 - discount / 100)`.
|
||
- `description`: array of `{ key, value }` pairs - free-form attributes per item.
|
||
- `translations`: optional object keyed by language code (`"ru"`, `"en"`, etc.) — each value may contain `name`, `simpleDescription`, `description[]`. The marketplace frontend should use these when rendering in the corresponding language, falling back to the default fields if a translation is absent.
|
||
- Auto-save from the backoffice fires `PATCH` with a single field every ~500 ms.
|
||
|
||
---
|
||
|
||
## Business Rules
|
||
|
||
### Nested Subcategories
|
||
|
||
The hierarchy works like this:
|
||
|
||
```
|
||
Category (e.g. Electronics)
|
||
Subcategory L1 (e.g. Kitchen) <- can add more children OR items, not both
|
||
Subcategory L2 (e.g. Big Kitchen) <- same rule
|
||
Subcategory L3 (e.g. Ovens) <- if this has items, it is a leaf
|
||
Items...
|
||
```
|
||
|
||
Rules:
|
||
- A category can always receive new subcategories (categories never hold items directly).
|
||
- A subcategory that **has items** (`hasItems: true`) **cannot** receive child subcategories.
|
||
- `POST /api/subcategories/:id/subcategories` on a node with `hasItems: true` -> `400 Bad Request`.
|
||
- A subcategory that **has children** cannot have items added to it (items only go to leaf nodes).
|
||
- When the **first item** is created in a subcategory -> `hasItems` becomes `true`.
|
||
- When the **last item** is deleted -> `hasItems` becomes `false`; child subcategories can be added again.
|
||
|
||
### URL Structure (Marketplace Frontend)
|
||
|
||
Subcategory and item `id` fields are used directly in the marketplace URL path:
|
||
|
||
```
|
||
/{categoryId}/{sub1Id}/{sub2Id}/.../{itemId}
|
||
|
||
Examples:
|
||
/electronics/smartphones/iphone-15
|
||
/electronics/smartphones/apple/iphone-15-pro
|
||
/furniture/living-room/sofas/corner-sofa-modelo
|
||
```
|
||
|
||
The `id` field on subcategories is editable via `PATCH` to allow renaming slugs.
|
||
|
||
### Comments
|
||
|
||
- `stars` is optional, integer 1-5.
|
||
- `author` is optional (anonymous comments allowed).
|
||
- `createdAt` is ISO 8601 string.
|
||
|
||
### Cascade Deletes
|
||
|
||
| Delete target | Also deletes |
|
||
|---|---|
|
||
| Category | all subcategories (recursive) and their items |
|
||
| Subcategory | all nested subcategories (recursive) and their items |
|
||
| Item | nothing else |
|
||
|
||
---
|
||
|
||
## Internationalization (i18n)
|
||
|
||
The API supports localized content for items and categories via the `translations` field.
|
||
|
||
### `translations` object structure
|
||
|
||
Any entity that supports translations accepts the same nested shape:
|
||
|
||
```json
|
||
{
|
||
"translations": {
|
||
"ru": {
|
||
"name": "Название",
|
||
"simpleDescription": "Краткое описание",
|
||
"description": [
|
||
{ "key": "Цвет", "value": "Чёрный" }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
- Keys are ISO 639-1 language codes (`"ru"`, `"en"`, etc.).
|
||
- All sub-fields are optional — omit what you don't need.
|
||
- Currently supported in the backoffice: `ru` (Russian).
|
||
|
||
### Requesting a Specific Language
|
||
|
||
Pass `?lang=ru` to any GET endpoint. The backend will:
|
||
|
||
1. Return the default top-level fields as-is.
|
||
2. Merge the matching `translations[lang]` values into the response, overwriting the default fields.
|
||
3. Fall back gracefully to the default language if no translation exists for the requested lang.
|
||
|
||
```
|
||
GET /api/items/:itemId?lang=ru
|
||
GET /api/subcategories/:subcategoryId/items?lang=ru
|
||
```
|
||
|
||
Alternatively, you can use the `Accept-Language` header:
|
||
|
||
```
|
||
Accept-Language: ru
|
||
```
|
||
|
||
### Fallback Behaviour
|
||
|
||
| Scenario | Returned value |
|
||
|---|---|
|
||
| Translation exists for requested lang | Translated value |
|
||
| Translation missing for requested lang | Default (base) field value |
|
||
| `lang` param omitted | Default (base) field value |
|
||
|
||
> 🇷🇺 See [API.ru.md](./API.ru.md) for full documentation in Russian.
|