- добавлен contract DTO для picture sources и fallback image - реализована выдача versioned Gateway URLs по static presets - обновлена документация business API и dev smoke flow
10 KiB
Черновик 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-variantjob в RabbitMQ; - дождаться
readyдо timeout, чтобы первыйnext/imagerequest мог получить картинку; - вернуть 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_assetsrow; - создать
image_asset_versionsrow версии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.