- 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>
404 lines
10 KiB
Markdown
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
|