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 .env2. 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.json3. 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=trueQuick Links
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