Node.js Integration

Server-side calligraphy generation with Node.js

Backend RuntimeExpress.jsNPM Packages
Quick Start

1. Initialize Project

bash
Click to copy
# Create new Node.js project
mkdir calligraphy-node-app
cd calligraphy-node-app

# Initialize package.json
npm init -y

# Install dependencies
npm install express axios dotenv multer cors helmet
npm install --save-dev nodemon @types/express

# Create basic structure
mkdir src routes middleware utils
touch src/app.js src/server.js .env

2. Project Structure

text
Click to copy
calligraphy-node-app/
├── src/
│   ├── app.js                 # Express app setup
│   ├── server.js              # Server startup
│   └── services/
│       └── calligraphyService.js
├── routes/
│   ├── calligraphy.js         # Calligraphy routes
│   └── api.js                 # API routes
├── middleware/
│   ├── auth.js                # Authentication
│   ├── validation.js          # Request validation
│   └── errorHandler.js        # Error handling
├── utils/
│   ├── logger.js              # Logging utility
│   └── helpers.js             # Helper functions
├── uploads/                   # File uploads directory
├── .env                       # Environment variables
└── package.json

3. Basic Express Setup

javascript
Click to copy
// src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const path = require('path');
require('dotenv').config();

const calligraphyRoutes = require('../routes/calligraphy');
const apiRoutes = require('../routes/api');
const errorHandler = require('../middleware/errorHandler');
const logger = require('../utils/logger');

const app = express();

// Security middleware
app.use(helmet());

// CORS configuration
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
  credentials: true
}));

// Body parsing middleware
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// Static files
app.use('/uploads', express.static(path.join(__dirname, '../uploads')));

// Request logging
app.use((req, res, next) => {
  logger.info(`${req.method} ${req.url} - ${req.ip}`);
  next();
});

// Routes
app.use('/api/calligraphy', calligraphyRoutes);
app.use('/api', apiRoutes);

// Health check
app.get('/health', (req, res) => {
  res.json({ 
    status: 'OK', 
    timestamp: new Date().toISOString(),
    uptime: process.uptime()
  });
});

// 404 handler
app.use('*', (req, res) => {
  res.status(404).json({ 
    error: 'Route not found',
    path: req.originalUrl
  });
});

// Global error handler
app.use(errorHandler);

module.exports = app;

4. Server Startup

javascript
Click to copy
// src/server.js
const app = require('./app');
const logger = require('../utils/logger');

const PORT = process.env.PORT || 3000;
const HOST = process.env.HOST || 'localhost';

const server = app.listen(PORT, HOST, () => {
  logger.info(`🚀 Server running on http://${HOST}:${PORT}`);
  logger.info(`📚 Environment: ${process.env.NODE_ENV || 'development'}`);
});

// Graceful shutdown
process.on('SIGTERM', () => {
  logger.info('SIGTERM received. Shutting down gracefully...');
  server.close(() => {
    logger.info('Process terminated');
    process.exit(0);
  });
});

process.on('SIGINT', () => {
  logger.info('SIGINT received. Shutting down gracefully...');
  server.close(() => {
    logger.info('Process terminated');
    process.exit(0);
  });
});

module.exports = server;
Calligraphy Service

API Service Class

javascript
Click to copy
// src/services/calligraphyService.js
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const logger = require('../utils/logger');

class CalligraphyService {
  constructor() {
    this.baseURL = 'https://api.calligraphymaker.com/api/v2';
    this.apiKey = process.env.CALLIGRAPHY_API_KEY;
    this.timeout = 30000;
    
    if (!this.apiKey) {
      throw new Error('CALLIGRAPHY_API_KEY environment variable is required');
    }
    
    // Create axios instance
    this.client = axios.create({
      baseURL: this.baseURL,
      timeout: this.timeout,
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': this.apiKey
      }
    });
    
    // Request interceptor
    this.client.interceptors.request.use(
      (config) => {
        logger.info(`API Request: ${config.method?.toUpperCase()} ${config.url}`);
        return config;
      },
      (error) => {
        logger.error('Request interceptor error:', error.message);
        return Promise.reject(error);
      }
    );
    
