How to deploy a Java and Angular webapp in one JAR/WAR
- UPDATE: Angular 17 and Spring Boot 3.2
- Deploy Angular and Spring Boot in the same artifact
- One JAR with Spring Boot or Java EE / Jakarta EE
- Frontend pom.xml
- The Java Backend pom.xml
- Spring
- Angular
- Deploy a WAR with Tomcat
- Bonus: features showcase
- Branches
- Request a feature
UPDATE: Angular 17 and Spring Boot 3.2
As reminder, I think that a more elegant architecture is the separation between backend, frontend and delivery with fewer dependencies.
But ... I had a lot of requests and questions about solution presented here (and many friends prefer this one!!!), I updated it to the latest frameworks.
The alternative solution is available here:
https://marmo.dev/angular-with-java
Deploy Angular and Spring Boot in the same artifact
This new version build a webapp using (the images are not updated yet)~~~~:
- Angular 17
- Spring Boot 21
- Node 20
- Java 21
In your project you can deploy your Angular and Java application in the same artifact (.jar or .war) or in separate artifacts.
An ultimate solution doesn't exist, you should adopt the architecture chosen by your team.
If frontend and backend teams are working independently, and the frontend team is using a monorepo, separate artifacts could have some advantages.
A quick (2 minutes) video tutorial is here, the video for the old version is available here:
One JAR with Spring Boot or Java EE / Jakarta EE
Your project contains 3 modules, the parent and 2 children (frontend and backend).
In this example we use Spring Boot, the same apply for a Java EE / Jakarta EE project.
We are building a Spring fat jar that contains the backend and the frontend.
You can find the project on GitHub here: https://github.com/marco76/java-angular-basic
Here you can see how the project is structured:
The backend project artifact will contain the Java compiled classes and the Angular application:
To build and start the project locally:
git clone https://github.com/marco76/java-angular-basic.git
cd ./java-angular-basic.git
mvn clean package
cd ./backend/target
java -jar backend-0.1.1-SNAPSHOT.jar
parent pom.xml
The parent pom.xml
declares the 2 modules to build. frontend
for Angular and backend
for Java.
The 2 modules can be started separately during the development. The Angular app will use the port 4200, the Java application will use the port 8080.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
</parent>
<groupId>dev.marco</groupId>
<artifactId>java-angular-example</artifactId>
<packaging>pom</packaging>
<version>0.1.1-SNAPSHOT</version>
<modules>
<module>frontend</module>
<module>backend</module>
</modules>
<properties>
<!-- this defines the java version for the project -->
<java.version>21</java.version>
</properties>
</project>
Frontend pom.xml
The frontend module contains a standard Angular CLI application.
During the development you can start the application with a standard ng serve
The heavy lifting is done by the plugin https://github.com/eirslett/frontend-maven-plugin. This plugin download node, install the libraries and build the project.
The result is a jar
file that contains the Angular application compiled.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>dev.marco</groupId>
<artifactId>java-angular-example</artifactId>
<version>0.1.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>frontend</artifactId>
<packaging>jar</packaging>
<build>
<plugins>
<!-- clean the dist directory used by Angular -->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.3.2</version>
<configuration>
<filesets>
<fileset>
<directory>dist</directory>
</fileset>
</filesets>
</configuration>
</plugin>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.15.0</version>
<executions>
<!-- Install node and npm -->
<execution>
<id>Install Node and NPM</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<nodeVersion>v20.10.0</nodeVersion>
</configuration>
</execution>
<!-- clean install -->
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
</execution>
<!-- code validation -->
<execution>
<id>npm run lint</id>
<goals>
<goal>npm</goal>
</goals>
<phase>test</phase>
<configuration>
<arguments>run lint</arguments>
</configuration>
</execution>
<!-- build app -->
<execution>
<id>npm run build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build --prod</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<!-- we copy the content of the frontend directory in the final artifact -->
<directory>dist/frontend/browser</directory>
</resource>
</resources>
</build>
</project>
The Java Backend pom.xml
You have to import the frontend module, the frontend code will be included in your final package.
There a dependency with the frontend module. This solution is not ideal because the backend module is not completely independent.
We use the maven-dependency-plugin
to unpack the content of the frontend and copy the content in our backend artifact.
The Angular application is copied in the folder /classes/static
, this folder is used to serve static content in Spring Boot.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>dev.marco</groupId>
<artifactId>java-angular-example</artifactId>
<version>0.1.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.outputEncoding>UTF-8</project.build.outputEncoding>
</properties>
<artifactId>backend</artifactId>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>frontend</artifactId>
<version>${project.version}</version>
<type>jar</type>
<!-- we add optional to work with the backend package without building the frontend -->
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- This is used to document the API -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.2.1</version>
<configuration>
<mainClass>dev.marco.example.springboot.Application</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<id>merge</id>
<phase>initialize</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>${project.groupId}</groupId>
<artifactId>frontend</artifactId>
<type>jar</type>
<overWrite>true</overWrite>
<outputDirectory>${project.build.directory}/classes/static</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Spring
In our example we added a REST controller to deploy and execute the basic application.
During the development phase we are starting the Spring backend application and the Angular application in separated instances.
To allow the communication between the HttpClient
of Angular and the RestController
of Spring we have to enable a Cross Origin Resource Sharing in Spring Boot or our requests will be refused.
@RestController
// we allow localhost:4200 for testing purposes
@CrossOrigin(origins = "http://localhost:4200")
public class HelloController {
@RequestMapping(value = "/message", produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, String> index() {
return Collections.singletonMap("message", " from marmo.dev (Spring Boot 3.x)!");
}
}
Angular
Our Angular application doesn't do anything special, we simply call the backend service and we show the message received:
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, HttpClientModule],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent implements OnInit {
title = 'frontend';
message = '';
constructor(private readonly http: HttpClient) {
}
ngOnInit(): void {
this.http.get<{message: string}>('http://localhost:8080/message').pipe(
first(),
tap(result => console.log('Message received from the server: ', result)),
map(result => this.message = result.message)
).subscribe();
}
}
Deploy a WAR with Tomcat
If you need to deploy a WAR using Tomcat you can use the following branch
(To update!) https://github.com/marco76/java-angular-basic/tree/feature/war-for-tomcat
You need to
-
mvn clean package
-
In backend there will be a file: angular-spring-example.war
-
Deploy the file, it should be visible under: localhost:8080/angular-spring-example, if you want to run it as / you have to rename it in ROOT before the deployment.
Major changes in relation to the JAR version:
backend/pom.xml
We change the type of packaging and we add a dependency to spring-boot-starter-tomcat
<packaging>war</packaging>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
backend/src/main/java/dev/marco/example/springboot/Application.java
@SpringBootApplication
// we extend SpringBootServletInitializer
public class Application extends SpringBootServletInitializer {
It's particularly important in case of WAR to check if the deployment context is correct, here you can find more information: https://marmo.dev/angular-with-java#war-deployment-tomcat-wildfly-etc
Bonus: features showcase
To the original minimalistic example I started to add some features. I think the example project is more interesting as showcase than as 'bootstrap'.
I added OpenAPI with SpringDocs, when you start the project is available here: http://localhost:8080/swagger-ui
I added some tests to show the difference between RestTemplateTest
and MockMvc
.
I planned to add other features like WebSockets, Spring Data and Spring Security.
If you are interested to see how some features are implemented you can leave a comment or send me a message.
Branches
Some features are integrated in separate branches:
- Open API Spring Boot example: https://github.com/marco76/java-angular-basic/tree/external-api-open-api
- Build a WAR and not a JAR: https://github.com/marco76/java-angular-basic/tree/feature/war-for-tomcat
- Latest Angular version (13): https://github.com/marco76/java-angular-basic/tree/origin/angular-13-spring-boot-262
Request a feature
You can contact me or fill a feature request if you would like to see a feature included in this showcase.