feat: implement runtime configuration with API proxy pattern

Replace build-time NEXT_PUBLIC_* environment variables with server-side
runtime configuration. This allows changing the Pastel API URL without
rebuilding the Docker image.

**Changes:**
- Add Next.js API proxy route at /api/pastel/[...path] for server-side proxying
- Update API client to use proxy endpoint instead of direct API URL
- Replace NEXT_PUBLIC_API_URL with server-side PASTEL_API_URL
- Remove build arguments from Dockerfile (no longer needed)
- Simplify docker-compose.yml to use runtime environment variables only
- Update all .env files to reflect new configuration approach
- Add comprehensive DOCKER.md documentation

**Benefits:**
- No rebuild required to change API URL
- Same image works across all environments (dev/staging/prod)
- Better security (API URL not exposed in client bundle)
- Simpler deployment and configuration management

**Migration:**
Old: NEXT_PUBLIC_API_URL (build-time, embedded in bundle)
New: PASTEL_API_URL (runtime, read by server proxy)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
valknarness
2025-11-07 16:05:55 +01:00
parent 8fa5511660
commit 27ff7f7ffd
6 changed files with 372 additions and 22 deletions

View File

@@ -0,0 +1,117 @@
import { NextRequest, NextResponse } from 'next/server';
/**
* API Proxy Route for Pastel API
*
* This proxy allows runtime configuration of the Pastel API URL
* without rebuilding the Docker image. The API URL is read from
* the server-side environment variable at runtime.
*
* Client requests go to: /api/pastel/*
* Proxied to: PASTEL_API_URL/*
*/
const PASTEL_API_URL = process.env.PASTEL_API_URL || 'http://localhost:3001';
export async function GET(
request: NextRequest,
{ params }: { params: { path: string[] } }
) {
return proxyRequest(request, params.path, 'GET');
}
export async function POST(
request: NextRequest,
{ params }: { params: { path: string[] } }
) {
return proxyRequest(request, params.path, 'POST');
}
export async function PUT(
request: NextRequest,
{ params }: { params: { path: string[] } }
) {
return proxyRequest(request, params.path, 'PUT');
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { path: string[] } }
) {
return proxyRequest(request, params.path, 'DELETE');
}
export async function PATCH(
request: NextRequest,
{ params }: { params: { path: string[] } }
) {
return proxyRequest(request, params.path, 'PATCH');
}
async function proxyRequest(
request: NextRequest,
pathSegments: string[],
method: string
) {
try {
const path = pathSegments.join('/');
const targetUrl = `${PASTEL_API_URL}/api/v1/${path}`;
// Get request body if present
const body = method !== 'GET' && method !== 'DELETE'
? await request.text()
: undefined;
// Forward the request to the Pastel API
const response = await fetch(targetUrl, {
method,
headers: {
'Content-Type': 'application/json',
// Forward relevant headers
...(request.headers.get('accept') && {
'Accept': request.headers.get('accept')!
}),
},
body,
});
// Get response data
const data = await response.text();
// Return response with same status and headers
return new NextResponse(data, {
status: response.status,
headers: {
'Content-Type': response.headers.get('content-type') || 'application/json',
// Add CORS headers if needed
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
} catch (error) {
console.error('Proxy error:', error);
return NextResponse.json(
{
success: false,
error: {
code: 'PROXY_ERROR',
message: error instanceof Error ? error.message : 'Failed to proxy request',
},
},
{ status: 500 }
);
}
}
// Handle OPTIONS requests for CORS
export async function OPTIONS() {
return new NextResponse(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}