2026-05-05 09:59:21 +03:00
# Черновик Backend Контракта
2026-05-05 13:25:28 +03:00
Это черновик бизнес-контракта для NestJS backend. Сейчас реализованы system endpoints, Swagger, `POST /api/assets` и internal image ensure MVP.
2026-05-05 09:59:21 +03:00
## System
```text
GET /api/health
GET /docs
GET /docs-json
```
NestJS backend отдаёт JSON, metadata, statuses и URLs. Public image origin находится в Fastify Gateway. Backend владеет PostgreSQL, S3 orchestration и RabbitMQ jobs.
## Internal Image Ensure
Этот internal endpoint вызывается Gateway на L1 miss. Gateway не ходит в DB/S3 напрямую.
```text
POST /api/internal/images/ensure
```
2026-05-05 13:25:28 +03:00
Текущий статус реализации - MVP реализован: Backend создаёт/переиспользует variant row, публикует RabbitMQ job, ждёт `ready` до timeout и возвращает image bytes из S3.
2026-05-05 09:59:21 +03:00
Request body:
```json
{
"assetId": "asset_123",
"version": 4,
"preset": "card",
"width": 640,
2026-05-05 13:25:28 +03:00
"height": 0,
2026-05-05 09:59:21 +03:00
"quality": 80,
2026-05-05 13:25:28 +03:00
"requestedFormat": "auto",
"resize": "fit",
2026-05-05 09:59:21 +03:00
"format": "webp"
}
```
Query params:
| Param | Описание |
|---|---|
2026-05-05 13:25:28 +03:00
| `w` | целевая ширина; обязательна для responsive presets и custom, запрещена/фиксирована для fixed presets |
| `h` | целевая высота; используется для custom, для fixed preset берётся из config |
| `q` | качество; если не передано, берётся из preset, иначе должно входить в allowlist preset |
2026-05-05 09:59:21 +03:00
| `f` | `auto` , `avif` , `webp` , `jpg` ; для Next/image по умолчанию `auto` |
2026-05-05 13:25:28 +03:00
| `fit` | `fit` или `fill` ; используется только для custom transforms |
2026-05-05 09:59:21 +03:00
Responsibilities:
2026-05-05 13:25:28 +03:00
- проверить `assetId` и статический preset/custom transform config;
- нормализовать width, height, resize, quality и format;
2026-05-05 09:59:21 +03:00
- вычислить deterministic `variantHash` ;
- проверить PostgreSQL и S3;
- если variant готов в S3, вернуть bytes или stream metadata для Gateway;
- если variant отсутствует, создать/переиспользовать variant row;
- поставить `generate-variant` job в RabbitMQ;
- дождаться `ready` до timeout, чтобы первый `next/image` request мог получить картинку;
- вернуть image response или metadata для Gateway.
Response headers:
```http
Content-Type: image/avif | image/webp | image/jpeg
Cache-Control: public, max-age=31536000, immutable
Vary: Accept
ETag: "..."
```
Ошибки:
| Status | Когда |
|---|---|
| `400` | некорректные query params |
| `404` | asset или preset не найден |
2026-05-05 13:25:28 +03:00
| `502` | external imgproxy недоступен или S3 object отсутствует после `ready` |
| `504` | generation не завершилась до `IMAGE_ENSURE_WAIT_MS` |
2026-05-05 09:59:21 +03:00
Public endpoint реализуется в Fastify Gateway, internal ensure endpoint - в Backend:
```text
client -> CDN -> Gateway /images/* -> Backend ensure -> RabbitMQ -> Worker -> imgproxy -> S3
```
Важно: `/images/*` не должен жить в NestJS `/api` . Gateway остаётся public image origin, Backend остаётся управлением процессов.
Public image URL versioned: `/images/{assetId}/v{version}/{preset}?w={width}&q={quality}&f=auto` .
2026-05-05 13:25:28 +03:00
Примеры:
```text
/images/asset_demo/v1/card?w=640& q=80& f=auto
/images/asset_demo/v1/avatar?f=auto
/images/asset_demo/v1/custom?w=777& h=333& q=72& fit=fill& f=webp
```
2026-05-05 09:59:21 +03:00
## Allowed Hosts
```text
GET /allowed-hosts
POST /allowed-hosts
PATCH /allowed-hosts/:id
DELETE /allowed-hosts/:id
```
## Assets
```text
POST /assets
2026-05-05 13:25:28 +03:00
GET /assets
GET /assets/:publicId
2026-05-05 13:31:45 +03:00
POST /assets/:publicId/versions
2026-05-05 13:25:28 +03:00
GET /assets/:publicId/variants
POST /assets/:publicId/variants
2026-05-05 09:59:21 +03:00
DELETE /assets/:id
```
2026-05-05 13:31:45 +03:00
Сейчас реализованы `POST /assets` , `GET /assets` , `GET /assets/:publicId` , `POST /assets/:publicId/versions` , `GET /assets/:publicId/variants` , `POST /assets/:publicId/variants` .
2026-05-05 13:25:28 +03:00
2026-05-05 09:59:21 +03:00
`POST /assets` request:
```json
{
2026-05-05 13:25:28 +03:00
"sourceUrl": "https://storage.yandexcloud.net/shared1318/img/1.jpg",
"publicId": "asset_demo"
}
```
Response:
```json
{
"id": "59fcf4f6-9891-4df4-8bb7-a4dbe570bb66",
"publicId": "asset_demo",
"version": 1,
"sourceHost": "storage.yandexcloud.net",
"imageBasePath": "/images/asset_demo/v1/card"
2026-05-05 09:59:21 +03:00
}
```
Responsibilities:
- validate source URL;
2026-05-05 13:25:28 +03:00
- check mock allowlist `SOURCE_ALLOWED_HOSTS` , если `SOURCE_HOST_ALLOW_ALL=false` ;
- создать `image_assets` row;
- создать `image_asset_versions` row версии `1` ;
2026-05-05 09:59:21 +03:00
- optionally save original to S3 later.
2026-05-05 13:25:28 +03:00
`GET /assets` response:
```json
{
"assets": [
{
"id": "59fcf4f6-9891-4df4-8bb7-a4dbe570bb66",
"publicId": "asset_demo",
"currentVersion": 1,
"status": "active",
"sourceUrl": "https://storage.yandexcloud.net/shared1318/img/1.jpg",
"sourceHost": "storage.yandexcloud.net",
"createdAt": "2026-05-05T12:00:00.000Z",
"updatedAt": "2026-05-05T12:00:00.000Z"
}
]
}
```
`GET /assets/:publicId/variants` возвращает rows из `image_variants` с public Gateway URL, S3 key и status.
2026-05-05 13:31:45 +03:00
`POST /assets/:publicId/versions` request:
```json
{
"sourceUrl": "https://storage.yandexcloud.net/shared1318/img/1.jpg"
}
```
Response:
```json
{
"assetId": "59fcf4f6-9891-4df4-8bb7-a4dbe570bb66",
"versionId": "3b5da974-bb7f-4d73-b172-d6ad9c244528",
"publicId": "asset_demo",
"previousVersion": 1,
"version": 2,
"sourceUrl": "https://storage.yandexcloud.net/shared1318/img/1.jpg",
"sourceHost": "storage.yandexcloud.net",
"imageBasePath": "/images/asset_demo/v2/card",
"createdAt": "2026-05-05T12:00:00.000Z"
}
```
Новая версия становится `currentVersion` asset. Старые Gateway URLs с `/v1/` остаются immutable и не требуют purge.
2026-05-05 09:59:21 +03:00
## Variants
```text
2026-05-05 13:25:28 +03:00
GET /assets/:publicId/variants
POST /assets/:publicId/variants
2026-05-05 09:59:21 +03:00
POST /variants/:id/regenerate
DELETE /variants/:id
```
2026-05-05 13:25:28 +03:00
`POST /assets/:publicId/variants` request:
2026-05-05 09:59:21 +03:00
```json
{
"preset": "card",
2026-05-05 13:25:28 +03:00
"mode": "single",
2026-05-05 09:59:21 +03:00
"format": "webp",
2026-05-05 13:25:28 +03:00
"width": 640,
"quality": 80
}
```
Family generation:
```json
{
"preset": "card",
"mode": "family"
2026-05-05 09:59:21 +03:00
}
```
2026-05-05 13:25:28 +03:00
Для `family` Backend создаёт набор variants по разрешённым widths/formats static preset и ставит RabbitMQ jobs. Endpoint не ждёт image bytes, в отличие от internal ensure.
Response:
2026-05-05 09:59:21 +03:00
```json
{
2026-05-05 13:25:28 +03:00
"publicId": "asset_demo",
"version": 1,
"variants": [
{
"id": "variant_123",
"preset": "card",
"version": 1,
"width": 640,
"height": 0,
"resize": "fit",
"quality": 80,
"requestedFormat": "webp",
"format": "webp",
"status": "pending",
"url": "http://localhost:8888/images/asset_demo/v1/card?w=640& q=80& f=webp",
"s3Key": "variants/asset_demo/v1/abc.webp"
}
]
2026-05-05 09:59:21 +03:00
}
```
2026-05-05 13:25:28 +03:00
## Presets
```text
GET /presets
```
Возвращает static presets, custom transform limits и mock allowlist source hosts.
2026-05-05 09:59:21 +03:00
```json
{
2026-05-05 13:25:28 +03:00
"presets": [
{
"name": "card",
"mode": "responsive",
"formats": ["avif", "webp", "jpg"],
"qualities": [75, 80],
"quality": 80,
"resize": "fit",
"widths": [320, 640, 960]
}
],
"custom": {
"enabled": true,
"formats": ["avif", "webp", "jpg", "png"],
"maxWidth": 4096,
"maxHeight": 4096,
"quality": 80
},
"allowedSourceHosts": ["storage.yandexcloud.net"]
2026-05-05 09:59:21 +03:00
}
```
## Image URLs For UI
Для ручного UI можно добавить endpoint, который возвращает готовый набор URLs для `<picture>` /`srcset` . Для `next/image` основным контрактом остаётся custom loader из `docs/next-image-provider.md` .
```text
GET /assets/:id/picture?preset=card
```
Example response:
```json
{
"assetId": "asset_123",
"preset": "card",
"sources": [
{
"type": "image/avif",
"srcset": "http://localhost:8888/images/asset_123/v4/card?w=320& q=80& f=avif 320w, http://localhost:8888/images/asset_123/v4/card?w=640& q=80& f=avif 640w"
},
{
"type": "image/webp",
"srcset": "http://localhost:8888/images/asset_123/v4/card?w=320& q=80& f=webp 320w, http://localhost:8888/images/asset_123/v4/card?w=640& q=80& f=webp 640w"
}
],
"fallback": {
"src": "http://localhost:8888/images/asset_123/v4/card?w=640& q=80& f=jpg",
"width": 640,
"height": null
}
}
```
## Worker Lifecycle
Worker выполняет задачи из RabbitMQ. Задачи создаёт Backend.
RabbitMQ topology:
```text
exchange: image-platform.jobs
queue: image.generate-variant
dead-letter exchange: image-platform.jobs.dlx
dead-letter queue: image.generate-variant.dlq
```
Job payload должен быть минимальным:
```json
{
"jobId": "job_123",
"variantId": "variant_123"
}
```
Worker читает детали variant из PostgreSQL, вызывает imgproxy, пишет результат в S3 и обновляет status в PostgreSQL.
Если генерация тяжёлая или не успела завершиться до timeout, Backend может вернуть Gateway `504` , а job продолжит выполняться/retry по очереди.
PostgreSQL может выступить первой очередью:
```text
SELECT * FROM image_variants
WHERE status = 'pending'
FOR UPDATE SKIP LOCKED
LIMIT 1
```
PostgreSQL-backed очередь не используется как основной механизм: для jobs выбран RabbitMQ.