React SPA + ASP.NET Core Integration
Key Points
- Project layout options: monorepo with separate
client/+server/, orMicrosoft.AspNetCore.SpaServices.Extensionsintegration. Modern: separate apps, served independently or via reverse proxy. - Vite + React + TypeScript is the modern frontend stack. The
dotnet new reacttemplate is dated; build your own. - Dev: Vite dev server proxies API calls;
aspnetcore-httpsfor trust. - Production: build React → static assets → serve via NGINX/CDN/Blob; .NET API separately. OR ship React build into wwwroot.
- Aspire can orchestrate both for local dev.
Concepts (deep dive)
Layout
my-app/
├── api/ # ASP.NET Core API
│ ├── MyApp.Api.csproj
│ └── Program.cs
├── web/ # React SPA
│ ├── package.json
│ ├── vite.config.ts
│ └── src/
└── docker-compose.yml # or Aspire AppHost
Independent build/test/deploy lifecycles. Modern monorepo style.
Vite + React setup
vite.config.ts:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': { target: 'https://localhost:7001', secure: false, changeOrigin: true }
}
},
build: { outDir: 'dist' }
});
/api/* proxies to .NET API. SPA-side fetch:
Dev experience
Or use Aspire AppHost:
var api = builder.AddProject<Projects.MyApp_Api>("api");
var web = builder.AddNpmApp("web", "../web", "dev")
.WithReference(api)
.WithHttpEndpoint(env: "PORT")
.WithExternalHttpEndpoints();
aspire run → both up + dashboard.
Production deployment
Option A: separate hosting
CORS configured on API. Simplest scale.
Option B: same origin (reverse proxy)
Same origin = no CORS. BFF pattern friendly.
Option C: ship React in wwwroot
Then app.UseStaticFiles() and SPA fallback:
Single deployable. Good for monolith.
Routing fallback
SPA routes (e.g., /users/123) need to fall back to index.html:
API routes registered first; everything else → SPA.
CORS for separate origins
builder.Services.AddCors(o =>
o.AddPolicy("spa", p => p
.WithOrigins("https://app.contoso.com")
.AllowCredentials()
.AllowAnyHeader()
.AllowAnyMethod()));
app.UseCors("spa");
For BFF / same-origin, no CORS needed.
TypeScript types from OpenAPI
npx openapi-typescript-codegen --input http://localhost:5000/swagger/v1/swagger.json --output ./src/api
Or use Kiota (Microsoft's tool) — generates strongly-typed client.
React state management
| Lib | Notes |
|---|---|
| Tanstack Query | Server state — fetching, caching |
| Zustand | Client state — small, ergonomic |
| Redux Toolkit | Heavier; team familiarity |
| Jotai / Recoil | Atom-based |
For most apps: Tanstack Query + Zustand is the modern combo.
File watching / hot reload
Vite HMR is fast (<100ms). .NET hot reload via dotnet watch run. Both running independently.
Package manager
npm (default), pnpm (faster, less disk), yarn. Pick one consistently.
Build pipeline
- run: npm ci
- run: npm run build
- run: npm test
- run: dotnet build api
- run: dotnet publish api -o publish
CI
- name: Web
run: |
cd web
npm ci && npm run build && npm test
- name: API
run: |
cd api
dotnet build && dotnet test
Static asset optimization
- Hash filenames (Vite default).
- Long Cache-Control on hashed assets.
- gzip + brotli compression.
- CDN for static.
- HTTP/2 push or preload critical resources.
Environment variables
VITE_* prefix exposed to client; others kept secret.
Server-side rendering (SSR) with React
Next.js for SSR; React Server Components. Out of scope for this guide — but if you need SEO + React, consider Next.js + .NET API.
Aspire integration
Aspire orchestrates — API + web + DBs locally. aspire run shows both in dashboard.
Code: correct vs wrong
❌ Wrong: hardcoded API URL
✅ Correct: env / proxy
❌ Wrong: CORS allow-any
✅ Correct: scoped
Design patterns for this topic
Pattern 1 — "Separate apps; Vite + .NET"
- Intent: modern, clean, scalable.
Pattern 2 — "Same-origin via reverse proxy"
- Intent: no CORS; BFF-ready.
Pattern 3 — "OpenAPI-driven types"
- Intent: typed API client for TS.
Pattern 4 — "Aspire AppHost for local dev"
- Intent: one-command full stack.
Pattern 5 — "Tanstack Query for server state"
- Intent: caching, retry, devtools.
Pros & cons / trade-offs
| Layout | Pros | Cons |
|---|---|---|
| Separate hosting | Independent scale | CORS |
| Same-origin proxy | No CORS; BFF | Coupling |
| wwwroot ship | Single deploy | Tightly coupled |
When to use / when to avoid
- Use Vite + separate apps for new projects.
- Use Aspire for local orchestration.
- Avoid dotnet's old SpaServices integration.
Interview Q&A
Q1. Best React + .NET layout in 2026? Separate apps (api/, web/). Independent lifecycles. Aspire for orchestration.
Q2. SPA fallback? app.MapFallbackToFile("index.html") — non-API routes go to React.
Q3. CORS or same-origin? Same-origin via proxy is cleanest. CORS when truly separate domains.
Q4. Type generation from API? OpenAPI → openapi-typescript-codegen or Kiota. Strongly-typed clients.
Q5. Vite proxy? Dev-time proxy /api/* → API. Avoids CORS in dev.
Q6. State management 2026? Tanstack Query for server, Zustand for client. Redux only if team is invested.
Q7. SSR with React + .NET? Next.js + .NET API. React Server Components becoming standard.
Q8. Aspire NPM integration? AddNpmApp orchestrates the React dev server alongside .NET.
Q9. ENV vars? VITE_* prefix exposed to client. Secrets stay server-only.
Q10. Build artifact? dist/ static assets — host on CDN, Blob, or wwwroot.
Q11. Auth flow? OIDC via BFF (cookie session). Or token directly with PKCE (worse).
Q12. Hot reload? Vite HMR + dotnet watch run. Both independent.
Gotchas / common mistakes
- ⚠️ Hardcoded API URLs — env-vary instead.
- ⚠️ CORS allow-any — security risk.
- ⚠️ Ancient SpaServices — use Vite.
- ⚠️ No type generation — drift.
- ⚠️ No same-origin when BFF would help.