From b8e75eac377f3e92ec0e081be179729bae0e5cae Mon Sep 17 00:00:00 2001 From: Mathias Schneider Date: Mon, 20 Apr 2026 17:10:55 +0200 Subject: [PATCH] SSR_Migration_Learnings.md --- SSR_Migration_Learnings.md | 927 +++++++++++++++++++++++++++++++++++++ 1 file changed, 927 insertions(+) create mode 100755 SSR_Migration_Learnings.md diff --git a/SSR_Migration_Learnings.md b/SSR_Migration_Learnings.md new file mode 100755 index 0000000..fbca113 --- /dev/null +++ b/SSR_Migration_Learnings.md @@ -0,0 +1,927 @@ +# SSR Migration Learnings: From Static Site to Server-Side Rendering + +## Overview + +This document captures the challenges, learnings, and solutions from migrating a Nuxt frontend application from static site generation (SSG) to server-side rendering (SSR) mode, particularly focusing on backend API communication in a Docker/containerized environment. + +## Table of Contents + +- [Background Context](#background-context) +- [Key Challenges](#key-challenges) +- [Core Concepts](#core-concepts) +- [Detailed Problem Analysis & Solutions](#detailed-problem-analysis--solutions) +- [Architecture Patterns](#architecture-patterns) +- [Best Practices & Lessons Learned](#best-practices--lessons-learned) +- [Future Reference](#future-reference) + +--- + +## Background Context + +### Initial State (Static Site Generation) +- **Architecture**: Nuxt app built with `npm run generate` → static HTML/CSS/JS files +- **Serving**: Nginx serving static files directly +- **API Communication**: Browser → Nginx `/api/*` → Backend service +- **Configuration**: `BACKEND_HOST` set at **build time** (baked into static files) + +### Target State (Server-Side Rendering) +- **Architecture**: Nuxt app running as Node.js server (Nitro) +- **Serving**: Nginx → Nuxt SSR server (port 3000) → Backend service +- **API Communication**: Browser → Nuxt Server (internal) → Backend service +- **Configuration**: `BACKEND_HOST` set at **runtime** (environment variable) + +### Why SSR? +1. **Security**: Backend API completely hidden from browser +2. **Architecture**: Frontend server acts as secure proxy/gateway +3. **Flexibility**: Can add middleware, caching, request transformation +4. **SEO**: Server-rendered HTML (bonus benefit) + +--- + +## Key Challenges + +### Challenge 1: "Network Error" on Login +**Symptom**: Login attempts failed with generic "Network Error" + +**Root Causes**: +1. Environment variable name mismatch +2. Missing API proxy routes in SSR server +3. Client trying to access internal Docker network addresses + +### Challenge 2: Hidden Default Values +**Symptom**: Hard-coded `http://localhost:8000` defaults masked configuration errors + +**Root Causes**: +1. Fallback values in `nuxt.config.ts` +2. Configuration errors failing silently +3. Difficult to debug misconfiguration in production + +### Challenge 3: Runtime vs Build-time Configuration +**Symptom**: Environment variables not taking effect in SSR mode + +**Root Causes**: +1. Nuxt's runtime config requires specific environment variable naming +2. Confusion between build-time and runtime configuration +3. SSR hydration passing wrong values to client + +--- + +## Core Concepts + +### 1. Nuxt Runtime Configuration + +#### Build-time vs Runtime + +**Build-time** (Static Generation): +```typescript +// nuxt.config.ts +export default defineNuxtConfig({ + runtimeConfig: { + public: { + apiBase: process.env.BACKEND_HOST || 'http://localhost:8000' + } + } +}) +``` +- Value read **during `npm run build`** +- Baked into compiled JavaScript +- **Cannot change** without rebuilding + +**Runtime** (SSR Mode): +```typescript +// nuxt.config.ts +export default defineNuxtConfig({ + runtimeConfig: { + public: { + apiBase: process.env.NUXT_PUBLIC_API_BASE || process.env.BACKEND_HOST + } + } +}) +``` +- Value read **when server starts** +- Can be overridden via environment variables +- **Can change** without rebuilding (just restart container) + +#### Nuxt Environment Variable Naming Convention + +Nuxt automatically injects environment variables into runtime config if they follow specific patterns: + +| Runtime Config Path | Auto-injected Env Var | Manual Env Var | +|---------------------|----------------------|----------------| +| `runtimeConfig.public.apiBase` | `NUXT_PUBLIC_API_BASE` | `process.env.BACKEND_HOST` | +| `runtimeConfig.secret` | `NUXT_SECRET` | `process.env.SECRET` | +| `runtimeConfig.public.foo.bar` | `NUXT_PUBLIC_FOO_BAR` | `process.env.FOO_BAR` | + +**Key Insight**: Using `NUXT_PUBLIC_*` prefix enables automatic runtime override without code changes. + +### 2. SSR Request Flow + +#### Traditional SSG (Static Site) +``` +┌─────────┐ ┌───────┐ ┌─────────┐ +│ Browser │─────▶│ Nginx │─────▶│ Backend │ +└─────────┘ └───────┘ └─────────┘ + /api/* → proxy +``` +- Browser sees `/api` endpoints +- Nginx proxies to backend +- CORS required if different origins + +#### Modern SSR Pattern +``` +┌─────────┐ ┌───────┐ ┌────────────┐ ┌─────────┐ +│ Browser │─────▶│ Nginx │─────▶│ Nuxt SSR │─────▶│ Backend │ +└─────────┘ └───────┘ │ (Node.js) │ └─────────┘ + └────────────┘ + Proxy routes +``` +- Browser never sees backend +- Nuxt server makes backend calls +- No CORS needed (server-to-server) + +### 3. Client vs Server Context in SSR + +In Nuxt SSR, code runs in **two contexts**: + +#### Server Context +- Runs on Node.js server +- Has access to Docker internal network +- Can reach `http://backend:8000` +- No browser APIs (no `window`, `localStorage`, etc.) + +#### Client Context +- Runs in browser +- Only sees public internet/nginx +- **Cannot** reach `http://backend:8000` (internal Docker address) +- Has browser APIs + +**Critical Insight**: axios `baseURL` must be different for server vs client! + +--- + +## Detailed Problem Analysis & Solutions + +### Problem 1: Environment Variable Configuration + +#### Issue: NUXT_PUBLIC_API_BASE vs BACKEND_HOST + +**What Happened**: +```yaml +# docker-compose.test.yml (WRONG) +environment: + - BACKEND_HOST=http://backend:8000 # ❌ Not recognized by Nuxt at runtime +``` + +```typescript +// nuxt.config.ts +runtimeConfig: { + public: { + apiBase: process.env.BACKEND_HOST // Read at build time, not runtime! + } +} +``` + +**Why It Failed**: +1. Nuxt's runtime config injection only works with `NUXT_PUBLIC_*` prefix +2. `process.env.BACKEND_HOST` was `undefined` during Docker runtime +3. Config fell back to hardcoded default + +**Solution Strategy** (Generalized): + +When working with framework runtime configuration: +1. **Check framework conventions** for environment variable naming +2. **Support both patterns**: Framework convention + custom names +3. **Fail fast** without defaults to catch configuration errors early + +**Our Implementation**: + +```typescript +// nuxt.config.ts +runtimeConfig: { + public: { + // NUXT_PUBLIC_API_BASE auto-injected by Nuxt + // BACKEND_HOST read explicitly for local dev + apiBase: process.env.NUXT_PUBLIC_API_BASE || process.env.BACKEND_HOST + } +} +``` + +```yaml +# docker-compose.yml (PRODUCTION) +environment: + - NUXT_PUBLIC_API_BASE=http://backend:8000 # ✅ Auto-injected by Nuxt +``` + +```bash +# .env (LOCAL DEVELOPMENT) +BACKEND_HOST=http://localhost:8000 # ✅ Simpler name for developers +``` + +**Key Takeaway**: Use framework conventions in production, but provide developer-friendly alternatives for local dev. + +--- + +### Problem 2: Client-Side API Calls in SSR + +#### Issue: Browser Cannot Reach Internal Docker Network + +**What Happened**: +```typescript +// app/plugins/http.client.ts (WRONG) +const http = axios.create({ + baseURL: config.public.apiBase // "http://backend:8000" +}) +``` + +When browser tried to call APIs: +```javascript +// Browser console +POST http://backend:8000/auth/token +// ❌ DNS_PROBE_FINISHED_NXDOMAIN +// Backend is a Docker service name, not resolvable from browser! +``` + +**Root Cause Analysis**: + +SSR hydration works like this: +1. Server renders page with `config.public.apiBase = "http://backend:8000"` +2. This config is serialized into HTML: `window.__NUXT__.config.public.apiBase` +3. Browser hydrates with server config → tries to call `http://backend:8000` +4. Browser DNS lookup fails (internal Docker name) + +**Solution Strategy** (Generalized): + +When building SSR applications with different network contexts: + +1. **Identify the boundary**: Where does client context differ from server? +2. **Use context detection**: `import.meta.server` vs `import.meta.client` +3. **Configure per-context**: Different base URLs for different execution environments +4. **Create proxy routes**: Let server handle external communication + +**Our Implementation**: + +#### Part 1: Context-Aware axios Configuration + +```typescript +// app/plugins/http.client.ts +export default defineNuxtPlugin(() => { + const config = useRuntimeConfig() + + // Different baseURL based on execution context + const apiBase = import.meta.server + ? config.public.apiBase // Server: "http://backend:8000" + : '' // Client: "" (relative URLs) + + const http = axios.create({ + baseURL: apiBase, + withCredentials: false, + }) + + return { provide: { http } } +}) +``` + +#### Part 2: Server-Side API Proxy + +```typescript +// server/routes/auth/[...].ts +// Catch-all route: matches /auth/*, /auth/token, /auth/me, etc. +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig() + + // event.path = "/auth/token" + // backendUrl = "http://backend:8000/auth/token" + const backendUrl = `${config.public.apiBase}${event.path}` + + // Proxy request to backend + return proxyRequest(event, backendUrl) +}) +``` + +**Request Flow**: + +1. **Browser**: `axios.post('/auth/token', data)` → relative URL +2. **Nuxt Server**: Catches `/auth/token` via `server/routes/auth/[...].ts` +3. **Proxy**: Forwards to `http://backend:8000/auth/token` (internal Docker network) +4. **Backend**: Processes request, returns response +5. **Nuxt Server**: Returns response to browser +6. **Browser**: Receives response as if it came from same origin + +**Key Insight**: In SSR, the client should **never** make external API calls directly. All external communication goes through the SSR server. + +--- + +### Problem 3: Hardcoded Defaults Hiding Configuration Errors + +#### Issue: Silent Failures + +**What Happened**: +```typescript +// nuxt.config.ts (WRONG) +runtimeConfig: { + public: { + apiBase: process.env.BACKEND_HOST || 'http://localhost:8000' // ❌ Silent fallback + } +} +``` + +**Why This Is Problematic**: + +1. **Local dev masking**: Forgot to set `BACKEND_HOST`? No problem, silently uses `localhost:8000` +2. **Production confusion**: Docker env var not set? Silently uses `localhost:8000` (wrong!) +3. **Debugging nightmare**: App appears to work but calls wrong backend +4. **No visibility**: Developers unaware of misconfiguration + +**Real-World Scenario**: +```bash +# Developer forgot to copy .env file +$ npm run dev +# App starts fine, uses hardcoded localhost:8000 +# Works... but by accident! + +# Later in production: +$ docker-compose up +# NUXT_PUBLIC_API_BASE not set in docker-compose +# Falls back to localhost:8000 +# Frontend tries to call localhost inside container → fails +# But error is unclear: "Network Error" +``` + +**Solution Strategy** (Generalized): + +**Fail Fast Principle**: +1. **Explicit over implicit**: Require explicit configuration +2. **Fail loudly**: Missing config should cause obvious errors +3. **Clear error messages**: Tell user exactly what's missing +4. **Documentation**: Example files with clear instructions + +**Our Implementation**: + +#### Remove Defaults +```typescript +// nuxt.config.ts (CORRECT) +runtimeConfig: { + public: { + apiBase: process.env.NUXT_PUBLIC_API_BASE || process.env.BACKEND_HOST + // No || 'http://localhost:8000' fallback! + } +} +``` + +Now if neither env var is set: +```javascript +config.public.apiBase = undefined +``` + +When axios tries to use it: +```javascript +axios.create({ baseURL: undefined }) +// Makes requests to relative URLs +// If proxy not configured: 404 errors (obvious!) +``` + +#### Provide Example Configuration +```bash +# .env.example +# REQUIRED: Backend API base URL +# +# For local development: +# BACKEND_HOST=http://localhost:8000 +# +# IMPORTANT: You must create a .env file: +# cp .env.example .env +# +BACKEND_HOST=http://localhost:8000 +``` + +#### Update Documentation +```markdown +# README.md + +**IMPORTANT:** `BACKEND_HOST` is **required** and must be set via `.env` file. +There is no default value. + +**Local development:** +Create a `.env` file from the example: +```bash +cp .env.example .env +``` +``` + +**Benefits**: +- ✅ Configuration errors obvious immediately +- ✅ Developers forced to understand configuration +- ✅ Production deployments fail fast if misconfigured +- ✅ Clear documentation guides proper setup + +**Key Takeaway**: Defaults are tempting but dangerous. Explicit configuration prevents silent failures. + +--- + +## Architecture Patterns + +### Pattern 1: SSR API Proxy Gateway + +**Use Case**: Frontend needs to call backend API, but backend should be hidden from browser + +**Implementation**: + +```typescript +// server/routes/[api]/[...].ts +// Generic catch-all for any API prefix + +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig() + + // Extract path: /api/users/123 → /users/123 + const path = event.path.replace(/^\/api/, '') + + // Build backend URL + const backendUrl = `${config.public.apiBase}${path}` + + // Forward everything: method, headers, body, query params + return proxyRequest(event, backendUrl) +}) +``` + +**Benefits**: +- 🔒 Backend URL completely hidden from browser +- 🛡️ Can add authentication, rate limiting, caching in proxy +- 🔧 Easy to add request/response transformation +- 📊 Centralized logging of all API calls + +**Variations**: + +1. **Multiple backends**: +```typescript +// Route different prefixes to different backends +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig() + + if (event.path.startsWith('/api/auth')) { + return proxyRequest(event, `${config.authService}${event.path}`) + } else if (event.path.startsWith('/api/data')) { + return proxyRequest(event, `${config.dataService}${event.path}`) + } +}) +``` + +2. **With authentication injection**: +```typescript +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig() + const backendUrl = `${config.public.apiBase}${event.path}` + + // Add server-side auth token + return proxyRequest(event, backendUrl, { + headers: { + 'Authorization': `Bearer ${config.serverApiToken}` + } + }) +}) +``` + +### Pattern 2: Context-Aware Plugin Configuration + +**Use Case**: Plugin behavior differs between server and client contexts + +**Implementation**: + +```typescript +// plugins/http.client.ts +export default defineNuxtPlugin(() => { + const config = useRuntimeConfig() + + // Server context: full backend URL (internal network) + // Client context: empty (relative URLs to same origin) + const baseURL = import.meta.server + ? config.public.apiBase + : '' + + const http = axios.create({ baseURL }) + + // Client-only: add auth from localStorage + if (import.meta.client) { + http.interceptors.request.use((req) => { + const token = localStorage.getItem('access_token') + if (token) { + req.headers.Authorization = `Bearer ${token}` + } + return req + }) + } + + return { provide: { http } } +}) +``` + +**Key Points**: +- Use `import.meta.server` / `import.meta.client` for context detection +- Server context: can access Docker internal network +- Client context: browser APIs available (localStorage, etc.) + +### Pattern 3: Environment Variable Layering + +**Use Case**: Support multiple deployment environments with different naming conventions + +**Strategy**: + +```typescript +// nuxt.config.ts +export default defineNuxtConfig({ + runtimeConfig: { + public: { + // Priority order (first defined wins): + // 1. NUXT_PUBLIC_API_BASE (Docker production, Nuxt convention) + // 2. BACKEND_HOST (local dev, CI/CD, simpler name) + // 3. undefined (fail fast) + apiBase: process.env.NUXT_PUBLIC_API_BASE || process.env.BACKEND_HOST + } + } +}) +``` + +**Usage Patterns**: + +| Environment | Variable Used | Reason | +|-------------|---------------|--------| +| Local Dev | `BACKEND_HOST=http://localhost:8000` | Simpler for developers | +| Docker Compose | `NUXT_PUBLIC_API_BASE=http://backend:8000` | Nuxt convention, runtime override | +| CI/CD Build | `BACKEND_HOST=http://mock:8000` | Test environment | +| Production | `NUXT_PUBLIC_API_BASE=http://api-service:8000` | Kubernetes service name | + +--- + +## Best Practices & Lessons Learned + +### 1. Configuration Management + +#### ✅ Do's + +**Always fail fast without defaults** +```typescript +// Good +apiBase: process.env.BACKEND_HOST // undefined if not set + +// Bad +apiBase: process.env.BACKEND_HOST || 'http://localhost:8000' // Silent fallback +``` + +**Provide clear example files** +```bash +# .env.example with EXTENSIVE comments +# Explain why each variable is needed +# Show example values for different environments +BACKEND_HOST=http://localhost:8000 # Local dev +# BACKEND_HOST=http://backend:8000 # Docker +``` + +**Document required vs optional** +```markdown +## Required Environment Variables + +- `BACKEND_HOST`: Backend API base URL (REQUIRED) +- `DATABASE_URL`: Database connection string (REQUIRED) + +## Optional Environment Variables + +- `LOG_LEVEL`: Logging verbosity (default: 'info') +``` + +#### ❌ Don'ts + +**Don't hide configuration in code** +```typescript +// Bad: Magic values scattered in code +const apiUrl = 'http://localhost:8000' +``` + +**Don't use different names for same concept** +```typescript +// Bad: Confusing naming +BACKEND_URL=... +API_ENDPOINT=... +SERVER_ADDRESS=... + +// Good: Consistent naming +BACKEND_HOST=... +``` + +### 2. SSR Development + +#### ✅ Do's + +**Always check execution context** +```typescript +// Server-only code +if (import.meta.server) { + // Database queries, file system access +} + +// Client-only code +if (import.meta.client) { + // localStorage, window APIs, browser events +} +``` + +**Use relative URLs in client** +```typescript +// Good: Let SSR server handle routing +axios.get('/api/users') + +// Bad: Absolute URLs bypass SSR benefits +axios.get('http://backend:8000/api/users') +``` + +**Create comprehensive proxy routes** +```typescript +// server/routes/auth/[...].ts - Handles /auth/* +// server/routes/api/[...].ts - Handles /api/* +// server/routes/data/[...].ts - Handles /data/* +``` + +#### ❌ Don'ts + +**Don't access browser APIs in server context** +```typescript +// Bad: Crashes on server +const token = localStorage.getItem('token') + +// Good: Guard with context check +const token = import.meta.client + ? localStorage.getItem('token') + : null +``` + +**Don't expose internal URLs to client** +```typescript +// Bad: Client gets internal Docker URL +const config = { + apiBase: 'http://backend:8000' // Browser can't resolve this! +} + +// Good: Client uses relative URLs +const apiBase = import.meta.server + ? 'http://backend:8000' + : '' +``` + +### 3. Docker & Containerization + +#### ✅ Do's + +**Use service names for internal communication** +```yaml +# docker-compose.yml +services: + frontend: + environment: + - NUXT_PUBLIC_API_BASE=http://backend:8000 # ✅ Service name + backend: + # Backend service +``` + +**Separate build-time and runtime config** +```dockerfile +# Build stage - no environment variables needed +FROM node:25-alpine AS build +COPY . . +RUN npm run build + +# Runtime stage - environment variables matter here +FROM node:25-alpine AS runtime +ENV NODE_ENV=production +CMD ["node", "server/index.mjs"] +``` + +**Expose only what's necessary** +```yaml +services: + backend: + expose: + - "8000" # ✅ Only internal Docker network + # NO ports: section - not accessible from host + + frontend: + expose: + - "3000" # ✅ Only to nginx + # NO ports: section + + nginx: + ports: + - "80:80" # ✅ Only nginx exposed to host +``` + +#### ❌ Don'ts + +**Don't expose internal services to host** +```yaml +# Bad: Backend directly accessible +backend: + ports: + - "8000:8000" # ❌ Defeats purpose of SSR proxy +``` + +**Don't use localhost in containers** +```yaml +# Bad: localhost inside container is the container itself +environment: + - BACKEND_HOST=http://localhost:8000 # ❌ Wrong! + +# Good: Use service names +environment: + - BACKEND_HOST=http://backend:8000 # ✅ Correct +``` + +### 4. Debugging SSR Issues + +#### Diagnostic Checklist + +When facing "Network Error" or similar issues: + +1. **Check environment variables** +```bash +docker exec container_name env | grep BACKEND +docker exec container_name env | grep NUXT +``` + +2. **Verify network connectivity** +```bash +# From frontend to backend +docker exec frontend_container wget -O- http://backend:8000/health + +# Check if backend is running +docker logs backend_container +``` + +3. **Check Nuxt runtime config** +```bash +# Add temporary logging in nuxt.config.ts +console.log('API Base:', process.env.NUXT_PUBLIC_API_BASE) +``` + +4. **Inspect rendered HTML** +```bash +curl http://localhost:8080/login | grep apiBase +# Look for: window.__NUXT__.config +``` + +5. **Check axios requests in browser DevTools** +``` +Network tab → Look at request URLs +- Relative URLs (e.g., /auth/token) → ✅ Good +- Internal URLs (e.g., http://backend:8000) → ❌ Problem +``` + +6. **Verify proxy routes are built** +```bash +# Check Nuxt build output +ls -la .output/server/chunks/routes/ +# Should see auth/_..._.mjs or similar +``` + +#### Common Error Patterns + +| Error | Likely Cause | Solution | +|-------|--------------|----------| +| `DNS_PROBE_FINISHED_NXDOMAIN` | Client trying to reach internal Docker name | Check axios baseURL, add proxy route | +| `Network Error` (generic) | Multiple possible causes | Check all diagnostics above | +| `ECONNREFUSED` | Service not running or wrong port | Verify backend is up, check ports | +| `Not authenticated` (401) | Proxy working, auth issue | Check credentials, token handling | +| `Cannot find module` | Build issue | Rebuild without cache | + +--- + +## Future Reference + +### Quick Decision Tree + +```mermaid +graph TD + A[Need backend API in frontend?] -->|Yes| B[SSR or SSG?] + B -->|SSR| C[Use proxy pattern] + B -->|SSG| D[Direct calls with CORS] + C --> E[Create server/routes proxy] + C --> F[Use empty baseURL in client] + C --> G[Use internal URL in server] + D --> H[Configure CORS on backend] + D --> I[Use full backend URL everywhere] +``` + +### SSR vs SSG Decision Matrix + +| Factor | SSR | SSG | +|--------|-----|-----| +| **Backend Hidden** | ✅ Yes | ❌ No (exposed to browser) | +| **Server Resources** | Higher (Node.js) | Lower (static files) | +| **Configuration** | Runtime env vars | Build-time vars | +| **SEO** | ✅ Excellent | ✅ Excellent | +| **Dynamic Content** | ✅ Real-time | ❌ Build-time only | +| **Deploy Complexity** | Medium (Node.js server) | Low (just files) | +| **Security** | ✅ Backend completely hidden | ⚠️ Backend URL exposed | +| **Caching** | Complex (server-side) | Simple (CDN) | + +**Choose SSR when**: +- Backend must be hidden from browser +- Need server-side request transformation +- Real-time content crucial +- Middleware/authentication needed + +**Choose SSG when**: +- Content mostly static +- Backend can be public (with CORS) +- Want simplest deployment +- Need maximum performance (CDN) + +### Environment Variable Naming Conventions + +| Framework | Public Runtime Config | Private Runtime Config | +|-----------|----------------------|------------------------| +| **Nuxt 3/4** | `NUXT_PUBLIC_*` | `NUXT_*` | +| **Next.js** | `NEXT_PUBLIC_*` | (none - server-side only) | +| **Vite** | `VITE_*` | (none - build-time only) | +| **Create React App** | `REACT_APP_*` | (none - build-time only) | + +**Key Point**: Most frameworks require prefixes for public runtime config auto-injection. + +### File Structure Reference + +``` +project/ +├── app/ +│ ├── plugins/ +│ │ └── http.client.ts # Context-aware axios setup +│ ├── composables/ +│ │ └── useAuth.ts # Uses $http from plugin +│ └── pages/ +│ └── login.vue # Makes API calls +├── server/ +│ └── routes/ +│ └── auth/ +│ └── [...].ts # Proxy for /auth/* endpoints +├── .env.example # Template with docs +├── .env # Local config (gitignored) +├── nuxt.config.ts # Runtime config setup +├── docker-compose.yml # Production config +└── tests/deployment/ + ├── docker-compose.test.yml # Test deployment + └── nginx.test.conf # Nginx config +``` + +--- + +## Summary + +### Critical Success Factors + +1. **Understand SSR dual execution contexts** (server vs client) +2. **Use framework conventions** for environment variables +3. **Fail fast** without defaults - explicit configuration +4. **Create proxy routes** for all backend APIs +5. **Context-aware base URLs** (full URL server, empty client) +6. **Document everything** - future you will thank you + +### Final Checklist for SSR Migration + +- [ ] Remove all hardcoded defaults from config +- [ ] Support both framework and custom env var names +- [ ] Create server proxy routes for all API endpoints +- [ ] Configure axios baseURL based on execution context +- [ ] Update docker-compose with proper env vars +- [ ] Test both client-side and server-side API calls +- [ ] Document required environment variables +- [ ] Provide .env.example with clear instructions +- [ ] Update README with SSR-specific instructions +- [ ] Verify backend is not exposed to browser (network tab) + +### Key Learnings + +> **The most important insight**: In SSR, the frontend runs in TWO places (server and browser), and each place has different network access. Configuration must account for both contexts. + +> **The second most important insight**: Explicit configuration with no defaults prevents entire classes of bugs. The inconvenience of setup is far outweighed by the clarity and reliability gained. + +> **The third most important insight**: Framework conventions exist for good reasons. Use `NUXT_PUBLIC_*` for Nuxt, `NEXT_PUBLIC_*` for Next.js, etc. Don't fight the framework - learn its patterns. + +--- + +## Additional Resources + +### Documentation Links + +- [Nuxt 3 Runtime Config](https://nuxt.com/docs/guide/going-further/runtime-config) +- [Nuxt 3 Server Routes](https://nuxt.com/docs/guide/directory-structure/server) +- [h3 Event Handlers](https://h3.unjs.io/guide/event-handler) +- [Docker Compose Environment Variables](https://docs.docker.com/compose/environment-variables/) + +### Related Patterns + +- API Gateway Pattern +- Backend for Frontend (BFF) +- Proxy Pattern +- Configuration as Code + +--- + +*Document created: 2026-04-16* +*Last updated: 2026-04-16* +*Context: SSR migration from static site to server-side rendering*