17  Monorepo React (vite) + DotNet

I’ll help you design a mono-repo structure for a React SPA frontend with a .NET Web API backend. This is a great architecture choice where your .NET API can serve both the static SPA files and API endpoints.

17.2 Key Configuration Files

17.2.1 1. Root package.json (Orchestration)

{
  "name": "my-app-monorepo",
  "private": true,
  "scripts": {
    "dev": "concurrently \"npm run dev:frontend\" \"npm run dev:backend\"",
    "dev:frontend": "cd frontend && npm run dev",
    "dev:backend": "cd backend/MyApp.Api && dotnet watch run",
    "build": "npm run build:frontend && npm run build:backend",
    "build:frontend": "cd frontend && npm run build && npm run copy-to-backend",
    "build:backend": "cd backend && dotnet build",
    "clean": "rm -rf frontend/dist backend/MyApp.Api/wwwroot/*"
  },
  "devDependencies": {
    "concurrently": "^8.2.0"
  }
}

17.2.2 2. Frontend package.json (Add copy script)

{
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "copy-to-backend": "rm -rf ../backend/MyApp.Api/wwwroot/* && cp -r dist/* ../backend/MyApp.Api/wwwroot/"
  }
}

17.2.3 3. Backend Program.cs (Serve SPA)

var builder = WebApplication.CreateBuilder(args);

// Add services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// Serve static files (React SPA)
app.UseDefaultFiles();
app.UseStaticFiles();

// API routes
app.MapControllers();

// Fallback to index.html for SPA routing
app.MapFallbackToFile("index.html");

app.Run();

17.3 Development Workflow

┌─────────────────────────────────────────────────────  ┐
│                  Development Mode                     │
├───────────────────────────────────────────────────────┤
│                                                       │
│  ┌──────────────┐          ┌──────────────┐           │
│  │   Frontend   │ :3000    │   Backend    │ :5000     │
│  │   (Vite)     │ ──────>  │  (.NET API)  │           │
│  └──────────────┘  proxy   └──────────────┘           │
│                                                       │
└───────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│                  Production Build                   │
├─────────────────────────────────────────────────────┤
│                                                     │
│  1. Build React → dist/                             │
│  2. Copy dist/* → backend/wwwroot/                  │
│  3. .NET serves both API + SPA from single port     │
│                                                     │
└─────────────────────────────────────────────────────┘

17.4 Frontend Vite Configuration

// frontend/vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:5000',
        changeOrigin: true
      }
    }
  },
  build: {
    outDir: 'dist',
    assetsDir: 'assets',
    sourcemap: false
  }
})

17.5 API Service Example (Frontend)

// frontend/src/services/api.ts
const API_BASE_URL = import.meta.env.DEV 
  ? 'http://localhost:5000/api' 
  : '/api';

export class ApiService {
  static async get<T>(endpoint: string): Promise<T> {
    const response = await fetch(`${API_BASE_URL}${endpoint}`);
    if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
    return response.json();
  }
  
  static async post<T>(endpoint: string, data: any): Promise<T> {
    const response = await fetch(`${API_BASE_URL}${endpoint}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
    return response.json();
  }
}

17.6 Build Script Example

#!/bin/bash
# scripts/build.sh

echo "🏗️  Building Full Stack Application..."

# Build frontend
echo "📦 Building frontend..."
cd frontend
npm ci
npm run build

# Copy to backend wwwroot
echo "📋 Copying frontend to wwwroot..."
rm -rf ../backend/MyApp.Api/wwwroot/*
cp -r dist/* ../backend/MyApp.Api/wwwroot/

# Build backend
echo "🔧 Building backend..."
cd ../backend
dotnet restore
dotnet build -c Release

echo "✅ Build complete!"

17.7 Key Benefits of This Structure

  1. Clear Separation: Frontend and backend are clearly separated but in the same repo
  2. Single Deployment: Deploy as a single .NET application serving both API and SPA
  3. Development Experience: Hot reload for both frontend (Vite) and backend (dotnet watch)
  4. Type Safety: Share TypeScript types between frontend and generate from C# models
  5. Scalability: Can easily split into microservices later if needed

17.8 Additional Tips

  1. Environment Variables: Use .env files for frontend and appsettings.json for backend
  2. Docker Support: Add Dockerfile in root for containerization
  3. CI/CD: Use GitHub Actions to automate builds and deployments
  4. Testing: Add frontend/tests/ and backend/MyApp.Tests/ directories
  5. Shared Types: Consider using tools like NSwag to generate TypeScript clients from your C# API