    // Response interceptor
    this.client.interceptors.response.use(
      (response) => {
        logger.info(`API Response: ${response.status} ${response.config.url}`);
        return response;
      },
      (error) => {
        logger.error(`API Error: ${error.response?.status} ${error.config?.url}`);
        return Promise.reject(this.handleError(error));
      }
    );
  }
  
  /**
   * Generate calligraphy
   */
  async generate(options) {
    try {
      const requestData = {
        text: options.text,
        language: options.language || 'hindi',
        fontStyle: options.fontStyle || 'calligraphy',
        count: options.count || 1,
        imageFormat: options.imageFormat || 'png'
      };
      
      logger.info('Generating calligraphy:', requestData);
      
      const response = await this.client.post('/generate', requestData);
      
      if (!response.data.success) {
        throw new Error(response.data.error || 'Generation failed');
      }
      
      return response.data.data;
    } catch (error) {
      logger.error('Generate calligraphy error:', error.message);
      throw error;
    }
  }
  
  /**
   * Download SVG
   */
  async downloadSVG(resultText, fontName) {
    try {
      const requestData = {
        resultText,
        fontName
      };
      
      logger.info('Downloading SVG:', requestData);
      
      const response = await this.client.post('/download-svg', requestData);
      
      if (!response.data.success) {
        throw new Error(response.data.error || 'SVG download failed');
      }
      
      return response.data.data;
    } catch (error) {
      logger.error('Download SVG error:', error.message);
      throw error;
    }
  }
  
  /**
   * Batch generate calligraphy
   */
  async batchGenerate(requests) {
    try {
      logger.info(`Batch generating ${requests.length} requests`);
      
      const response = await this.client.post('/batch/generate', { requests });
      
      if (!response.data.success) {
        throw new Error(response.data.error || 'Batch generation failed');
      }
      
      return response.data.data;
    } catch (error) {
      logger.error('Batch generate error:', error.message);
      throw error;
    }
  }
  
  /**
   * Download image from URL and save locally
   */
  async downloadAndSaveImage(imageUrl, filename) {
    try {
      const response = await axios.get(imageUrl, {
        responseType: 'stream',
        timeout: this.timeout
      });
      
      const uploadsDir = path.join(__dirname, '../uploads');
      if (!fs.existsSync(uploadsDir)) {
        fs.mkdirSync(uploadsDir, { recursive: true });
      }
      
      const filePath = path.join(uploadsDir, filename);
      const writer = fs.createWriteStream(filePath);
      
      response.data.pipe(writer);
      
      return new Promise((resolve, reject) => {
        writer.on('finish', () => {
          logger.info(`Image saved: ${filePath}`);
          resolve(filePath);
        });
        writer.on('error', reject);
      });
    } catch (error) {
      logger.error('Download and save image error:', error.message);
      throw error;
    }
  }
  
  /**
   * Handle API errors
   */
  handleError(error) {
    if (error.response) {
      const { status, data } = error.response;
      
      const errorMessage = (() => {
        switch (status) {
          case 401:
            return 'Invalid API key. Please check your credentials.';
          case 402:
            return 'Insufficient credits. Please upgrade your plan.';
          case 429:
            return 'Rate limit exceeded. Please try again later.';
          case 400:
            return data.error || 'Invalid request parameters.';
          case 500:
            return 'Internal server error. Please try again later.';
          default:
            return data.error || `HTTP ${status} error occurred.`;
        }
      })();
      
      const customError = new Error(errorMessage);
      customError.status = status;
      customError.data = data;
      return customError;
    }
    
    if (error.code === 'ECONNABORTED') {
      const timeoutError = new Error('Request timeout. Please try again.');
      timeoutError.status = 408;
      return timeoutError;
    }
    
    if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
      const networkError = new Error('Network error. Please check your internet connection.');
      networkError.status = 503;
      return networkError;
    }
    
    return error;
  }
}

module.exports = new CalligraphyService();
API Routes

