16  Monorepo Next JS + FastAPI

Example Templates:

Excellent choice! Next.js can be configured for SPA/CSR mode using output: 'export'. This gives you Next.js’s excellent developer experience while maintaining a static SPA architecture.

16.1 Monorepo Structure with Next.js + FastAPI

my-app/
│
├── frontend/                   # Next.js TypeScript SPA
│   ├── src/
│   │   ├── app/               # Next.js 13+ App Router
│   │   │   ├── layout.tsx
│   │   │   ├── page.tsx
│   │   │   ├── about/
│   │   │   │   └── page.tsx
│   │   │   └── dashboard/
│   │   │       └── page.tsx
│   │   ├── components/
│   │   │   ├── ui/
│   │   │   └── layouts/
│   │   ├── lib/               # Utilities & API client
│   │   │   ├── api/
│   │   │   │   ├── client.ts
│   │   │   │   └── endpoints.ts
│   │   │   └── utils/
│   │   ├── hooks/
│   │   ├── types/
│   │   └── styles/
│   │       └── globals.css
│   ├── public/
│   ├── next.config.js         # Configure for static export
│   ├── package.json
│   ├── tsconfig.json
│   ├── tailwind.config.ts     # If using Tailwind
│   ├── .env.local
│   └── .env.production
│
├── backend/                    # Python FastAPI (same as before)
│   ├── app/
│   │   ├── __init__.py
│   │   ├── main.py
│   │   ├── api/
│   │   │   └── v1/
│   │   │       ├── __init__.py
│   │   │       ├── endpoints/
│   │   │       └── api.py
│   │   ├── core/
│   │   ├── models/
│   │   ├── schemas/
│   │   └── services/
│   │
│   ├── static/                # ← Next.js build output goes here
│   │   └── (frontend out/ content)
│   │
│   ├── requirements.txt
│   ├── .env
│   └── pyproject.toml
│
├── scripts/
│   ├── build.sh
│   └── dev.sh
│
├── docker-compose.yml
├── Makefile
└── README.md

16.2 Next.js Configuration for SPA/CSR

// frontend/next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',           // Enable static exports
  distDir: 'out',            // Output directory
  trailingSlash: true,        // Important for static hosting
  images: {
    unoptimized: true         // Required for static export
  },
  // No basePath needed if serving from root
  // basePath: '',
  
  // For development API proxy
  async rewrites() {
    // Only apply in development
    if (process.env.NODE_ENV !== 'production') {
      return [
        {
          source: '/api/:path*',
          destination: 'http://localhost:8000/api/:path*'
        }
      ];
    }
    return [];
  }
};

module.exports = nextConfig;

16.3 TypeScript Configuration

// frontend/tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"],
      "@/components/*": ["./src/components/*"],
      "@/lib/*": ["./src/lib/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

16.4 Package.json with Scripts

// frontend/package.json
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev -p 3000",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "build:prod": "npm run build && npm run copy-to-backend",
    "copy-to-backend": "rm -rf ../backend/static/* && cp -r out/* ../backend/static/",
    "type-check": "tsc --noEmit"
  },
  "dependencies": {
    "next": "14.1.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "axios": "^1.6.5",
    "@tanstack/react-query": "^5.17.0"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "typescript": "^5",
    "eslint": "^8",
    "eslint-config-next": "14.1.0",
    "tailwindcss": "^3.4.0",
    "autoprefixer": "^10",
    "postcss": "^8"
  }
}

16.5 API Client Setup

// frontend/src/lib/api/client.ts
import axios, { AxiosInstance } from 'axios';

const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || '/api/v1';

class ApiClient {
  private client: AxiosInstance;

  constructor() {
    this.client = axios.create({
      baseURL: API_BASE_URL,
      headers: {
        'Content-Type': 'application/json',
      },
    });

    // Request interceptor
    this.client.interceptors.request.use(
      (config) => {
        // Add auth token if exists
        const token = localStorage.getItem('token');
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      },
      (error) => Promise.reject(error)
    );

    // Response interceptor
    this.client.interceptors.response.use(
      (response) => response,
      (error) => {
        if (error.response?.status === 401) {
          // Handle unauthorized
          localStorage.removeItem('token');
          window.location.href = '/login';
        }
        return Promise.reject(error);
      }
    );
  }

  get<T>(url: string) {
    return this.client.get<T>(url);
  }

  post<T>(url: string, data?: any) {
    return this.client.post<T>(url, data);
  }

  put<T>(url: string, data?: any) {
    return this.client.put<T>(url, data);
  }

  delete<T>(url: string) {
    return this.client.delete<T>(url);
  }
}

export default new ApiClient();

16.6 Root Layout (App Router)

// frontend/src/app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import '@/styles/globals.css';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: 'My App',
  description: 'Next.js SPA with FastAPI',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        {children}
      </body>
    </html>
  );
}

16.7 Enhanced FastAPI Main with Better Static Serving

# backend/app/main.py
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from fastapi.middleware.cors import CORSMiddleware
from pathlib import Path

from app.api.v1.api import api_router
from app.core.config import settings

