# 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) ```javascript 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 ```bash 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: ```javascript 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`**: ```javascript 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`**: ```javascript 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): ```javascript '^/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`**: ```typescript // 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`**: ```typescript 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 ```json "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: ```bash # Build frontend npm run build # Start combined server (serves dist/ + API) npm run start ``` **Environment variables (.env)**: ```bash 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:** ```bash # Start API-only server npm run start:api ``` **Environment variables (.env for API server)**: ```bash PORT=3000 API_ONLY=true ANTHROPIC_API_KEY=sk-ant-... ALLOWED_ORIGINS=https://your-frontend.com,https://www.your-frontend.com ``` **Frontend (Static Host):** ```bash # 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)**: ```bash 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/`: ```javascript // 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