Calligraphy Routes

javascript
Click to copy
// routes/calligraphy.js
const express = require('express');
const router = express.Router();
const calligraphyService = require('../src/services/calligraphyService');
const { validateGenerate, validateDownloadSVG } = require('../middleware/validation');
const logger = require('../utils/logger');

/**
 * POST /api/calligraphy/generate
 * Generate calligraphy
 */
router.post('/generate', validateGenerate, async (req, res, next) => {
  try {
    const { text, language, fontStyle, count, imageFormat } = req.body;
    
    logger.info('Generate request received:', { text, language, fontStyle, count });
    
    const results = await calligraphyService.generate({
      text,
      language,
      fontStyle,
      count,
      imageFormat
    });
    
    res.json({
      success: true,
      data: results,
      timestamp: new Date().toISOString()
    });
    
  } catch (error) {
    logger.error('Generate route error:', error.message);
    next(error);
  }
});

/**
 * POST /api/calligraphy/download-svg
 * Download SVG format
 */
router.post('/download-svg', validateDownloadSVG, async (req, res, next) => {
  try {
    const { resultText, fontName } = req.body;
    
    logger.info('SVG download request received:', { resultText, fontName });
    
    const svgData = await calligraphyService.downloadSVG(resultText, fontName);
    
    res.json({
      success: true,
      data: svgData,
      timestamp: new Date().toISOString()
    });
    
  } catch (error) {
    logger.error('Download SVG route error:', error.message);
    next(error);
  }
});

/**
 * POST /api/calligraphy/batch
 * Batch generate calligraphy
 */
router.post('/batch', async (req, res, next) => {
  try {
    const { requests } = req.body;
    
    if (!Array.isArray(requests) || requests.length === 0) {
      return res.status(400).json({
        success: false,
        error: 'Requests array is required and cannot be empty'
      });
    }
    
    if (requests.length > 10) {
      return res.status(400).json({
        success: false,
        error: 'Maximum 10 requests allowed per batch'
      });
    }
    
    logger.info(`Batch request received with ${requests.length} items`);
    
    const results = await calligraphyService.batchGenerate(requests);
    
    res.json({
      success: true,
      data: results,
      timestamp: new Date().toISOString()
    });
    
  } catch (error) {
    logger.error('Batch route error:', error.message);
    next(error);
  }
});

/**
 * POST /api/calligraphy/download-and-save
 * Download image and save locally
 */
router.post('/download-and-save', async (req, res, next) => {
  try {
    const { imageUrl, filename } = req.body;
    
    if (!imageUrl) {
      return res.status(400).json({
        success: false,
        error: 'Image URL is required'
      });
    }
    
    const generatedFilename = filename || `calligraphy-${Date.now()}.png`;
    
    logger.info('Download and save request:', { imageUrl, filename: generatedFilename });
    
    const savedPath = await calligraphyService.downloadAndSaveImage(imageUrl, generatedFilename);
    
    res.json({
      success: true,
      data: {
        filename: generatedFilename,
        path: savedPath,
        url: `/uploads/${generatedFilename}`
      },
      timestamp: new Date().toISOString()
    });
    
  } catch (error) {
    logger.error('Download and save route error:', error.message);
    next(error);
  }
});

/**
 * GET /api/calligraphy/stats
 * Get service statistics
 */
router.get('/stats', (req, res) => {
  res.json({
    success: true,
    data: {
      uptime: process.uptime(),
      memory: process.memoryUsage(),
      version: process.version,
      environment: process.env.NODE_ENV || 'development'
    },
    timestamp: new Date().toISOString()
  });
});

module.exports = router;

Validation Middleware

javascript
Click to copy
// middleware/validation.js
const logger = require('../utils/logger');

/**
 * Validate generate request
 */
