Deploy Angular with Spring Boot in the same executable JAR

Updated: 2023-12-28

Update: Spring 3, Java 21, Angular 17 and WAR deployment

architecture

Sources and alternatives

GitHub project sources: https://github.com/marco76/SpringAngularDemo

The goal is to build a Spring Boot 3 and Angular 17 minimalistic application with a clean and elegant architecture in a single JAR or WAR.

Alternative solution (frontend build copied inside the backend), many of my Dev friends prefer this alternative. I think it depends on the context:
Java and Angular in one jar/war

Show me the code and how it works ...

If you want the code and / or see it running in your development environment, here you get the instructions.

This demo runs with Angular 17 and Java 21, be sure that your environment can support these frameworks ;).

git clone https://github.com/marco76/SpringAngularDemo.git 
cd ./SpringAngularDemo 
mvn clean package 
java -jar ./delivery/target/MyBeautifulApp-0.0.2-SNAPSHOT.jar 

If you run it and everything works correctly you can navigate to localhost:8080 and see ... a simple Angular webapp that calls the Java backend via REST:

navigate the app

... now deep dive in the theory

Goals

  • Deploy only 1 JAR (/ WAR)
  • Easy to develop with and to maintain
  • Independence between modules (frontend, backend, delivery)
  • No external dependencies besides standard Angular / Spring / Maven librairies
  • Elegant architecture
  • Easy integration with external CI/CD pipelines

In the last few years, I published a few examples of integration between Java and Angular (React) projects. They did the job but I didn't like them too much their architecture, in my opinion they were non 'elegant'. My goal is to have a consistent architecture to handle projects that use these frameworks and simplify the workflow for us developers.

Every project and company has different requirements and automated integration systems, for this reason there is no 'best' or 'fit-all' solution.

In this demo example, we build a simple but effective architecture for Angular and Java that can produce a deliverable (JAR) that can be deployed in a Docker container or other CD pipeline.

Compared to the previous solutions I proposed in the blog:

  • there are fewer (no) dependencies with external plugins
  • the backend is independent of the frontend, it doesn't depend on the static resources built in the frontend and it can run in a separate server
  • there is more flexibility for integration with external CI/CD architectures
  • the single JAR packaging is an optional extra step. The Angular frontend and the Java backend produce artifacts that can run in separate servers.

I find in general this architecture more elegant than the previous solutions suggested in this blog.

3 Maven modules: backend, frontend, delivery

This solution uses 3 maven modules:

  • backend: contains the Java code
  • frontend: contains the angular / react code
  • delivery: is responsible to assemble the backend and frontend in a final deliverable and (optional) distribute it.

Here the structure of the complete solution:

directory structure

Compared to the last solution presented in my blog, in this new one backend and frontend are completely independent and can be splitted between teams in different projects.

The third module has been added as an aggregator and could be responsible to deliver to third parties the package.

The orchestrator of the operation is Maven which build 2 packages (frontend.jar and backend.jar).

The final JAR contains the 2 artifacts that we created and the dependencies required to run the application.

The solution works because the frontend structure containing the javascript code is compatible with the backend required folder structure for static files. In our case, Spring Boot accepts the files in /static.
When the application starts the packages are opened, the backend and frontend are at this point mixed together.

We simply leverage the features of Maven and Java without complex solutions that unzip / merge / rebuild the artifacts.

Deep dive

Here we look at the different modules in detail. The goal is to understand how this architecture works. The application is really bare-bone.

Main pom.xml

In our main module at the root of the project we define the parent : spring-boot-starter-parent and the modules that are included in the project.

Maven will use this information to build correctly the project.

<?xml version="1.0" encoding="UTF-8"?> 
<project xmlns="http://maven.apache.org/POM/4.0.0" [...]> 
  <parent> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-parent</artifactId> 
    <version>3.2.1</version> 
  </parent> 
  <groupId>dev.marco.demo</groupId> 
  <artifactId>parent</artifactId> 
  <version>0.0.2-SNAPSHOT</version> 
  <packaging>pom</packaging> 
  <name>Spring Angular Demo</name> 
  <description>Demo project for Spring Boot</description> 
	<modules> 
      <module>backend</module> 
      <module>frontend</module> 
      <module>delivery</module> 
	</modules> 
</project> 

Backend

This is a simple minimalist Spring Boot application. We added a controller to serve a response, just to test / show that everything works correctly.

public class HelloController {  
  // simple GET response for our example purpose, we return a JSON structure 
  @RequestMapping(value = "/api/message", produces = MediaType.APPLICATION_JSON_VALUE) 
  public Map<String, String> index() { 
    return Collections.singletonMap("message", "Greetings from Spring Boot!"); 
  } 
} 

We use api in the endpoints paths. This is a common practice in real-world applications.

