Using Webpack with Spring Boot

Webpack is a powerful tool for frontend development. It allows us to improve the structure of our JavaScript/CSS/SCSS code and to prepare it for production usage. How can we integrate Webpack into our Spring Boot application?

Spoiler: in Bootify's Free plan, Spring Boot applications can be initialized with their custom database schema and frontend stack. This includes Webpack with Bootstrap or Tailwind CSS for the frontend, so you get the whole following configuration including DevServer and build integration exactly matching your individual setup. Start a new project - without registration directly in the browser.

Prepare Node.js

The starting point for working with Webpack is Node.js, which uses npm to manage the external dependencies for the frontend. For this we first need to create the package.json in the top folder of our Spring Boot project.

{
  "name": "my-app-frontend",
  "author": "Bootify.io",
  "scripts": {
    "devserver": "webpack serve --mode development",
    "build": "webpack --mode production"
  },
  "dependencies": {
    "@popperjs/core": "^2.11.6",
    "bootstrap": "^5.2.3"
  },
  "devDependencies": {
    "@babel/core": "^7.20.7",
    "@babel/preset-env": "^7.20.2",
    "autoprefixer": "10.4.13",
    "babel-loader": "^9.1.0",
    "css-loader": "^6.7.3",
    "css-minimizer-webpack-plugin": "^4.2.2",
    "mini-css-extract-plugin": "^2.7.2",
    "postcss-loader": "^7.0.2",
    "postcss-preset-env": "^7.8.3",
    "sass": "^1.57.1",
    "sass-loader": "^13.2.0",
    "style-loader": "^3.3.1",
    "warnings-to-errors-webpack-plugin": "^2.3.0",
    "webpack": "^5.75.0",
    "webpack-cli": "^5.0.1",
    "webpack-dev-server": "^4.11.1"
  }
}

Our package.json with Webpack and Bootstrap dependencies

In the scripts section we can define our own commands that can be executed with npm run <script-name>. Here we are already preparing to start the DevServer we will use during development, as well as building our JS/CSS files for their usage in the final jar of our Spring Boot app. The background on this follows later on.

In the dependencies section are the libraries that will be available in the browser in the actually delivered frontend. In our example we include Bootstrap in the current version 5.2.3 and the required Popper. In devDependencies are the packages that are relevant for development and will not be available in the browser. Especially Webpack is included here as well as other dependencies for processing our JS/CSS files.

After the package.json is created and Node.js is installed on the system, we can initialize the project once with npm install. All defined dependencies will be automatically downloaded to the node_modules folder. New required libraries can be added later on with npm install <package-name>. The --save-dev parameter would put them in the devDependencies area.

Configure Webpack

The central file for managing Webpack is webpack.config.js.

module.exports = (env, argv) => ({
  entry: './src/main/resources/js/app.js',
  output: {
    path: path.resolve(__dirname, './target/classes/static'),
    filename: 'js/bundle.js'
  },
  devtool: argv.mode === 'production' ? false : 'eval-source-map',
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin(),
      new CssMinimizerPlugin()
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "css/bundle.css"
    }),
    new WarningsToErrorsPlugin()
  ],
  module: {
    rules: [
      {
        test: /\.js$'/,
        include: path.resolve(__dirname, './src/main/resources/js'),
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
      {
        test: /\.scss$/,
        include: path.resolve(__dirname, './src/main/resources/scss'),
        use: [
          argv.mode === 'production' ? MiniCssExtractPlugin.loader : 'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              sourceMap: true
            }
          },
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [
                  require('autoprefixer'),
                ]
              },
              sourceMap: true
            }
          },
          {
            loader: 'sass-loader',
            options: { sourceMap: true }
          }
        ]
      }
    ]
  },
  resolve: {
    modules: [
      path.resolve(__dirname, './src/main/resources'),
      'node_modules'
    ],
  }
});

Webpack configuration for creating bundle.js and bundle.css

The entry and output sections define the entry and output points for processing our JS/CSS files. Entry point is exclusively the resources/js/app.js file, but it references other SCSS files and will automatically split them up for the build. With the configuration of the devtool parameter, source maps are available during development to see our actual written sources in the browser's DevTools.

