Skip to content

React SPA + ASP.NET Core Integration

Key Points

  • Project layout options: monorepo with separate client/ + server/, or Microsoft.AspNetCore.SpaServices.Extensions integration. Modern: separate apps, served independently or via reverse proxy.
  • Vite + React + TypeScript is the modern frontend stack. The dotnet new react template is dated; build your own.
  • Dev: Vite dev server proxies API calls; aspnetcore-https for 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

cd web && npm create vite@latest . -- --template react-ts
npm install

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:

const r = await fetch('/api/users', { credentials: 'include' });

Dev experience

# Terminal 1
cd api && dotnet run

# Terminal 2
cd web && npm run dev

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

React build → static (S3, Blob, NGINX, CDN)
API → ACA / App Service

CORS configured on API. Simplest scale.

Option B: same origin (reverse proxy)

[Browser] → [YARP / NGINX]
              ├── /api/* → API
              └── /*     → SPA static files

Same origin = no CORS. BFF pattern friendly.

Option C: ship React in wwwroot

cd web && npm run build
cp -r dist/* ../api/wwwroot/

Then app.UseStaticFiles() and SPA fallback:

app.MapFallbackToFile("index.html");

Single deployable. Good for monolith.

Routing fallback

SPA routes (e.g., /users/123) need to fall back to index.html:

app.MapFallbackToFile("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

# .env.production
VITE_API_URL=https://api.prod.contoso.com
const url = import.meta.env.VITE_API_URL;

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

var web = builder.AddNpmApp("web", "../web", "dev");

Aspire orchestrates — API + web + DBs locally. aspire run shows both in dashboard.


Code: correct vs wrong

❌ Wrong: hardcoded API URL

const r = await fetch('https://localhost:7001/api/users');

✅ Correct: env / proxy

const r = await fetch('/api/users');   // proxy in dev; same-origin in prod

❌ Wrong: CORS allow-any

o.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());

✅ Correct: scoped

.WithOrigins("https://app.contoso.com").AllowCredentials();

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.

Further reading