When you start your environments locally (frontend with port 4200, backend with port 8080) you will have a CORS error, for this reason we tell Spring that Cross Origin requests from port 4200 are allowed :

@CrossOrigin(origins =  {"${app.dev.frontend.local}"}) 

In a real world application you will need to configure this in a security class or limit this exception to a development profile. When deployed this application won't need this 'exception' because frontend and backend will run on the same server with the same port.

The pom.xml has nothing special:

The module inherits from a parent and it is named backend (you can call it as you wish)

<parent> 
  <groupId>dev.marco.demo</groupId> 
  <artifactId>parent</artifactId> 
  <version>0.0.2-SNAPSHOT</version> 
</parent> 
<artifactId>backend</artifactId> 

The module has only a dependency, without surprise, spring-boot-starter-web.

<dependency> 
  <groupId>org.springframework.boot</groupId> 
  <artifactId>spring-boot-starter-web</artifactId> 
</dependency> 

Frontend

The frontend is a standard Angular 15 application, you can create it with ng new inside of the frontend folder.

What we changed inside the default Angular project is the following.

angular.json

We added a proxy configuration to simplify the communication between the development frontend server (port 4200) and backend (8080). I have a post dedicated to this setup: Angular Dev server configuration.

"development": { 
 "browserTarget": 
	"frontend:build:development", 
  "proxyConfig": "proxy.config.json" 
} 

In the root folder of your Angular project you have to create the following proxy.config.json file:

{ 
  "/api": { 
    "target": "http://localhost:8080", 
    "secure": false 
  } 
} 

This code will redirect all the api calls to the local development server.

app.component.html

We removed the original Angular code to simply show the answer from the server:

<div> 
  Hi! Ciao! Salut! Hoi! 
  Here you get the message from Java: 
</div> 
<div> 
    {{(serverMessage | async)?.message}} 
</div> 

app.component.ts

Here we have a basic call to the backend:

serverMessage = this.httpClient.get<{message: string}>("api/message"); 

the proxy will add the correct server url to the endpoint path.

pom.xml

The biggest change and complexity is in the maven configuration of the frontend.

In the pom.xml we add a build configuration:

<build> 
  <resources> 
    <resource> 
      <directory>${basedir}/dist/${app.name}</directory> 
      <targetPath>public</targetPath> 
      <filtering>false</filtering> 
    </resource> 
  </resources> 

We tell maven to copy the build fields generated by ng build from the directory dist/[your_project_name] and save them in the public folder of the JAR created.

public is accepted by Spring Boot as a destination of static web files.

This is the JavaScript code after the build:
directory structure

and here is where Maven copies it in the JAR:
directory structure

In the pom.xml we need to add the Exec Maven Plugin, with this plugin maven will build the Angular application calling first npm install followed by ng build in the package phase of Maven:

<plugin> 
  <groupId>org.codehaus.mojo</groupId> 
    <artifactId>exec-maven-plugin</artifactId> 
    <version>3.1.0</version> 
    <executions> 
      <execution> 
        <id>install-dependencies</id> 
          <phase>package</phase> 
          <goals> 
            <goal>exec</goal> 
          </goals> 
          <configuration><executable>npm</executable> 
            <arguments> 
              <argument>install</argument></arguments> 
          </configuration> 
      </execution> 
      <execution> 
        <id>build-frontend</id> 
        <phase>package</phase> 
        <goals> 
          <goal>exec</goal> 
        </goals> 
        <configuration> 
          <executable>ng</executable> 
            <arguments> 
              <argument>build</argument> 
            </arguments> 
        </configuration> 
    </execution> 
  </executions> 
</plugin> 

Delivery module

With backend and frontend we build 2 independent JAR files, now we have to prepare the final package to deliver to our server.

The maven delivery module consists in only 1 pom.xml file, I skip the standard (boring) part to concentrate in what is important for us.

<dependencies> 
  <dependency> 
    <groupId>${project.groupId}</groupId> 
      <artifactId>frontend</artifactId><version>${project.version}</version> 
  </dependency> 
	 
  <dependency> 
    <groupId>${project.groupId}</groupId> 
      <artifactId>backend</artifactId><version>${project.version}</version> 
  </dependency> 
</dependencies> 

We add the dependencies with the frontend and the backend JAR files, this will import the artifacts in the final JAR.

<build> 
  <plugins> 
    <plugin> 
      <groupId>org.springframework.boot</groupId> 
        <artifactId>spring-boot-maven-plugin</artifactId> 
        <executions> 
          <execution> 
            <id>repackage</id> 
            <goals> 
              <goal>repackage</goal> 
            </goals> 
        </execution> 
      </executions> 
      <configuration> 
        <mainClass>dev.marco.demo.backend.SpringAngularDemoApplication</mainClass> 
        <finalName>MyBeautifulApp-${project.version}</finalName> 
        <skip>false</skip> 
      </configuration> 
    </plugin> 
  </plugins> 
