Files
reactbin/specs/017-short-id-migration/contracts/image-api.md
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

116 lines
3.3 KiB
Markdown

# 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.
```json
{
"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)
```typescript
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;
}
```