const validateGenerate = (req, res, next) => {
  const { text, language, fontStyle, count, imageFormat } = req.body;
  const errors = [];
  
  // Text validation
  if (!text || typeof text !== 'string') {
    errors.push('Text is required and must be a string');
  } else if (text.trim().length === 0) {
    errors.push('Text cannot be empty');
  } else if (text.length > 1000) {
    errors.push('Text cannot exceed 1000 characters');
  }
  
  // Language validation
  if (language && !['hindi', 'english', 'marathi', 'gujarati'].includes(language)) {
    errors.push('Invalid language. Supported: hindi, english, marathi, gujarati');
  }
  
  // Font style validation
  if (fontStyle && !['calligraphy', 'modern', 'traditional'].includes(fontStyle)) {
    errors.push('Invalid font style. Supported: calligraphy, modern, traditional');
  }
  
  // Count validation
  if (count !== undefined) {
    if (!Number.isInteger(count) || count < 1 || count > 4) {
      errors.push('Count must be an integer between 1 and 4');
    }
  }
  
  // Image format validation
  if (imageFormat && !['png', 'jpg', 'jpeg'].includes(imageFormat)) {
    errors.push('Invalid image format. Supported: png, jpg, jpeg');
  }
  
  if (errors.length > 0) {
    logger.warn('Validation errors:', errors);
    return res.status(400).json({
      success: false,
      error: 'Validation failed',
      details: errors
    });
  }
  
  next();
};

/**
 * Validate download SVG request
 */
const validateDownloadSVG = (req, res, next) => {
  const { resultText, fontName } = req.body;
  const errors = [];
  
  if (!resultText || typeof resultText !== 'string') {
    errors.push('Result text is required and must be a string');
  }
  
  if (!fontName || typeof fontName !== 'string') {
    errors.push('Font name is required and must be a string');
  }
  
  if (errors.length > 0) {
    logger.warn('SVG validation errors:', errors);
    return res.status(400).json({
      success: false,
      error: 'Validation failed',
      details: errors
    });
  }
  
  next();
};

/**
 * Rate limiting middleware
 */
const rateLimit = (windowMs = 15 * 60 * 1000, max = 100) => {
  const requests = new Map();
  
  return (req, res, next) => {
    const clientId = req.ip;
    const now = Date.now();
    
    if (!requests.has(clientId)) {
      requests.set(clientId, { count: 1, startTime: now });
      return next();
    }
    
    const clientData = requests.get(clientId);
    
    if (now - clientData.startTime > windowMs) {
      // Reset window
      requests.set(clientId, { count: 1, startTime: now });
      return next();
    }
    
    if (clientData.count >= max) {
      logger.warn(`Rate limit exceeded for ${clientId}`);
      return res.status(429).json({
        success: false,
        error: 'Rate limit exceeded',
        retryAfter: Math.ceil((clientData.startTime + windowMs - now) / 1000)
      });
    }
    
    clientData.count++;
    next();
  };
};

module.exports = {
  validateGenerate,
  validateDownloadSVG,
  rateLimit
};
Utilities & Middleware

Logger Utility

javascript
Click to copy
// utils/logger.js
const fs = require('fs');
const path = require('path');

class Logger {
  constructor() {
    this.logDir = path.join(__dirname, '../logs');
    this.ensureLogDir();
  }
  
  ensureLogDir() {
    if (!fs.existsSync(this.logDir)) {
      fs.mkdirSync(this.logDir, { recursive: true });
    }
  }
  
  formatMessage(level, message, data = null) {
    const timestamp = new Date().toISOString();
    const logEntry = {
      timestamp,
      level,
      message,
      ...(data && { data })
    };
    
    return JSON.stringify(logEntry);
  }
  
  writeToFile(level, message) {
    const filename = `${level}-${new Date().toISOString().split('T')[0]}.log`;
    const filepath = path.join(this.logDir, filename);
    
    fs.appendFile(filepath, message + '\n', (err) => {
      if (err) console.error('Failed to write to log file:', err);
    });
  }
  
