Files
image-platform/docs/backend-contract-draft.md
S.Gromov 56d551b43b feat: добавить endpoint picture/srcset
- добавлен contract DTO для picture sources и fallback image
- реализована выдача versioned Gateway URLs по static presets
- обновлена документация business API и dev smoke flow
2026-05-05 13:35:25 +03:00

10 KiB
Raw Permalink Blame History

Черновик Backend Контракта

Это черновик бизнес-контракта для NestJS backend. Сейчас реализованы system endpoints, Swagger, POST /api/assets и internal image ensure MVP.

System

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 напрямую.

POST /api/internal/images/ensure

Текущий статус реализации - MVP реализован: Backend создаёт/переиспользует variant row, публикует RabbitMQ job, ждёт ready до timeout и возвращает image bytes из S3.

Request body:

{
  "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:

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:

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.

Примеры:

/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

GET /allowed-hosts
POST /allowed-hosts
PATCH /allowed-hosts/:id
DELETE /allowed-hosts/:id

Assets

POST /assets
GET /assets
GET /assets/:publicId
POST /assets/:publicId/versions
GET /assets/:publicId/picture
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/picture, GET /assets/:publicId/variants, POST /assets/:publicId/variants.

POST /assets request:

{
  "sourceUrl": "https://storage.yandexcloud.net/shared1318/img/1.jpg",
  "publicId": "asset_demo"
}

Response:

{
  "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:

{
  "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:

{
  "sourceUrl": "https://storage.yandexcloud.net/shared1318/img/1.jpg"
}

Response:

{
  "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.

GET /assets/:publicId/picture?preset=card&sizes=100vw response:

{
  "publicId": "asset_demo",
  "preset": "card",
  "version": 2,
  "quality": 80,
  "sizes": "100vw",
  "widths": [320, 640, 960],
  "image": {
    "src": "http://localhost:8888/images/asset_demo/v2/card?w=960&q=80&f=jpg",
    "format": "jpg",
    "type": "image/jpeg",
    "width": 960,
    "height": 0
  },
  "sources": [
    {
      "format": "avif",
      "type": "image/avif",
      "srcSet": "http://localhost:8888/images/asset_demo/v2/card?w=320&q=80&f=avif 320w, http://localhost:8888/images/asset_demo/v2/card?w=640&q=80&f=avif 640w, http://localhost:8888/images/asset_demo/v2/card?w=960&q=80&f=avif 960w"
    }
  ]
}

Endpoint не ставит generation jobs: Gateway lazy path сгенерирует bytes на первом запросе или отдаст cache/S3.

Variants

GET /assets/:publicId/variants
POST /assets/:publicId/variants
POST /variants/:id/regenerate
DELETE /variants/:id

POST /assets/:publicId/variants request:

{
  "preset": "card",
  "mode": "single",
  "format": "webp",
  "width": 640,
  "quality": 80
}

Family generation:

{
  "preset": "card",
  "mode": "family"
}

Для family Backend создаёт набор variants по разрешённым widths/formats static preset и ставит RabbitMQ jobs. Endpoint не ждёт image bytes, в отличие от internal ensure.

Response:

{
  "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

GET /presets

Возвращает static presets, custom transform limits и mock allowlist source hosts.

{
  "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 для <picture>/srcset. Для next/image основным контрактом остаётся custom loader из docs/next-image-provider.md.

GET /assets/:id/picture?preset=card

Example response:

{
  "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:

exchange: image-platform.jobs
queue: image.generate-variant
dead-letter exchange: image-platform.jobs.dlx
dead-letter queue: image.generate-variant.dlq

Job payload должен быть минимальным:

{
  "jobId": "job_123",
  "variantId": "variant_123"
}

Worker читает детали variant из PostgreSQL, вызывает imgproxy, пишет результат в S3 и обновляет status в PostgreSQL.

Если генерация тяжёлая или не успела завершиться до timeout, Backend может вернуть Gateway 504, а job продолжит выполняться/retry по очереди.

PostgreSQL может выступить первой очередью:

SELECT * FROM image_variants
WHERE status = 'pending'
FOR UPDATE SKIP LOCKED
LIMIT 1

PostgreSQL-backed очередь не используется как основной механизм: для jobs выбран RabbitMQ.