app = FastAPI(
    title="My App API",
    version="1.0.0",
    docs_url="/api/docs",
    redoc_url="/api/redoc",
    openapi_url="/api/openapi.json"
)

# CORS for development
if settings.BACKEND_CORS_ORIGINS:
    app.add_middleware(
        CORSMiddleware,
        allow_origins=settings.BACKEND_CORS_ORIGINS,
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

# API routes
app.include_router(api_router, prefix="/api/v1")

# Serve Next.js static export
static_path = Path(__file__).parent.parent / "static"

if static_path.exists():
    # Mount static files for assets
    app.mount("/_next", StaticFiles(directory=static_path / "_next"), name="next")
    
    # Serve other static files
    @app.get("/{full_path:path}")
    async def serve_nextjs(request: Request, full_path: str):
        # Skip API routes
        if full_path.startswith("api/"):
            return {"error": "Not found"}, 404
        
        # Try to serve the exact file first
        target_file = static_path / full_path
        
        # Handle Next.js routing
        if full_path == "":
            target_file = static_path / "index.html"
        elif target_file.is_dir():
            # Try directory/index.html (Next.js with trailingSlash)
            target_file = target_file / "index.html"
        elif not target_file.exists() and not full_path.endswith(".html"):
            # Try adding .html extension
            target_file = static_path / f"{full_path}.html"
        
        # Serve file if exists
        if target_file.exists() and target_file.is_file():
            return FileResponse(target_file)
        
        # Fallback to 404.html if exists
        not_found = static_path / "404.html"
        if not_found.exists():
            return FileResponse(not_found)
        
        return {"error": "Page not found"}, 404

16.8 Makefile for Development & Build

# Makefile
.PHONY: help install dev build clean deploy

help:
    @echo "Available commands:"
    @echo "  make install       - Install dependencies"
    @echo "  make dev          - Run development servers"
    @echo "  make build        - Build frontend for production"
    @echo "  make clean        - Clean build artifacts"
    @echo "  make deploy       - Deploy to production"

install:
    cd frontend && npm install
    cd backend && pip install -r requirements.txt

dev-frontend:
    cd frontend && npm run dev

dev-backend:
    cd backend && uvicorn app.main:app --reload --port 8000

dev:
    @echo "Starting development servers..."
    @echo "Frontend: http://localhost:3000"
    @echo "Backend:  http://localhost:8000"
    @echo "API Docs: http://localhost:8000/api/docs"
    make -j 2 dev-frontend dev-backend

build:
    @echo "Building Next.js app..."
    cd frontend && npm run build
    @echo "Copying to backend static folder..."
    rm -rf backend/static/*
    cp -r frontend/out/* backend/static/
    @echo "Build complete!"

clean:
    rm -rf frontend/out
    rm -rf frontend/.next
    rm -rf backend/static/*
    find backend -type d -name "__pycache__" -exec rm -rf {} +

test:
    cd frontend && npm run type-check
    cd backend && pytest

run-prod:
    cd backend && uvicorn app.main:app --host 0.0.0.0 --port 8000

16.9 Development Workflow Diagram

Development Mode:
┌────────────────┐         ┌────────────────┐
│   Next.js Dev  │ ◄──────►│   Browser      │
│  localhost:3000│ Proxy   │                │
└────────────────┘ API     └────────────────┘
        │          Calls           │
        │                          │
        ▼                          │
┌────────────────┐                 │
│  FastAPI Dev   │ ◄───────────────┘
│  localhost:8000│   Direct API
└────────────────┘   (if needed)

Production Mode:
┌────────────────┐         ┌────────────────┐
│    FastAPI     │ ◄──────►│   Browser      │
│  localhost:8000│         │                │
│                │         └────────────────┘
│  ├─ /api       │
│  └─ /* (SPA)   │
└────────────────┘

16.10 Environment Variables

# frontend/.env.local (development)
NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1

# frontend/.env.production
NEXT_PUBLIC_API_URL=/api/v1

# backend/.env
PROJECT_NAME=my-app
VERSION=1.0.0
API_V1_STR=/api/v1
BACKEND_CORS_ORIGINS=["http://localhost:3000"]

16.11 Docker Compose for Development

# docker-compose.yml
version: '3.8'

services:
  frontend:
    build:
      context: ./frontend
      dockerfile: ../Dockerfile.frontend
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
    volumes:
      - ./frontend:/app
      - /app/node_modules
    command: npm run dev

  backend:
    build:
      context: ./backend
      dockerfile: ../Dockerfile.backend
    ports:
      - "8000:8000"
    environment:
      - PYTHONPATH=/app
    volumes:
      - ./backend:/app
    command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

16.12 Key Advantages of Next.js SPA with FastAPI

  1. Better DX: Next.js provides excellent TypeScript support and tooling
  2. File-based Routing: Automatic routing based on file structure
  3. Image Optimization: Even in static mode (with unoptimized flag)
  4. Built-in CSS Support: CSS Modules, Tailwind, CSS-in-JS
  5. Fast Refresh: Better hot reload than plain React
  6. Future Flexibility: Can easily switch to SSR/SSG if needed

This setup gives you the best of both worlds: Next.js’s superior developer experience while maintaining a pure SPA/CSR architecture that can be served as static files from FastAPI!