# Черновик Backend Контракта Это черновик будущего бизнес-контракта для NestJS backend. Сейчас реализованы system endpoints, Swagger и placeholder для internal image ensure. ## 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 ``` Текущий статус реализации - endpoint зарезервирован и возвращает `501 Not Implemented`. Gateway `/images/*` пока тоже возвращает placeholder `501 Not Implemented`. Request body: ```json { "assetId": "asset_123", "version": 4, "preset": "card", "width": 640, "quality": 80, "format": "webp" } ``` Query params: | Param | Описание | |---|---| | `w` | целевая ширина, должна быть разрешена preset или округлена до ближайшей разрешённой | | `q` | качество, должно быть из allowlist качества | | `f` | `auto`, `avif`, `webp`, `jpg`; для Next/image по умолчанию `auto` | Responsibilities: - проверить `assetId` и `preset`; - вычислить 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 не найден | | `409` | variant уже генерируется и sync ожидание отключено | | `422` | source image нельзя обработать | | `502` | external imgproxy недоступен | 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`. ## Allowed Hosts ```text GET /allowed-hosts POST /allowed-hosts PATCH /allowed-hosts/:id DELETE /allowed-hosts/:id ``` ## Assets ```text GET /assets POST /assets GET /assets/:id DELETE /assets/:id ``` `POST /assets` request: ```json { "sourceUrl": "https://example.com/photo.jpg" } ``` Responsibilities: - validate source URL; - check `allowed_image_hosts`; - create or reuse `image_assets` row; - optionally save original to S3 later. ## Variants ```text GET /assets/:id/variants POST /assets/:id/variants POST /variants/:id/regenerate DELETE /variants/:id ``` `POST /assets/:id/variants` request: ```json { "preset": "card", "format": "webp", "width": 640 } ``` Response if ready: ```json { "id": "variant_123", "status": "ready", "url": "http://localhost:8888/images/asset_123/v4/card?w=640&q=80&f=webp" } ``` Response if generation is async: ```json { "id": "variant_123", "status": "pending", "url": null } ``` ## 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.