  log(level, message, data = null) {
    const formattedMessage = this.formatMessage(level, message, data);
    
    // Console output with colors
    const colors = {
      INFO: '\x1b[36m',  // Cyan
      WARN: '\x1b[33m',  // Yellow
      ERROR: '\x1b[31m', // Red
      DEBUG: '\x1b[35m'  // Magenta
    };
    
    const resetColor = '\x1b[0m';
    const color = colors[level] || '';
    
    console.log(`${color}[${level}]${resetColor} ${message}`, data || '');
    
    // File output
    if (process.env.NODE_ENV === 'production') {
      this.writeToFile(level, formattedMessage);
    }
  }
  
  info(message, data) {
    this.log('INFO', message, data);
  }
  
  warn(message, data) {
    this.log('WARN', message, data);
  }
  
  error(message, data) {
    this.log('ERROR', message, data);
  }
  
  debug(message, data) {
    if (process.env.NODE_ENV === 'development') {
      this.log('DEBUG', message, data);
    }
  }
}

module.exports = new Logger();

Error Handler

javascript
Click to copy
// middleware/errorHandler.js
const logger = require('../utils/logger');

const errorHandler = (error, req, res, next) => {
  // Log the error
  logger.error(`Error in ${req.method} ${req.url}:`, {
    message: error.message,
    stack: error.stack,
    status: error.status,
    body: req.body,
    params: req.params,
    query: req.query
  });
  
  // Default error response
  let status = error.status || 500;
  let message = error.message || 'Internal Server Error';
  
  // Handle specific error types
  if (error.name === 'ValidationError') {
    status = 400;
    message = 'Validation Error';
  } else if (error.name === 'CastError') {
    status = 400;
    message = 'Invalid ID format';
  } else if (error.code === 'ENOTFOUND') {
    status = 503;
    message = 'Service temporarily unavailable';
  }
  
  // Don't expose internal errors in production
  if (process.env.NODE_ENV === 'production' && status === 500) {
    message = 'Internal Server Error';
  }
  
  res.status(status).json({
    success: false,
    error: message,
    ...(process.env.NODE_ENV === 'development' && {
      stack: error.stack,
      details: error.data
    }),
    timestamp: new Date().toISOString()
  });
};

module.exports = errorHandler;

Package.json Scripts

json
Click to copy
{
  "name": "calligraphy-node-app",
  "version": "1.0.0",
  "description": "Node.js server for Calligraphy API integration",
  "main": "src/server.js",
  "scripts": {
    "start": "node src/server.js",
    "dev": "nodemon src/server.js",
    "test": "echo \"Error: no test specified\" && exit 1",
    "lint": "eslint src/ routes/ middleware/ utils/",
    "format": "prettier --write src/ routes/ middleware/ utils/"
  },
  "keywords": ["calligraphy", "api", "node", "express"],
  "author": "Your Name",
  "license": "MIT",
  "dependencies": {
    "express": "^4.18.2",
    "axios": "^1.6.0",
    "dotenv": "^16.3.1",
    "cors": "^2.8.5",
    "helmet": "^7.1.0",
    "multer": "^1.4.5"
  },
  "devDependencies": {
    "nodemon": "^3.0.2",
    "@types/express": "^4.17.21",
    "eslint": "^8.56.0",
    "prettier": "^3.1.1"
  },
  "engines": {
    "node": ">=16.0.0"
  }
}

Environment Configuration

bash
Click to copy
# .env
# Server Configuration
PORT=3000
HOST=localhost
NODE_ENV=development

# API Configuration
CALLIGRAPHY_API_KEY=your_api_key_here

# CORS Configuration
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001

# Upload Configuration
UPLOAD_LIMIT=10mb
MAX_FILE_SIZE=5242880

# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100

# Logging
LOG_LEVEL=info
LOG_TO_FILE=true
What You'll Build
REST API Server
Express.js with full middleware stack
File Operations
Image download and local storage
Error Handling
Comprehensive error management
Logging & Monitoring
Production-ready logging system
Requirements
Node.js 16+
NPM or Yarn
API Key from Calligraphy API
Basic Express.js knowledge
API Endpoints
POST /api/calligraphy/generate
Generate calligraphy
POST /api/calligraphy/download-svg
Download SVG format
POST /api/calligraphy/batch
Batch generation
GET /health
Health check