# Черновик Backend Контракта Это черновик бизнес-контракта для NestJS backend. Сейчас реализованы system endpoints, Swagger, `POST /api/assets` и internal image ensure MVP. ## 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 ``` Текущий статус реализации - MVP реализован: Backend создаёт/переиспользует variant row, публикует RabbitMQ job, ждёт `ready` до timeout и возвращает image bytes из S3. Request body: ```json { "assetId": "asset_123", "version": 4, "preset": "card", "width": 640, "height": 0, "quality": 80, "requestedFormat": "auto", "resize": "fit", "format": "webp" } ``` Query params: | Param | Описание | |---|---| | `w` | целевая ширина; обязательна для responsive presets и custom, запрещена/фиксирована для fixed presets | | `h` | целевая высота; используется для custom, для fixed preset берётся из config | | `q` | качество; если не передано, берётся из preset, иначе должно входить в allowlist preset | | `f` | `auto`, `avif`, `webp`, `jpg`; для Next/image по умолчанию `auto` | | `fit` | `fit` или `fill`; используется только для custom transforms | Responsibilities: - проверить `assetId` и статический preset/custom transform config; - нормализовать width, height, resize, quality и format; - вычислить 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 не найден | | `502` | external imgproxy недоступен или S3 object отсутствует после `ready` | | `504` | generation не завершилась до `IMAGE_ENSURE_WAIT_MS` | 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`. Примеры: ```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 ``` ## Allowed Hosts ```text GET /allowed-hosts POST /allowed-hosts PATCH /allowed-hosts/:id DELETE /allowed-hosts/:id ``` ## Assets ```text POST /assets GET /assets GET /assets/:publicId POST /assets/:publicId/versions GET /assets/:publicId/variants POST /assets/:publicId/variants DELETE /assets/:id ``` Сейчас реализованы `POST /assets`, `GET /assets`, `GET /assets/:publicId`, `POST /assets/:publicId/versions`, `GET /assets/:publicId/variants`, `POST /assets/:publicId/variants`. `POST /assets` request: ```json { "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" } ``` Responsibilities: - validate source URL; - check mock allowlist `SOURCE_ALLOWED_HOSTS`, если `SOURCE_HOST_ALLOW_ALL=false`; - создать `image_assets` row; - создать `image_asset_versions` row версии `1`; - optionally save original to S3 later. `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. `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. ## Variants ```text GET /assets/:publicId/variants POST /assets/:publicId/variants POST /variants/:id/regenerate DELETE /variants/:id ``` `POST /assets/:publicId/variants` request: ```json { "preset": "card", "mode": "single", "format": "webp", "width": 640, "quality": 80 } ``` Family generation: ```json { "preset": "card", "mode": "family" } ``` Для `family` Backend создаёт набор variants по разрешённым widths/formats static preset и ставит RabbitMQ jobs. Endpoint не ждёт image bytes, в отличие от internal ensure. Response: ```json { "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" } ] } ``` ## Presets ```text GET /presets ``` Возвращает static presets, custom transform limits и mock allowlist source hosts. ```json { "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"] } ``` ## Image URLs For UI Для ручного UI можно добавить endpoint, который возвращает готовый набор URLs для ``/`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.