Tech Blog

Bundling Vue3/React Frontend Builds with Spring Boot for Automatic Serving

by Tech Writer
Spring Boot Vue3 Vite Maven

Introduction

When developing with a Vue 3 + Spring Boot separated architecture, there are challenges at production deployment time.

  • Where do you put the frontend build artifacts?
  • How does Spring Boot serve Vue’s static files?
  • When building with Maven, can the frontend npm build run together?

This article explains “the configuration for creating an executable jar that includes the frontend build with a single mvn package command.”


Final Structure

dvd-rental-customer-app/
  backend/
    pom.xml                    ← Maven config (includes frontend build)
    src/main/resources/
      static/                  ← Frontend build artifact destination
        (auto-placed after build)
  frontend/
    package.json
    vite.config.ts
    src/
      App.vue
      ...

Step 1: Change Vite Build Output Destination

By default, output goes to frontend/dist/, but we change it to a location Spring Boot can serve.

// frontend/vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  build: {
    outDir: resolve(__dirname, '../backend/src/main/resources/static'),
    emptyOutDir: true,  // Clear static/ before building
  },
  server: {
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://localhost:8082',
        changeOrigin: true,
      }
    }
  }
})

Running npm run build puts the artifacts in backend/src/main/resources/static/.


Step 2: frontend-maven-plugin Configuration

<!-- backend/pom.xml -->
<build>
    <plugins>
        <!-- Automatically run Node installation and npm build -->
        <plugin>
            <groupId>com.github.eirslett</groupId>
            <artifactId>frontend-maven-plugin</artifactId>
            <version>1.15.0</version>
            <configuration>
                <!-- Frontend directory (relative path from pom.xml) -->
                <workingDirectory>../frontend</workingDirectory>
                <!-- Node/npm installation destination -->
                <installDirectory>target</installDirectory>
            </configuration>
            <executions>
                <!-- Install Node.js and npm -->
                <execution>
                    <id>install-node-and-npm</id>
                    <goals>
                        <goal>install-node-and-npm</goal>
                    </goals>
                    <configuration>
                        <nodeVersion>v22.0.0</nodeVersion>
                        <npmVersion>10.5.2</npmVersion>
                    </configuration>
                </execution>
                
                <!-- npm install -->
                <execution>
                    <id>npm-install</id>
                    <goals>
                        <goal>npm</goal>
                    </goals>
                    <configuration>
                        <arguments>install</arguments>
                    </configuration>
                </execution>
                
                <!-- npm run build -->
                <execution>
                    <id>npm-build</id>
                    <goals>
                        <goal>npm</goal>
                    </goals>
                    <phase>generate-resources</phase>
                    <configuration>
                        <arguments>run build</arguments>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Step 3: Spring Boot Static File Serving Configuration

Spring Boot uses classpath:/static/ as the static file root by default.
No special configuration is required, but to explicitly state it:

# application.yml
spring:
  web:
    resources:
      static-locations: classpath:/static/

Step 4: Combining with Vue Router (SPA Support)

When using the History API with Vue Router, directly accessing /films/123 in the browser
causes Spring Boot to look for the path /films/123 and return a 404.

// Setting for Spring Boot to handle SPA routing
@Controller
public class SpaForwardController {
    
    @GetMapping(value = {"/", "/{path:[^\\.]*}", "/{path:^(?!api|actuator).*$}/**"})
    public String forward() {
        return "forward:/index.html";
    }
}
  • /api/** → Handled by REST API endpoints
  • Everything else → Returns index.html (Vue Router handles it)

Build-Time Behavior

# When running mvn package...
mvn -f backend/pom.xml package

# 1. frontend-maven-plugin downloads Node.js (first time only)
# 2. Runs npm install
# 3. Runs npm run build → outputs to backend/src/main/resources/static/
# 4. Everything bundled into Spring Boot jar
# 5. Executable jar is generated

Just running the generated jar makes both frontend and backend work.

java -jar backend/target/dvd-rental-customer-backend-0.1.0-SNAPSHOT.jar

Development Flow

It’s important to separate production builds from development flow.

# Development (2 processes in parallel)
# Terminal 1: Vite dev server (HMR enabled, port 5173)
cd frontend && npm run dev

# Terminal 2: Spring Boot (port 8082)
cd backend && ../mvnw spring-boot:run -Dspring-boot.run.profiles=local

During development, Vite’s HMR (Hot Module Replacement) is available, so file changes reflect instantly in the browser.


Combining with Docker

You can follow the same steps when building with Docker.

# Multi-stage build
FROM node:22-alpine AS frontend-build
WORKDIR /frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build

FROM eclipse-temurin:21-jdk AS backend-build
WORKDIR /app
COPY backend/ .
# Copy frontend build artifacts to static/
COPY --from=frontend-build /frontend/dist ./src/main/resources/static/
RUN ./mvnw package -DskipTests -Dfrontend.skip=true

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=backend-build /app/target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

Summary

  • Using frontend-maven-plugin allows creating a jar including the frontend build with a single mvn package
  • Pointing Vite’s build output to backend/src/main/resources/static/ enables automatic bundling
  • When using Vue Router’s History API, SPA forwarding is required on the Spring Boot side
  • During development, run 2 processes in parallel to utilize Vite’s HMR

With this configuration, there’s no need to “deploy the frontend separately” at deployment time — just copying a single jar is enough.


Article Map for This Series

Building a DVD Rental End-User App alongside the Admin App — Vue 3 + Spring Boot Full Architecture and Article Map

Feel free to send a message

Please send a message if you have any technical questions, feedback, or inquiries.