- 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>
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.jsin 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:
expressvite-expressexpress-http-proxydotenv
Implementation Plan
Phase 1: Install Dependencies
npm install express vite-express dotenv cors
express- Web frameworkvite-express- Vite integration for combined deploymentdotenv- Environment variable loadingcors- 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_URLis 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/ + APIstart:api: API-only mode for split deployment- Removed
preview(usestartinstead)
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:
- Development: Runs Vite's dev server as middleware, providing HMR
- 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:
- Handle
/api/*routes directly - Serve static files from
dist/ - Fall back to
dist/index.htmlfor 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
npm install express vite-express dotenv cors- Create
server/api/index.js - Create
server/api/claude.js - Create
src/util/apiConfig.ts - Update
src/react/services/diagramAI.tsto useapiUrl() - Update
server.js(root) with full Express + CORS setup - Remove
/api/claudeproxy fromvite.config.ts - Update
package.jsonscripts - Test combined:
npm run devand verify Claude API works - (Optional) Test split: Set
VITE_API_URLandAPI_ONLY=true
Notes
- WebSocket server unchanged:
server/server.js(port 8080) runs separately - Minimal frontend changes: Only
diagramAI.tsupdated to useapiUrl() - Environment variables:
ANTHROPIC_API_KEYalready in.env.local - Node version: Requires Node 18+ for native
fetch - CORS: Only enabled when
ALLOWED_ORIGINSis set (split deployment) - Backward compatible: Works as combined deployment by default