Enterprise Java

Spring Boot 404 Redirect to Single Page Application

Single-page application (SPA) such as those built with React, Angular, or Vue handle routing on the client side. When deployed with a Spring Boot backend, direct navigation to routes like /dashboard or /profile can result in a 404 Not Found error because the server does not recognize these routes. To solve this, Spring Boot needs to be configured to redirect all unknown routes (404 errors) to the SPA’s index.html. This ensures the frontend router can take over and render the correct view.

1. Understanding 404 HTTP Error

A HTTP 404 Not Found error occurs when the server cannot find the requested resource. In traditional server-side applications, every route corresponds to a backend endpoint or file. However, in SPAs:

  • The frontend handles routing.
  • The backend typically serves only static assets and APIs.
  • Routes like /home or /about do not exist on the server.

1.1 How SPA + Backend Work Together

In a typical SPA architecture, the frontend and backend have clearly separated responsibilities, where the backend (Spring Boot) exposes REST APIs and serves static assets, while the frontend handles routing, UI rendering, and state management; this decoupling allows independent development and deployment but requires proper server configuration to support client-side routing.

1.2 Why Default Behavior Fails

By default, Spring Boot attempts to resolve every incoming request as a server-side resource or API endpoint, which conflicts with SPA routing where many valid routes exist only on the client side.

2. Code Example

2.1 Maven Dependency

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>spa-demo</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
    </parent>

    <dependencies>

        <!-- Spring Boot Web Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Optional: DevTools for hot reload -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- Optional: Testing -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <properties>
        <java.version>17</java.version>
    </properties>

</project>

This Maven pom.xml configuration defines a Spring Boot project where the root <project> element specifies metadata such as <modelVersion>4.0.0</modelVersion> (the POM model version), <groupId>com.example</groupId> (the base package identifier), <artifactId>spa-demo</artifactId> (the project name), <version>1.0.0</version> (the application version), and <packaging>jar</packaging> (indicating the app will be built as a runnable JAR); it inherits from <parent>spring-boot-starter-parent</parent> (version 3.2.5), which provides dependency management and default configurations for Spring Boot projects; under <dependencies>, the spring-boot-starter-web dependency enables building REST APIs and web applications by including Spring MVC and an embedded server like Tomcat, spring-boot-devtools (runtime scope) is an optional dependency that supports automatic restarts and live reload during development, and spring-boot-starter-test (test scope) provides libraries like JUnit and Mockito for writing and running tests; finally, the <properties> section sets <java.version>17</java.version>, ensuring the project is compiled and run using Java 17.

2.2 Creating the Main Application

// DemoApplication.java
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

This DemoApplication class is the main entry point of the Spring Boot application, defined under the package com.example.demo; the @SpringBootApplication annotation is a convenience annotation that combines @Configuration, @EnableAutoConfiguration, and @ComponentScan, enabling automatic configuration, bean registration, and component scanning within the package and its sub-packages; the main method serves as the standard Java entry point, and within it, SpringApplication.run(DemoApplication.class, args) bootstraps the Spring Boot application by initializing the Spring context, starting the embedded web server (such as Tomcat), and wiring all configured components together, thereby making the application ready to handle incoming HTTP requests.

2.3 Create SPA (index.html)

<!DOCTYPE html>
<html>
<head>
    <title>Simple SPA</title>
    <script>
        function navigate(route) {
            history.pushState({}, '', route);
            document.getElementById('content').innerText = "Current route: " + route;
        }

        window.onpopstate = function() {
            document.getElementById('content').innerText = "Current route: " + location.pathname;
        }
    </script>
</head>
<body>
    <h1>Spring Boot SPA</h1>
    <button onclick="navigate('/home')">Home</button>
    <button onclick="navigate('/dashboard')">Dashboard</button>

    <div id="content">Current route: /</div>
</body>
</html>

This HTML file represents a minimal Single Page Application (SPA) where the <head> section defines the page title and includes a script that handles client-side routing logic; the navigate(route) function uses history.pushState() to update the browser URL without triggering a full page reload, simulating navigation to routes like /home or /dashboard, and dynamically updates the UI by setting the text of the <div id="content"> element to reflect the current route; additionally, the window.onpopstate event ensures proper handling of browser back and forward navigation by updating the displayed route based on location.pathname; in the <body>, buttons are provided to trigger route changes via the navigate() function, while the content div displays the active route, demonstrating how SPAs manage routing entirely on the client side without requiring server interaction for each navigation.

2.4 Configure Spring Boot to Redirect 404

