Spring Boot backend and web frontend in separate Maven modules

I like to keep my backend from my frontend separated during development. This sounds like an obvious best practice, but it’s not easily done when your frontend is a web app and your backend is Spring Boot: because Spring Boot is self-contained, it encourages you to keep everything in one big module. This is convenient in many ways: Spring Boot takes care of serving your API and web app, hot-reloading when there are changes, and it can package the whole thing in a standalone fat jar using a dedicated Maven plugin. On the other hand, it has some drawbacks: the hot-reload feature of Spring Boot is not the most efficient, if your webapp involves webpack or some other build mechanism it becomes seriously complicated to work in dev mode, and most importantly: it couples together two pieces of software that should only be linked by a set of URLs.

What I want to do

Keep my backend and frontend each in a separate Maven module, while keeping the ability to package them together into a Spring Boot standalone jar.

Justin Calleja’s method

While researching how to achieve this, I found Justin Calleja’s very useful blog post: Serving a Webpack bundle in Spring Boot , which was my biggest influence. In short his solution is as follows:

  • frontend
    • module uses Maven plugin  frontend-maven-plugin to wrap node / npm / webpack
    • webpack is configured to generate its output into target/classes/META-INF/resources/webjars/module/version
    • packaged as a jar
  • backend
    • has a Maven dependency on the frontend module
    • Spring MVC configured to serve URLs beginning with /webjars from its classpath (which includes the jar from the frontend module)

What I wanted to change

At first I liked that solution very much, but I was put off by the fact that the backend module has a dependency on the frontend module. This is not right.

I understand that since Spring Boot manages the final packaging, it’s convenient if it has access to the client code too, but this is not what I wanted to achieve, which is: independence of backend and frontend.

This is a classic dependency inversion problem, so the solution is obvious: add a third module that depends on both others, and let it do the packaging.

What I ended up doing

I will only highlight the differences with Justin Calleja’s solution.

Frontend

The frontend module is quite similar, except that my web app was generated with vue-cli so I didn’t want to mess with webpack’s configuration. I let it generate its stuff into the dist subdirectory, and I declare this directory as a resource for Maven build. Here’s the relevant POM part:

        <resources>
            <resource>
                <!-- The webapp directory as generated by webpack -->
                <directory>${webapp.directory}/dist</directory>
                <!-- We put this into the static directory so that Spring Boot serves it through HTTP as static content -->
                <targetPath>static</targetPath>
            </resource>
        </resources>

By ultimately putting it into the static directory, we make sure that Spring Boot will serve it as static content (once it has been merged with the backend jar). The rest of the POM is just the frontend-maven-plugin configured just as in  Justin Calleja’s solution.

Backend

The backend module is different in the following ways:

  • it does not have a dependency on the frontend module
  • it does not use spring-boot-maven-plugin to repackage the jar into an executable jar
  • The specific Spring MVC configuration for webjars is not required, as the merge will ensure that the webapp files are directly available in the main jar.

Bundle

Here comes a new module: bundle. The bundle module has dependencies on both the backend  and frontend modules; its goal is to merge the two jars into one and package it as a Spring Boot self-contained executable jar.

How do we merge two jars? I’ve tried several approaches but the one that seems to work best is to use maven-dependency-plugin to unpack the dependencies into target/classes, and then let the jar plugin create a single jar from the unpacked files.

            <plugin>
                <artifactId>maven-dependency-plugin</artifactId>
                <executions>
                    <execution>
                        <id>unpack-dependencies</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>unpack-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.directory}/classes</outputDirectory>
                            <excludeTransitive>true</excludeTransitive>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

Now we just need to add spring-boot-maven-plugin to repackage this jar into an executable jar.

Running in deployed mode

The whole point of this setup was to generate this last artefact, a Spring Boot app packaged as a self-contained, executable jar.

To let Maven manage the build order of our three modules, the Maven idiom is to create a root module one level up with a “pom” packaging, like this:

    <packaging>pom</packaging>
    <modules>
        <module>backend</module>
        <module>frontend</module>
        <module>bundle</module>
    </modules>

To build it we just run “mvn package” from the root maven module; the Maven reactor will build in sequence: the backend module, the frontend module, and the bundle. The final jar will be in bundle/target. If all goes well we should be able to run it with “java -jar bundle-x-y-z-SNAPSHOT.jar”

As you will notice, this process is quite long and is not intended to be used during the development period, but only to generate the production-ready deliverable. However, we need to make sure the process works as intended, so we can safely work in dev mode and be assured that we can generate the production deliverable anytime.

Running in dev mode

The part regarding webpack-dev-server from Justin Calleja’s post still applies; however if you used vue-cli (or any other similar cli) a lot of the tedious work has been done for you.

