aspnet-core-integracion-frontend-react-vanilla-html

Connecting React, Vue, or HTML with ASP.NET Core

  • 5 min

The frontend-backend integration is the way a web interface consumes an ASP.NET Core API.

You already have your API working, secured, and documented. But let’s be honest: your users won’t use curl or Swagger to buy products. They need a visual interface.

The million-dollar question is: Where do I put my React/Angular/HTML code? Inside the .NET project? On another server?

Today we’ll look at the two main strategies to match the Frontend with the Backend.

Decoupled architecture

This is one of the most common ways to work with modern SPAs.

  • Backend: Runs on port 5000 (or on Azure/AWS).
  • Frontend: It’s a separate project (e.g., created with Vite or Angular CLI) that runs on a different port or domain.

They are two different applications that live in different repositories and only communicate via HTTP.

Development flow

You start the API (dotnet run -> localhost:5000).

You start the Frontend (npm run dev -> localhost:5173).

The Frontend makes fetch('http://localhost:5000/api/...') requests.

For this to work, you need to properly configure CORS on the API and allow localhost:5173 (or the real frontend domain) to request data.

Advantages

  • Specialization: Each part uses its own tools and workflow.
  • Independent deployment: You can update the frontend without restarting the backend server.

Serving static files

This strategy is ideal for:

  • Small or personal projects.
  • Internal corporate applications (Intranet).
  • Vanilla HTML/JS where there’s no complex build process.

Here, ASP.NET Core serves both the API and the HTML. The browser downloads the HTML from .NET and then the JS calls the API on the same domain.

The wwwroot folder

By convention, ASP.NET Core looks for static files (HTML, CSS, JS, Images) in a folder called wwwroot at the project root.

Create the folder and put an index.html inside it:

<!DOCTYPE html>
<html>
<head>
    <title>My .NET Store</title>
</head>
<body>
    <h1>Products</h1>
    <ul id="list"></ul>

    <script>
        // Being on the same domain, we DON'T need to put http://localhost:5000
        // We use relative routes. Goodbye CORS issues!
        fetch('/api/products')
            .then(res => res.json())
            .then(data => {
                const ul = document.getElementById('list');
                data.forEach(p => {
                    const li = document.createElement('li');
                    li.textContent = p.name + " - " + p.price + "€";
                    ul.appendChild(li);
                });
            });
    </script>
</body>
</html>
Copied!

Activating the middleware

In Program.cs, we need to tell the application to allow serving these files.

var app = builder.Build();

// ... other middlewares ...

app.UseDefaultFiles(); // 1. Looks for index.html or default.html
app.UseStaticFiles();  // 2. Allows serving files from wwwroot

app.MapControllers();
app.Run();
Copied!

Now, if you go to http://localhost:5000, you’ll see your web page. And the call to /api/products will work instantly without configuring CORS, because the origin is the same.

Integrating an SPA within .NET

What if you want to use React but you want to deploy a single artifact (a single .exe or Docker container) that has everything?

You can build your React project (npm run build) and copy the result (the dist or build folder) inside .NET’s wwwroot.

There’s an additional problem: routing. In an SPA (Single Page Application), you navigate to /products/5 and it’s React that shows you the view. But if you press F5 (Refresh), the request reaches the .NET server. .NET will look for a file or controller called /products/5, won’t find it, and will return a 404 Not Found.

Fallback to index.html

We need to tell .NET: “If the user requests a route that is NOT from the API and NOT a physical file (image/css), always return the index.html and let React handle it”.

// Program.cs

app.UseStaticFiles(); // Serves React's JS and CSS

// API routes
app.MapControllers();

// Wildcard for SPA: Anything else -> index.html
app.MapFallbackToFile("index.html");
Copied!

With this, if you refresh on /clients, .NET returns the index.html, React loads, reads the URL /clients and shows the correct screen.

Consuming the API: best practices

Regardless of the strategy, when writing your JavaScript code (React/Vue/Vanilla), follow these rules:

Don’t hardcode URLs in the code

❌ Bad:

fetch('http://localhost:5000/api/products')
Copied!

This will fail when you go to production because the URL won’t be localhost.

✅ Good (Environment Variables in Vite/React): Create a .env file:

VITE_API_URL=http://localhost:5000/api
Copied!

And use it in your code:

const baseUrl = import.meta.env.VITE_API_URL;
fetch(`${baseUrl}/products`)
Copied!

Use async and await

Modern code is much cleaner than .then() promises.

async function loadProducts() {
    try {
        const response = await fetch('/api/products');

        if (!response.ok) {
            throw new Error('Error loading');
        }

        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.error("Something exploded:", error);
    }
}
Copied!

Manage the JWT token

If your API uses a Bearer token, you must send it with every request. Storing it in localStorage is simple, but exposes it to any script that manages to execute via XSS; an HttpOnly cookie prevents that access, although it requires protection against CSRF. The strategy depends on the application’s threat model.

const token = localStorage.getItem('my_jwt_token');

fetch('/api/profile', {
    headers: {
        'Authorization': `Bearer ${token}` // 👈 The key
    }
})
Copied!