Quickly adding a new Vue.js application to an existing Spring Boot project should be pretty easy, or at least a googleable problem, or so we thought. But in the end, it wasn't. However, with the right combination of configuration, components, and some custom code, we made it work.
Requirements
Creating a RESTful service in Spring Boot is pretty straightforward, as is using templates to dynamically generate HTML pages with data from, e.g. a database. (have a look at serving Web Content with Spring MVC). But we wanted a modern frontend built with Vue.js (we could have also used any other framework that supports static build outputs like Angular or React). To follow this blog post you should have basic knowledge of Vue.js and Spring Boot.
Some more requirements our solution should fulfill:
- Works for a small number of pages (less than 100)
- Only limited scalability is required (no CDN etc.)
- Easy deployment with an executable jar file that contains the whole application. That means that no Node.js, Reverse Proxy, etc. is necessary on the production side.
Our Vue.js application should be a single page application, which means that the page isn’t reloaded on each navigation inside the application. The url in the address bar of the browser should be a proper one like /users/4711
and should not be based on the hash part. These urls should be bookmarkable, meaning that it should be possible to directly visit a specific page, instead of always having to navigate there from the homepage. Communication between Spring and Vue should work through a defined REST-API.
Additionally, it is required that the Same Origin Policy is always respected, but it shouldn’t be necessary to configure CORS headers for this purpose, neither during development nor during production. And for all the developers working with the setup, the whole stack should be fast and easy to work with during development. That means that changes in the backend and frontend should be visible in the browser without long delays, i.e., without running a complete and lengthy build.
After development, a relatively simple build should create a single jar file that can be started with java -jar. Maven can delegate part of the build process to other tools like npm.
Project structure
The fundamental project structure is very simple. The Spring Boot application can be created using start.spring.io (Spring Initializr) and serves as the base for our project. For this project, we chose to use Maven as the build tool, but the concepts we talk about in this article can be applied to a Gradle based project as well. Just the maven-specific build plugins we use have to be substituted for their Gradle equivalent.
The Vue.js app is nested in the src folder of the Spring project. There we can now run npm create vue@latest to create it. You should choose support for TypeScript and Vue-Router. We also chose to add ESLint, Prettier and the dev tools. If you select the Vue-Router, this command will automatically create two routes and the corresponding components. We can use those to check if the navigation between different pages and the direct access of pages other than the homepage is working.
If we then build the application using npm run build all the static files will be created under src/vueapp/dist
. In order for those files to be served correctly by Spring, they have to be part of the build output of Maven. To achieve that we use the maven-resources plugin, which copies all files from src/vueapp/dist
to target/classes/static
.
Configuring Spring
Now that we have a Vue application, we can begin to configure Spring to serve the static files of said Vue application. Without any configuration, Spring will serve all files in the static directory on the classpath based on their path. For example, a file static/index.html will be delivered when surfing to localhost:8080/index.html
. If we now build the application using maven clean package and execute the jar file, we will be able to access localhost:8080/index.html
and will see our Vue application. We can now use the shown link to navigate to the about page, which will be shown without issues. However, if we now try to refresh the page, we will get an error message.
But before we look at how to configure Spring to serve our application under paths other than index.html
, we should first have a look at how a Vue application is structured once it is built. The entry point is always an index.html
file that contains the base structure of the website. It references two files that will be loaded once this file is displayed in the browser. This first is a stylesheet containing all global styles and those for the homepage. The second is a script that contains most of the Vue application logic which then loads another JavaScript file containing a component depending on which page was accessed. Those files are all contained in the assets' folder. That means that whatever page is loaded, this index.html
file should be returned.
By default, Spring checks the controllers first, and if none of them are responsible, the static directory follows. To do that, Spring normally uses the PathResourceResolver
, which has to be the last one in the revolver chain as it doesn’t delegate to the next one. If we configure it after the VueResourceResolver
, we would have to check if the path can be resolved to a static file in the VueResourceResolver
, meaning we would have to rebuild half the PathResourceResolver
. Instead, we extend the PathResourceResolver
to delegate and register that extended one before our VueResourceResolver
.
A resource resolver resolves an url to a resource object, which is then used by Spring to return the contained file as the response. The default resolver used by Spring is the PathResourceResolver
which tries to look up the path in the filesystem or the classpath. Our newly created VueResourceResolver
will return the same resource regardless of the path.
Before our VueResourceResolver
is used we have to tell Spring that it exists and should be used at all. To do that we create the MvcConfig class, in which we override the addResourceHandlers
method. In that method we configure the VueResourceResolver
to be used for all possible paths.
1@Override
2public void addResourceHandlers(ResourceHandlerRegistry registry) {
3 registry.addResourceHandler("*", "**")
4 .addResourceLocations("classpath:/static/")
5 .setUseLastModified(true)
6 .setCachePeriod(3600)
7 .resourceChain(true)
8 .addResolver(new ChainedPathResourceResolver())
9 .addResolver(new VueResourceResolver(indexController));
10}
Sadly, this method doesn’t work to also return the index.html
file for the path /
, as Spring doesn’t call any resource resolvers for empty paths (see ResourceHandlerUtils.shouldIgnoreInputPath
) To solve this problem we create a new Controller with a single GetMapping for /
. We move the loading of the index.html
file there and call it from the VueResourceResolver
.
1@Value("classpath:/static/index.html")
2private Resource indexResource;
3
4@GetMapping(value = "/", produces = MediaType.TEXT_HTML_VALUE)
5@ResponseBody
6public Resource loadIndexHtml() {
7 return indexResource;
8}
Configuring the Vue application
During development, we want to start the Vue Application using npm run dev while simultaneously running the Spring Application from the IDE. At the same time we want changes in our code to be directly visible, without doing a full build.
The integrated server started when running npm run dev, has a lot of useful features for development; like autoreload, that will automatically reload the website in the browser every time the code is changed, either partially or completely depending on which parts of the code were changed.
If we now try to call the Spring Backend from Vue, we will get an error: The Cross-Origin-Request was blocked because it violates the Same-Origin-Policy.
But what even is this Same-Origin-Policy? This policy is a security measure of browsers that restricts how a script can interact with resources from different origins. It, for example, prevents malicious scripts from accessing a Webmail application on your behalf, that you are signed in to, to read your emails. Two urls have the same origin if the protocol, port and (sub-)domain are exactly the same. If you want to interact with another origin anyway, that origin has to explicitly allow it. However, this only works for other domains and ports; the protocol still has to be the same. To do that, this other origin has to set a CORS header specifying all domains (and ports) that are allowed to access it.
Thus we have two possible solutions to our problem:
- Configure Spring CORS to allow our application to access it with Cross-Origin-Requests.
- Use the built-in proxy of Vue or rather Vite. That way the Vue application doesn’t call Spring directly, it only talks to the built-in server that which then forwards the requests to Spring.
For this project, we decided to use the second solution, as it is straightforward to implement and the proxy won’t be active in production.
1export default defineConfig({ 2 [...] 3 server: { 4 proxy: { 5 '/api': { 6 target: 'http://localhost:8080', 7 changeOrigin: true, 8 secure: false, 9 ws: true, 10 } 11 } 12 } 13}
What now?
We have successfully created a Spring application that serves our Vue application. But what do we do now? In the next part of this blog we will look at how to connect the two to allow data to be exchanged between them. We will create an OpenAPI specification that we will use to automatically generate parts of the backend and frontend to have a defined interface between both parts.
The full project including the results from the second part can be found in our GitHub repository.
More articles
fromRoger Butenuth & Nils Winking
Your job at codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
More articles in this subject area
Discover exciting further topics and let the codecentric world inspire you.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog authors
Roger Butenuth
Senior Integration Architect
Do you still have questions? Just send me a message.
Nils Winking
IT Consultant Integration
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.