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 [];
};
}
.exports = nextConfig; module
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({
: API_BASE_URL,
baseURL: {
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) {
.headers.Authorization = `Bearer ${token}`;
config
}return config;
,
}=> Promise.reject(error)
(error) ;
)
// Response interceptor
this.client.interceptors.response.use(
=> response,
(response) => {
(error) if (error.response?.status === 401) {
// Handle unauthorized
.removeItem('token');
localStoragewindow.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
= FastAPI(
app ="My App API",
title="1.0.0",
version="/api/docs",
docs_url="/api/redoc",
redoc_url="/api/openapi.json"
openapi_url
)
# CORS for development
if settings.BACKEND_CORS_ORIGINS:
app.add_middleware(
CORSMiddleware,=settings.BACKEND_CORS_ORIGINS,
allow_origins=True,
allow_credentials=["*"],
allow_methods=["*"],
allow_headers
)
# API routes
="/api/v1")
app.include_router(api_router, prefix
# Serve Next.js static export
= Path(__file__).parent.parent / "static"
static_path
if static_path.exists():
# Mount static files for assets
"/_next", StaticFiles(directory=static_path / "_next"), name="next")
app.mount(
# 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
= static_path / full_path
target_file
# Handle Next.js routing
if full_path == "":
= static_path / "index.html"
target_file elif target_file.is_dir():
# Try directory/index.html (Next.js with trailingSlash)
= target_file / "index.html"
target_file elif not target_file.exists() and not full_path.endswith(".html"):
# Try adding .html extension
= static_path / f"{full_path}.html"
target_file
# Serve file if exists
if target_file.exists() and target_file.is_file():
return FileResponse(target_file)
# Fallback to 404.html if exists
= static_path / "404.html"
not_found 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/*"__pycache__" -exec rm -rf {} +
find backend -type d -name
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
- Better DX: Next.js provides excellent TypeScript support and tooling
- File-based Routing: Automatic routing based on file structure
- Image Optimization: Even in static mode (with unoptimized flag)
- Built-in CSS Support: CSS Modules, Tailwind, CSS-in-JS
- Fast Refresh: Better hot reload than plain React
- 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!