Last updated: Jul 25, 2025, 10:08 AM UTC

Single Endpoint Routing Analysis for Convert to Markdown

Generated: 2025-07-23
Status: Analysis Complete
Updated: 2025-07-24 - Added authentication, usage API, and structured error handling
Next Steps: Implement clean cut migration with new API design

Current Problem

The domain convert-to-markdown.knowcode.tech is only mapped to the xlsx-converter Cloud Run service. Other functions like pdf-to-md, docx-to-html, etc. return 500 errors because:

  1. Cloud Run domain mapping only supports one service per domain
  2. No native path-based routing to different Cloud Functions
  3. Each function has its own Cloud Run service URL but can't share a custom domain

Current Working URLs

Individual Cloud Run Services:

  • https://xlsx-converter-qpg64cvnga-uk.a.run.app
  • https://pdf-to-md-qpg64cvnga-uk.a.run.app
  • https://docx-to-html-qpg64cvnga-uk.a.run.app
  • https://docx-to-md-qpg64cvnga-uk.a.run.app
  • https://xlsx-to-md-qpg64cvnga-uk.a.run.app

Cloud Functions URLs:

  • https://us-east4-convert-to-markdown-us-east4.cloudfunctions.net/xlsx-converter
  • https://us-east4-convert-to-markdown-us-east4.cloudfunctions.net/pdf-to-md
  • etc.

Custom Domain (Only xlsx-converter works):

  • https://convert-to-markdown.knowcode.tech/xlsx-converter
  • https://convert-to-markdown.knowcode.tech/pdf-to-md 500 error

Solution Options Analysis

Option 1: Single Endpoint Router (Recommended)

Concept: Create one Cloud Function that handles all requests and routes internally based on the URL path.

URL Structure:

# Conversion endpoints
https://convert-to-markdown.knowcode.tech/v1/convert/excel-to-json
https://convert-to-markdown.knowcode.tech/v1/convert/excel-to-markdown
https://convert-to-markdown.knowcode.tech/v1/convert/pdf-to-markdown
https://convert-to-markdown.knowcode.tech/v1/convert/word-to-html
https://convert-to-markdown.knowcode.tech/v1/convert/word-to-markdown
https://convert-to-markdown.knowcode.tech/v1/convert/powerpoint-to-markdown

# Usage API endpoints
https://convert-to-markdown.knowcode.tech/v1/usage/summary
https://convert-to-markdown.knowcode.tech/v1/usage/history
https://convert-to-markdown.knowcode.tech/v1/usage/current
https://convert-to-markdown.knowcode.tech/v1/usage/breakdown
https://convert-to-markdown.knowcode.tech/v1/usage/performance

Implementation Approach:

1. Express.js Router Pattern with Authentication & Error Handling

// index.js - New single endpoint function with authentication
const functions = require('@google-cloud/functions-framework');
const express = require('express');
const cors = require('cors');
const { Firestore } = require('@google-cloud/firestore');

// Import existing converters
const { excelToJson } = require('./src/xlsxConverterToJS');
const { excelToMarkdown } = require('./src/xlsxConverterToMD');
const { pdfToMarkdown } = require('./src/pdfConverterToMD');
const { docxToHtml } = require('./src/docxToHtml');
const { docxToMarkdown } = require('./src/docxToMarkdown');
const { pptxToMarkdown } = require('./src/pptxToMarkdown');

// Import usage API handlers
const { getUserUsageSummary, getUserUsageHistory, recordUsage } = require('./lib/usage');

const app = express();
const db = new Firestore();

// Structured error class for Google Cloud Logging
class ApiError extends Error {
  constructor(code, message, httpStatus = 500, details = {}) {
    super(message);
    this.name = 'ApiError';
    this.code = code;
    this.httpStatus = httpStatus;
    this.details = details;
    this.timestamp = new Date().toISOString();
  }
  
  toJSON() {
    return {
      name: this.name,
      code: this.code,
      message: this.message,
      httpStatus: this.httpStatus,
      details: this.details,
      timestamp: this.timestamp,
      stack: this.stack
    };
  }
}

// Error codes enum
const ErrorCodes = {
  // Authentication errors
  MISSING_API_KEY: 'AUTH_001',
  INVALID_API_KEY: 'AUTH_002',
  EXPIRED_API_KEY: 'AUTH_003',
  
  // Validation errors
  INVALID_FILE_TYPE: 'VAL_001',
  FILE_TOO_LARGE: 'VAL_002',
  MISSING_FILE: 'VAL_003',
  
  // Processing errors
  CONVERSION_FAILED: 'PROC_001',
  TIMEOUT: 'PROC_002',
  OUT_OF_MEMORY: 'PROC_003',
  
  // Usage errors
  QUOTA_EXCEEDED: 'USAGE_001',
  RATE_LIMITED: 'USAGE_002'
};

