Files

4.9 KiB

API Contract: Reaction Image Board v1

Base URL: /api/v1 Format: JSON (application/json) for all request/response bodies except file uploads (multipart/form-data) Auth: None in v1 (NoOpAuthProvider) Error envelope (all 4xx/5xx responses):

{ "detail": "<human-readable message>", "code": "<machine-readable code>" }

Image Resource

Image object shape

{
  "id":           "uuid",
  "hash":         "sha256-hex-string-64-chars",
  "filename":     "original.jpg",
  "mime_type":    "image/jpeg",
  "size_bytes":   102400,
  "width":        800,
  "height":       600,
  "storage_key":  "sha256-hex-string-64-chars",
  "created_at":   "2026-05-01T12:00:00Z",
  "tags":         ["cat", "funny"],
  "duplicate":    false
}

duplicate is only present on POST responses; omit or set to false on GET.


POST /api/v1/images — Upload Image

Request: Content-Type: multipart/form-data

Field Required Type Notes
file yes file The image binary
tags no string Comma-separated, e.g. "cat,funny,reaction"

Processing:

  1. Validate MIME type → 422 invalid_mime_type if not accepted
  2. Validate file size ≤ MAX_UPLOAD_BYTES → 422 file_too_large if exceeded
  3. Compute SHA-256 hash of raw bytes
  4. Query DB for existing record with same hash
  5. If duplicate: return existing record, duplicate: true, HTTP 200
  6. If new: write to storage, insert record, upsert tags, HTTP 201

Responses:

Status Condition Body
201 New image stored Image object with duplicate: false
200 Duplicate detected Image object with duplicate: true
422 Invalid MIME type {"detail":"...", "code":"invalid_mime_type"}
422 File too large {"detail":"...", "code":"file_too_large"}
422 Invalid tag {"detail":"...", "code":"invalid_tag"}

GET /api/v1/images — List / Search Images

Query parameters:

Param Type Default Notes
tags string Comma-separated. AND logic — all tags must be present.
limit integer 50 Max 100
offset integer 0 Pagination offset

Response 200:

{
  "items":  [ { ...image object... } ],
  "total":  142,
  "limit":  50,
  "offset": 0
}

Each item includes the tags array. duplicate field omitted. Results ordered by created_at descending.


GET /api/v1/images/{id} — Get Single Image

Path: {id} is a UUID.

Response 200: Single image object (tags included, duplicate omitted).

Status Code Condition
404 image_not_found No image with that UUID

GET /api/v1/images/{id}/file — Serve Image File

Path: {id} is a UUID.

Response 302: Redirect to pre-signed S3 URL (1-hour expiry). Location header contains the URL.

Status Code Condition
404 image_not_found No image with that UUID

PATCH /api/v1/images/{id}/tags — Update Tags

Request: Content-Type: application/json

{ "tags": ["cat", "funny", "new-tag"] }

Replaces the full tag set. Empty array removes all tags from the image. Tag records themselves are never deleted.

Response 200: Full image object with updated tags.

Status Code Condition
404 image_not_found No image with that UUID
422 invalid_tag A tag fails validation

DELETE /api/v1/images/{id} — Delete Image

Path: {id} is a UUID.

Processing:

  1. Look up image record
  2. Delete all image_tags rows for this image
  3. Delete the image record
  4. Delete the S3 object at storage_key

Response 204: No body.

Status Code Condition
404 image_not_found No image with that UUID

Tag Resource

Tag object shape

{
  "id":          "uuid",
  "name":        "cat",
  "image_count": 42
}

GET /api/v1/tags — List Tags

Query parameters:

Param Type Default Notes
q string Prefix filter on tag name
limit integer 100 Max 200
offset integer 0 Pagination offset

Response 200:

{
  "items":  [ { "id": "uuid", "name": "cat", "image_count": 42 } ],
  "total":  7,
  "limit":  100,
  "offset": 0
}

Results ordered alphabetically by name.


Health

GET /api/v1/health

Response 200:

{ "status": "ok" }

No auth or error cases.


Constraints Summary

Constraint Value
Accepted MIME types image/jpeg, image/png, image/gif, image/webp
Max upload size 52 428 800 bytes (50 MiB); configurable via MAX_UPLOAD_BYTES env var
Tag name pattern ^[a-z0-9_-]{1,64}$ (after normalisation)
Pre-signed URL expiry 3 600 seconds (1 hour)
Max limit for images list 100
Max limit for tags list 200
Default limit for images list 50
Default limit for tags list 100