# Property Image Tagging REST API - Cursor AI Implementation Guide ## Project Overview Build a production-ready REST API for automatic property image tagging using Claude AI (Anthropic). The system uses Node.js, Express, MySQL, and Clean Architecture patterns. ## Core Requirements ### 1. Technology Stack - **Runtime**: Node.js 18+ - **Framework**: Express.js - **Database**: MySQL 8.0+ - **AI Provider**: Anthropic Claude API (Claude Sonnet 4.5) - **Architecture**: Clean Architecture (Domain, Application, Infrastructure, Presentation layers) - **Image Processing**: Sharp library for format conversion and optimization - **Authentication**: Simple API key system (no rate limiting, no usage tracking) ### 2. Key Features - Upload property images and get 20-30 AI-generated tags - Duplicate detection using SHA256 image hashing (cache results to avoid re-tagging) - Support multiple image formats (JPEG, PNG, WebP, HEIC, TIFF, BMP, GIF) - Search images by tags using MySQL JSON queries - Simple API key authentication (SHA256 hashed) - RESTful API with Clean Architecture ### 3. Architecture Principles - **Clean Architecture**: Strict separation of Domain, Application, Infrastructure, Presentation - **Dependency Injection**: Use container pattern for service management - **Repository Pattern**: Abstract database operations - **Use Case Pattern**: Each business operation is a separate use case class - **DTO Pattern**: Data Transfer Objects for API boundaries ## Project Structure Create this exact structure: ``` src/ ├── domain/ │ ├── entities/ │ │ ├── ImageTag.js │ │ └── TaggingResult.js │ └── interfaces/ │ ├── IImageTaggingService.js │ └── IImageRepository.js ├── application/ │ ├── dtos/ │ │ ├── TagImageRequestDto.js │ │ └── TagImageResponseDto.js │ └── useCases/ │ ├── TagImageUseCase.js │ └── TagBase64ImageUseCase.js ├── infrastructure/ │ ├── ai/ │ │ └── ClaudeAIProvider.js │ ├── repositories/ │ │ ├── MySQLImageRepository.js │ │ └── ApiKeyRepository.js │ └── config/ │ ├── dependencyContainer.js │ └── corsConfig.js ├── presentation/ │ ├── controllers/ │ │ └── ImageTaggingController.js │ ├── middleware/ │ │ ├── errorHandler.js │ │ ├── apiKeyAuth.js │ │ └── requestId.js │ ├── routes/ │ │ └── imageRoutes.js │ └── validators/ │ └── imageValidator.js ├── shared/ │ ├── errors/ │ │ └── AppError.js │ └── utils/ │ ├── logger.js │ ├── responseFormatter.js │ └── apiKeyGenerator.js └── server.js scripts/ └── manage-api-keys.js migrations/ └── 001_initial_schema.sql ``` ## Database Schema ### Table: images ```sql CREATE TABLE images ( id CHAR(36) PRIMARY KEY, file_name VARCHAR(255) NOT NULL, original_name VARCHAR(255), file_size INT UNSIGNED, mime_type VARCHAR(50), width INT UNSIGNED, height INT UNSIGNED, image_hash VARCHAR(64) UNIQUE NOT NULL, s3_key VARCHAR(500), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_image_hash (image_hash) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ``` ### Table: tagging_results ```sql CREATE TABLE tagging_results ( id CHAR(36) PRIMARY KEY, image_id CHAR(36) NOT NULL, tags JSON NOT NULL, summary TEXT, total_tags INT UNSIGNED, model_version VARCHAR(50) DEFAULT 'claude-sonnet-4-20250514', processing_time_ms INT UNSIGNED, was_duplicate BOOLEAN DEFAULT FALSE, tagged_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE, INDEX idx_image_id (image_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ``` ### Table: api_keys ```sql CREATE TABLE api_keys ( id CHAR(36) PRIMARY KEY, key_prefix VARCHAR(20) NOT NULL, key_hash VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, is_active BOOLEAN DEFAULT TRUE, environment ENUM('development', 'staging', 'production') DEFAULT 'development', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, expires_at TIMESTAMP NULL, revoked_at TIMESTAMP NULL, revoked_reason TEXT, INDEX idx_key_hash (key_hash), INDEX idx_is_active (is_active) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ``` ## Implementation Requirements ### Domain Layer **ImageTag Entity:** - Properties: category, value, confidence - Method: isHighConfidence() - returns true if confidence >= 0.8 - Method: toJSON() - serialize to plain object - Validate category and value are not empty **TaggingResult Entity:** - Properties: imageId, tags array, summary, createdAt - Methods: - getTagsByCategory(category) - getHighConfidenceTags() - getTotalTags() - Validate tags array is not empty **Interfaces:** - IImageTaggingService: generateTags(base64Image, mediaType) - IImageRepository: save(taggingResult, imageBuffer), findById(imageId), findByImageHash(hash) ### Application Layer **TagImageUseCase:** 1. Validate input (fileBuffer, mimeType required) 2. Calculate SHA256 hash of image buffer 3. Check database for existing hash 4. If duplicate → return cached result with isDuplicate: true, costSavings message 5. If new → convert to base64, call AI service 6. Create TaggingResult entity 7. Save to database with image buffer (for hash) 8. Return TagImageResponseDto **TagImageRequestDto:** - fileBuffer (Buffer) - mimeType (string) - fileName (string) - Method: toBase64() - convert buffer to base64 string **TagImageResponseDto:** - imageId, tags, summary, totalTags, isDuplicate, cachedResult, processedAt - Static method: fromTaggingResult(taggingResult) ### Infrastructure Layer **ClaudeAIProvider:** - Use @anthropic-ai/sdk npm package - Model: claude-sonnet-4-20250514 - Wrap API call with async-retry (3 retries, exponential backoff) - Send this exact prompt: ``` Analyze this property image and generate 20-30 descriptive tags categorized as follows: Tag Categories: 1. View: (e.g., Burj Khalifa view, ocean view, downtown skyline, marina view) 2. Furnishing: (e.g., fully furnished, unfurnished, modern, contemporary, luxury) 3. Kitchen: (e.g., with appliances, open kitchen, modular, closed kitchen) 4. Flooring: (e.g., wooden, marble, tile, carpet, laminate, porcelain) 5. Room Type: (e.g., bedroom, living room, bathroom, kitchen, balcony) 6. Style: (e.g., modern, traditional, scandinavian, industrial) 7. Features: (e.g., high ceiling, floor-to-ceiling windows, built-in wardrobes) 8. Condition: (e.g., newly renovated, well-maintained, ready to move) 9. Lighting: (e.g., natural light, ambient lighting, LED lighting) 10. Color Scheme: (e.g., neutral tones, warm colors, monochrome) Return ONLY a JSON object in this exact format: { "tags": [ {"category": "View", "value": "marina view", "confidence": 0.95}, {"category": "Furnishing", "value": "fully furnished", "confidence": 0.90} ], "summary": "Brief one-sentence description" } Include 20-30 tags total. ``` **MySQLImageRepository:** - Use mysql2/promise with connection pool (max 20 connections) - _calculateImageHash(buffer) - SHA256 hash using crypto - findByImageHash(hash) - returns existing TaggingResult if found - save(taggingResult, imageBuffer) - stores both image and tags in transaction - Calculate hash - Check for duplicate first - If duplicate, return cached result - Insert into images table - Insert into tagging_results table - Commit transaction - findById(imageId) - returns TaggingResult - findByTagValue(value) - use JSON_SEARCH for MySQL - Support JSON queries using JSON_CONTAINS and JSON_SEARCH **ApiKeyRepository:** - validateKey(apiKey) - hash key, check database, return key data if valid - createKey(data) - generate secure key with ApiKeyGenerator, hash it, store - revokeKey(keyId, reason) - set is_active=false - getAllKeys() - list all keys ### Presentation Layer **ImageValidator:** - validateUpload(file) - async function - Check file exists - Check mime type in allowed list - Use file-type npm package to verify actual type (magic number) - Use Sharp to validate image and check dimensions (max 15000px) - Max file size 50MB - convertToClaudeSupportedFormat(buffer, mimeType) - If already JPEG/PNG/WebP/GIF → optimize only - If HEIC/TIFF/BMP → convert to JPEG - Use Sharp for conversion - optimizeForAI(buffer) - resize to max 2048px if larger **ApiKeyAuthMiddleware:** - authenticate() middleware function - Skip if SKIP_AUTH=true in development - Extract key from X-API-Key header or Authorization: Bearer - Validate key format: key_(test|live)_[64 hex chars] - Call ApiKeyRepository.validateKey() - If invalid → 401/403 with clear error - If valid → attach req.apiKey = {id, name, environment} - Log authentication **ImageTaggingController:** - tagUploadedImage(req, res, next) - Validate upload - Convert format if needed - Create TagImageRequestDto - Execute TagImageUseCase - Return formatted response - tagBase64Image(req, res, next) - Validate base64 input with Joi - Execute TagBase64ImageUseCase - Return formatted response - searchByTag(req, res, next) - Get tag value from query - Call repository.findByTagValue() - Return results - getStats(req, res, next) - Get statistics from repository - Return stats - getHealth(req, res) - Check database connection - Check memory usage - Return health status **Routes:** - GET /api/images/health - public - POST /api/images/tag - requires auth, multipart upload - POST /api/images/tag-base64 - requires auth, JSON body - GET /api/images/search - requires auth - GET /api/images/stats - requires auth ### Shared Utilities **AppError Classes:** ```javascript class AppError extends Error { constructor(message, statusCode = 500, isOperational = true) { super(message); this.statusCode = statusCode; this.isOperational = isOperational; Error.captureStackTrace(this, this.constructor); } } class ValidationError extends AppError { constructor(message) { super(message, 400); } } class AIServiceError extends AppError { constructor(message) { super(message, 503); } } ``` **Logger (Winston):** - Daily rotating file transport - JSON formatting - Separate error.log and combined.log - Console output in development - Methods: info(message, meta), error(message, error), warn(message, meta), debug(message, meta) **ApiKeyGenerator:** - generate(prefix) - create secure 64-char hex key with prefix - hash(apiKey) - SHA256 hash - mask(apiKey) - show only first 13 and last 4 chars - isValidFormat(apiKey) - validate pattern - extractPrefix(apiKey) - get prefix part **ResponseFormatter:** - success(data, message) - {success: true, message, data, timestamp} - error(error, message) - {success: false, message, error, timestamp} ### Server Setup (src/server.js) ```javascript require('dotenv').config(); const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const compression = require('compression'); const morgan = require('morgan'); // Import middleware and routes const corsConfig = require('./infrastructure/config/corsConfig'); const container = require('./infrastructure/config/dependencyContainer'); const createImageRoutes = require('./presentation/routes/imageRoutes'); const ImageTaggingController = require('./presentation/controllers/ImageTaggingController'); const ApiKeyAuthMiddleware = require('./presentation/middleware/apiKeyAuth'); const errorHandler = require('./presentation/middleware/errorHandler'); const requestIdMiddleware = require('./presentation/middleware/requestId'); const logger = require('./shared/utils/logger'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(helmet()); app.use(compression()); app.use(cors(corsConfig)); app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); app.use(requestIdMiddleware); app.use(morgan('combined')); // Root app.get('/', (req, res) => { res.json({ service: 'Property Image Tagging API', version: '1.0.0', authentication: 'Simple API Key' }); }); // Dependency injection const tagImageUseCase = container.get('tagImageUseCase'); const tagBase64ImageUseCase = container.get('tagBase64ImageUseCase'); const imageRepository = container.get('imageRepository'); const apiKeyRepository = container.get('apiKeyRepository'); const imageController = new ImageTaggingController( tagImageUseCase, tagBase64ImageUseCase, imageRepository, logger ); const authMiddleware = new ApiKeyAuthMiddleware(apiKeyRepository, logger); // Routes const imageRoutes = createImageRoutes(imageController, authMiddleware); app.use('/api/images', imageRoutes); // Error handler (last) app.use(errorHandler); // Graceful shutdown const gracefulShutdown = async (signal) => { logger.info(`${signal} received, shutting down`); const pool = container.get('pool'); await pool.end(); process.exit(0); }; process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGINT', () => gracefulShutdown('SIGINT')); // Start if (process.env.NODE_ENV !== 'test') { app.listen(PORT, () => { logger.info(`Server running on port ${PORT}`); }); } module.exports = app; ``` ### CLI Tool (scripts/manage-api-keys.js) Create command-line tool with these commands: - create [name] [environment] [description] - Generate new API key - list - Display all keys - revoke [reason] - Deactivate key - activate - Reactivate key Use ApiKeyRepository methods. Show plain text key ONLY on creation. ## Critical Requirements ### MUST DO: ✅ Use connection pooling for MySQL (max 20) ✅ Use transactions for saving image + tags ✅ Implement retry logic for Claude API (3 retries) ✅ Validate JSON from Claude before parsing ✅ Hash API keys with SHA256 (never store plain text) ✅ Use prepared statements for SQL ✅ Optimize images before sending to Claude (max 2048px) ✅ Calculate image hash for duplicate detection ✅ Return helpful error messages ✅ Use async-retry package for Claude API calls ✅ Use file-type package for magic number validation ✅ Use Sharp for image processing ✅ Use Joi for input validation ✅ Use Winston for logging ### MUST NOT DO: ❌ Store plain text API keys ❌ Trust file extensions alone ❌ Skip image hash calculation ❌ Return database errors to client ❌ Skip input validation ❌ Hardcode values (use env vars) ## Expected API Response Examples ### Successful New Image Tagging: ```json { "success": true, "message": "✅ New image tagged successfully", "data": { "imageId": "abc-123-def", "tags": [ {"category": "View", "value": "Burj Khalifa view", "confidence": 0.98}, {"category": "Furnishing", "value": "fully furnished", "confidence": 0.95} ], "summary": "Modern luxury apartment with Burj Khalifa view", "totalTags": 27, "isDuplicate": false, "processedAt": "2025-10-31T10:30:00.000Z" } } ``` ### Duplicate Image Detection: ```json { "success": true, "message": "✅ Duplicate detected - returned cached tags (no cost)", "data": { "imageId": "abc-123-def", "tags": [...], "isDuplicate": true, "cachedResult": true, "costSavings": "This request was FREE - used cached result" } } ``` ### Error Response: ```json { "success": false, "message": "API key required. Include X-API-Key header.", "timestamp": "2025-10-31T10:30:00.000Z" } ``` ## Package Dependencies ```json { "dependencies": { "@anthropic-ai/sdk": "^0.32.1", "async-retry": "^1.3.3", "compression": "^1.7.4", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.2", "file-type": "^19.5.0", "helmet": "^8.0.0", "joi": "^17.13.3", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", "mysql2": "^3.11.5", "sharp": "^0.33.5", "uuid": "^11.0.3", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { "nodemon": "^3.1.9" } } ``` ## Environment Variables Create .env file: ``` NODE_ENV=development PORT=3000 ANTHROPIC_API_KEY=your_key_here DB_HOST=localhost DB_PORT=3306 DB_NAME=property_tagging DB_USER=root DB_PASSWORD=your_password SKIP_AUTH=true ALLOWED_ORIGIN=* LOG_LEVEL=info ``` ## Testing Checklist After implementation: - [ ] Upload JPEG → verify tags returned - [ ] Upload same image twice → verify duplicate detected - [ ] Upload HEIC → verify converted and tagged - [ ] Upload without API key → verify 401 - [ ] Upload with invalid key → verify 403 - [ ] Upload corrupted file → verify 400 - [ ] Search by tag → verify results - [ ] Check database has proper indexes - [ ] Verify Claude API retry works - [ ] Test graceful shutdown ## Success Metrics - API responds within 5 seconds - Duplicate detection works (same image = cached) - All image formats supported - Authentication blocks invalid keys - Errors handled gracefully - Database queries optimized - Logs contain useful info Start implementation from Domain layer → Application → Infrastructure → Presentation. Follow Clean Architecture strictly. Implement comprehensive error handling at every layer.