// Middleware
app.use(cors({ origin: '*' }));
app.use(express.json({ limit: '10mb' }));

// Request logging for observability
app.use((req, res, next) => {
  console.log(JSON.stringify({
    severity: 'INFO',
    request: {
      method: req.method,
      path: req.path,
      headers: req.headers,
      timestamp: new Date().toISOString()
    }
  }));
  next();
});

// Authentication middleware
async function authenticate(req, res, next) {
  try {
    const apiKey = req.headers['x-api-key'] || 
                   req.headers.authorization?.replace('Bearer ', '');
    
    if (!apiKey) {
      throw new ApiError(
        ErrorCodes.MISSING_API_KEY,
        'API key is required',
        401,
        { headers: ['X-API-Key', 'Authorization'] }
      );
    }
    
    // Validate API key
    const keyDoc = await db.collection('api_keys').doc(apiKey).get();
    if (!keyDoc.exists || !keyDoc.data().active) {
      throw new ApiError(
        ErrorCodes.INVALID_API_KEY,
        'Invalid or expired API key',
        401
      );
    }
    
    const user = keyDoc.data();
    
    // Check usage quota
    const usage = await db.collection('usage_stats')
      .doc(user.userId)
      .get();
    
    const currentUsage = usage.data()?.currentMonth || 0;
    const limit = user.plan === 'free' ? 50 : 10000;
    
    if (currentUsage >= limit) {
      throw new ApiError(
        ErrorCodes.QUOTA_EXCEEDED,
        `Monthly quota exceeded (${limit} conversions)`,
        429,
        { used: currentUsage, limit: limit }
      );
    }
    
    req.user = {
      id: user.userId,
      apiKeyId: apiKey,
      plan: user.plan,
      usage: currentUsage,
      limit: limit
    };
    
    next();
  } catch (error) {
    next(error);
  }
}

// Usage tracking middleware
async function trackUsage(req, res, next) {
  const startTime = Date.now();
  const originalSend = res.send;
  
  res.send = function(data) {
    res.send = originalSend;
    
    if (res.statusCode === 200) {
      // Fire and forget - don't block response
      setImmediate(async () => {
        try {
          await recordUsage({
            userId: req.user.id,
            apiKeyId: req.user.apiKeyId,
            endpoint: req.path,
            fileType: req.file?.mimetype,
            fileSize: req.file?.size || 0,
            processingTime: Date.now() - startTime,
            success: true,
            timestamp: new Date()
          });
        } catch (error) {
          console.error('Failed to record usage:', error);
        }
      });
    }
    
    return res.send(data);
  };
  
  next();
}

// Health check (no auth required)
app.get('/v1/health', (req, res) => {
  res.json({ 
    status: 'ok', 
    version: 'v1',
    timestamp: new Date().toISOString() 
  });
});

// List available endpoints (no auth required)
app.get('/v1', (req, res) => {
  res.json({
    version: 'v1',
    endpoints: {
      conversion: [
        'POST /v1/convert/excel-to-json',
        'POST /v1/convert/excel-to-markdown',
        'POST /v1/convert/pdf-to-markdown',
        'POST /v1/convert/word-to-html',
        'POST /v1/convert/word-to-markdown',
        'POST /v1/convert/powerpoint-to-markdown'
      ],
      usage: [
        'GET /v1/usage/summary',
        'GET /v1/usage/history',
        'GET /v1/usage/current'
      ]
    }
  });
});

// Converter routes with authentication and usage tracking
app.post('/v1/convert/excel-to-json', authenticate, trackUsage, excelToJson);
app.post('/v1/convert/excel-to-markdown', authenticate, trackUsage, excelToMarkdown);
app.post('/v1/convert/pdf-to-markdown', authenticate, trackUsage, pdfToMarkdown);
app.post('/v1/convert/word-to-html', authenticate, trackUsage, docxToHtml);
app.post('/v1/convert/word-to-markdown', authenticate, trackUsage, docxToMarkdown);
app.post('/v1/convert/powerpoint-to-markdown', authenticate, trackUsage, pptxToMarkdown);

// Usage API endpoints
app.get('/v1/usage/summary', authenticate, async (req, res, next) => {
  try {
    const summary = await getUserUsageSummary(req.user.id);
    res.json(summary);
  } catch (error) {
    next(error);
  }
});

