# Image URL Fetching — Design Spec ## Overview Add the ability to fetch images from external URLs via the API, download them to local storage, and preserve the original source URL as metadata. API-only feature — no frontend changes. ## New Endpoint ### `POST /api/images/from-url` **Request:** ```json { "url": "https://example.com/photo.jpg" } ``` **Validation (Zod):** - `url` — valid URL string, required **Server-side behavior:** 1. Fetch the URL with a 10-second timeout 2. Check response `Content-Type` is one of: `image/jpeg`, `image/png`, `image/webp` 3. Check `Content-Length` does not exceed 5MB (match existing upload limit) 4. Stream response body to `uploads/` directory using existing naming: `${Date.now()}-${randomUUID()}.${ext}` 5. If any check fails, return 400 with descriptive error **Response:** ```json { "filename": "1712160000000-abc123.jpg", "sourceUrl": "https://example.com/photo.jpg" } ``` Callers can use `filename` for `imageFilename` and `sourceUrl` for `imageSourceUrl` when creating/updating items or candidates. **Error responses:** - `400` — invalid URL, unsupported content type, file too large, fetch failed - `500` — server error during download/save ## Schema Changes ### `items` table Add column: ``` imageSourceUrl: text("image_source_url") // nullable ``` ### `threadCandidates` table Add column: ``` imageSourceUrl: text("image_source_url") // nullable ``` ### Zod schemas Add `imageSourceUrl: z.string().url().optional()` to: - `createItemSchema` - `updateItemSchema` - `createCandidateSchema` - `updateCandidateSchema` ### Types Types are inferred from Zod schemas and Drizzle tables — no manual updates needed. ## Existing Behavior Unchanged - `POST /api/images` (file upload) remains as-is - All existing image display, cleanup, and serving logic unchanged - `imageFilename` continues to work identically ## Test Helper Updates Add `image_source_url TEXT` column to the `items` and `thread_candidates` CREATE TABLE statements in `tests/helpers/db.ts`. ## Testing - Service test: fetch from a valid URL, verify file saved and filename returned - Route test: POST to `/api/images/from-url` with valid/invalid URLs - Validation tests: wrong content type, oversized image, invalid URL format, timeout