A Spring Boot server with a React front end
This example builds a small web application in one fabr project: a React single-page application
written in TypeScript, and a Spring Boot server that provides its API and serves its files. One
command builds both, fabr run -w restarts the server with a fresh bundle as you edit either half,
and a release build packs the lot into one executable jar for java -jar.
It assumes you have read the Java and JavaScript quick starts, and installed fabr with the Java plugin:
npm install -g @fabr-build/cli @fabr-build/javaThe project
Section titled “The project”PROJECT.fabrfrontend/ BUILD.fabr public/index.html src/main.tsx src/App.tsx src/main.cssbackend/ BUILD.fabr src/main/java/com/example/shop/ShopApplication.java src/main/java/com/example/shop/GreetingController.java src/main/resources/application.propertiesPROJECT.fabr uses both plugins, sets what the two halves are compiled for, and includes each
half’s build file:
plugin @fabr-build/java;plugin @fabr-build/js;
JVM_TARGET = jvm21;JS_TARGET = es2022-esm-browser;
include ./frontend/BUILD.fabr;include ./backend/BUILD.fabr;JVM_TARGET is the Java release the server’s classes are compiled for, and JS_TARGET says the
bundle is an ES module for browsers. Each setting is read only by the targets of its own language.
The front end
Section titled “The front end”frontend/BUILD.fabr declares the bundle:
js_bundle web { entry = src:main.tsx; srcs = src:** dom @npm:react:19.3.1 @npm:react-dom:19.3.1 @npm:@types/react:19.3.0 @npm:@types/react-dom:19.3.0; output = main-*.js -> main.js;}srcs lists what may go into the bundle: your sources, the dom flag, which gives TypeScript the
browser’s types, and the packages to build in. output names the bundle main.js, without the
content hash it is otherwise named with, because the page below loads it by that name. The sources are type-checked by the TypeScript
compiler before esbuild bundles them, so a type error fails the build.
frontend/src/main.tsx mounts the application:
import { createRoot } from "react-dom/client";import { App } from "./App";import "./main.css";
createRoot(document.getElementById("root")!).render(<App />);frontend/src/App.tsx calls the server’s API with a path relative to the page, which works
because the same server serves both:
import { useState } from "react";
export function App() { const [name, setName] = useState(""); const [greeting, setGreeting] = useState<string>();
async function greet() { const response = await fetch(`/api/greeting?name=${encodeURIComponent(name)}`); const body: { message: string } = await response.json(); setGreeting(body.message); }
return ( <main> <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Your name" /> <button onClick={greet}>Greet</button> {greeting && <p>{greeting}</p>} </main> );}frontend/src/main.css is any stylesheet; the bundle collects the CSS the entry imports into
main.css:
body { font-family: system-ui, sans-serif; margin: 2rem; }The bundle produces main.js, main.css, in a debug build their source maps, and esbuild’s
metafile.json; nothing writes an HTML page for you. frontend/public/index.html is written by hand and loads the two files
from an assets/ directory beside it:
<!doctype html><html> <head> <meta charset="utf-8"> <title>Greeter</title> <link rel="stylesheet" href="assets/main.css"> </head> <body> <div id="root"></div> <script type="module" src="assets/main.js"></script> </body></html>You can build the bundle on its own and look at it:
fabr build webfabr ls web # main.css, main.css.map, main.js, main.js.map, metafile.jsonThe server
Section titled “The server”backend/BUILD.fabr declares the server as a jar_package:
jar_package server { srcs = src/main/java:**/*.java; resources = src/main/resources:** ../frontend/public:** -> static/** web:main.* -> static/assets/main.*; deps = @mvn:org.springframework.boot:spring-boot-dependencies:4.1.2 @mvn:org.springframework.boot:spring-boot-starter-webmvc java21; main_class = com.example.shop.ShopApplication;}deps starts with Spring Boot’s bill of materials, which gives the version of the starter after
it; see BOMs. java21 says the sources are written for Java 21.
The resources are where the two halves meet. Spring Boot serves the files it finds under
static/ on the classpath, and serves static/index.html at /. So the resources put:
| In the jar | From |
|---|---|
application.properties |
src/main/resources:**, the usual Spring Boot resources. |
static/index.html |
../frontend/public:** -> static/**: the page, renamed under static/. |
static/assets/main.js, main.css, … |
web:main.* -> static/assets/main.*: the bundle and its stylesheet, with their source maps in a debug build. |
web is the bundle’s target name, so building the server builds the bundle first, and a change to
the front end’s sources changes the jar. web:main.* selects the bundle’s main files and leaves
out its metafile.json, a description of the build that the browser has no use for. Each -> is a
rename: the files the left side selects, each placed
under the new name, its * filled in from the file it selects.
The Java sources are an ordinary Spring Boot application. ShopApplication.java:
package com.example.shop;
import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplicationpublic class ShopApplication { public static void main(String[] args) { SpringApplication.run(ShopApplication.class, args); }}and GreetingController.java:
package com.example.shop;
import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;
@RestControllerpublic class GreetingController { public record Greeting(String message) {}
@GetMapping("/api/greeting") public Greeting greeting(@RequestParam(defaultValue = "world") String name) { return new Greeting("Hello, " + name + "!"); }}@RequestParam finds the name query parameter by the Java parameter’s name, which works because
fabr compiles with javac -parameters; see
Parameter names.
src/main/resources/application.properties holds the application’s settings:
spring.application.name=shopRunning it during development
Section titled “Running it during development”fabr run -w serverfabr builds the bundle and the server and starts ShopApplication with the jar and its
dependencies on the classpath. Open http://localhost:8080/, type a name and press Greet.
Arguments after the target name go to the application, so Spring Boot’s own options work as
usual: fabr run -w server --server.port=9090.
With -w, fabr keeps watching the sources of both halves. When you save a change to App.tsx, it
recompiles and rebundles the front end, rebuilds the jar and restarts the server; a change to a
Java source recompiles the server and restarts it. Reload the page to see the change: nothing
tells the browser. A build error is reported and the running server is left as it was until the
change that fixes it. Watch mode & dev servers
describes the restart.
Each change to the front end restarts the JVM, since the files are inside the jar. That costs the
application’s start-up time, about a second for this one; for a larger application, you can work
on the front end against a server you leave running with a separate
serve target, which replaces the
files it serves without a restart.
Building for deployment
Section titled “Building for deployment”fabr run starts the server from its jar and its dependencies’ jars, which suits development. To
deploy, you usually want Spring Boot’s executable jar: one file holding the application and every
jar it needs, started with java -jar. With Maven or Gradle, Spring Boot’s own plugin
(spring-boot-maven-plugin, or the bootJar task) builds it. In fabr, a spring_boot_jar
target builds it. Its target type and rule are written in .fabr, in an optional file of the Java
plugin, which you include. Add to backend/BUILD.fabr:
include @fabr-build/java:lib/spring-boot.fabr;
spring_boot_jar shop { app = server; main_class = com.example.shop.ShopApplication; loader = @mvn:org.springframework.boot:spring-boot-loader:4.1.2;}app = server makes the executable jar from the server jar as it is, with everything it
requires, so that what you deploy is exactly what you run in development. loader is Spring
Boot’s loader, of the same release as the application. See
Spring Boot executable jars for the
layout and how it differs from the one Spring Boot’s own plugins write.
Build it as a release build, which minifies the bundle and leaves out its source maps:
fabr build -DBUILD_TYPE=release shopfabr cp -DBUILD_TYPE=release shop ./dist # writes ./dist/shop/shop.jarjava -jar dist/shop/shop.jar-DBUILD_TYPE=release applies to everything shop is built from, the bundle included. The jar
runs on any Java 21 or later.
Next steps
Section titled “Next steps”- Bundling: assets, constants with
defines, and splitting a bundle into chunks. - Testing and JavaScript testing: add
teststo the server and ajs_testfor the front end, and run both withfabr test. - Projects with several packages: sharing versions and settings as the project grows.
- Writing your own rules: how a rule like
spring_boot_jar’s is written in a.fabrfile.