In the optimization and module sections we define the processing rules. By using Babel we can write modern ES6 JavaScript, which will be transformed for maximum compatibility. We write our styles with SCSS, but it is transformed and minimized to CSS for later delivery.

Setting up DevServer for development

The DevServer is recommended during development to be able to track all changes directly in the browser. Otherwise, the build would have to be run after each change. The DevServer can be configured with the following extension to our webpack.config.js.

{
  // ...
  devServer: {
    port: 8081,
    compress: true,
    watchFiles: [
      'src/main/resources/templates/**/*.html',
      'src/main/resources/js/**/*.js',
      'src/main/resources/scss/**/*.scss'
    ],
    proxy: {
      '**': {
        target: 'http://localhost:8080',
        secure: false,
        prependPath: false,
        headers: {
          'X-Devserver': '1',
        }
      }
    }
  }
}

Extension to configure the DevServer

With this setup, the DevServer runs on port 8081 and forwards all requests that it cannot answer itself to our Spring Boot app on port 8080. So during development, we should access our running application via localhost:8081. By defining watchFiles the browser will reload automatically after every file change - here the HTML extension also includes our Thymeleaf templates. This works best by setting up hot reload for Thymeleaf.

The integration of our JS and CSS scripts is now done with the following extension in the <head> area of our HTML.

<link th:if="${T(io.bootify.WebUtils).getRequest().getHeader('X-Devserver') != '1'}"
        th:href="@{/css/bundle.css}" rel="stylesheet" />
<script th:src="@{/js/bundle.js}" defer></script>

Integrating JS/CSS files

By using the DevServer, we can include the files like normal static resources. However, during development, the DevServer always delivers the JavaScript along with the CSS, so in this case we don't need to include the styles separately and disable them via th:if.

Build integration with Maven or Gradle

In webpack.config.js we have defined that the processed files are written to target/classes/static. So they will be included in our final jar and can be accessed as a static resource.

For the integration with Maven the frontend-maven-plugin is required. This runs as an additional step within mvnw package, downloads Node.js independently and executes npm install and npm run build.

<plugin>
    <groupId>com.github.eirslett</groupId>
    <artifactId>frontend-maven-plugin</artifactId>
    <version>1.12.1</version>
    <executions>
        <execution>
            <id>nodeAndNpmSetup</id>
            <goals>
                <goal>install-node-and-npm</goal>
            </goals>
        </execution>
        <execution>
            <id>npmInstall</id>
            <goals>
                <goal>npm</goal>
            </goals>
            <configuration>
                <arguments>install</arguments>
            </configuration>
        </execution>
        <execution>
            <id>npmRunBuild</id>
            <goals>
                <goal>npm</goal>
            </goals>
            <configuration>
                <arguments>run build</arguments>
            </configuration>
        </execution>
    </executions>
    <configuration>
        <nodeVersion>v18.12.1</nodeVersion>
    </configuration>
</plugin>

Configuration of the frontend-maven-plugin for Webpack

For Gradle, the Node plugin is used, which also executes npm run build. By configuring inputs and outputs we enable caching and the steps are skipped during gradlew build if there have been no changes to the source files.

plugins {
    // ...
    id 'com.github.node-gradle.node' version '3.5.0'
}

// ...

node {
    download.set(true)
    version.set('18.12.1')
}

task npmRunBuild(type: NpmTask) {
    args = ['run', 'build']
    dependsOn npmInstall

    inputs.files(fileTree('node_modules'))
    inputs.files(fileTree('src/main/resources'))
    inputs.file('package.json')
    inputs.file('webpack.config.js')
    outputs.dir("$buildDir/resources/main/static")
}

processResources {
    dependsOn npmRunBuild
}

Integration of the frontend build into the Gradle build

With this we have configured Webpack for its usage in our Spring Boot app! The DevServer supports us during development, and with the right plugin the JS/CSS files are prepared and integrated into our fat jar.

Bootify can be used to initialize Spring Boot applications with their own database schema and frontend. Here, Webpack can be chosen with Bootstrap or Tailwind CSS. In the Professional Plan advanced features like multi-module projects and Spring Security are available.

» Start Project on Bootify.io

Further readings

Install Node.js
Frontend Maven Plugin
Gradle Plugin for Node