app.get('/v1/usage/history', authenticate, async (req, res, next) => {
  try {
    const { start_date, end_date, granularity = 'daily' } = req.query;
    const history = await getUserUsageHistory(req.user.id, {
      startDate: start_date,
      endDate: end_date,
      granularity
    });
    res.json(history);
  } catch (error) {
    next(error);
  }
});

// 404 handler
app.use('*', (req, res) => {
  res.status(404).json({ 
    error: {
      code: 'NOT_FOUND',
      message: 'Endpoint not found'
    },
    help: 'See available endpoints at /v1',
    documentation: 'https://convert-to-markdown.knowcode.tech/docs'
  });
});

// Global error handler
app.use((err, req, res, next) => {
  // Ensure it's an ApiError
  if (!(err instanceof ApiError)) {
    err = new ApiError(
      'UNKNOWN_ERROR',
      err.message || 'An unexpected error occurred',
      500,
      { originalError: err.toString() }
    );
  }
  
  // Log structured error for Google Cloud Logging
  console.error(JSON.stringify({
    severity: 'ERROR',
    error: err.toJSON(),
    request: {
      method: req.method,
      path: req.path,
      headers: req.headers,
      user: req.user?.id
    }
  }));
  
  // Send user-friendly response
  res.status(err.httpStatus).json({
    error: {
      code: err.code,
      message: err.message,
      details: err.details
    },
    help: getHelpMessage(err.code),
    documentation: `https://convert-to-markdown.knowcode.tech/docs/errors#${err.code}`
  });
});

// Helper function for error messages
function getHelpMessage(errorCode) {
  const helpMessages = {
    [ErrorCodes.MISSING_API_KEY]: 'Include your API key in X-API-Key header or Authorization: Bearer {key}',
    [ErrorCodes.INVALID_API_KEY]: 'Check your API key or generate a new one at https://convert-to-markdown.knowcode.tech',
    [ErrorCodes.QUOTA_EXCEEDED]: 'Upgrade to Pro plan for 10,000 monthly conversions',
    [ErrorCodes.FILE_TOO_LARGE]: 'Maximum file size is 10MB. Try compressing your file.',
    [ErrorCodes.INVALID_FILE_TYPE]: 'Check supported file types at /v1',
  };
  return helpMessages[errorCode] || 'Please check our documentation for more information.';
}

functions.http('api', app);

2. Deployment Configuration

# Deploy single router function
gcloud functions deploy convert-api \
  --gen2 \
  --runtime=nodejs20 \
  --region=us-east4 \
  --source=. \
  --entry-point=convertApi \
  --memory=1GB \
  --timeout=120s \
  --max-instances=100 \
  --allow-unauthenticated \
  --trigger-http

3. Domain Mapping Update

# Remove current mapping
gcloud beta run domain-mappings delete convert-to-markdown.knowcode.tech --region=us-east4

# Map to new router function
gcloud beta run domain-mappings create \
  --service=convert-api \
  --domain=convert-to-markdown.knowcode.tech \
  --region=us-east4

Advantages:

  • Single domain handles all endpoints
  • Reduced cold start frequency (one function stays warmer)
  • Shared dependencies and initialization
  • Easier to maintain and deploy
  • Better for development and testing
  • Lower infrastructure costs
  • Clean, consistent API structure

Disadvantages:

  • All endpoints share same memory/timeout limits
  • Larger deployment package size
  • One endpoint failure could affect others
  • Can't scale individual converters independently
  • Slightly more complex error isolation

Performance Impact:

  • Cold start: Better (single function vs. 5 separate functions)
  • Memory usage: Potentially higher (all converters loaded)
  • Concurrent requests: Up to 1000 per instance (Cloud Run Gen2)

Option 2: Application Load Balancer

Concept: Use Google Cloud Load Balancer with path-based routing rules.

Implementation:

# Create backend services for each function
gcloud compute backend-services create xlsx-converter-backend --global
gcloud compute backend-services create pdf-converter-backend --global

# Create URL map with path rules
gcloud compute url-maps create convert-api-map --default-service=xlsx-converter-backend
gcloud compute url-maps add-path-matcher convert-api-map \
  --path-matcher-name=api-routes \
  --path-rules="/pdf-to-md/*=pdf-converter-backend,/xlsx-converter/*=xlsx-converter-backend"

Advantages:

  • Each function maintains independence
  • Can scale functions individually
  • Better fault isolation
  • Native Google Cloud solution
  • CDN and caching capabilities

