← Back to Articles         
Why can refreshing an Angular route produce a 404 after deployment?

Why can refreshing an Angular route produce a 404 after deployment?

Published on 02 Oct 2026     7 min read Angular
Deployment

Why can refreshing an Angular route produce a 404 after deployment?

What is the issue?

Angular normally uses client-side routing.

Suppose your Angular application has:

export const routes: Routes = [
  { path: '', component: Home },
  { path: 'products', component: Products },
  { path: 'contact', component: Contact }
];

When the user navigates to:

https://www.example.com/products

Angular Router knows that /products should display the Products component.

However, if the user presses F5 / Refresh, the browser sends /products directly to the web server.

If the server doesn't know about that route, it returns:

404 Not Found

Why does normal navigation work?

Suppose the application initially loads:

https://www.example.com/

The server returns:

index.html

Angular starts in the browser.

Then the user clicks:

<a routerLink="/products">Products</a>

The flow is:

User clicks Products
        ↓
Angular Router
        ↓
URL changes to /products
        ↓
Products component displayed

Angular is already running, so Angular Router handles /products.

The web server doesn't need to locate a /products HTML file.


Why does Refresh fail?

Now suppose the browser is showing:

https://www.example.com/products

and the user presses F5.

The browser performs a completely new request:

GET /products

The request goes to the web server:

Browser
   ↓
GET /products
   ↓
Web Server

The web server may look for:

/products
/products.html
/products directory
/server endpoint for /products

None exists because /products is an Angular route, not a physical file.

Therefore:

GET /products
      ↓
Web Server
      ↓
"Where is /products?"
      ↓
Not found
      ↓
404

The Solution: SPA Fallback

The production server must be configured with an SPA fallback.

The rule is essentially:

If the request isn't a real static file or backend route, return Angular's index.html.

Then Angular Router can process the URL.

Correct flow:

Browser
   ↓
GET /products
   ↓
Web Server
   ↓
No physical /products
   ↓
Return index.html
   ↓
Angular starts
   ↓
Angular Router reads /products
   ↓
Products component

Full Simple Example

app.routes.ts

import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./home').then(m => m.Home)
  },
  {
    path: 'products',
    loadComponent: () =>
      import('./products').then(m => m.Products)
  },
  {
    path: 'contact',
    loadComponent: () =>
      import('./contact').then(m => m.Contact)
  }
];

Suppose Angular is deployed at:

https://www.example.com

Normal navigation

https://www.example.com
        ↓
index.html
        ↓
Angular starts
        ↓
Click Products
        ↓
Angular Router
        ↓
/products
        ↓
Products

Works correctly.

Refresh

Without server configuration:

https://www.example.com/products
        ↓
Press F5
        ↓
GET /products
        ↓
Web Server
        ↓
No /products file
        ↓
404

With SPA fallback:

GET /products
        ↓
Web Server
        ↓
No physical file
        ↓
index.html
        ↓
Angular starts
        ↓
Angular Router
        ↓
/products
        ↓
Products

ASP.NET Core Solution

If ASP.NET Core hosts the Angular static files, suppose you have:

wwwroot/
├── index.html
├── main-ABC123.js
├── styles-XYZ123.css
└── chunk-ABC123.js

Configure Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseDefaultFiles();
app.UseStaticFiles();

app.UseAuthorization();

app.MapControllers();

app.MapFallbackToFile("index.html");

app.Run();

The important line is:

app.MapFallbackToFile("index.html");

How does MapFallbackToFile() work?

Suppose the request is:

GET /products

There is no ASP.NET Core endpoint for /products.

Therefore the fallback returns:

index.html

Then Angular handles /products.

GET /products
      ↓
ASP.NET Core
      ↓
No matching endpoint
      ↓
MapFallbackToFile()
      ↓
index.html
      ↓
Angular Router
      ↓
Products

What about ASP.NET Core API routes?

Suppose you have:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        return Ok(new[]
        {
            new { Id = 1, Name = "Laptop" },
            new { Id = 2, Name = "Mouse" }
        });
    }
}

Now there are two completely different routes:

/products
     ↓
Angular Router


/api/products
     ↓
ASP.NET Core ProductsController

That's why we normally map controllers before the fallback:

app.MapControllers();

app.MapFallbackToFile("index.html");

Conceptually:

Request
   │
   ├── /api/products
   │        ↓
   │   ASP.NET Core
   │
   ├── /main-ABC.js
   │        ↓
   │   Static file
   │
   └── /products
            ↓
        index.html
            ↓
        Angular Router

IIS Solution

If Angular is hosted directly by IIS, IIS itself needs an SPA rewrite/fallback rule.

Conceptually:

GET /products
      ↓
IIS
      ↓