</build> 

Very important, you cannot simply combine the frontend and backend JAR and run the application. This is a Spring application, Spring requires some dependencies and a specific structure in the final JAR.

Here an example of the Spring bootable JAR:

spring boot jar structure

... and where our JARs are copied:

our jars in the built

To build this JAR we can use the goal repackage in the spring-boot-maven-plugin. We have to tell Spring where to locate the main class to run:

<mainClass>dev.marco.demo.backend.SpringAngularDemoApplication</mainClass> 

We change the final name to prevent Spring to give the module name (delivery) to the JAR deliverable:

<finalName>MyBeautifulApp-${project.version}</finalName> 

Build and run the application

To build the application you can simply run:

mvn clean package from the root (parent) folder of your application.

If everything is ok you should see something like:

build successfully

from the same directory you can start the application with maven: java -jar ./delivery/target/MyBeautifulApp-0.0.2-SNAPSHOT.jar

After a few second, you will be able to navigate the new fancy application running as single JAR at port 8080:

navigate the app

How to run the web application

From GitHub

git clone https://github.com/marco76/SpringAngularDemo.git 
cd ./SpringAngularDemo 
mvn clean package 
java -jar ./delivery/target/MyBeautifulApp-0.0.2-SNAPSHOT.jar 

Development

For the development you can start and run the 2 separate applications (using the classical ng serveand java -jar or launching from your IDE). The Java application will run on port 8080 and the Angular webapp will run on 4200. We added a proxy in the configuration of Angular, all the requests for the endpoint api will be redirected to the url localhost:8080. This won't apply when deployed in production.

Production

After you build the JAR with mvn clean build in the parent module you will find the package ready to be deployed / run in the delivery module.

You can locally start the application with: java -jar ./delivery/target/MyBeautifulApp-0.0.2-SNAPSHOT.jar

delivery package

WAR Deployment (Tomcat, Wildfly etc.)

I tend to deploy self-contained JAR, for this reason the main solution is a JAR.

Many projects require a WAR for deployment, for this reason I added a branch (feature/war-for-tomcat) that build a WAR ready to be deployed.

Here you can find the branch: https://github.com/marco76/SpringAngularDemo/tree/feature/war-for-tomcat

For the build process only few changes are required.

Change the typoe of Spring Application

The Application needs to extend SpringBootServletInitializer to be deployed in a Servlet container (Tomcat, etc.):

@SpringBootApplication 
// we extend SpringBootServletInitializer 
    public class Application extends SpringBootServletInitializer { 

Server path - Application context different from ROOT

You can deploy as root renaming the WAR file in ROOT.war or changing the deployment configuration in your application server.

If the application doesn't run on [server]/ but on a different Application context, e.g. '[server]/[AppName]/' you need to adapt the configuration for Angular.

In the file index.html of Angular you need to specify the correct path or the frontend won't find the resources:

Angular documentation: https://angular.io/guide/deployment#the-base-tag
example <base href="/AppName/"> (AppName represents your Application Context)

For the development you don't need to change the path, there is an index.dev.html that uses <base href="/"> and it is started in development mode.

Docker

To run in Docker you can use a similar Dockerfile:

FROM eclipse-temurin:21-jre 
 
RUN mkdir /opt/app 
COPY delivery/target/*.jar /opt/app/myApp.jar 
CMD ["java", "-jar", "/opt/app/myApp.jar"] 

npm WARN EBADENGINE and error after starting the application

If you see a warning similar to

npm WARN cli npm v10.2.5 does not support Node.js v20.1.0. This version of npm supports the following node versions: `^18.17.0 || >=20.5.0`. You can find the latest version at https://nodejs.org/. 
npm WARN EBADENGINE Unsupported engine { 
npm WARN EBADENGINE   package: '@angular-devkit/architect@0.1700.8', 
npm WARN EBADENGINE   required: { 
npm WARN EBADENGINE     node: '^18.13.0 || >=20.9.0', 
npm WARN EBADENGINE     npm: '^6.11.0 || ^7.5.6 || >=8.0.0', 
npm WARN EBADENGINE     yarn: '>= 1.13.0' 

and the application starts but there is an error, you need to verify that your npm version and your node version are compatible.


You could be interested in

Angular, Node, Typescript version compatibility and new features

Compatibility Matrix Angular, Node and TypeScript
2022-01-17

logo of the category: spring-big.svg Spring Boot OpenAPI generator example

How to generate your REST methods and Spring controllers using OpenAPI / Swagger
2022-02-15

How to configure development and production server in Angular

Angular: Use tsconfig or proxyConfig to easily configure your environment
2023-04-09
WebApp built by Marco using SpringBoot 3.2.4 and Java 21, in a Server in Switzerland without 'Cloud'.