// WebConfig.java
package com.example.demo;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/{spring:[^\\.]*}")
                .setViewName("forward:/index.html");

        registry.addViewController("/**/{spring:[^\\.]*}")
                .setViewName("forward:/index.html");

        registry.addViewController("/{spring:[^\\.]*}/**{spring:?!(\\.js|\\.css)$}")
                .setViewName("forward:/index.html");
    }
}

This WebConfig class is a Spring configuration component annotated with @Configuration and implements WebMvcConfigurer to customize Spring MVC behavior for handling Single Page Application (SPA) routing; inside the overridden addViewControllers method, multiple route patterns such as /{spring:[^\\.]*} and /**/{spring:[^\\.]*} are registered using regular expressions where [^\\.]* ensures that only paths without a dot are matched, effectively excluding static resources like .js, .css, and images, while the final pattern further refines this exclusion; for all such unmatched or unknown routes, setViewName("forward:/index.html") forwards the request internally to the SPA entry point without changing the browser URL, ensuring that all unknown routes are redirected to index.html where the client-side router takes over, enabling seamless navigation and preventing 404 errors during direct access or page refresh.

2.5 application.properties

spring.mvc.throw-exception-if-no-handler-found=true
spring.web.resources.add-mappings=true

These Spring Boot configuration properties control how the application handles unmapped requests and static resources, where spring.mvc.throw-exception-if-no-handler-found=true instructs Spring MVC to throw an exception when no matching handler (controller or resource) is found for a request instead of silently returning a default 404 response, enabling better control over error handling and allowing custom routing logic (such as forwarding to an SPA); meanwhile, spring.web.resources.add-mappings=true ensures that Spring Boot continues to automatically serve static resources (like .js, .css, images, etc.) from standard locations such as /static or /public, which is essential for a Single Page Application so that its assets load correctly while still allowing custom handling of unknown routes.

2.6 Code Run and Output

Execute the command mvn spring-boot:run from the project root directory to start the Spring Boot application, which will bootstrap the application context and launch the embedded server; Application starts on: once the startup is complete, the application will be accessible at http://localhost:8080, where you can open the URL in a browser to view and interact with the Single Page Application.

2.6.1 Case 1: Root Access

When the user accesses the base URL http://localhost:8080/, Spring Boot serves the default static resource index.html from the /static directory without any routing intervention, and the Single Page Application loads successfully with the initial state displayed as "Current route: /"; this serves as the entry point of the SPA where the frontend JavaScript initializes, sets up client-side routing, and prepares to handle further navigation events without requiring additional server calls for each route change.

URL: http://localhost:8080/
Output: SPA loads with "Current route: /"

2.6.2 Case 2: Client-side Navigation

When the user interacts with the UI (for example, by clicking the Home or Dashboard buttons), the SPA uses JavaScript via the navigate() function to update the URL using history.pushState() without triggering a full page reload, allowing smooth and fast transitions between views; the content area is dynamically updated to reflect the current route (e.g., /dashboard), and since no request is sent to the server during this process, the backend remains unaware of these route changes, demonstrating how routing is fully handled on the client side for a seamless user experience.

Click "Dashboard"
URL becomes: /dashboard
Output: "Current route: /dashboard"
(No page reload)

2.6.3 Case 3: Direct URL Access (Deep Link)

When a user directly enters a URL such as http://localhost:8080/dashboard in the browser or refreshes the page on that route, Spring Boot initially attempts to resolve it as a backend endpoint or static resource, which would normally result in a 404 Not Found error; however, with the configured routing rules, all such unknown routes are intercepted and internally forwarded to index.html using forward:/index.html without changing the browser URL, allowing the Single Page Application to load and the client-side router to interpret the path (e.g., /dashboard) and render the appropriate view, thereby enabling seamless deep linking and proper page refresh behavior.

URL: http://localhost:8080/dashboard

Before configuration: 404 Not Found

After configuration:
1) index.html is served
2) SPA renders: "Current route: /dashboard"

One of the most important advantages of this configuration is that users can refresh the page or directly access deep links (such as /dashboard or /home) without encountering 404 Not Found errors, as all unknown routes are seamlessly forwarded to the Single Page Application, allowing the client-side router to handle navigation and ensuring a consistent, smooth user experience across all entry points.

3. Conclusion

Configuring Spring Boot to redirect 404 errors to a Single Page Application is essential when working with modern frontend frameworks, as without this setup, direct URL access (deep linking) and page refreshes would result in broken navigation and 404 Not Found errors, negatively impacting the user experience; by forwarding all unknown routes to index.html, the backend delegates routing responsibility to the frontend, ensuring seamless client-side navigation, an improved and consistent user experience, and proper integration between the backend and frontend layers, making this approach a widely adopted best practice in production-grade Spring Boot applications serving SPAs.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button