Files
agatha 61d923d5be Feat: Replace UUID image identifiers with 8-character base62 short IDs
Short IDs become the canonical identifier in URLs (/i/:short_id),
MinIO/R2 storage keys, and all API responses. Hash-based deduplication
is preserved. Includes two-phase Alembic migration (003 adds nullable
column, 004 enforces NOT NULL) with a backfill script to copy storage
objects and populate short_id for existing images.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 00:13:55 +00:00

3.3 KiB

Contract: Image API (Short ID Update)

ImageRecord Response Schema

All image endpoints return this shape. short_id is a new field; all other fields are unchanged.

{
  "id": "7343d164-80bb-473b-b239-717f2842ae4e",
  "short_id": "xK7mN2pQ",
  "hash": "163dec08460650439f1e7439721e8e566aff7d8aaad60cf451e7d3518a334a23",
  "filename": "image.gif",
  "mime_type": "image/gif",
  "size_bytes": 1957149,
  "width": 265,
  "height": 199,
  "storage_key": "xK7mN2pQ",
  "thumbnail_key": "xK7mN2pQ-thumb",
  "file_url": "https://cdn.reactbin.juggalol.com/xK7mN2pQ",
  "thumbnail_url": "https://cdn.reactbin.juggalol.com/xK7mN2pQ-thumb",
  "created_at": "2026-05-09T02:46:29.520296+00:00",
  "tags": ["kfc"]
}

Constraints:

  • short_id: exactly 8 alphanumeric characters [a-zA-Z0-9]{8}
  • storage_key: equals short_id (post-migration)
  • thumbnail_key: equals {short_id}-thumb or null if no thumbnail exists
  • file_url: {cdn_base}/{short_id} when CDN is configured; /api/v1/images/{short_id}/file otherwise
  • thumbnail_url: {cdn_base}/{short_id}-thumb or null

Route Changes

All routes that previously accepted {image_id} as a UUID now accept {short_id} as an 8-character alphanumeric string.

GET /api/v1/images/{short_id}

Fetch a single image by short ID.

  • Path param: short_id — 8-char alphanumeric string
  • Response 200: ImageRecord
  • Response 404: {"detail": "Image not found", "code": "image_not_found"}
  • Response 422: {"detail": "Invalid image ID", "code": "invalid_short_id"} if param is not 8 alphanumeric chars

PATCH /api/v1/images/{short_id}/tags

Update tags on an image. Auth required.

  • Path param: short_id — 8-char alphanumeric string
  • Body: {"tags": ["tag1", "tag2"]}
  • Response 200: ImageRecord (updated)
  • Response 404/422: same shape as above

DELETE /api/v1/images/{short_id}

Delete an image and its storage objects. Auth required.

  • Path param: short_id — 8-char alphanumeric string
  • Response 204: no body
  • Response 404: error envelope

GET /api/v1/images/{short_id}/file

Serve the raw image file (proxy mode, when CDN is not configured).

  • Path param: short_id
  • Response 200: raw image bytes with correct Content-Type

GET /api/v1/images/{short_id}/thumbnail

Serve the thumbnail (proxy mode).

  • Path param: short_id
  • Response 200: WebP bytes or original image if no thumbnail

POST /api/v1/images (upload — unchanged route, updated response)

  • Response: ImageRecord with short_id populated

Frontend Route Change

Old route New route
/images/:id /i/:id

The :id segment now contains the short_id value (8 alphanumeric chars) rather than a UUID.


ImageRecord TypeScript Interface (updated)

export interface ImageRecord {
  id: string;           // UUID — retained, not used for routing
  short_id: string;     // NEW — 8-char base62, used for all routing and API calls
  hash: string;
  filename: string;
  mime_type: string;
  size_bytes: number;
  width: number;
  height: number;
  storage_key: string;
  thumbnail_key: string | null;
  file_url: string;
  thumbnail_url: string | null;
  created_at: string;
  tags: string[];
  duplicate?: boolean;
}