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
- Angular normally uses client-side routing.
routerLinknavigation is handled by Angular Router in the browser.- Pressing Refresh/F5 sends the current URL directly to the web server.
/productsmay not exist as a physical server resource.- The server can therefore return 404 Not Found.
- Configure an SPA fallback to return
index.html. - ASP.NET Core can use:
app.MapFallbackToFile("index.html");
- IIS can use a URL Rewrite rule.
- Nginx commonly uses:
try_files $uri $uri/ /index.html;
- 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 sendsGET /productsdirectly to the production web server. Since/productsis 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 returnindex.html, after which Angular Router handles/products. In ASP.NET Core, this can be done withMapFallbackToFile("index.html").