Disadvantages:

  • Additional cost (~$18/month minimum)
  • More complex setup and maintenance
  • Higher latency (extra hop)
  • More moving parts to debug

Option 3: Multiple Subdomains

Concept: Create subdomains for each converter.

URL Structure:

https://excel.convert-to-markdown.knowcode.tech/to-json
https://excel.convert-to-markdown.knowcode.tech/to-markdown
https://pdf.convert-to-markdown.knowcode.tech/to-markdown
https://word.convert-to-markdown.knowcode.tech/to-html

Advantages:

  • Each function maintains independence
  • Easy to implement
  • Clear separation of concerns

Disadvantages:

  • Multiple DNS entries to manage
  • Multiple SSL certificates
  • Less clean API structure
  • Potential SEO impact

Option 4: Firebase Hosting with Rewrites

Concept: Use Firebase Hosting to proxy requests to Cloud Functions.

// firebase.json
{
  "hosting": {
    "public": "public",
    "rewrites": [
      {
        "source": "/api/excel-to-json",
        "function": "xlsx-converter"
      },
      {
        "source": "/api/pdf-to-markdown", 
        "function": "pdf-to-md"
      }
    ]
  }
}

Advantages:

  • Native Firebase integration
  • CDN capabilities
  • Easy configuration

Disadvantages:

  • Requires Firebase setup
  • Another service to manage
  • May have latency overhead

Implementation Plan for Single Endpoint Router

Phase 1: Preparation (1-2 hours)

  1. Create Router Function

    mkdir router-function
    cd router-function
    npm init -y
    npm install @google-cloud/functions-framework express cors
    
  2. Code Structure

    router-function/
    ├── index.js              # Router implementation
    ├── package.json
    ├── src/                  # Copy existing converters
    │   ├── xlsxConverterToJS.js
    │   ├── xlsxConverterToMD.js
    │   ├── pdfConverterToMD.js
    │   ├── docxToHtml.js
    │   └── docxToMarkdown.js
    ├── lib/                  # Copy shared utilities
    └── middleware/           # New middleware
        ├── cors.js
        ├── errorHandler.js
        └── validation.js
    
  3. Test Locally

    npx functions-framework --target=convertApi --port=8080
    
    # Test each endpoint
    curl -X POST http://localhost:8080/api/excel-to-json -F "file=@test.xlsx"
    curl -X POST http://localhost:8080/api/pdf-to-markdown -F "file=@test.pdf"
    

Phase 2: Deployment (30 minutes)

  1. Deploy Router Function

    gcloud functions deploy convert-api \
      --gen2 \
      --runtime=nodejs20 \
      --region=us-east4 \
      --entry-point=convertApi \
      --memory=1GB \
      --timeout=60s \
      --allow-unauthenticated \
      --trigger-http
    
  2. Test Cloud Function

    FUNCTION_URL=$(gcloud functions describe convert-api --region=us-east4 --format="value(serviceConfig.uri)")
    curl -X POST $FUNCTION_URL/api/health
    

Phase 3: Domain Migration (15 minutes)

  1. Update Domain Mapping

    # Remove current mapping
    gcloud beta run domain-mappings delete convert-to-markdown.knowcode.tech --region=us-east4
    
    # Map to new function
    gcloud beta run domain-mappings create \
      --service=convert-api \
      --domain=convert-to-markdown.knowcode.tech \
      --region=us-east4
    
  2. Test All Endpoints

    curl -X POST https://convert-to-markdown.knowcode.tech/api/health
    curl -X POST https://convert-to-markdown.knowcode.tech/api/excel-to-json -F "file=@test.xlsx"
    curl -X POST https://convert-to-markdown.knowcode.tech/api/pdf-to-markdown -F "file=@test.pdf"
    

Phase 4: Documentation Update (30 minutes)

  1. Update README.md

    • Change all API examples to use new endpoints
    • Update demo interface URLs
  2. Update API Documentation

    • Document new endpoint structure
    • Add backward compatibility notes

Migration Strategy - Clean Cut Approach

Implementation Plan

  1. Deploy new router function with all features
  2. Test thoroughly using Cloud Function URLs
  3. Delete old functions - clean removal
  4. Switch domain mapping to new router
  5. Update all documentation with new endpoints

Benefits of Clean Cut

  • No confusion with multiple versions
  • Simpler deployment and maintenance
  • Clear migration point
  • No backward compatibility complexity
  • User explicitly approved outage window

Cost Analysis

Current Architecture (5 functions)

  • 5 Cloud Functions × $0.40/million requests = $2.00/million requests
  • 5 Cold starts per endpoint type
  • Higher memory usage during cold starts

