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

404 lines
10 KiB
Markdown

# 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