In dev mode, we will be running two distinct servers: the Spring Boot app will serve the API requests, while webpack-dev-server will serve the static contents. This way we have the best of both worlds: any change on the backend side will only require hot-reloading the Spring Boot app, and any change on the client side will be picked up by webpack and processed to update the frontend in near-real-time.


The backend is started as any Spring Boot app, likely through your IDE. Obviously both servers cannot listen on the same port, so we will change the webpack-dev-server port to something other than the Spring Boot port (8080 by default). That’s the only change we will make to the webpack configuration.

If you’ve used vue-cli, then this change takes place in config/index.js:

    // Various Dev Server settings
    host: 'localhost', // can be overwritten by process.env.HOST
    port: 8081, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined

vue-cli also generates a complete package.json file, including a “dev” script that starts webpack-dev-server, so all we have to do to start the frontend dev server is to run “npm run dev” from the webapp directory. After a while your app will be available on http://localhost:8081.

Since the API requests will come from a page that has been loaded from a different server, we will also need to enable CORS, by simply declaring a bean in the Spring Boot app as follows:

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**").allowedOrigins("http://localhost:8081");
            }
        };
    }

After this changed you can then check that API calls to http://localhost:8080 are processed correctly.

Wrapping up

We have achieved our goal which was to have independent Maven modules for backend and frontend, while being able to generate a standalone Spring Boot executable jar, and maintaining the ability to have a fast turnaround time in dev mode.

However there’s always room for improvement. I’d like to hear your similar stories.

 


11 thoughts on “Spring Boot backend and web frontend in separate Maven modules

    1. Thanks! It’s not code so I’m not sure Github is a better place to share…

      1. Thank you so so so much for this.

        It would be really helpful if you showed the full output of your POM files for this project.

        1. You mean the full contents of the POM? I included the significant bits that differ from Justin Calleja’s solution, the full POMs have a lot of noise so I’d need to rework them before publishing them. But if you’d like I can send them to you.

  1. Cool post Olivier – thanks!

    Totally makes sense to separate the two and make them both standalone. At the time of writing, I think the thing on my mind was “how can I just pull in my frontend in my backend project after publishing the frontend to a package manager / artifact repository?” It was easy enough to do when working with Node.js, but I didn’t know how to go about it in a Java based project. So I specifically set out to depend on my frontend from my backend and totally missed that it would be even better to publish them both as finished artifacts and to pull them in from another project.

    Unfortunately, I also never used this approach in a real world project. Never got the opportunity. Instead, the teams I’ve worked in opted for developing another server in e.g. Node.js which would serve frontend bundles as well as proxy to the “real” backend for data (which would be e.g. something using Spring Boot).

    Coincidentally, during development, this is the same approach suggested by leon: https://disqus.com/by/disqus_MxPdtNt43R/
    i.e. Webpack has the option to proxy requests to it’s dev server’s /api to X where X can be the Spring Boot server. This makes the enabling CORS unnecessary. This is simpler if you only need to enable CORS to get around this issue in development. Of course, if you’re enabling CORS anyway for other reasons (e.g. bundles published on CDN), you might opt to use CORS to solve this issue in development anyway – just good to know. I’m sure vue-cli has a way of proxying too: https://github.com/lincenying/vue-cli/blob/master/docs/proxy.md – (haven’t used myself).

    I have left a comment linking to this post on my post. Sorry, I managed to break how I build my blog and haven’t bothered fixing it… I just use Medium instead ^^;

    Nice day!

    1. Thanks for the cool suggestions ! I’ve used this approach on a real world project and it works quite satisfactorily, so I would definitely use it again. For the moment I’m fine with CORS but of course it shouldn’t be left enabled in the production version unless required.

      Cheers!

  2. I’ve attempted to follow your steps and well as the steps outlined in the source post, and i’m getting stuck. I’ve got the package bundling successfully, but when i try to run the jar as outlined in the deployed package step i see the following:

    no main manifest attribute

    I’m not a java dev by trade, but a front end one, so i’m a little lost. Any advice to remedy this error?
    https://stackoverflow.com/questions/9689793/cant-execute-jar-file-no-main-manifest-attribute is what i’m going to reference for now while i troubleshoot

    1. Are you building a SpringBoot runnable jar? Did you include the spring-boot-maven-plugin in your POM ?

  3. Thank you for this great articel!
    The only think I recognized after following your instruction was, that I need to add the execution goal repackage to my bundle pom.xml within the spring-boot-maven-plugin. Only after that I get a MANIFEST which was executable.

    org.springframework.boot
    spring-boot-maven-plugin
    2.0.3.RELEASE

    repackage

    1. You’re totally right! I didn’t mention that because not everyone wants to build a standalone runnable jar, but maybe I should…

Leave a Reply

Your email address will not be published. Required fields are marked *