Files
it-know-how/SSR_Migration_Learnings.md
T

928 lines
26 KiB
Markdown
Executable File

# 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*