image_tagger/CURSOR_PROMPT.md
2025-11-03 13:22:29 +05:30

17 KiB

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

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

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

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:

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)

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:

{
  "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:

{
  "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:

{
  "success": false,
  "message": "API key required. Include X-API-Key header.",
  "timestamp": "2025-10-31T10:30:00.000Z"
}

Package Dependencies

{
  "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.