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>
This commit is contained in:
Michael Mainguy 2025-12-20 12:32:49 -06:00
parent 1ccdab2780
commit 1152ab0d0c
8 changed files with 1663 additions and 56 deletions

403
EXPRESS_API_PLAN.md Normal file
View File

@ -0,0 +1,403 @@
# 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

167
NEXT_MIGRATION_PLAN.md Normal file
View File

@ -0,0 +1,167 @@
# Vite to Next.js Migration Plan
## Goal
Migrate from Vite to Next.js App Router to get proper API route support, with minimal changes to existing code.
## Configuration
- **Router**: App Router with `'use client'` on all pages
- **Rendering**: CSR only (no SSR) - simplifies migration since BabylonJS can't SSR
- **API Routes**: Claude API now, structured for future expansion
- **External Proxies**: Keep sync/create-db/images as Next.js rewrites to deepdiagram.com
---
## Phase 1: Setup (No Breaking Changes)
### 1.1 Install Next.js
```bash
npm install next
```
### 1.2 Create `next.config.js`
```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{ source: '/sync/:path*', destination: 'https://www.deepdiagram.com/sync/:path*' },
{ source: '/create-db', destination: 'https://www.deepdiagram.com/create-db' },
{ source: '/api/images', destination: 'https://www.deepdiagram.com/api/images' },
];
},
webpack: (config, { isServer }) => {
config.experiments = { ...config.experiments, asyncWebAssembly: true };
return config;
},
};
module.exports = nextConfig;
```
### 1.3 Update `tsconfig.json`
Add path alias:
```json
"baseUrl": ".",
"paths": { "@/*": ["./*"] }
```
---
## Phase 2: Create New Files
### 2.1 `src/react/providers.tsx` (extract from webApp.tsx)
- Move Auth0Provider and FeatureProvider wrapping here
- Add `'use client'` directive
- Handle window/document checks for SSR safety
### 2.2 `app/layout.tsx`
- Root layout with html/body tags
- Metadata (title, favicon from current index.html)
- Import global CSS
### 2.3 `app/globals.css`
```css
@import '../src/react/styles.css';
@import '@mantine/core/styles.css';
```
### 2.4 `app/api/claude/[...path]/route.ts`
- POST handler that proxies to api.anthropic.com
- Injects `ANTHROPIC_API_KEY` from env
- Adds `x-api-key` and `anthropic-version` headers
### 2.5 Page files (all with `'use client'`)
| Route | File | Component |
|-------|------|-----------|
| `/` | `app/page.tsx` | About |
| `/documentation` | `app/documentation/page.tsx` | Documentation |
| `/examples` | `app/examples/page.tsx` | Examples |
| `/pricing` | `app/pricing/page.tsx` | Pricing |
| `/db/[visibility]/[db]` | `app/db/[visibility]/[db]/page.tsx` | VrExperience |
| 404 | `app/not-found.tsx` | NotFound |
### 2.6 `src/react/components/ProtectedPage.tsx`
- Next.js version of route protection
- Uses `useRouter` from `next/navigation` for redirects
---
## Phase 3: Modify Existing Files
### 3.1 `src/react/pages/vrExperience.tsx`
**Changes:**
- Remove `useParams()` from react-router-dom
- Accept `visibility` and `db` as props instead
- Replace `useNavigate()` with `useRouter()` from `next/navigation`
### 3.2 `src/react/pageHeader.tsx`
**Changes:**
- Replace `import {Link} from "react-router-dom"` with `import Link from "next/link"`
- Change `to={item.href}` to `href={item.href}` on Link components
### 3.3 `src/react/marketing/about.tsx`
**Changes:**
- Replace `useNavigate()` with `useRouter()` from `next/navigation`
- Change `navigate('/path')` to `router.push('/path')`
### 3.4 `package.json`
```json
"scripts": {
"dev": "next dev -p 3001",
"build": "node versionBump.js && next build",
"start": "next start -p 3001",
"test": "vitest",
"socket": "node server/server.js",
"serverBuild": "cd server && tsc"
}
```
---
## Phase 4: Delete Old Files
| File | Reason |
|------|--------|
| `vite.config.ts` | Replaced by next.config.js |
| `index.html` | Next.js generates HTML |
| `src/webApp.ts` | Entry point no longer needed |
| `src/react/webRouter.tsx` | Replaced by app/ routing |
| `src/react/webApp.tsx` | Logic moved to providers.tsx |
| `src/react/components/ProtectedRoute.tsx` | Replaced by ProtectedPage.tsx |
---
## Critical Files to Modify
- `src/react/pages/vrExperience.tsx` - useParams -> props
- `src/react/pageHeader.tsx` - react-router Link -> Next.js Link
- `src/react/marketing/about.tsx` - useNavigate -> useRouter
- `src/react/webApp.tsx` - extract to providers.tsx
- `package.json` - scripts update
- `tsconfig.json` - path aliases
---
## Migration Order
1. Install next, create next.config.js
2. Update tsconfig.json
3. Create app/globals.css
4. Create src/react/providers.tsx
5. Create app/layout.tsx
6. Create app/api/claude/[...path]/route.ts
7. Create src/react/components/ProtectedPage.tsx
8. Modify vrExperience.tsx (accept props)
9. Create all app/*/page.tsx files
10. Modify pageHeader.tsx (Next.js Link)
11. Modify about.tsx (useRouter)
12. Update package.json scripts
13. Delete old files (vite.config.ts, index.html, webApp.ts, webRouter.tsx, webApp.tsx)
14. Test all routes
---
## Notes
- **Havok WASM**: Move `HavokPhysics.wasm` to `public/` folder
- **react-router-dom**: Can be removed from dependencies after migration
- **vite devDependencies**: Can be removed (vite, vite-plugin-cp)

1006
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,12 +8,12 @@
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
"scripts": { "scripts": {
"dev": "vite", "dev": "node server.js",
"test": "vitest", "test": "vitest",
"build": "node versionBump.js && vite build", "build": "node versionBump.js && vite build",
"preview": "vite preview", "start": "NODE_ENV=production node server.js",
"start:api": "API_ONLY=true node server.js",
"socket": "node server/server.js", "socket": "node server/server.js",
"serve": "node server.js",
"serverBuild": "cd server && tsc", "serverBuild": "cd server && tsc",
"havok": "cp ./node_modules/@babylonjs/havok/lib/esm/HavokPhysics.wasm ./node_modules/.vite/deps" "havok": "cp ./node_modules/@babylonjs/havok/lib/esm/HavokPhysics.wasm ./node_modules/.vite/deps"
}, },
@ -27,38 +27,42 @@
"@babylonjs/materials": "^8.16.2", "@babylonjs/materials": "^8.16.2",
"@babylonjs/serializers": "^8.16.2", "@babylonjs/serializers": "^8.16.2",
"@emotion/react": "^11.13.0", "@emotion/react": "^11.13.0",
"@giphy/js-fetch-api": "^5.6.0",
"@giphy/react-components": "^9.6.0",
"@mantine/core": "^7.17.8", "@mantine/core": "^7.17.8",
"@mantine/form": "^7.17.8", "@mantine/form": "^7.17.8",
"@mantine/hooks": "^7.17.8", "@mantine/hooks": "^7.17.8",
"@giphy/react-components": "^9.6.0",
"@giphy/js-fetch-api": "^5.6.0",
"@maptiler/client": "1.8.1", "@maptiler/client": "1.8.1",
"@picovoice/cobra-web": "^2.0.3", "@picovoice/cobra-web": "^2.0.3",
"@picovoice/eagle-web": "^1.0.0", "@picovoice/eagle-web": "^1.0.0",
"@picovoice/web-voice-processor": "^4.0.9", "@picovoice/web-voice-processor": "^4.0.9",
"@tabler/icons-react": "^3.14.0",
"@types/node": "^18.14.0", "@types/node": "^18.14.0",
"@types/react": "^18.2.72", "@types/react": "^18.2.72",
"@types/react-dom": "^18.2.22", "@types/react-dom": "^18.2.22",
"axios": "^1.10.0", "axios": "^1.10.0",
"canvas-hypertxt": "1.0.3", "canvas-hypertxt": "1.0.3",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"events": "^3.3.0", "events": "^3.3.0",
"express": "^5.2.1",
"hash-wasm": "4.11.0", "hash-wasm": "4.11.0",
"hls.js": "^1.1.4", "hls.js": "^1.1.4",
"js-crypto-aes": "1.0.6", "js-crypto-aes": "1.0.6",
"loglevel": "^1.9.1", "loglevel": "^1.9.1",
"meaningful-string": "^1.4.0", "meaningful-string": "^1.4.0",
"peer-lite": "2.0.2", "peer-lite": "2.0.2",
"use-pouchdb": "^2.0.2",
"pouchdb": "^8.0.1", "pouchdb": "^8.0.1",
"pouchdb-find": "^8.0.1", "pouchdb-find": "^8.0.1",
"query-string": "^8.1.0", "query-string": "^8.1.0",
"react-router-dom": "^6.26.1", "react-router-dom": "^6.26.1",
"@tabler/icons-react": "^3.14.0",
"recordrtc": "^5.6.0", "recordrtc": "^5.6.0",
"rfc4648": "^1.5.3", "rfc4648": "^1.5.3",
"round": "^2.0.1", "round": "^2.0.1",
"uint8-to-b64": "^1.0.2", "uint8-to-b64": "^1.0.2",
"use-pouchdb": "^2.0.2",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vite-express": "^0.21.1",
"websocket": "^1.0.34", "websocket": "^1.0.34",
"websocket-ts": "^2.1.5" "websocket-ts": "^2.1.5"
}, },
@ -69,4 +73,4 @@
"vite-plugin-cp": "^1.0.0", "vite-plugin-cp": "^1.0.0",
"vitest": "^1.4.0" "vitest": "^1.4.0"
} }
} }

View File

@ -1,13 +1,41 @@
import express from "express"; import express from "express";
import ViteExpress from "vite-express"; import ViteExpress from "vite-express";
import cors from "cors";
import dotenv from "dotenv"; import dotenv from "dotenv";
import expressProxy from "express-http-proxy"; import apiRoutes from "./server/api/index.js";
// Load .env.local first, then fall back to .env
dotenv.config({ path: '.env.local' });
dotenv.config(); dotenv.config();
const app = express(); const app = express();
app.use("/api", expressProxy("local.immersiveidea.com"));
ViteExpress.listen(app, process.env.PORT || 3001, () => console.log("Server is listening...")); // 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}`);
});
}

38
server/api/claude.js Normal file
View File

@ -0,0 +1,38 @@
import { Router } from "express";
const router = Router();
const ANTHROPIC_API_URL = "https://api.anthropic.com";
// Express 5 uses named parameters for wildcards
router.post("/*path", 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)
// Express 5 returns path segments as an array
const pathParam = req.params.path;
const path = "/" + (Array.isArray(pathParam) ? pathParam.join("/") : pathParam || "");
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.message);
res.status(500).json({ error: "Failed to proxy request to Claude API" });
}
});
export default router;

14
server/api/index.js Normal file
View File

@ -0,0 +1,14 @@
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;

View File

@ -39,23 +39,8 @@ export default defineConfig(({mode}) => {
'^/api/images': { '^/api/images': {
target: 'https://www.deepdiagram.com/', target: 'https://www.deepdiagram.com/',
changeOrigin: true, changeOrigin: true,
},
'^/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;
console.log(` API KEY: ${apiKey}`);
if (apiKey) {
proxyReq.setHeader('x-api-key', apiKey);
proxyReq.setHeader('anthropic-version', '2023-06-01');
}
});
}
} }
// /api/claude is now handled by Express server
} }
}, },
@ -73,22 +58,8 @@ export default defineConfig(({mode}) => {
'^/api/images': { '^/api/images': {
target: 'https://www.deepdiagram.com/', target: 'https://www.deepdiagram.com/',
changeOrigin: true, changeOrigin: true,
},
'^/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;
console.log(` API KEY: ${apiKey}`);
if (apiKey) {
proxyReq.setHeader('x-api-key', apiKey);
proxyReq.setHeader('anthropic-version', '2023-06-01');
}
});
}
} }
// /api/claude is now handled by Express server
} }
}, },
base: "/" base: "/"