immersive2/EXPRESS_API_PLAN.md
Michael Mainguy 1152ab0d0c Add Express API server for Claude API proxy
- Add Express server with vite-express for combined frontend/API serving
- Create modular API route structure (server/api/)
- Implement Claude API proxy with proper header injection
- Support split deployment via API_ONLY and ALLOWED_ORIGINS env vars
- Remove Claude proxy from Vite config (now handled by Express)
- Add migration plan documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 12:32:49 -06:00

10 KiB

Express.js API Server Plan

Goal

Add an Express.js backend server to handle API routes (starting with Claude API), with support for either combined or split deployment.

Advantages Over Next.js Migration

  • Minimal frontend changes - only API URL configuration
  • No routing changes - keep react-router-dom as-is
  • Flexible deployment - combined or split frontend/backend
  • Already partially exists - server.js in root has Express + vite-express scaffolding

Deployment Options

Option A: Combined (Single Server)

Express Server (vite-express)
├── Serves static files from dist/
└── Handles /api/* routes
  • Simpler setup, one deployment
  • Good for: VPS, Railway, Fly.io, DigitalOcean App Platform

Option B: Split (Separate Hosts)

Static Host (CDN)              API Server (Node.js)
├── Cloudflare Pages           ├── Railway
├── Netlify                    ├── Fly.io
├── Vercel                     ├── AWS Lambda
└── S3 + CloudFront            └── Any VPS

Serves dist/                   Handles /api/*
  • Better scalability, cheaper static hosting
  • Good for: High traffic, global CDN distribution

Current State

Existing server.js (incomplete)

import express from "express";
import ViteExpress from "vite-express";
import dotenv from "dotenv";
import expressProxy from "express-http-proxy";

dotenv.config();
const app = express();
app.use("/api", expressProxy("local.immersiveidea.com"));
ViteExpress.listen(app, process.env.PORT || 3001, () => console.log("Server is listening..."));

Missing Dependencies

The following packages are imported but not in package.json:

  • express
  • vite-express
  • express-http-proxy
  • dotenv

Implementation Plan

Phase 1: Install Dependencies

npm install express vite-express dotenv cors
  • express - Web framework
  • vite-express - Vite integration for combined deployment
  • dotenv - Environment variable loading
  • cors - Cross-origin support for split deployment

Phase 2: Create API Routes Structure

Create a modular API structure:

server/
├── server.js              # Existing WebSocket server (keep as-is)
├── api/
│   ├── index.js           # Main API router
│   └── claude.js          # Claude API proxy route

Phase 3: Update Root server.js

Replace the current incomplete server.js with:

import express from "express";
import ViteExpress from "vite-express";
import cors from "cors";
import dotenv from "dotenv";
import apiRoutes from "./server/api/index.js";

dotenv.config();

const app = express();

// CORS configuration for split deployment
// In combined mode, same-origin requests don't need CORS
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",") || [];
if (allowedOrigins.length > 0) {
  app.use(cors({
    origin: allowedOrigins,
    credentials: true,
  }));
}

app.use(express.json());

// API routes
app.use("/api", apiRoutes);

// Check if running in API-only mode (split deployment)
const apiOnly = process.env.API_ONLY === "true";

if (apiOnly) {
  // API-only mode: no static file serving
  app.listen(process.env.PORT || 3000, () => {
    console.log(`API server running on port ${process.env.PORT || 3000}`);
  });
} else {
  // Combined mode: Vite handles static files + SPA
  ViteExpress.listen(app, process.env.PORT || 3001, () => {
    console.log(`Server running on port ${process.env.PORT || 3001}`);
  });
}

Phase 4: Create API Router

server/api/index.js:

import { Router } from "express";
import claudeRouter from "./claude.js";

const router = Router();

// Claude API proxy
router.use("/claude", claudeRouter);

// Health check
router.get("/health", (req, res) => {
  res.json({ status: "ok" });
});

export default router;

server/api/claude.js:

import { Router } from "express";

const router = Router();
const ANTHROPIC_API_URL = "https://api.anthropic.com";

router.post("/*", async (req, res) => {
  const apiKey = process.env.ANTHROPIC_API_KEY;

  if (!apiKey) {
    return res.status(500).json({ error: "API key not configured" });
  }

  // Get the path after /api/claude (e.g., /v1/messages)
  const path = req.params[0] || req.path;

  try {
    const response = await fetch(`${ANTHROPIC_API_URL}${path}`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-api-key": apiKey,
        "anthropic-version": "2023-06-01",
      },
      body: JSON.stringify(req.body),
    });

    const data = await response.json();
    res.status(response.status).json(data);
  } catch (error) {
    console.error("Claude API error:", error);
    res.status(500).json({ error: "Failed to proxy request to Claude API" });
  }
});

export default router;

Phase 5: Update Vite Config

Remove the Claude proxy from vite.config.ts since Express handles it now.

Before (lines 41-56):

'^/api/claude': {
    target: 'https://api.anthropic.com',
    changeOrigin: true,
    rewrite: (path) => path.replace(/^\/api\/claude/, ''),
    configure: (proxy) => {
        proxy.on('proxyReq', (proxyReq) => {
            const apiKey = env.ANTHROPIC_API_KEY;
            // ...
        });
    }
}

After: Remove this block entirely. The Express server handles /api/claude/*.

Keep the other proxies (/sync/*, /create-db, /api/images) - they still proxy to deepdiagram.com in dev mode.

Phase 6: Add API URL Configuration (for Split Deployment)

Create a utility to get the API base URL:

src/util/apiConfig.ts:

// API base URL - empty string for same-origin (combined deployment)
// Set VITE_API_URL for split deployment (e.g., "https://api.yourdomain.com")
export const API_BASE_URL = import.meta.env.VITE_API_URL || '';

export function apiUrl(path: string): string {
  return `${API_BASE_URL}${path}`;
}

Update src/react/services/diagramAI.ts:

import { apiUrl } from '../../util/apiConfig';

// Change from:
const response = await fetch('/api/claude/v1/messages', { ... });

// To:
const response = await fetch(apiUrl('/api/claude/v1/messages'), { ... });

This change is backward-compatible:

  • Combined deployment: VITE_API_URL is empty, calls go to same origin
  • Split deployment: VITE_API_URL=https://api.example.com, calls go to API server

Phase 7: Update package.json Scripts

"scripts": {
  "dev": "node server.js",
  "build": "node versionBump.js && vite build",
  "start": "NODE_ENV=production node server.js",
  "start:api": "API_ONLY=true node server.js",
  "test": "vitest",
  "socket": "node server/server.js",
  "serverBuild": "cd server && tsc"
}

Changes:

  • dev: Runs Express + vite-express (serves Vite in dev mode)
  • start: Combined mode - serves dist/ + API
  • start:api: API-only mode for split deployment
  • Removed preview (use start instead)

File Changes Summary

Action File Description
Modify package.json Add dependencies, update scripts
Modify server.js Full Express server with CORS + API routes
Create server/api/index.js Main API router
Create server/api/claude.js Claude API proxy endpoint
Create src/util/apiConfig.ts API URL configuration utility
Modify src/react/services/diagramAI.ts Use apiUrl() for API calls
Modify vite.config.ts Remove /api/claude proxy block

How vite-express Works

vite-express is a simple integration that:

  1. Development: Runs Vite's dev server as middleware, providing HMR
  2. Production: Serves the built dist/ folder as static files

This means:

  • One server handles both API and frontend
  • No CORS issues (same origin)
  • HMR works in development
  • Production-ready with vite build

Production Deployment

Option A: Combined Deployment

Single server handles both frontend and API:

# Build frontend
npm run build

# Start combined server (serves dist/ + API)
npm run start

Environment variables (.env):

PORT=3001
ANTHROPIC_API_KEY=sk-ant-...

The Express server will:

  1. Handle /api/* routes directly
  2. Serve static files from dist/
  3. Fall back to dist/index.html for SPA routing

Option B: Split Deployment

Separate hosting for frontend (CDN) and API (Node server):

API Server:

# Start API-only server
npm run start:api

Environment variables (.env for API server):

PORT=3000
API_ONLY=true
ANTHROPIC_API_KEY=sk-ant-...
ALLOWED_ORIGINS=https://your-frontend.com,https://www.your-frontend.com

Frontend (Static Host):

# Build with API URL configured
VITE_API_URL=https://api.yourdomain.com npm run build

# Deploy dist/ to your static host (Cloudflare Pages, Netlify, etc.)

Environment variables (.env.production for frontend build):

VITE_API_URL=https://api.yourdomain.com

Deployment Examples

Deployment Frontend API Server Cost
Combined Railway (same) ~$5/mo
Combined Fly.io (same) Free tier
Split Cloudflare Pages (free) Railway ($5/mo) ~$5/mo
Split Netlify (free) Fly.io (free) Free
Split Vercel (free) AWS Lambda Pay-per-use

Future API Routes

To add more API routes, create new files in server/api/:

// server/api/index.js
import claudeRouter from "./claude.js";
import imagesRouter from "./images.js";  // future
import authRouter from "./auth.js";      // future

router.use("/claude", claudeRouter);
router.use("/images", imagesRouter);
router.use("/auth", authRouter);

Migration Order

  1. npm install express vite-express dotenv cors
  2. Create server/api/index.js
  3. Create server/api/claude.js
  4. Create src/util/apiConfig.ts
  5. Update src/react/services/diagramAI.ts to use apiUrl()
  6. Update server.js (root) with full Express + CORS setup
  7. Remove /api/claude proxy from vite.config.ts
  8. Update package.json scripts
  9. Test combined: npm run dev and verify Claude API works
  10. (Optional) Test split: Set VITE_API_URL and API_ONLY=true

Notes

  • WebSocket server unchanged: server/server.js (port 8080) runs separately
  • Minimal frontend changes: Only diagramAI.ts updated to use apiUrl()
  • Environment variables: ANTHROPIC_API_KEY already in .env.local
  • Node version: Requires Node 18+ for native fetch
  • CORS: Only enabled when ALLOWED_ORIGINS is set (split deployment)
  • Backward compatible: Works as combined deployment by default