Is /products a real file?
      ↓
No
      ↓
Rewrite → index.html
      ↓
Angular Router

A typical URL Rewrite configuration is:

<configuration>
  <system.webServer>
    <rewrite>
      <rules>

        <rule name="Angular Routes"
              stopProcessing="true">

          <match url=".*" />

          <conditions logicalGrouping="MatchAll">

            <add input="{REQUEST_FILENAME}"
                 matchType="IsFile"
                 negate="true" />

            <add input="{REQUEST_FILENAME}"
                 matchType="IsDirectory"
                 negate="true" />

          </conditions>

          <action type="Rewrite"
                  url="/" />

        </rule>

      </rules>
    </rewrite>
  </system.webServer>
</configuration>

The idea is simple:

Real file?
   ↓ Yes
Return file

   ↓ No

Real directory?
   ↓ Yes
Use directory

   ↓ No

Return Angular index.html

Nginx Solution

For Nginx, a common configuration is:

location / {
    try_files $uri $uri/ /index.html;
}

For:

/products

Nginx performs:

/products exists?
      ↓ No

/products/ exists?
      ↓ No

Use /index.html
      ↓
Angular Router

But for:

/main-ABC123.js

the file exists, so Nginx returns the actual JavaScript file.


Does Angular ** wildcard fix the problem?

No.

Suppose Angular has:

{
  path: '**',
  loadComponent: () =>
    import('./not-found').then(m => m.NotFound)
}

This only works after Angular has loaded.

Consider:

https://www.example.com/xyz

Without server fallback:

Browser
   ↓
GET /xyz
   ↓
Server
   ↓
404

Angular never starts.

Therefore Angular's:

path: '**'

never gets a chance to execute.

With server fallback:

GET /xyz
   ↓
Server
   ↓
index.html
   ↓
Angular starts
   ↓
Angular Router
   ↓
**
   ↓
NotFound component

So remember:

Server fallback
      ↓
Gets request into Angular

Angular **
      ↓
Handles unknown Angular route

They solve different problems.


Why does it work with ng serve?

During development:

ng serve

Angular's development server already supports SPA-style routing.

Therefore:

http://localhost:4200/products

can work after refresh.

But after deployment:

https://www.example.com/products

the production server—such as IIS or Nginx—must be configured separately.

This is why developers sometimes see:

Development
/products + F5
      ↓
Works


Production
/products + F5
      ↓
404

The Angular route may be perfectly correct.

The missing piece is the production web server's SPA fallback.


Key Points

  1. Angular normally uses client-side routing.
  2. routerLink navigation is handled by Angular Router in the browser.
  3. Pressing Refresh/F5 sends the current URL directly to the web server.
  4. /products may not exist as a physical server resource.
  5. The server can therefore return 404 Not Found.
  6. Configure an SPA fallback to return index.html.
  7. ASP.NET Core can use:
app.MapFallbackToFile("index.html");
  1. IIS can use a URL Rewrite rule.
  2. Nginx commonly uses:
try_files $uri $uri/ /index.html;
  1. Angular's ** wildcard route does not replace server-side fallback configuration.

Interview Questions and Answers

1. Why does refreshing an Angular route produce 404 after deployment?

Because a refresh sends the current URL directly to the web server. The server may not have a physical file or backend endpoint corresponding to the Angular client-side route, so it returns 404.

2. Why does clicking routerLink work?

Because Angular is already running and Angular Router handles the navigation in the browser without requesting a physical page for that route.

3. How do you fix the problem?

Configure the production server with an SPA fallback so unmatched client routes return:

index.html

Angular Router then handles the URL.

4. How do you fix it when ASP.NET Core hosts Angular?

Use:

app.MapControllers();

app.MapFallbackToFile("index.html");

5. Why should MapControllers() come before the fallback?

Because API URLs such as:

/api/products

should be handled by ASP.NET Core controllers, while Angular routes such as:

/products

should reach the SPA.

6. Does Angular's wildcard ** route fix server 404 errors?

No. The wildcard works only after Angular has loaded. The server must first return index.html.

7. Why does it work with ng serve but fail after deployment?

The Angular development server already supports SPA routing. Your production web server needs equivalent fallback/rewrite configuration.

8. What is the difference between /products and /api/products?

Typically:

/products
     ↓
Angular Router

/api/products
     ↓
ASP.NET Core Web API

Short interview answer

Angular uses client-side routing. When the user refreshes /products, the browser sends GET /products directly to the production web server. Since /products is an Angular route rather than a physical server resource, the server may return 404. The solution is to configure an SPA fallback so unmatched client routes return index.html, after which Angular Router handles /products. In ASP.NET Core, this can be done with MapFallbackToFile("index.html").

← Back to Articles