Hosting a single page application with spring boot
Asked Answered
V

4

7

So I am trying to host a Single Page Application alongside a normal REST API with spring.

What this means is that all requests that goes to the normal /api/ endpoints should be handled by the respective controller and all other requests should be directed to the resources in the folder /static/built

I have gotten this to work by catching all NoHandlerFoundExceptions and redirecting to either the js file or the html file. And then used a WebMvcConfigurer to map the static content.

But this all seems like a hack to me, so is there a less hacky way of doing it?

Vedetta answered 25/1, 2019 at 7:56 Comment(1)
You just need to copy the built frontend files to Spring boot static/ directory (you can use maven frontend plugin to automate that). Then Spring Boot will serve them as static files, you don't need to worry about redirecting to html or js. Are you using hash-based URL in your SPA (so.com/#/route/...)? If so you have nothing more to do in SpringFrieda
S
6

Managed to have React+ReactRouter app working by adding following mapping:

@Controller
public class RedirectController {

  @GetMapping(value = {"/{regex:\\w+}", "/**/{regex:\\w+}"})
  public String forward404() {
    return "forward:/";
  }

}

Moreover add the spring.mvc.pathmatch.matching-strategy=ant_path_matcher configuration to your application.properties to allow/activate the **-pattern. Otherwise your application will not start.

This was inspired by https://mcmap.net/q/326293/-spring-catch-all-route-for-index-html

Storeroom answered 8/5, 2019 at 16:18 Comment(1)
Hyphens are not a \w regex character but can be in an endpoint, so replace \\w with [\\w-] (regex square bracket to capture the extra hyphen character). Same for the period . but that's less common as an endpoint name, probably refers to a static file resource, so I wouldn't include it.Ambience
F
2

The easiest way I get my SPAs to work with a Spring backend API is to have 2 different controllers: one for the root index page of the SPA and the other controller is used to manage various RESTful API endpoints:

Here are my two controllers:

MainController.java

@Controller
public class MainController {

    @RequestMapping(value = "/")
    public String index() {
        return "index";
    }
}

MonitoringController.java

@RestController
@RequestMapping(value = "api")
public class MonitoringEndpoints {

    @GetMapping(path = "/health", produces = "application/hal+json")
    public ResponseEntity<?> checkHealth() throws Exception {
        HealthBean healthBean = new HealthBean();
        healthBean.setStatus("UP");

        return ResponseEntity.ok(healthBean);
    }
}

Notice how the API endpoint controller utilizes the '@RestConroller' annotation while the main controller utilizes the '@Conroller' annotation. This is because of how Thymeleaf utilizes it's ViewResolver. See:

Spring Boot MVC, not returning my view

Now go ahead and place your index.html page at src/main/resources/templates/index.html because Spring by default looks for your html pages within this location.

My index.html pages looks like this:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head lang="en">
        <meta charset="UTF-8"/>
        <title>EDP Monitoring Tool</title>
    </head>
    <body>
        <!-- Entry point to our ReactJS single page web application -->
        <div id="root"></div>

        <script type="text/javascript" src="built/bundle.js"></script>
    </body>
</html>

Not sure how you're wiring up your frontend, whether that is a ReactJS app or something but I believe this information would be helpful for you. Let me know if I can answer additional questions for you.

In addition, if you're using Webpack, you can set up the entry point of your JS files via the webpack.config.js file under the entry key so like so:

entry: ['./src/main/js/index.js']
Ferdinande answered 25/1, 2019 at 19:48 Comment(0)
T
0

I'm answering this because I saw quite a few different approaches, including using a Controller with a regex (as shown above) and even use a handler for 404 to serve the files. Actually there's another way: use an old-style Servlet Filter to do this. I have posted the complete code for this: https://github.com/chiralsoftware/SpringSinglePageApp

The key thing is to use a WebMvcConfigurer to add a ResourceHandler:

public class WebConfiguration implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/*").addResourceLocations("file:/opt/web/singlepageapp/");
    }

Note very importantly the file: prefix in the path, as well as the trailing /.

Then use a conventional Filter with a HttpServletRequestWrapper:

@WebFilter("/*")
public class IndexFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if(indexLocation == null || indexLocation.isBlank()) {
            LOG.warning("The indexLocation value was null or blank so this filter is disabled.");
            chain.doFilter(request, response);
            return;
        }
        final HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        final String relativePath = httpRequest.getRequestURI().substring(httpRequest.getContextPath().length());
        LOG.finest("the relative path is: " + relativePath);

        if(staticFiles.stream().anyMatch(f -> relativePath.startsWith(f))) {
            LOG.finest("it's a static file so chain forward");
            chain.doFilter(request, response);
            return;
        }
        
        chain.doFilter(new MyServletRequest(httpRequest), response);
    }
    
    private final class MyServletRequest extends HttpServletRequestWrapper {
        
        public MyServletRequest(HttpServletRequest request) {
            super(request);
        }
        
        @Override
        public String getRequestURI() {
            return getServletContext().getContextPath() + indexLocation;
        }
                
    }

See the Github link for the full code. It works exactly as needed and is a good way to serve a Vue single page app with Spring. The real trick is using the power of a Filter with the HttpServletRequestWrapper, something which is not obvious. This would be one line of code in Nginx but this approach lets you do it all in Spring. I also included Jetty deployment files in the repo.

Triatomic answered 1/9, 2023 at 19:15 Comment(0)
B
-3

I think you're looking for the term URL Rewrite.

E.g. https://getpostcookie.com/blog/url-rewriting-for-beginners/

Bacteriostasis answered 25/1, 2019 at 8:15 Comment(1)
This question has nothing to do with Apache .htaccessBoisterous

© 2022 - 2024 — McMap. All rights reserved.