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:
- Cloud Run domain mapping only supports one service per domain
- No native path-based routing to different Cloud Functions
- 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)
Create Router Function
mkdir router-function cd router-function npm init -y npm install @google-cloud/functions-framework express cors
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
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)
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
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)
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
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)
Update README.md
- Change all API examples to use new endpoints
- Update demo interface URLs
Update API Documentation
- Document new endpoint structure
- Add backward compatibility notes
Migration Strategy - Clean Cut Approach
Implementation Plan
- Deploy new router function with all features
- Test thoroughly using Cloud Function URLs
- Delete old functions - clean removal
- Switch domain mapping to new router
- 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:
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
Individual Functions Remain Available
- All original Cloud Run URLs still work
- Cloud Functions URLs still work
- No downtime for critical functions
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:
- Simplicity: Easiest to implement and maintain
- Cost: Significant cost savings (~60%)
- Performance: Better cold start characteristics
- User Experience: Clean, consistent API structure
- Maintainability: Single codebase to manage
- 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:
- URL Structure: RESTful paths with
/v1/convert/
and/v1/usage/
prefixes - Backward Compatibility: Not required - clean migration preferred
- Resource Settings: 60s timeout, 1GB memory
- Authentication: API key support with X-API-Key header or Bearer token
- Usage API: Full implementation from PRD specification
- 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
- Create new project structure for unified API
- Implement router with authentication and usage tracking
- Set up Firestore collections for API keys and usage data
- Deploy and test the new unified API
- Delete old functions and update domain mapping
- 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.