Next.js is now my favourite way to build front-end applications. With Next.js you can develop a full stack application, including your own API, however, sometimes you want to serve the front end by adding static assets to a backend application.
In this guide, we will learn how to serve Next.js static assets from a Spring Boot application.
How we usually deploy Next.js applications
Before we walk through how to serve Next.js assets from Spring Boot we should first understand how Next.js applications are usually deployed.
When we want to deploy a Next.js application we must first run next build
to build the application. As described in the official documentation here, this produces:
-
HTML files for pages using getStaticProps or Automatic Static Optimization
-
CSS files for global styles or for individually scoped styles
-
JavaScript for pre-rendering dynamic content from the Next.js server
-
JavaScript for interactivity on the client-side through React
We must then run next start
. This spins up a Node.js server that handles everything for us, including serving the static assets, handling API endpoints, routing, etc.
This is the recommended way to deploy Next.js applications however, in some cases, we don’t want to have a separate server for our front end if we’re already implementing back-end functionality in a different place, especially if we’re not using Node.js.
In the next section, we’ll explore how to serve our Next.js application using Spring Boot, without requiring a Node.js server.
Deploying a Next.js application using Spring Boot
The easiest way to explain how to do this is through a demo. In this section, I’ll walk through a step-by-step guide.
Setting up
First, let’s create a folder called spring-boot-nextjs-demo
:
<span>mkdir </span>spring-boot-nextjs-demo<span>cd </span>spring-boot-nextjs-demo<span>mkdir </span>spring-boot-nextjs-demo <span>cd </span>spring-boot-nextjs-demomkdir spring-boot-nextjs-demo cd spring-boot-nextjs-demo
Enter fullscreen mode Exit fullscreen mode
Then we’ll create a Spring Boot application using the Spring Initializr tool.
You can create the project with all of the required tools using the link here. Click “GENERATE” and extract the zip to the spring-boot-nextjs-demo
folder.
Next, let’s create the Next.js app. Make sure you’re in the spring-boot-nextjs-demo
folder and run:
npx create-next-app@latest <span>--ts</span>npx create-next-app@latest <span>--ts</span>npx create-next-app@latest --ts
Enter fullscreen mode Exit fullscreen mode
Call the app nextjs-app
and follow the rest of the defaults.
Setting up the Maven modules
In this project we will have the following Maven structure:
-
a parent Maven project called
spring-boot-nextjs-demo
-
a child module called
nextjs-app
, which is responsible for building the Next.js application -
a child module called
spring-boot-app
, which is responsible for building our Spring Boot application and adding our Next.js assets to the jar.
Parent Maven setup
We’ll create a pom.xml
file in the root folder, spring-boot-nextjs-demo
, which contains the following:
<span><?xml version="1.0" encoding="UTF-8"?></span><span><project</span> <span>xmlns=</span><span>"http://maven.apache.org/POM/4.0.0"</span> <span>xmlns:xsi=</span><span>"http://www.w3.org/2001/XMLSchema-instance"</span><span>xsi:schemaLocation=</span><span>"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"</span><span>></span><span><modelVersion></span>4.0.0<span></modelVersion></span><span><parent></span><span><groupId></span>org.springframework.boot<span></groupId></span><span><artifactId></span>spring-boot-starter-parent<span></artifactId></span><span><version></span>3.0.0<span></version></span><span><relativePath/></span> <span><!-- lookup parent from repository --></span><span></parent></span><span><groupId></span>com.stacktobasics<span></groupId></span><span><artifactId></span>spring-boot-nextjs-demo<span></artifactId></span><span><version></span>0.0.1-SNAPSHOT<span></version></span><span><packaging></span>pom<span></packaging></span><span><name></span>Spring Boot Nextjs Demo<span></name></span><span><modules></span><span><module></span>nextjs-app<span></module></span><span><module></span>spring-boot-app<span></module></span><span></modules></span><span><properties></span><span><java.version></span>17<span></java.version></span><span></properties></span><span></project></span><span><?xml version="1.0" encoding="UTF-8"?></span> <span><project</span> <span>xmlns=</span><span>"http://maven.apache.org/POM/4.0.0"</span> <span>xmlns:xsi=</span><span>"http://www.w3.org/2001/XMLSchema-instance"</span> <span>xsi:schemaLocation=</span><span>"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"</span><span>></span> <span><modelVersion></span>4.0.0<span></modelVersion></span> <span><parent></span> <span><groupId></span>org.springframework.boot<span></groupId></span> <span><artifactId></span>spring-boot-starter-parent<span></artifactId></span> <span><version></span>3.0.0<span></version></span> <span><relativePath/></span> <span><!-- lookup parent from repository --></span> <span></parent></span> <span><groupId></span>com.stacktobasics<span></groupId></span> <span><artifactId></span>spring-boot-nextjs-demo<span></artifactId></span> <span><version></span>0.0.1-SNAPSHOT<span></version></span> <span><packaging></span>pom<span></packaging></span> <span><name></span>Spring Boot Nextjs Demo<span></name></span> <span><modules></span> <span><module></span>nextjs-app<span></module></span> <span><module></span>spring-boot-app<span></module></span> <span></modules></span> <span><properties></span> <span><java.version></span>17<span></java.version></span> <span></properties></span> <span></project></span><?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 https://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.0.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.stacktobasics</groupId> <artifactId>spring-boot-nextjs-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>pom</packaging> <name>Spring Boot Nextjs Demo</name> <modules> <module>nextjs-app</module> <module>spring-boot-app</module> </modules> <properties> <java.version>17</java.version> </properties> </project>
Enter fullscreen mode Exit fullscreen mode
This quite simply inherits from the Spring Boot parent pom (as is usually the case for Spring Boot applications) and declares two child modules: our Next.js app and our Spring Boot app.
Next.js Maven setup
We’ll create a pom.xml
file in our nextjs-app
folder, which contains the following:
<span><?xml version="1.0" encoding="UTF-8"?></span><span><project</span> <span>xmlns=</span><span>"http://maven.apache.org/POM/4.0.0"</span> <span>xmlns:xsi=</span><span>"http://www.w3.org/2001/XMLSchema-instance"</span><span>xsi:schemaLocation=</span><span>"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"</span><span>></span><span><parent></span><span><artifactId></span>spring-boot-nextjs-demo<span></artifactId></span><span><groupId></span>com.stacktobasics<span></groupId></span><span><version></span>0.0.1-SNAPSHOT<span></version></span><span></parent></span><span><modelVersion></span>4.0.0<span></modelVersion></span><span><artifactId></span>nextjs-app<span></artifactId></span><span><build></span><span><plugins></span><span><plugin></span><span><groupId></span>com.github.eirslett<span></groupId></span><span><artifactId></span>frontend-maven-plugin<span></artifactId></span><span><version></span>1.11.3<span></version></span><span><executions></span><span><execution></span><span><id></span>install node and npm<span></id></span><span><goals></span><span><goal></span>install-node-and-npm<span></goal></span><span></goals></span><span></execution></span><span><execution></span><span><id></span>npm install<span></id></span><span><goals></span><span><goal></span>npm<span></goal></span><span></goals></span><span><phase></span>generate-resources<span></phase></span><span></execution></span><span><execution></span><span><id></span>npm run build<span></id></span><span><goals></span><span><goal></span>npm<span></goal></span><span></goals></span><span><phase></span>generate-resources<span></phase></span><span><configuration></span><span><arguments></span>run build<span></arguments></span><span></configuration></span><span></execution></span><span></executions></span><span><configuration></span><span><nodeVersion></span>v18.14.0<span></nodeVersion></span><span></configuration></span><span></plugin></span><span></plugins></span><span></build></span><span></project></span><span><?xml version="1.0" encoding="UTF-8"?></span> <span><project</span> <span>xmlns=</span><span>"http://maven.apache.org/POM/4.0.0"</span> <span>xmlns:xsi=</span><span>"http://www.w3.org/2001/XMLSchema-instance"</span> <span>xsi:schemaLocation=</span><span>"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"</span><span>></span> <span><parent></span> <span><artifactId></span>spring-boot-nextjs-demo<span></artifactId></span> <span><groupId></span>com.stacktobasics<span></groupId></span> <span><version></span>0.0.1-SNAPSHOT<span></version></span> <span></parent></span> <span><modelVersion></span>4.0.0<span></modelVersion></span> <span><artifactId></span>nextjs-app<span></artifactId></span> <span><build></span> <span><plugins></span> <span><plugin></span> <span><groupId></span>com.github.eirslett<span></groupId></span> <span><artifactId></span>frontend-maven-plugin<span></artifactId></span> <span><version></span>1.11.3<span></version></span> <span><executions></span> <span><execution></span> <span><id></span>install node and npm<span></id></span> <span><goals></span> <span><goal></span>install-node-and-npm<span></goal></span> <span></goals></span> <span></execution></span> <span><execution></span> <span><id></span>npm install<span></id></span> <span><goals></span> <span><goal></span>npm<span></goal></span> <span></goals></span> <span><phase></span>generate-resources<span></phase></span> <span></execution></span> <span><execution></span> <span><id></span>npm run build<span></id></span> <span><goals></span> <span><goal></span>npm<span></goal></span> <span></goals></span> <span><phase></span>generate-resources<span></phase></span> <span><configuration></span> <span><arguments></span>run build<span></arguments></span> <span></configuration></span> <span></execution></span> <span></executions></span> <span><configuration></span> <span><nodeVersion></span>v18.14.0<span></nodeVersion></span> <span></configuration></span> <span></plugin></span> <span></plugins></span> <span></build></span> <span></project></span><?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> <artifactId>spring-boot-nextjs-demo</artifactId> <groupId>com.stacktobasics</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>nextjs-app</artifactId> <build> <plugins> <plugin> <groupId>com.github.eirslett</groupId> <artifactId>frontend-maven-plugin</artifactId> <version>1.11.3</version> <executions> <execution> <id>install node and npm</id> <goals> <goal>install-node-and-npm</goal> </goals> </execution> <execution> <id>npm install</id> <goals> <goal>npm</goal> </goals> <phase>generate-resources</phase> </execution> <execution> <id>npm run build</id> <goals> <goal>npm</goal> </goals> <phase>generate-resources</phase> <configuration> <arguments>run build</arguments> </configuration> </execution> </executions> <configuration> <nodeVersion>v18.14.0</nodeVersion> </configuration> </plugin> </plugins> </build> </project>
Enter fullscreen mode Exit fullscreen mode
In this maven module we’re including the frontend-maven-plugin
.
When this module is built this plugin will run the execution steps that we’ve defined, npm install
and npm run build
in this case.
If we look in the package.json
file generated by Next.js we’ll see that npm run build
runs next build
, which generates all the files required for a production build in the out
folder.
Spring Boot Maven setup
Next, let’s edit the pom.xml
file in the spring-boot-app
folder to inherit from our new parent pom, and also to include our Next.js static assets in our Spring Boot application jar. The final pom.xml file will look like this:
<span><?xml version="1.0" encoding="UTF-8"?></span><span><project</span> <span>xmlns=</span><span>"http://maven.apache.org/POM/4.0.0"</span> <span>xmlns:xsi=</span><span>"http://www.w3.org/2001/XMLSchema-instance"</span><span>xsi:schemaLocation=</span><span>"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"</span><span>></span><span><modelVersion></span>4.0.0<span></modelVersion></span><span><!-- Change parent tag from spring-boot-starter-parent to spring-boot-nextjs-demo --></span><span><parent></span><span><groupId></span>com.stacktobasics<span></groupId></span><span><artifactId></span>spring-boot-nextjs-demo<span></artifactId></span><span><version></span>0.0.1-SNAPSHOT<span></version></span><span></parent></span><span><groupId></span>com.stacktobasics<span></groupId></span><span><artifactId></span>spring-boot-app<span></artifactId></span><span><version></span>0.0.1-SNAPSHOT<span></version></span><span><name></span>spring-boot-app<span></name></span><span><description></span>Demo project for Spring Boot that serves Next.js assets<span></description></span><span><properties></span><span><java.version></span>17<span></java.version></span><span></properties></span><span><dependencies></span><span><dependency></span><span><groupId></span>org.springframework.boot<span></groupId></span><span><artifactId></span>spring-boot-starter-web<span></artifactId></span><span></dependency></span><span><dependency></span><span><groupId></span>org.springframework.boot<span></groupId></span><span><artifactId></span>spring-boot-starter-test<span></artifactId></span><span><scope></span>test<span></scope></span><span></dependency></span><span></dependencies></span><span><build></span><span><plugins></span><span><plugin></span><span><groupId></span>org.springframework.boot<span></groupId></span><span><artifactId></span>spring-boot-maven-plugin<span></artifactId></span><span></plugin></span><span><plugin></span><span><artifactId></span>maven-resources-plugin<span></artifactId></span><span><version></span>3.2.0<span></version></span><span><executions></span><span><execution></span><span><id></span>copy-resources<span></id></span><span><phase></span>generate-resources<span></phase></span><span><goals></span><span><goal></span>copy-resources<span></goal></span><span></goals></span><span><configuration></span><span><outputDirectory></span>${basedir}/target/classes/static<span></outputDirectory></span><span><resources></span><span><resource></span><span><directory></span>${basedir}/../nextjs-app/out<span></directory></span><span><filtering></span>false<span></filtering></span><span></resource></span><span></resources></span><span></configuration></span><span></execution></span><span></executions></span><span></plugin></span><span></plugins></span><span></build></span><span></project></span><span><?xml version="1.0" encoding="UTF-8"?></span> <span><project</span> <span>xmlns=</span><span>"http://maven.apache.org/POM/4.0.0"</span> <span>xmlns:xsi=</span><span>"http://www.w3.org/2001/XMLSchema-instance"</span> <span>xsi:schemaLocation=</span><span>"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"</span><span>></span> <span><modelVersion></span>4.0.0<span></modelVersion></span> <span><!-- Change parent tag from spring-boot-starter-parent to spring-boot-nextjs-demo --></span> <span><parent></span> <span><groupId></span>com.stacktobasics<span></groupId></span> <span><artifactId></span>spring-boot-nextjs-demo<span></artifactId></span> <span><version></span>0.0.1-SNAPSHOT<span></version></span> <span></parent></span> <span><groupId></span>com.stacktobasics<span></groupId></span> <span><artifactId></span>spring-boot-app<span></artifactId></span> <span><version></span>0.0.1-SNAPSHOT<span></version></span> <span><name></span>spring-boot-app<span></name></span> <span><description></span>Demo project for Spring Boot that serves Next.js assets<span></description></span> <span><properties></span> <span><java.version></span>17<span></java.version></span> <span></properties></span> <span><dependencies></span> <span><dependency></span> <span><groupId></span>org.springframework.boot<span></groupId></span> <span><artifactId></span>spring-boot-starter-web<span></artifactId></span> <span></dependency></span> <span><dependency></span> <span><groupId></span>org.springframework.boot<span></groupId></span> <span><artifactId></span>spring-boot-starter-test<span></artifactId></span> <span><scope></span>test<span></scope></span> <span></dependency></span> <span></dependencies></span> <span><build></span> <span><plugins></span> <span><plugin></span> <span><groupId></span>org.springframework.boot<span></groupId></span> <span><artifactId></span>spring-boot-maven-plugin<span></artifactId></span> <span></plugin></span> <span><plugin></span> <span><artifactId></span>maven-resources-plugin<span></artifactId></span> <span><version></span>3.2.0<span></version></span> <span><executions></span> <span><execution></span> <span><id></span>copy-resources<span></id></span> <span><phase></span>generate-resources<span></phase></span> <span><goals></span> <span><goal></span>copy-resources<span></goal></span> <span></goals></span> <span><configuration></span> <span><outputDirectory></span>${basedir}/target/classes/static<span></outputDirectory></span> <span><resources></span> <span><resource></span> <span><directory></span>${basedir}/../nextjs-app/out<span></directory></span> <span><filtering></span>false<span></filtering></span> <span></resource></span> <span></resources></span> <span></configuration></span> <span></execution></span> <span></executions></span> <span></plugin></span> <span></plugins></span> <span></build></span> <span></project></span><?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <!-- Change parent tag from spring-boot-starter-parent to spring-boot-nextjs-demo --> <parent> <groupId>com.stacktobasics</groupId> <artifactId>spring-boot-nextjs-demo</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.stacktobasics</groupId> <artifactId>spring-boot-app</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-boot-app</name> <description>Demo project for Spring Boot that serves Next.js assets</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>3.2.0</version> <executions> <execution> <id>copy-resources</id> <phase>generate-resources</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${basedir}/target/classes/static</outputDirectory> <resources> <resource> <directory>${basedir}/../nextjs-app/out</directory> <filtering>false</filtering> </resource> </resources> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
Enter fullscreen mode Exit fullscreen mode
Then maven-resources-plugin
will copy our Next.js static assets created by our Next.js maven module, into the static folder in our jar.
Enable static export
Finally, we need to enable the exporting of our Next.js application to HTML. To do this, edit the build script in package.json
to include next export
:
<span>"scripts"</span><span>:</span><span> </span><span>{</span><span> </span><span>"dev"</span><span>:</span><span> </span><span>"next dev"</span><span>,</span><span> </span><span>"build"</span><span>:</span><span> </span><span>"next build && next export"</span><span>,</span><span> </span><span>"start"</span><span>:</span><span> </span><span>"next start"</span><span>,</span><span> </span><span>"lint"</span><span>:</span><span> </span><span>"next lint"</span><span> </span><span>}</span><span>,</span><span> </span><span>"scripts"</span><span>:</span><span> </span><span>{</span><span> </span><span>"dev"</span><span>:</span><span> </span><span>"next dev"</span><span>,</span><span> </span><span>"build"</span><span>:</span><span> </span><span>"next build && next export"</span><span>,</span><span> </span><span>"start"</span><span>:</span><span> </span><span>"next start"</span><span>,</span><span> </span><span>"lint"</span><span>:</span><span> </span><span>"next lint"</span><span> </span><span>}</span><span>,</span><span> </span>"scripts": { "dev": "next dev", "build": "next build && next export", "start": "next start", "lint": "next lint" },
Enter fullscreen mode Exit fullscreen mode
Now, let’s build the application!
Note : If you don’t have Maven installed then copy the .mvn
folder and the mvnw
file (or mvnw.cmd
file if you’re on windows) to the root directory, spring-boot-nextjs-demo
.
From the root directory, run mvnw clean install
.
Oops! The build failed!
You will see something like:
<span>[</span>INFO] info - using build directory: D:<span>\D</span>ocuments<span>\D</span>evelopment<span>\s</span>pring-boot-nextjs-demo<span>\n</span>extjs-app<span>\.</span>next<span>[</span>INFO] info - Copying <span>"static build"</span> directory<span>[</span>INFO] info - No <span>"exportPathMap"</span> found <span>in</span> <span>"D:</span><span>\D</span><span>ocuments</span><span>\D</span><span>evelopment</span><span>\s</span><span>pring-boot-nextjs-demo</span><span>\n</span><span>extjs-app</span><span>\n</span><span>ext.con fig.js"</span><span>.</span> Generating map from <span>"./pages"</span><span>[</span>INFO] Error: Image Optimization using Next.js<span>' default loader is not compatible with `next export`. [INFO] Possible solutions: [INFO] - Use `next start` to run a server, which includes the Image Optimization API. [INFO] - Configure `images.unoptimized = true` in `next.config.js` to disable the Image Optimization API. [INFO] Read more: https://nextjs.org/docs/messages/export-image-api [INFO] at D:\Documents\Development\spring-boot-nextjs-demo\nextjs-app\node_modules\next\dist\export\index.js :153:23 [INFO] at async Span.traceAsyncFn (D:\Documents\Development\spring-boot-nextjs-demo\nextjs-app\node_modules\ next\dist\trace\trace.js:79:20) [INFO] ------------------------------------------------------------------------ [INFO] Reactor Summary for Spring Boot Nextjs Demo 0.0.1-SNAPSHOT: [INFO] [INFO] Spring Boot Nextjs Demo ............................ SUCCESS [0.406 s] [INFO] nextjs-app ......................................... FAILURE [17.515 s] [INFO] spring-boot-app .................................... SKIPPED [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 18.369 s [INFO] Finished at: 2023-02-18T18:08:02Z [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal com.github.eirslett:frontend-maven-plugin:1.11.3:npm (npm run build) on project n extjs-app: Failed to run task: '</span>npm run build<span>' failed. org.apache.commons.exec.ExecuteException: Process exited with an error: 1 (Exit value: 1) -> [Help 1] </span><span>[</span>INFO] info - using build directory: D:<span>\D</span>ocuments<span>\D</span>evelopment<span>\s</span>pring-boot-nextjs-demo<span>\n</span>extjs-app<span>\.</span>next <span>[</span>INFO] info - Copying <span>"static build"</span> directory <span>[</span>INFO] info - No <span>"exportPathMap"</span> found <span>in</span> <span>"D:</span><span>\D</span><span>ocuments</span><span>\D</span><span>evelopment</span><span>\s</span><span>pring-boot-nextjs-demo</span><span>\n</span><span>extjs-app</span><span>\n</span><span>ext.con fig.js"</span><span>.</span> Generating map from <span>"./pages"</span> <span>[</span>INFO] Error: Image Optimization using Next.js<span>' default loader is not compatible with `next export`. [INFO] Possible solutions: [INFO] - Use `next start` to run a server, which includes the Image Optimization API. [INFO] - Configure `images.unoptimized = true` in `next.config.js` to disable the Image Optimization API. [INFO] Read more: https://nextjs.org/docs/messages/export-image-api [INFO] at D:\Documents\Development\spring-boot-nextjs-demo\nextjs-app\node_modules\next\dist\export\index.js :153:23 [INFO] at async Span.traceAsyncFn (D:\Documents\Development\spring-boot-nextjs-demo\nextjs-app\node_modules\ next\dist\trace\trace.js:79:20) [INFO] ------------------------------------------------------------------------ [INFO] Reactor Summary for Spring Boot Nextjs Demo 0.0.1-SNAPSHOT: [INFO] [INFO] Spring Boot Nextjs Demo ............................ SUCCESS [0.406 s] [INFO] nextjs-app ......................................... FAILURE [17.515 s] [INFO] spring-boot-app .................................... SKIPPED [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 18.369 s [INFO] Finished at: 2023-02-18T18:08:02Z [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal com.github.eirslett:frontend-maven-plugin:1.11.3:npm (npm run build) on project n extjs-app: Failed to run task: '</span>npm run build<span>' failed. org.apache.commons.exec.ExecuteException: Process exited with an error: 1 (Exit value: 1) -> [Help 1] </span>[INFO] info - using build directory: D:\Documents\Development\spring-boot-nextjs-demo\nextjs-app\.next [INFO] info - Copying "static build" directory [INFO] info - No "exportPathMap" found in "D:\Documents\Development\spring-boot-nextjs-demo\nextjs-app\next.con fig.js". Generating map from "./pages" [INFO] Error: Image Optimization using Next.js' default loader is not compatible with `next export`. [INFO] Possible solutions: [INFO] - Use `next start` to run a server, which includes the Image Optimization API. [INFO] - Configure `images.unoptimized = true` in `next.config.js` to disable the Image Optimization API. [INFO] Read more: https://nextjs.org/docs/messages/export-image-api [INFO] at D:\Documents\Development\spring-boot-nextjs-demo\nextjs-app\node_modules\next\dist\export\index.js :153:23 [INFO] at async Span.traceAsyncFn (D:\Documents\Development\spring-boot-nextjs-demo\nextjs-app\node_modules\ next\dist\trace\trace.js:79:20) [INFO] ------------------------------------------------------------------------ [INFO] Reactor Summary for Spring Boot Nextjs Demo 0.0.1-SNAPSHOT: [INFO] [INFO] Spring Boot Nextjs Demo ............................ SUCCESS [0.406 s] [INFO] nextjs-app ......................................... FAILURE [17.515 s] [INFO] spring-boot-app .................................... SKIPPED [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 18.369 s [INFO] Finished at: 2023-02-18T18:08:02Z [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal com.github.eirslett:frontend-maven-plugin:1.11.3:npm (npm run build) on project n extjs-app: Failed to run task: 'npm run build' failed. org.apache.commons.exec.ExecuteException: Process exited with an error: 1 (Exit value: 1) -> [Help 1]
Enter fullscreen mode Exit fullscreen mode
Disabling Next.js’ server-side features
The reason we see this error is that there are limitations when using next export
. We’ll cover this in more detail in the next section, but for now, change your next.config.js
file in nextjs-app
to the following:
<span>/** @type {import('next').NextConfig} */</span><span>const</span> <span>nextConfig</span> <span>=</span> <span>{</span><span>reactStrictMode</span><span>:</span> <span>true</span><span>,</span><span>images</span><span>:</span> <span>{</span><span>unoptimized</span><span>:</span> <span>true</span><span>},</span><span>}</span><span>module</span><span>.</span><span>exports</span> <span>=</span> <span>nextConfig</span><span>/** @type {import('next').NextConfig} */</span> <span>const</span> <span>nextConfig</span> <span>=</span> <span>{</span> <span>reactStrictMode</span><span>:</span> <span>true</span><span>,</span> <span>images</span><span>:</span> <span>{</span> <span>unoptimized</span><span>:</span> <span>true</span> <span>},</span> <span>}</span> <span>module</span><span>.</span><span>exports</span> <span>=</span> <span>nextConfig</span>/** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, images: { unoptimized: true }, } module.exports = nextConfig
Enter fullscreen mode Exit fullscreen mode
Now let’s run our build again using mvnw clean install
Hopefully, by the end of the build, you should see:
<span>[</span>INFO] <span>------------------------------------------------------------------------</span><span>[</span>INFO] Reactor Summary <span>for </span>Spring Boot Nextjs Demo 0.0.1-SNAPSHOT:<span>[</span>INFO]<span>[</span>INFO] Spring Boot Nextjs Demo ............................ SUCCESS <span>[</span>0.355 s]<span>[</span>INFO] nextjs-app ......................................... SUCCESS <span>[</span>01:39 min]<span>[</span>INFO] spring-boot-app .................................... SUCCESS <span>[</span>8.813 s]<span>[</span>INFO] <span>------------------------------------------------------------------------</span><span>[</span>INFO] BUILD SUCCESS<span>[</span>INFO] <span>------------------------------------------------------------------------</span><span>[</span>INFO] Total <span>time</span>: 01:49 min<span>[</span>INFO] Finished at: 2023-02-18T18:02:11Z<span>[</span>INFO] <span>------------------------------------------------------------------------</span><span>[</span>INFO] <span>------------------------------------------------------------------------</span> <span>[</span>INFO] Reactor Summary <span>for </span>Spring Boot Nextjs Demo 0.0.1-SNAPSHOT: <span>[</span>INFO] <span>[</span>INFO] Spring Boot Nextjs Demo ............................ SUCCESS <span>[</span>0.355 s] <span>[</span>INFO] nextjs-app ......................................... SUCCESS <span>[</span>01:39 min] <span>[</span>INFO] spring-boot-app .................................... SUCCESS <span>[</span>8.813 s] <span>[</span>INFO] <span>------------------------------------------------------------------------</span> <span>[</span>INFO] BUILD SUCCESS <span>[</span>INFO] <span>------------------------------------------------------------------------</span> <span>[</span>INFO] Total <span>time</span>: 01:49 min <span>[</span>INFO] Finished at: 2023-02-18T18:02:11Z <span>[</span>INFO] <span>------------------------------------------------------------------------</span>[INFO] ------------------------------------------------------------------------ [INFO] Reactor Summary for Spring Boot Nextjs Demo 0.0.1-SNAPSHOT: [INFO] [INFO] Spring Boot Nextjs Demo ............................ SUCCESS [0.355 s] [INFO] nextjs-app ......................................... SUCCESS [01:39 min] [INFO] spring-boot-app .................................... SUCCESS [8.813 s] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 01:49 min [INFO] Finished at: 2023-02-18T18:02:11Z [INFO] ------------------------------------------------------------------------
Enter fullscreen mode Exit fullscreen mode
Let’s test our spring boot app to see if it’s rendering our Next.js static assets.
Run java -jar spring-boot-app/target/spring-boot-app-0.0.1-SNAPSHOT.jar
.
Once you see c.s.s.SpringBootAppApplication : Started SpringBootAppApplication in 2.334 seconds (process running for 2.847)
go to http://localhost:8080/.
You should see the generated HTML from the index.tsx
page:
Adding an Endpoint to our API
To make sure that we can reach both our API and our HTML pages let’s add a simple controller.
Under spring-boot-app/src/main/java/com.stacktobasics.springbootapp
add the following HelloController.java
file:
<span>package</span> <span>com.stacktobasics.springbootapp</span><span>;</span><span>import</span> <span>org.springframework.web.bind.annotation.GetMapping</span><span>;</span><span>import</span> <span>org.springframework.web.bind.annotation.RestController</span><span>;</span><span>@RestController</span><span>public</span> <span>class</span> <span>HelloController</span> <span>{</span><span>@GetMapping</span><span>(</span><span>"/api/hello-world"</span><span>)</span><span>public</span> <span>String</span> <span>sayHello</span><span>()</span> <span>{</span><span>return</span> <span>"Hello!"</span><span>;</span><span>}</span><span>}</span><span>package</span> <span>com.stacktobasics.springbootapp</span><span>;</span> <span>import</span> <span>org.springframework.web.bind.annotation.GetMapping</span><span>;</span> <span>import</span> <span>org.springframework.web.bind.annotation.RestController</span><span>;</span> <span>@RestController</span> <span>public</span> <span>class</span> <span>HelloController</span> <span>{</span> <span>@GetMapping</span><span>(</span><span>"/api/hello-world"</span><span>)</span> <span>public</span> <span>String</span> <span>sayHello</span><span>()</span> <span>{</span> <span>return</span> <span>"Hello!"</span><span>;</span> <span>}</span> <span>}</span>package com.stacktobasics.springbootapp; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/api/hello-world") public String sayHello() { return "Hello!"; } }
Enter fullscreen mode Exit fullscreen mode
Let’s rebuild and re-run our application.
Stop the java -jar
process if you haven’t already and run mvnw clean install
, followed by java -jar spring-boot-app/target/spring-boot-app-0.0.1-SNAPSHOT.jar
.
Once our Spring Boot app has started, going to http://localhost:8080/ will again show us the landing page.
Going to http://localhost:8080/api/hello-world will return Hello!
as expected, proving that Spring Boot is still routing to our API correctly!
Adding another Next.js page
Lastly, let’s check that we’re able to navigate between different Next.js pages.
Add a new file called hello.tsx
under the nextjs-app\pages
folder with the following contents:
<span>export</span> <span>default</span> <span>function</span> <span>Home</span><span>()</span> <span>{</span><span>return</span> <span>(</span><span><></span><span>Hello</span> <span>world</span><span>!</span><span><</span><span>/</span><span>> </span> <span>)</span><span>}</span><span>export</span> <span>default</span> <span>function</span> <span>Home</span><span>()</span> <span>{</span> <span>return</span> <span>(</span> <span><></span> <span>Hello</span> <span>world</span><span>!</span> <span><</span><span>/</span><span>> </span> <span>)</span> <span>}</span>export default function Home() { return ( <> Hello world! </> ) }
Enter fullscreen mode Exit fullscreen mode
Next, we’ll create a link to this page. Open the index.tsx
file and import the next/link
package:
<span>import</span> <span>Link</span> <span>from</span> <span>"</span><span>next/link</span><span>"</span><span>;</span><span>import</span> <span>Link</span> <span>from</span> <span>"</span><span>next/link</span><span>"</span><span>;</span>import Link from "next/link";
Enter fullscreen mode Exit fullscreen mode
Then, add a link to our new page at the bottom:
...<span><div></span><span><Link</span> <span>href=</span><span>{"/hello"}</span><span>></span>Go to hello page<span></Link></span><span></div></span><span></div></span><span></main></span><span></></span>)}... <span><div></span> <span><Link</span> <span>href=</span><span>{"/hello"}</span><span>></span>Go to hello page<span></Link></span> <span></div></span> <span></div></span> <span></main></span> <span></></span> ) }... <div> <Link href={"/hello"}>Go to hello page</Link> </div> </div> </main> </> ) }
Enter fullscreen mode Exit fullscreen mode
Let’s rebuild and re-run our application.
Stop the java -jar
process if you haven’t already and run mvnw clean install
, followed by java -jar spring-boot-app/target/spring-boot-app-0.0.1-SNAPSHOT.jar
.
We will now see a link to our Hello
page at the bottom of the landing page:
Clicking on this link will bring us to our new page:
However, try refreshing the page. You’ll see a 404 error:
Huh? What happened?
Routing to pages without .html
When we clicked the link to go to our hello page from our index page the request to change to a different route was handled from the client side, which worked thanks to Next.js’s routing.
However, when we navigate directly to /hello
we’re going straight to the server. Our server does not have a resource that maps to /hello
, so it returns a 404.
We do however have a file called /hello.html
. If you navigate to localhost:8080/hello.html
you will see our hello page.
This isn’t great though, we want users to be able to get to our /hello
page directly, without the .html
extension!
One easy way to do this is to add a Spring MVC controller (Note the use of @Controller
, not @RestController
!) to map any request which does not have a file extension to the corresponding HTML page.
Add a class called HtmlController
containing the following:
<span>package</span> <span>com.stacktobasics.springbootapp</span><span>;</span><span>import</span> <span>org.slf4j.Logger</span><span>;</span><span>import</span> <span>org.slf4j.LoggerFactory</span><span>;</span><span>import</span> <span>org.springframework.stereotype.Controller</span><span>;</span><span>import</span> <span>org.springframework.web.bind.annotation.PathVariable</span><span>;</span><span>import</span> <span>org.springframework.web.bind.annotation.RequestMapping</span><span>;</span><span>@Controller</span><span>public</span> <span>class</span> <span>HtmlController</span> <span>{</span><span>Logger</span> <span>logger</span> <span>=</span> <span>LoggerFactory</span><span>.</span><span>getLogger</span><span>(</span><span>HtmlController</span><span>.</span><span>class</span><span>);</span><span>@RequestMapping</span><span>(</span><span>"/{page:^(?!.*[.].*$).*$}"</span><span>)</span><span>public</span> <span>String</span> <span>requestPage</span><span>(</span><span>@PathVariable</span><span>(</span><span>"page"</span><span>)</span> <span>String</span> <span>page</span><span>)</span> <span>{</span><span>String</span> <span>htmlPage</span> <span>=</span> <span>"/"</span><span>+</span><span>page</span><span>+</span><span>".html"</span><span>;</span><span>logger</span><span>.</span><span>info</span><span>(</span><span>"forwarding request to {}"</span><span>,</span> <span>htmlPage</span><span>);</span><span>return</span> <span>htmlPage</span><span>;</span><span>}</span><span>}</span><span>package</span> <span>com.stacktobasics.springbootapp</span><span>;</span> <span>import</span> <span>org.slf4j.Logger</span><span>;</span> <span>import</span> <span>org.slf4j.LoggerFactory</span><span>;</span> <span>import</span> <span>org.springframework.stereotype.Controller</span><span>;</span> <span>import</span> <span>org.springframework.web.bind.annotation.PathVariable</span><span>;</span> <span>import</span> <span>org.springframework.web.bind.annotation.RequestMapping</span><span>;</span> <span>@Controller</span> <span>public</span> <span>class</span> <span>HtmlController</span> <span>{</span> <span>Logger</span> <span>logger</span> <span>=</span> <span>LoggerFactory</span><span>.</span><span>getLogger</span><span>(</span><span>HtmlController</span><span>.</span><span>class</span><span>);</span> <span>@RequestMapping</span><span>(</span><span>"/{page:^(?!.*[.].*$).*$}"</span><span>)</span> <span>public</span> <span>String</span> <span>requestPage</span><span>(</span><span>@PathVariable</span><span>(</span><span>"page"</span><span>)</span> <span>String</span> <span>page</span><span>)</span> <span>{</span> <span>String</span> <span>htmlPage</span> <span>=</span> <span>"/"</span><span>+</span><span>page</span><span>+</span><span>".html"</span><span>;</span> <span>logger</span><span>.</span><span>info</span><span>(</span><span>"forwarding request to {}"</span><span>,</span> <span>htmlPage</span><span>);</span> <span>return</span> <span>htmlPage</span><span>;</span> <span>}</span> <span>}</span>package com.stacktobasics.springbootapp; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class HtmlController { Logger logger = LoggerFactory.getLogger(HtmlController.class); @RequestMapping("/{page:^(?!.*[.].*$).*$}") public String requestPage(@PathVariable("page") String page) { String htmlPage = "/"+page+".html"; logger.info("forwarding request to {}", htmlPage); return htmlPage; } }
Enter fullscreen mode Exit fullscreen mode
Let’s rebuild and re-run our application.
Stop the java -jar
process if you haven’t already and run mvnw clean install
, followed by java -jar spring-boot-app/target/spring-boot-app-0.0.1-SNAPSHOT.jar
.
Now going to localhost:8080/hello
will correctly render our hello page.
We also see the following log, showing that our HtmlController
is working:
[nio-8080-exec-2] c.s.springbootapp.HtmlController : forwarding request to /hello.html
Making local development easier
Whilst what we’ve done above works well for generating a production build it’s quite cumbersome for local development.
Instead, I would recommend using next dev
for local development and starting your Spring Boot app in the usual way – such as from an IDE like IntelliJ.
Let’s test this out!
Run npm run dev
(which runs next dev
) from the nextjs-app
folder and verify that you can load both localhost:3000
and localhost:3000/hello
.
Start up your Spring Boot app from your IDE.
You might have guessed a problem we will face with this approach (hint: it’s related to port numbers!).
When we’ve built our application using maven and running java -jar
then both our API and our front-end assets are being served from our Spring Boot application on port 8080.
However, when using next dev
the front end will be served from Node.js on port 3000.
This can be problematic when making calls to our back-end service from our front-end application.
Let’s show this issue with an example.
Firstly, stop both your Next.js and Spring Boot processes.
Secondly, change your hello.tsx
file to make a call to the api/hello-world
endpoint, like so:
<span>import</span> <span>{</span> <span>useState</span><span>,</span> <span>useEffect</span> <span>}</span> <span>from</span> <span>'</span><span>react</span><span>'</span><span>export</span> <span>default</span> <span>function</span> <span>Home</span><span>()</span> <span>{</span><span>const</span> <span>[</span><span>helloResponse</span><span>,</span> <span>setHelloResponse</span><span>]</span> <span>=</span> <span>useState</span><span><</span><span>string</span><span>|</span><span>null</span><span>></span><span>(</span><span>null</span><span>)</span><span>useEffect</span><span>(()</span> <span>=></span> <span>{</span><span>fetch</span><span>(</span><span>'</span><span>/api/hello-world</span><span>'</span><span>)</span><span>.</span><span>then</span><span>((</span><span>res</span><span>)</span> <span>=></span> <span>res</span><span>.</span><span>text</span><span>())</span><span>.</span><span>then</span><span>((</span><span>data</span><span>)</span> <span>=></span> <span>{</span><span>setHelloResponse</span><span>(</span><span>data</span><span>)</span><span>})</span><span>},</span> <span>[])</span><span>return</span> <span>(</span><span><></span><span>Hello</span> <span>world</span><span>!</span><span>Response</span> <span>from</span> <span>server</span> <span>call</span> <span>to</span> <span>/</span><span>api</span><span>/</span><span>hello</span><span>-</span><span>world</span><span>:</span> <span>{</span><span>helloResponse</span><span>}</span><span><</span><span>/</span><span>> </span> <span>)</span><span>}</span><span>import</span> <span>{</span> <span>useState</span><span>,</span> <span>useEffect</span> <span>}</span> <span>from</span> <span>'</span><span>react</span><span>'</span> <span>export</span> <span>default</span> <span>function</span> <span>Home</span><span>()</span> <span>{</span> <span>const</span> <span>[</span><span>helloResponse</span><span>,</span> <span>setHelloResponse</span><span>]</span> <span>=</span> <span>useState</span><span><</span><span>string</span><span>|</span><span>null</span><span>></span><span>(</span><span>null</span><span>)</span> <span>useEffect</span><span>(()</span> <span>=></span> <span>{</span> <span>fetch</span><span>(</span><span>'</span><span>/api/hello-world</span><span>'</span><span>)</span> <span>.</span><span>then</span><span>((</span><span>res</span><span>)</span> <span>=></span> <span>res</span><span>.</span><span>text</span><span>())</span> <span>.</span><span>then</span><span>((</span><span>data</span><span>)</span> <span>=></span> <span>{</span> <span>setHelloResponse</span><span>(</span><span>data</span><span>)</span> <span>})</span> <span>},</span> <span>[])</span> <span>return</span> <span>(</span> <span><></span> <span>Hello</span> <span>world</span><span>!</span> <span>Response</span> <span>from</span> <span>server</span> <span>call</span> <span>to</span> <span>/</span><span>api</span><span>/</span><span>hello</span><span>-</span><span>world</span><span>:</span> <span>{</span><span>helloResponse</span><span>}</span> <span><</span><span>/</span><span>> </span> <span>)</span> <span>}</span>import { useState, useEffect } from 'react' export default function Home() { const [helloResponse, setHelloResponse] = useState<string|null>(null) useEffect(() => { fetch('/api/hello-world') .then((res) => res.text()) .then((data) => { setHelloResponse(data) }) }, []) return ( <> Hello world! Response from server call to /api/hello-world: {helloResponse} </> ) }
Enter fullscreen mode Exit fullscreen mode
Run mvnw clean install
, followed by java -jar spring-boot-app/target/spring-boot-app-0.0.1-SNAPSHOT.jar
.
Then navigate to localhost:8080/hello
. This will show the following:
We can see the response is returned from our /api/hello-world
call, as expected.
This time, stop your java -jar
process, and run your Next.js app using npm run dev
and your Spring Boot app from your IDE.
Navigate again to localhost:8080/hello
.
This time you’ll see an error:
The reason we see this error is that your request is going to localhost:3000/api/hello-world
, rather than localhost:8080/api/hello-world
.
We can get around this by using Rewrites. To do this we must change our next.config.js
file to the following:
<span>/** @type {import('next').NextConfig} */</span><span>const</span> <span>nextConfig</span> <span>=</span> <span>{</span><span>reactStrictMode</span><span>:</span> <span>true</span><span>,</span><span>images</span><span>:</span> <span>{</span><span>unoptimized</span><span>:</span> <span>true</span><span>},</span><span>async</span> <span>rewrites</span><span>()</span> <span>{</span><span>return</span> <span>[</span><span>{</span><span>source</span><span>:</span> <span>'</span><span>/api/:path*</span><span>'</span><span>,</span><span>destination</span><span>:</span> <span>'</span><span>http://localhost:8080/api/:path*</span><span>'</span><span>,</span><span>},</span><span>]</span><span>}</span><span>}</span><span>module</span><span>.</span><span>exports</span> <span>=</span> <span>nextConfig</span><span>/** @type {import('next').NextConfig} */</span> <span>const</span> <span>nextConfig</span> <span>=</span> <span>{</span> <span>reactStrictMode</span><span>:</span> <span>true</span><span>,</span> <span>images</span><span>:</span> <span>{</span> <span>unoptimized</span><span>:</span> <span>true</span> <span>},</span> <span>async</span> <span>rewrites</span><span>()</span> <span>{</span> <span>return</span> <span>[</span> <span>{</span> <span>source</span><span>:</span> <span>'</span><span>/api/:path*</span><span>'</span><span>,</span> <span>destination</span><span>:</span> <span>'</span><span>http://localhost:8080/api/:path*</span><span>'</span><span>,</span> <span>},</span> <span>]</span> <span>}</span> <span>}</span> <span>module</span><span>.</span><span>exports</span> <span>=</span> <span>nextConfig</span>/** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, images: { unoptimized: true }, async rewrites() { return [ { source: '/api/:path*', destination: 'http://localhost:8080/api/:path*', }, ] } } module.exports = nextConfig
Enter fullscreen mode Exit fullscreen mode
Here we’ve added config to rewrite every request matching /api/**
to http://localhost:8080/api/**
.
Restart the Node server by re-running npm run dev
.
You’ll now see that navigating to localhost:3000/hello
renders the data from localhost:8080/api/hello-world
Limitations of hosting Next.js static assets from Spring Boot
We touched on the limitations of next export
briefly when we had to change our next.config.js
file to specify that images should not be optimised.
Next.js has a lot of features – some that work on the server-side and some that work on the client-side. When we run next export
we no longer have the Node.js server, and thus we can’t take advantage of the server-side features of Next.js that rely on a dedicated Node.js server.
It’s worth keeping this in mind as there may be features that you want to use that are server-side features. The following features are not supported:
-
Image Optimization (default loader)
-
Internationalized Routing
-
API Routes
-
Rewrites
-
Redirects
-
Headers
-
Middleware
-
Incremental Static Regeneration
-
fallback: true
-
getServerSideProps
More on this can be read in the official documentation here.
Conclusion
In this post, we have learned how to generate static assets for Next.js and serve them with a Spring Boot back-end application.
We have learned about some of the common pitfalls of this approach and found ways to get around them.
We have also learned the best way to do local development when using this approach by using Next.js Rewrites.
Finally, we have learned about some of the limitations of using the Export feature in Next.js.
All of the code from this post can be found here.
Till next time!
暂无评论内容