928 lines
26 KiB
Markdown
Executable File
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*
|