Single Router Architecture

  • 1 Cloud Function × $0.40/million requests = $0.40/million requests
  • 1 Cold start for all endpoints
  • Shared memory and initialization
  • Estimated savings: ~60% on execution costs

Rollback Plan

If issues arise with the single endpoint approach:

  1. Immediate Rollback

    # Revert domain mapping to xlsx-converter
    gcloud beta run domain-mappings delete convert-to-markdown.knowcode.tech --region=us-east4
    gcloud beta run domain-mappings create \
      --service=xlsx-converter \
      --domain=convert-to-markdown.knowcode.tech \
      --region=us-east4
    
  2. Individual Functions Remain Available

    • All original Cloud Run URLs still work
    • Cloud Functions URLs still work
    • No downtime for critical functions
  3. Data Integrity

    • No data stored in functions
    • Stateless architecture means no data loss risk

Monitoring and Observability

Metrics to Track

  • Response Times: Compare single vs. individual functions
  • Error Rates: Monitor each converter route separately
  • Cold Start Frequency: Should decrease significantly
  • Memory Usage: Monitor for any increases
  • Costs: Track monthly Cloud Functions costs

Alerting

# Set up error rate alerts
gcloud alpha monitoring policies create \
  --policy-from-file=error-rate-policy.yaml

Logging

// Enhanced logging in router
app.use((req, res, next) => {
  console.log({
    timestamp: new Date().toISOString(),
    method: req.method,
    path: req.path,
    userAgent: req.get('User-Agent'),
    contentLength: req.get('Content-Length')
  });
  next();
});

Recommendation

Choose Option 1: Single Endpoint Router

Reasoning:

  1. Simplicity: Easiest to implement and maintain
  2. Cost: Significant cost savings (~60%)
  3. Performance: Better cold start characteristics
  4. User Experience: Clean, consistent API structure
  5. Maintainability: Single codebase to manage
  6. Backward Compatibility: Can support old endpoints during transition

Timeline:

  • Today: Document review and decision
  • Tomorrow: Implementation (3-4 hours total)
  • Day 3: Testing and validation
  • Day 4: Production deployment
  • Week 2: Monitor and optimize

Implementation Decisions

Based on User Requirements:

  1. URL Structure: RESTful paths with /v1/convert/ and /v1/usage/ prefixes
  2. Backward Compatibility: Not required - clean migration preferred
  3. Resource Settings: 60s timeout, 1GB memory
  4. Authentication: API key support with X-API-Key header or Bearer token
  5. Usage API: Full implementation from PRD specification
  6. Error Handling: Structured errors for Google Cloud Logging integration

Database Schema for Usage Tracking

Firestore Collections:

// api_keys collection
{
  id: "api_key_string",  // The actual API key
  userId: "user_123",
  email: "user@example.com",
  plan: "free" | "pro",
  active: true,
  createdAt: Timestamp,
  lastUsed: Timestamp
}

// usage_stats collection
{
  id: "user_123",  // User ID
  currentMonth: 3847,  // Current month usage count
  monthStart: Timestamp,
  lastReset: Timestamp
}

// usage_records collection
{
  id: "auto_generated",
  userId: "user_123",
  apiKeyId: "api_key_string",
  timestamp: Timestamp,
  endpoint: "/v1/convert/excel-to-json",
  fileType: "application/vnd.ms-excel",
  fileSize: 1234567,
  processingTime: 1523,
  success: true,
  errorCode: null
}

// usage_daily_stats collection (for performance)
{
  id: "user_123_2025-01-24",
  userId: "user_123",
  date: "2025-01-24",
  totalConversions: 156,
  successfulConversions: 152,
  failedConversions: 4,
  byType: {
    "excel-to-json": 67,
    "pdf-to-markdown": 45,
    "word-to-html": 23,
    "word-to-markdown": 21
  },
  totalBytesProcessed: 123456789,
  avgProcessingTimeMs: 1832
}

Next Steps

  1. Create new project structure for unified API
  2. Implement router with authentication and usage tracking
  3. Set up Firestore collections for API keys and usage data
  4. Deploy and test the new unified API
  5. Delete old functions and update domain mapping
  6. Update documentation with new endpoints and authentication

Implementation Timeline

  • Day 1: Set up project structure and implement core router
  • Day 2: Add authentication and usage tracking
  • Day 3: Implement usage API endpoints
  • Day 4: Testing and deployment
  • Day 5: Migration and documentation updates

The single endpoint router with authentication and usage tracking provides the perfect foundation for a professional, scalable API service.