Build a Persistent Inventory Dashboard
Build a React inventory dashboard backed by a persistent Spring Boot API.
Introduction
30 Second Summary
Stock counts can look reliable right up until a program closes. If the numbers disappear overnight, the next person starts with an empty inventory.
In this project, you will build a local inventory dashboard that keeps product records after the backend restarts. A React client sends updates through a Spring Boot REST API to PostgreSQL.
What You'll Build
Picture your saved product still sitting in a browser table after you restart the Java backend.
By the end of this project, you'll have:
- A live inventory table that loads current stock from the Java API. You can inspect the same data as JSON at http://localhost:8080/api/products.
- A product form for submitting a SKU, product name, or quantity. The saved row appears immediately.
- A persistence demo where an added product remains after you restart the Java backend.
- Secret Mission: Add a low-stock switch that filters the table. Products with quantities below five receive low-stock styling.
Are there any prerequisites?
You need basic Java knowledge plus some React experience.
Docker Desktop requires a supported 64-bit Windows system with at least 8 GB of RAM, WSL 2 support, and hardware virtualization.
Before We Start
This is your moment to commit to a local inventory dashboard that tracks each product's SKU, name, and quantity. Defining why persistent stock data matters keeps every technical choice tied to a clear backend goal.
Set Up Your Windows Toolchain
Your inventory dashboard uses a Spring Boot API. It later connects a React client to PostgreSQL storage.
An unverified toolchain can make a runtime or PATH problem look like an application bug. This step establishes a known-good Windows environment before you build the inventory features.
In this step, get ready to:
- Install Eclipse Temurin JDK 25 with IntelliJ IDEA.
- Install Node.js 24.22.1 LTS with Docker Desktop.
- Create a Maven project that runs a Spring Boot server on port 8080.
Install Java and IntelliJ IDEA
The Eclipse Temurin JDK compiles your Java source code. IntelliJ IDEA gives you the editor plus bundled Maven support used to load Spring Boot dependencies.
- Check your current Java installation by running this command in PowerShell:
java -version
What does this command check?
The java launcher prints the installed runtime version. The result determines whether you can continue or need Eclipse Temurin JDK 25.
✔️ I see Temurin 25
Your Java runtime is ready. That removes the first source of build and startup problems.
ⓧ I see an older version
The existing runtime is below the version used by this project. Install Eclipse Temurin JDK 25 to give Maven the required compiler.
- Install Eclipse Temurin JDK 25 by running this command in PowerShell:
winget install EclipseAdoptium.Temurin.25.JDK
What does this command install?
Windows Package Manager downloads the Eclipse Temurin JDK package. The installer also makes the Java tools available to new terminal sessions.
- Close the current PowerShell window.
- Open a new PowerShell window through Windows search.
- Verify the new Java runtime by running this command:
java -version
What should the new check confirm?
The fresh terminal reads the updated PATH. You should now see Eclipse Temurin version 25 in the output.
Still seeing the older Java version?
Restart Windows so every process reloads the updated PATH. If the older runtime still appears, ask for help finding which Java installation Windows is using.
ⓧ Command not found
Windows cannot find a Java runtime on PATH. Install Eclipse Temurin JDK 25 with Windows Package Manager.
- Install Eclipse Temurin JDK 25 by running this command in PowerShell:
winget install EclipseAdoptium.Temurin.25.JDK
What does this command install?
Windows Package Manager downloads the Eclipse Temurin JDK package. The installation supplies the compiler plus the runtime needed by Spring Boot.
- Close the current PowerShell window.
- Open a new PowerShell window through Windows search.
- Confirm Java is available by running this command:
java -version
What should the check confirm?
The command should now print Eclipse Temurin version 25. That confirms Windows can locate the Java launcher.
Java still unavailable?
Restart Windows before checking again. If the command remains unavailable, ask for help checking your JDK installation and PATH.
IntelliJ IDEA manages your source files plus Maven project model. Its bundled Maven installation means you do not need a separate Maven download.
- Visit the official IntelliJ IDEA installation guide to get the Windows installer.
- Download the Windows .exe installer.
- Run the downloaded installer.
- Keep the default choices in the installer wizard.
- Complete the Installation Options step.
- Press the Windows key to open Windows search.
- Type IntelliJ IDEA and press Enter.
You should see the IntelliJ IDEA Welcome screen. Java development now has both a verified JDK and an editor that can load Maven projects.
IntelliJ IDEA does not open?
Confirm that the installer completed before searching again. If Windows still cannot launch the application, ask for help checking the IntelliJ IDEA installation.
Install Node.js and Docker Desktop
The frontend uses Node.js to run Vite plus npm. The database later runs inside Docker Desktop through Docker Compose.
- Check your current Node.js version by running this command in PowerShell:
node -v
What does this command check?
The node command prints the installed runtime version. This project uses Node.js 24.22.1 LTS.
✔️ I see v24.22.1
Node.js is ready for the React dashboard. You can use its bundled npm command to install frontend packages.
ⓧ I see an older version
Install the project version from the official Node.js download page. The LTS installer updates the Windows runtime used by new terminal sessions.
- Visit the official Node.js download page.
- Download the Windows installer labelled v24.22.1 LTS.
- Run the downloaded installer.
- Keep the installer defaults.
- Close the current PowerShell window.
- Open a new PowerShell window through Windows search.
- Verify the installed version by running this command:
node -v
What should the new check confirm?
The fresh terminal should print v24.22.1. That confirms the LTS runtime is active.
Still seeing the older Node.js version?
Restart Windows so the PATH refresh reaches every application. If the old version remains, ask for help locating the active Node.js installation.
ⓧ Command not found
Node.js is missing from PATH. Install the Windows LTS release from the official download page.
- Visit the official Node.js download page.
- Download the Windows installer labelled v24.22.1 LTS.
- Run the downloaded installer.
- Keep the installer defaults.
- Close the current PowerShell window.
- Open a new PowerShell window through Windows search.
- Confirm Node.js is available by running this command:
node -v
What should the check confirm?
The command should print v24.22.1. That result confirms Windows can locate Node.js.
Node.js still unavailable?
Restart Windows before checking again. If the command remains unavailable, ask for help checking the Node.js installer and PATH.
- Confirm npm was installed with Node.js by running this command:
npm -v
What does this command confirm?
The npm command prints its version number. A version result confirms the package manager is ready to install the React dependencies later.
You should see a version number from npm. Node.js plus npm can now run the frontend toolchain.
npm does not report a version?
Close PowerShell before opening a fresh window. If npm remains unavailable after Node.js works, ask for help repairing the npm installation.
Docker Desktop requires supported Windows hardware virtualization plus the WSL 2 backend. Its first launch may take several minutes while the Docker Engine starts.
- Check whether Docker Compose is already available by running this command:
docker compose version
What does this command check?
The command asks the Docker CLI for its Compose version. Spring Boot requires Docker Compose 2.2.0 or higher for its development service support.
✔️ I see version 2.2.0 or higher
Docker Compose meets Spring Boot's minimum version. Start Docker Desktop so the server component is ready for the final check.
- Press the Windows key to open Windows search.
- Type Docker Desktop and press Enter.
- Wait for Docker Desktop to report that the Docker Engine is running.
The first startup can take a few minutes. The delay comes from starting the WSL 2 environment plus the Docker Engine.
ⓧ I see an older version
The installed Compose release is below Spring Boot's minimum. Update Docker Desktop to receive a current Docker CLI plus Compose plugin.
- Visit the official Docker Desktop installation guide for Windows.
- Download the Windows installer.
- Run the downloaded installer.
- Use the WSL 2 backend during setup.
- Restart Windows if the installer requests it.
- Press the Windows key to open Windows search.
- Type Docker Desktop and press Enter.
- Wait for Docker Desktop to report that the Docker Engine is running.
The first startup can take a few minutes while Docker Desktop prepares its WSL 2 environment.
ⓧ Command not found
The Docker CLI is unavailable. Install Docker Desktop with its WSL 2 backend to add Docker Engine plus Compose.
- Visit the official Docker Desktop installation guide for Windows.
- Download the Windows installer.
- Run the downloaded installer.
- Use the WSL 2 backend during setup.
- Restart Windows if the installer requests it.
- Press the Windows key to open Windows search.
- Type Docker Desktop and press Enter.
- Wait for Docker Desktop to report that the Docker Engine is running.
Docker Desktop may stay busy for a few minutes on its first launch. That wait covers the initial WSL 2 setup plus engine startup.
Before you check Docker, consider whether both the client and server components are ready to answer.
- Verify Docker Engine plus Docker Compose by running these commands:
docker version
docker compose version
What do these checks prove?
- The first command reports Docker client plus server details.
- The second command reports the installed Docker Compose version.
You should see both client and server information from Docker. You should also see Docker Compose version 2.2.0 or higher.
Docker server details missing?
Keep Docker Desktop open until the engine finishes starting. If the server remains unavailable, ask for help checking Docker Desktop and WSL 2.
Create and run the Spring Boot starter
A Maven project starts with a pom.xml file that declares Java plus Spring Boot settings. You will create the exact project structure inside inventory-dashboard/inventory-api.
- Press Windows+E to open File Explorer.
- Select Desktop in the left navigation pane.
- Select New in the File Explorer toolbar.
- Select Folder.
- Enter inventory-dashboard as the folder name.
You should now see the inventory-dashboard folder on your Desktop.
- Open the inventory-dashboard folder.
- Select New in the File Explorer toolbar.
- Select Folder.
- Enter inventory-api as the folder name.
The path on your Desktop now ends with inventory-dashboard/inventory-api.
- Return to the IntelliJ IDEA Welcome screen.
- Click Open.
- Select the inventory-api folder inside inventory-dashboard.
- Click Open.
- Select Trust Project if IntelliJ IDEA asks whether you trust the folder.
The empty inventory-api folder should now appear in the Project tool window.
- Right-click inventory-api in the Project tool window.
- Select New.
- Select File.
- Enter pom.xml as the file name.
- Build the Maven project definition by replacing the empty file with this code:
<?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>4.1.2</version><relativePath/></parent>
<groupId>com.example</groupId><artifactId>inventory-api</artifactId><version>0.0.1-SNAPSHOT</version><name>inventory-api</name><description>Inventory REST API</description>
<properties><java.version>25</java.version></properties>
<dependencies>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webmvc</artifactId></dependency>
</dependencies>
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
What does this project file define?
- The Spring Boot parent supplies managed dependency plus plugin versions for release 4.1.2.
- The Java property tells Maven to compile the project with JDK 25.
- The Spring MVC starter supplies the embedded web server plus REST controller support.
- The Spring Boot Maven plugin packages and runs the application.
- Press Ctrl+S to save pom.xml.
- Select File from the IntelliJ IDEA main menu.
- Select Open.
- Select pom.xml inside inventory-api.
- Click Open as Project.
- Select New Window if IntelliJ IDEA asks where to open the project.
- Wait for IntelliJ IDEA to finish syncing the Maven project.
The first Maven sync can take a minute or two while it downloads Spring Boot dependencies. You should see the Maven tool window after the project model loads.
Maven project not loading?
Confirm that the opened file is inventory-api/pom.xml. If dependency downloads fail, ask for help checking the Maven sync.
✔️ Awesome, I've got everything!
Great. Save pom.xml before creating the Java source file.
ⓧ I'd like to double check the full code
<?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>4.1.2</version><relativePath/></parent>
<groupId>com.example</groupId><artifactId>inventory-api</artifactId><version>0.0.1-SNAPSHOT</version><name>inventory-api</name><description>Inventory REST API</description>
<properties><java.version>25</java.version></properties>
<dependencies>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webmvc</artifactId></dependency>
</dependencies>
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
How to compare this file
Check each element against your saved pom.xml. Pay close attention to the Spring Boot version plus closing XML tags.
- Right-click inventory-api in the Project tool window.
- Select New.
- Select Directory.
- Enter src/main/java/com/example/inventory as the directory path.
- Right-click the new inventory directory.
- Select New.
- Select File.
- Enter InventoryApiApplication.java as the file name.
The Project tool window should now show the Java file under src/main/java/com/example/inventory.
- Create the Spring Boot entry point by replacing the empty file with this code:
package com.example.inventory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class InventoryApiApplication {
public static void main(String[] args) {
SpringApplication.run(InventoryApiApplication.class, args);
}
}
What does this class do?
- The package name matches the source directory structure.
- The application annotation activates Spring Boot configuration plus component scanning.
- The main method asks Spring Boot to create the application context and start the embedded server.
- Press Ctrl+S to save InventoryApiApplication.java.
Before you run the class, consider whether Maven now has everything it needs to start the web server.
- Click the gutter icon beside the InventoryApiApplication class declaration.
- Select Run.
The first run may take a minute while Maven completes its downloads. You should see the Run tool window report that the server started on port 8080.
Server does not start?
Confirm that IntelliJ IDEA uses JDK 25 for the Maven project. If the Run tool window reports a build failure, ask for help checking the first error in the Maven output.
✔️ Awesome, I've got everything!
Your Java entry point is saved. The Spring Boot server can now start from IntelliJ IDEA.
ⓧ I'd like to double check the full code
package com.example.inventory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class InventoryApiApplication {
public static void main(String[] args) {
SpringApplication.run(InventoryApiApplication.class, args);
}
}
How to compare this file
Check the package declaration plus both imports first. The class name must remain InventoryApiApplication everywhere it appears.
- Right-click inventory-api in the Project tool window.
- Select New.
- Select Directory.
- Enter src/main/resources as the directory path.
- Right-click the new resources directory.
- Select New.
- Select File.
- Enter application.properties as the file name.
The configuration file should now appear under src/main/resources.
- Set the backend port by adding this configuration:
server.port=8080
What does this setting control?
The server.port property fixes the embedded web server to port 8080. Later steps can use the same API address consistently.
- Press Ctrl+S to save application.properties.
- Stop the running application in the Run tool window.
- Click the gutter icon beside the InventoryApiApplication class declaration.
- Select Run.
You should see the Spring Boot server start on port 8080 again. Your saved configuration now makes that port explicit.
Port 8080 already in use?
Confirm that an older run of InventoryApiApplication is not still active. If another process owns the port, ask for help identifying the process using port 8080.
✔️ Awesome, I've got everything!
The application configuration is saved. Your starter project now matches the required end state for this step.
ⓧ I'd like to double check the full code
server.port=8080
How to compare this file
Your file should contain this single property. Check the property name plus the port value character by character.
Before the final check, consider which five commands you expect to report version information successfully.
- Run the complete toolchain check in PowerShell:
java -version
node -v
npm -v
docker version
docker compose version
What does the final check cover?
- Java reports the JDK used to compile and run the backend.
- Node.js plus npm report the tools used by the React client.
- Docker reports both client and server components.
- Docker Compose reports the service orchestration plugin used by Spring Boot.
You should see version information from every command. Docker should include server details from the running Docker Engine.
- Return to the IntelliJ IDEA window containing inventory-api.
- Confirm that InventoryApiApplication remains running in the Run tool window.
You should see the Run tool window report the Spring Boot server listening on port 8080. That confirms the Windows toolchain plus starter project work together.
Your Windows development environment is ready. Next, you will turn this running server into an inventory API that returns products as JSON.
Build Your First Inventory API
Your Spring Boot starter already runs on port 8080. It has no route that can answer an inventory request.
A small API carries Java objects over HTTP as JSON. You will build that API with an explicit REST controller.
The first version keeps its products in a Java list. This gives you a visible response before a database adds more moving parts.
In this step, get ready to:
- Define a temporary product model.
- Create a controller with GET and POST routes.
- Confirm the browser receives starter inventory as JSON.
Create the temporary product model
The Product model describes one inventory record. Its fields give every product the same shape throughout the API.
- Expand src/main/java/com/example/inventory in IntelliJ IDEA's file tree.
- Right-click the com.example.inventory package to create a Java class named Product.
- Replace the generated contents of Product.java with the following model:
package com.example.inventory;
public class Product {
private Long id;
private String sku;
private String name;
private int quantity;
public Product() {}
public Product(Long id, String sku, String name, int quantity) { this.id = id; this.sku = sku; this.name = name; this.quantity = quantity; }
public Long getId() { return id; }
public String getSku() { return sku; }
public String getName() { return name; }
public int getQuantity() { return quantity; }
}
What does this model do?
- The four fields hold the database-style ID, SKU, product name, and stock quantity.
- The empty constructor lets Spring create a product from an incoming request.
- The populated constructor creates complete product records for the temporary list.
- The getters expose each value when Spring converts a product into JSON.
- Save Product.java.
- Return to InventoryApiApplication.java.
- Stop the active application with the square stop control in IntelliJ IDEA's Run tool window.
- Start the main class by selecting Run from its gutter icon.
The Run tool window reports that the Spring Boot server is active on port 8080.
Solid start. Your application now understands the shape of every inventory product.
Application not starting?
- Check that Product.java sits inside the com.example.inventory package.
- Confirm that the first line declares package com.example.inventory;.
Help me diagnose why my Spring Boot application stopped starting after I added Product.java.
✔️ Awesome, I've got everything!
Great. Save Product.java before moving on.
ⓧ I'd like to double check the full code
package com.example.inventory;
public class Product {
private Long id;
private String sku;
private String name;
private int quantity;
public Product() {}
public Product(Long id, String sku, String name, int quantity) { this.id = id; this.sku = sku; this.name = name; this.quantity = quantity; }
public Long getId() { return id; }
public String getSku() { return sku; }
public String getName() { return name; }
public int getQuantity() { return quantity; }
}
Build the inventory controller
The ProductController connects the URL to your Java code. Its mutable list supplies one starter product immediately.
Your next-step React client runs on a different port. CORS permission allows that browser client to call this API.
Why use an explicit controller?
Spring Data REST can generate endpoints automatically. An explicit controller shows exactly how each request reaches your Java methods.
You can see the URL mapping, the in-memory storage, and the returned product. That visibility keeps the backend lesson concrete.
- Right-click the com.example.inventory package to create another Java class.
- Create the class with the name ProductController.
- Replace the generated contents of ProductController.java with the following controller:
package com.example.inventory;
import java.util.ArrayList;
import java.util.List;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/products")
@CrossOrigin(origins = "http://localhost:5173")
public class ProductController {
private final List<Product> products = new ArrayList<>(List.of(new Product(1L, "STARTER-001", "Starter product", 10)));
@GetMapping public List<Product> all() { return products; }
@PostMapping public Product create(@RequestBody Product product) {
Product saved = new Product((long) products.size() + 1, product.getSku(), product.getName(), product.getQuantity());
products.add(saved);
return saved;
}
}
What's happening here?
- The @RestController and @RequestMapping annotations place this controller at /api/products.
- The @CrossOrigin annotation permits requests from http://localhost:5173.
- The mutable products list starts with one temporary inventory record.
- The @GetMapping method returns every product in the list.
- The @PostMapping method assigns an ID before storing a submitted product.
- Save ProductController.java.
- Return to InventoryApiApplication.java.
- Stop the active application with the square stop control in the Run tool window.
- Start the main class by selecting Run from its gutter icon.
The Run tool window confirms that the application starts on port 8080 with the new controller loaded.
Your backend now has a live inventory route. It can return existing products or accept new ones.
Controller not compiling?
- Check that ProductController.java starts with the com.example.inventory package declaration.
- Compare every Spring annotation import with the controller code above.
- Confirm that Product.java contains the four getter methods used by the controller.
Help me fix my ProductController compilation problem.
✔️ Awesome, I've got everything!
Your controller is saved. Keep the Spring Boot application running for the browser check.
ⓧ I'd like to double check the full code
package com.example.inventory;
import java.util.ArrayList;
import java.util.List;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/products")
@CrossOrigin(origins = "http://localhost:5173")
public class ProductController {
private final List<Product> products = new ArrayList<>(List.of(new Product(1L, "STARTER-001", "Starter product", 10)));
@GetMapping public List<Product> all() { return products; }
@PostMapping public Product create(@RequestBody Product product) {
Product saved = new Product((long) products.size() + 1, product.getSku(), product.getName(), product.getQuantity());
products.add(saved);
return saved;
}
}
Inspect the JSON response
A browser visit sends a GET request to the product route. Spring maps that request to the controller's all() method.
Before you check, do you expect one starter product or an empty array.
- Press the Windows key to open the Windows search bar.
- Type the name of your preferred web browser.
- Press Enter to open the browser.
- Enter http://localhost:8080/api/products in the address bar.
- Press Enter to request the inventory.
You'll see a JSON array containing the starter product STARTER-001.
That is your first inventory API working in the browser. Your Java route is now serving live product data.
What does this response prove?
- The browser reached the Spring Boot application on port 8080.
- The path /api/products reached the controller's GET method.
- Spring converted the Java product into JSON using its getter methods.
- The temporary list supplied the starter product without a database.
Inventory response not showing?
- Confirm that the Spring Boot application is still running in IntelliJ IDEA.
- Check that the browser address ends with /api/products.
- Confirm that ProductController.java uses @RequestMapping("/api/products").
Help me troubleshoot why my browser cannot load the inventory JSON.
Your first inventory endpoint is live. Next, you'll connect these routes to a React dashboard.
Connect the React Dashboard
Your Spring Boot API now returns inventory as JSON at http://localhost:8080/api/products. That response proves your Java objects can travel through an HTTP endpoint.
A React dashboard gives the API a form plus a table. The final check tests whether a newly added row survives a backend restart.
In this step, get ready to:
- Create the React frontend files.
- Load products from the Spring Boot API.
- Add a product before testing a backend restart.
Create the React project shell
Vite serves the browser client during development. The new inventory-ui folder sits beside inventory-api inside inventory-dashboard.
- Return to the IntelliJ IDEA window that contains inventory-api.
- Open Terminal from IntelliJ IDEA's tool-window bar.
- Create the sibling frontend folder plus its empty files by running these PowerShell commands:
cd ..
mkdir inventory-ui
cd inventory-ui
mkdir src
New-Item package.json, vite.config.js, index.html, src/main.jsx, src/App.jsx, src/styles.css -ItemType File
What do these commands create?
- The first three commands move to inventory-dashboard. They create inventory-ui as the frontend folder.
- The fourth command creates the src folder for the React source files.
- The final command creates the six empty files that form the client.
- Click File in IntelliJ IDEA's top menu.
- Select Open.
- Select the new inventory-ui folder inside inventory-dashboard.
- Choose New Window so the backend stays running in its original window.
You'll see three empty files at the top level. You'll also see three empty files inside src.
Files in the wrong folder?
- Check whether inventory-ui sits beside inventory-api inside inventory-dashboard.
- Move inventory-ui into inventory-dashboard if it was created inside the backend folder.
Help me correct the folder structure for my Spring Boot and React project.
- Select package.json in the second IntelliJ IDEA window.
- Define the frontend packages by pasting this code:
{
"name": "inventory-ui",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {"dev": "vite", "build": "vite build", "preview": "vite preview"},
"dependencies": {"react": "19.3.1", "react-dom": "19.3.1"},
"devDependencies": {"@vitejs/plugin-react": "6.1.2", "vite": "8.3.5"}
}
What does this file configure?
- The dependencies provide React plus the browser renderer.
- The development dependencies provide Vite plus its React plugin.
- The scripts give npm the commands for development builds plus production previews.
- Save package.json.
- Confirm IntelliJ IDEA recognizes the file as JSON through its syntax highlighting.
JSON showing syntax errors?
- Check that every property except the final property in each object has a comma.
- Check that the file begins with { and ends with }.
Help me find the syntax problem in my package.json file.
- Select vite.config.js.
- Connect Vite to its React plugin by pasting this code:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({ plugins: [react()] });
What does this configuration do?
- The React plugin lets Vite process JSX from your source files.
- The exported configuration activates that plugin when the development server starts.
- Save vite.config.js.
- Confirm the file shows both imports plus one exported configuration.
Imports marked as unresolved?
The packages still need to be installed if IntelliJ IDEA cannot resolve these imports. Complete the next command before changing the import names.
Help me diagnose unresolved Vite imports in IntelliJ IDEA.
- Open Terminal from the second IntelliJ IDEA window's tool-window bar.
- Install the packages declared in package.json by running:
npm install
What does this command do?
The command reads package.json and downloads its dependencies. It also creates the local package metadata used by the project.
The terminal returns to its prompt when installation finishes. IntelliJ IDEA can now resolve the Vite plus React imports.
Dependency installation failed?
- Confirm the terminal path ends with inventory-ui.
- Confirm package.json is saved before running the command again.
- Confirm your internet connection can reach the package registry.
Help me troubleshoot npm install in my inventory-ui folder.
- Select index.html.
- Create the browser entry document by pasting this code:
<!doctype html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Inventory Dashboard</title></head><body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>
What does this document provide?
- The root element gives React a place to render the dashboard.
- The module script loads src/main.jsx as the browser entry point.
- Save index.html.
- Confirm the document contains a root element plus the module script.
HTML structure showing errors?
Check that the script path begins with /src/. Check that the render target uses id="root".
Help me check the entry document for my React app.
- Select src/main.jsx.
- Mount the dashboard component by pasting this code:
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './styles.css';
createRoot(document.getElementById('root')).render(<App />);
What does this entry point do?
- The imports bring in the browser renderer plus the dashboard component.
- The stylesheet import applies the visual rules to the page.
- The final line renders App inside the HTML element named root.
- Save src/main.jsx.
- Confirm the src folder now contains main.jsx beside App.jsx.
App import showing an error?
Confirm the component file is named App.jsx with a capital A. Confirm it sits in the same src folder as main.jsx.
Help me fix the imports in src/main.jsx.
- Select src/styles.css.
- Style the form plus inventory table by pasting this code:
:root { font-family: Inter, system-ui, sans-serif; color: #172033; background: #eef2f8; }
* { box-sizing: border-box; }
body { margin: 0; }
button, input { font: inherit; }
.app-shell { min-height: 100vh; display: grid; place-items: center; padding: 32px; }
.panel { width: min(900px, 100%); padding: 32px; border-radius: 20px; background: white; box-shadow: 0 18px 50px rgba(23, 32, 51, 0.12); }
.eyebrow { margin: 0; color: #3568d4; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; }
h1 { margin: 8px 0; font-size: clamp(2rem, 5vw, 3.4rem); }
.status { min-height: 24px; color: #5d6678; }
.product-form { display: grid; grid-template-columns: 1fr 2fr 1fr auto; gap: 12px; margin: 24px 0; }
input, button { border: 1px solid #cbd3e1; border-radius: 10px; padding: 12px 14px; }
button { border-color: #3568d4; color: white; background: #3568d4; cursor: pointer; }
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 14px; border-bottom: 1px solid #e3e7ef; text-align: left; }
@media (max-width: 720px) { .product-form { grid-template-columns: 1fr; } }
What do these styles control?
- The shell centers the dashboard panel against a light page background.
- The form uses four columns on wide screens.
- The media rule changes the form to one column on narrow screens.
- The table rules keep inventory rows aligned plus readable.
- Save src/styles.css.
- Confirm IntelliJ IDEA recognizes the selectors as CSS.
Stylesheet showing syntax errors?
Check that every selector opens with { and closes with }.
Help me find the CSS syntax problem in src/styles.css.
- Select src/App.jsx.
- Create the first runnable dashboard by pasting this code:
import { useEffect, useState } from 'react';
const API_URL = 'http://localhost:8080/api/products';
export default function App() {
const [products, setProducts] = useState([]);
const [form, setForm] = useState({ sku: '', name: '', quantity: 0 });
const [message, setMessage] = useState('Loading inventory...');
function updateField(event) {
const { name, value } = event.target;
setForm(current => ({ ...current, [name]: name === 'quantity' ? Number(value) : value }));
}
async function addProduct(event) {
event.preventDefault(); setMessage('Saving product...');
try {
const response = await fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form) });
if (!response.ok) throw new Error('Could not save the product.');
const savedProduct = await response.json();
setProducts(current => [...current, savedProduct]);
setForm({ sku: '', name: '', quantity: 0 }); setMessage('Product saved.');
} catch (error) { setMessage(error.message); }
}
return <main className="app-shell"><section className="panel"><p className="eyebrow">Spring Boot Inventory</p><h1>Stock dashboard</h1><p className="status">{message}</p><form className="product-form" onSubmit={addProduct}><input name="sku" value={form.sku} onChange={updateField} placeholder="SKU" required /><input name="name" value={form.name} onChange={updateField} placeholder="Product name" required /><input name="quantity" type="number" min="0" value={form.quantity} onChange={updateField} required /><button type="submit">Add product</button></form><div className="table-wrap"><table><thead><tr><th>SKU</th><th>Product</th><th>Quantity</th></tr></thead><tbody>{products.map(product => <tr key={product.id}><td>{product.sku}</td><td>{product.name}</td><td>{product.quantity}</td></tr>)}</tbody></table></div></section></main>;
}
What does this component do?
- The three state values hold the current products, form fields plus status message.
- The updateField function keeps the form state synchronized with each input.
- The addProduct function sends the form as JSON through an HTTP POST request.
- The returned JSX renders the form plus one table row for every product in state.
- Save src/App.jsx.
- Start the Vite development server by running this command in the inventory-ui terminal:
npm run dev
What does this command start?
The command runs the vite script from package.json. The development server keeps control of this terminal while it serves the app.
- Keep the frontend terminal running for the rest of this step.
- Open a browser tab.
- Visit http://localhost:5173.
You'll see a centered Stock dashboard with three form fields plus an empty table. The status reads Loading inventory... because the loading effect comes next.
Dashboard not loading?
- Confirm the frontend terminal remains open with the development server running.
- Confirm the browser address is http://localhost:5173.
- Confirm index.html points to /src/main.jsx.
Help me debug a Vite dashboard that does not load in the browser.
Load and create inventory products
The dashboard can already submit a form. A React effect now loads the existing products as soon as the component connects to the page.
- Return to src/App.jsx in the second IntelliJ IDEA window.
- Find the line that initializes message.
- Add this effect immediately below that line:
useEffect(() => {
let ignore = false;
async function loadProducts() {
try {
const response = await fetch(API_URL);
if (!response.ok) throw new Error('Could not load inventory.');
const data = await response.json();
if (!ignore) { setProducts(data); setMessage(data.length === 0 ? 'No products yet.' : 'Inventory loaded.'); }
} catch (error) { if (!ignore) setMessage(error.message); }
}
loadProducts();
return () => { ignore = true; };
}, []);
How does loading work?
- The effect runs after React mounts the dashboard.
- The loadProducts function sends a GET request to API_URL.
- The parsed response replaces the empty products state.
- The cleanup flag prevents a completed request from updating a component that has already left the page.
- Save src/App.jsx.
- Return to the browser tab at http://localhost:5173.
You'll see Inventory loaded. above the form. The table now contains STARTER-001 with Starter product plus a quantity of 10.
Starter product not appearing?
- Confirm InventoryApiApplication is still running in the original IntelliJ IDEA window.
- Confirm http://localhost:8080/api/products still displays the starter JSON in a browser tab.
- Confirm API_URL matches that endpoint exactly.
Help me debug why the React dashboard cannot load products from Spring Boot.
Why can the browser call this API?
The browser treats ports 5173 plus 8080 as different origins. The controller's CORS permission allows requests from the Vite origin.
- Enter DEMO-002 in the SKU field.
- Enter USB cable in the Product name field.
- Enter 4 in the quantity field.
- Click Add product.
That connection is working. You'll see Product saved. plus a new DEMO-002 row without refreshing the page.
Use this reference to confirm every project file before testing the restart behavior.
✔️ Awesome, I've got everything!
Great. Save every frontend file while both development processes remain running.
ⓧ I'd like to double check the full code
- Compare each file with the matching reference below.
<?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>4.1.2</version><relativePath/></parent>
<groupId>com.example</groupId><artifactId>inventory-api</artifactId><version>0.0.1-SNAPSHOT</version><name>inventory-api</name><description>Inventory REST API</description>
<properties><java.version>25</java.version></properties>
<dependencies>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webmvc</artifactId></dependency>
</dependencies>
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
What this file does
The Maven configuration keeps the backend on its Spring MVC starter. This step does not change the backend dependencies.
package com.example.inventory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class InventoryApiApplication {
public static void main(String[] args) {
SpringApplication.run(InventoryApiApplication.class, args);
}
}
What this file does
The application class remains the backend entry point. Running it keeps the API available on port 8080.
package com.example.inventory;
public class Product {
private Long id;
private String sku;
private String name;
private int quantity;
public Product() {}
public Product(Long id, String sku, String name, int quantity) { this.id = id; this.sku = sku; this.name = name; this.quantity = quantity; }
public Long getId() { return id; }
public String getSku() { return sku; }
public String getName() { return name; }
public int getQuantity() { return quantity; }
}
What this file does
The temporary product model carries each inventory row between Java plus JSON.
package com.example.inventory;
import java.util.ArrayList;
import java.util.List;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/products")
@CrossOrigin(origins = "http://localhost:5173")
public class ProductController {
private final List<Product> products = new ArrayList<>(List.of(new Product(1L, "STARTER-001", "Starter product", 10)));
@GetMapping public List<Product> all() { return products; }
@PostMapping public Product create(@RequestBody Product product) {
Product saved = new Product((long) products.size() + 1, product.getSku(), product.getName(), product.getQuantity());
products.add(saved);
return saved;
}
}
What this file does
The controller lists plus creates products through the same mutable Java list. Its origin permission lets the frontend send browser requests.
server.port=8080
What this file does
The backend continues to listen on port 8080.
<!doctype html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Inventory Dashboard</title></head><body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>
What this file does
The HTML document provides the React render target plus the source entry path.
{
"name": "inventory-ui",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {"dev": "vite", "build": "vite build", "preview": "vite preview"},
"dependencies": {"react": "19.3.1", "react-dom": "19.3.1"},
"devDependencies": {"@vitejs/plugin-react": "6.1.2", "vite": "8.3.5"}
}
What this file does
The package manifest defines the client dependencies plus Vite scripts.
import { useEffect, useState } from 'react';
const API_URL = 'http://localhost:8080/api/products';
export default function App() {
const [products, setProducts] = useState([]);
const [form, setForm] = useState({ sku: '', name: '', quantity: 0 });
const [message, setMessage] = useState('Loading inventory...');
useEffect(() => {
let ignore = false;
async function loadProducts() {
try {
const response = await fetch(API_URL);
if (!response.ok) throw new Error('Could not load inventory.');
const data = await response.json();
if (!ignore) { setProducts(data); setMessage(data.length === 0 ? 'No products yet.' : 'Inventory loaded.'); }
} catch (error) { if (!ignore) setMessage(error.message); }
}
loadProducts();
return () => { ignore = true; };
}, []);
function updateField(event) {
const { name, value } = event.target;
setForm(current => ({ ...current, [name]: name === 'quantity' ? Number(value) : value }));
}
async function addProduct(event) {
event.preventDefault(); setMessage('Saving product...');
try {
const response = await fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form) });
if (!response.ok) throw new Error('Could not save the product.');
const savedProduct = await response.json();
setProducts(current => [...current, savedProduct]);
setForm({ sku: '', name: '', quantity: 0 }); setMessage('Product saved.');
} catch (error) { setMessage(error.message); }
}
return <main className="app-shell"><section className="panel"><p className="eyebrow">Spring Boot Inventory</p><h1>Stock dashboard</h1><p className="status">{message}</p><form className="product-form" onSubmit={addProduct}><input name="sku" value={form.sku} onChange={updateField} placeholder="SKU" required /><input name="name" value={form.name} onChange={updateField} placeholder="Product name" required /><input name="quantity" type="number" min="0" value={form.quantity} onChange={updateField} required /><button type="submit">Add product</button></form><div className="table-wrap"><table><thead><tr><th>SKU</th><th>Product</th><th>Quantity</th></tr></thead><tbody>{products.map(product => <tr key={product.id}><td>{product.sku}</td><td>{product.name}</td><td>{product.quantity}</td></tr>)}</tbody></table></div></section></main>;
}
What this file does
The component loads products with GET requests. It creates products with POST requests plus renders the resulting state.
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './styles.css';
createRoot(document.getElementById('root')).render(<App />);
What this file does
The source entry point mounts the dashboard plus loads its stylesheet.
:root { font-family: Inter, system-ui, sans-serif; color: #172033; background: #eef2f8; }
* { box-sizing: border-box; }
body { margin: 0; }
button, input { font: inherit; }
.app-shell { min-height: 100vh; display: grid; place-items: center; padding: 32px; }
.panel { width: min(900px, 100%); padding: 32px; border-radius: 20px; background: white; box-shadow: 0 18px 50px rgba(23, 32, 51, 0.12); }
.eyebrow { margin: 0; color: #3568d4; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; }
h1 { margin: 8px 0; font-size: clamp(2rem, 5vw, 3.4rem); }
.status { min-height: 24px; color: #5d6678; }
.product-form { display: grid; grid-template-columns: 1fr 2fr 1fr auto; gap: 12px; margin: 24px 0; }
input, button { border: 1px solid #cbd3e1; border-radius: 10px; padding: 12px 14px; }
button { border-color: #3568d4; color: white; background: #3568d4; cursor: pointer; }
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 14px; border-bottom: 1px solid #e3e7ef; text-align: left; }
@media (max-width: 720px) { .product-form { grid-template-columns: 1fr; } }
What this file does
The stylesheet turns the component into a responsive form plus inventory table.
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({ plugins: [react()] });
What this file does
The Vite configuration activates JSX support through the React plugin.
Test the restart boundary
The dashboard now proves that GET plus POST requests work across both applications. The remaining question is where the new product actually lives.
Before you restart the backend, do you think DEMO-002 will still appear after the Java process starts again?
- Switch back to the original IntelliJ IDEA window that is running InventoryApiApplication.
- Click Stop in the Run tool window.
- Open InventoryApiApplication.java from the project sidebar.
- Click the green gutter icon beside main().
- Select Run.
- Wait until the Run tool window reports that the server is listening on port 8080.
- Return to the dashboard in your browser.
- Refresh the page.
You'll see the starter product again. The DEMO-002 row has disappeared.
Why did the product disappear?
The controller keeps products in a mutable Java list. That list belongs to the running backend process.
Restarting the process constructs a fresh controller with only the starter product. This is the persistence gap your next change needs to close.
Your dashboard now proves the full browser-to-Java request flow. Next, you'll give inventory a storage layer that survives a backend restart.
Persist Inventory with PostgreSQL
Your React dashboard now sends products through a Spring Boot API. The restart test exposed the weak point: every added row lived only in Java process memory.
PostgreSQL gives those products durable storage. Spring Data JPA replaces the temporary list with repository-backed database operations.
Docker Compose development support starts the database alongside the backend. The named volume keeps its records when only the Java application restarts.
In this step, get ready to:
- Define a PostgreSQL service with persistent storage.
- Replace the temporary product list with a JPA repository.
- Restart the backend and prove that a saved product remains.
Connect Spring Boot to PostgreSQL
Spring Boot can discover a local database from compose.yaml when its Docker Compose module is present. The Compose file also defines the named volume that holds your inventory data.
The first database startup can take a few minutes while Docker downloads the PostgreSQL image. A quiet pause during that download is expected.
- Stop InventoryApiApplication from the IntelliJ IDEA Run tool window.
- Press the Windows key to open search.
- Type Docker Desktop into Windows search.
- Press Enter to open Docker Desktop.
- Wait until Docker Desktop reports that its engine is running.
- Select inventory-api/pom.xml in the IntelliJ IDEA file tree.
- Replace the file contents with this Maven configuration:
<?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>4.1.2</version><relativePath/></parent>
<groupId>com.example</groupId><artifactId>inventory-api</artifactId><version>0.0.1-SNAPSHOT</version><name>inventory-api</name><description>Inventory REST API</description>
<properties><java.version>25</java.version></properties>
<dependencies>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webmvc</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
<dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><scope>runtime</scope></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-docker-compose</artifactId><optional>true</optional></dependency>
</dependencies>
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
What do these dependencies add?
- The spring-boot-starter-data-jpa dependency adds entity and repository support.
- The postgresql dependency supplies the database driver at runtime.
- The spring-boot-docker-compose dependency lets Spring Boot discover the Compose service and create its connection details.
The first Maven reload may take a moment while the new dependencies download. The activity indicator in IntelliJ IDEA confirms that work is still progressing.
- Save inventory-api/pom.xml.
- Allow IntelliJ IDEA to reload the Maven project.
- Confirm that IntelliJ IDEA no longer marks the three new dependencies as unresolved.
- Create compose.yaml inside the inventory-api folder using the IntelliJ IDEA file tree.
- Paste this PostgreSQL service definition into the new file:
services:
postgres:
image: postgres:18
environment:
POSTGRES_DB: inventory
POSTGRES_USER: inventory
POSTGRES_PASSWORD: inventory
ports:
- "5432"
volumes:
- inventory-data:/var/lib/postgresql
volumes:
inventory-data:
What does this Compose file do?
- The postgres:18 image provides the PostgreSQL service.
- The three environment values create the local inventory database account.
- The port entry lets Docker assign a host port for PostgreSQL.
- The inventory-data volume stores database files outside the container.
- Save inventory-api/compose.yaml.
- Return to InventoryApiApplication.java in IntelliJ IDEA.
- Run InventoryApiApplication from its gutter icon.
The Run tool window shows Docker Compose starting the PostgreSQL service. Spring Boot then reports that the server is listening on port 8080.
- Switch back to the browser tab showing http://localhost:8080/api/products.
- Refresh the page.
You still see the temporary starter product as JSON. This confirms that the existing API can run while the PostgreSQL service is available.
- Stop InventoryApiApplication from the IntelliJ IDEA Run tool window.
- Select inventory-api/src/main/resources/application.properties in the IntelliJ IDEA file tree.
- Replace the file contents with these settings:
server.port=8080
spring.jpa.hibernate.ddl-auto=update
What do these settings control?
- The server.port=8080 setting keeps the API at its existing address.
- The spring.jpa.hibernate.ddl-auto=update setting tells Hibernate to update the local database schema when required.
- Save inventory-api/src/main/resources/application.properties.
- Return to InventoryApiApplication.java.
- Run InventoryApiApplication from its gutter icon.
The backend starts again on port 8080. Your database service and schema setting are now ready for the repository migration.
Database service not starting?
- Confirm that Docker Desktop still reports its engine as running.
- Check that the indentation in compose.yaml matches the reference exactly.
- Confirm that compose.yaml is directly inside the inventory-api folder.
Help me diagnose why Spring Boot cannot start the PostgreSQL Compose service.
Replace memory with a repository
A JPA entity maps a Java object to a database record. A repository then provides the operations that load and save those records.
This migration updates three connected Java files. IntelliJ IDEA may underline intermediate references until all three files match.
- Stop InventoryApiApplication from the IntelliJ IDEA Run tool window.
- Select inventory-api/src/main/java/com/example/inventory/Product.java in the IntelliJ IDEA file tree.
- Replace the file contents with this JPA entity:
package com.example.inventory;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Product {
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String sku;
private String name;
private int quantity;
public Product() {}
public Product(String sku, String name, int quantity) { this.sku = sku; this.name = name; this.quantity = quantity; }
public Long getId() { return id; }
public String getSku() { return sku; }
public void setSku(String sku) { this.sku = sku; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
}
What does this entity change?
- The @Entity annotation marks Product for database persistence.
- The @Id annotation identifies the primary key.
- The @GeneratedValue annotation lets the database generate each product ID.
- The setters let incoming JSON populate the product fields.
- Save inventory-api/src/main/java/com/example/inventory/Product.java.
- Create ProductRepository.java inside inventory-api/src/main/java/com/example/inventory using the IntelliJ IDEA file tree.
- Paste this repository interface into the new file:
package com.example.inventory;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {}
What does this repository provide?
Extending JpaRepository<Product, Long> gives the API database operations for Product records. The Long type matches the entity ID.
- Save inventory-api/src/main/java/com/example/inventory/ProductRepository.java.
- Confirm that ProductRepository.java now appears beside Product.java in the IntelliJ IDEA file tree.
- Select inventory-api/src/main/java/com/example/inventory/ProductController.java.
- Replace the temporary list controller with this repository-backed controller:
package com.example.inventory;
import java.util.List;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/products")
@CrossOrigin(origins = "http://localhost:5173")
public class ProductController {
private final ProductRepository repository;
public ProductController(ProductRepository repository) { this.repository = repository; }
@GetMapping public List<Product> all() { return repository.findAll(); }
@PostMapping public Product create(@RequestBody Product product) {
return repository.save(new Product(product.getSku(), product.getName(), product.getQuantity()));
}
}
How does the controller use the database?
- The constructor receives the ProductRepository dependency.
- The repository.findAll() call loads every saved product.
- The repository.save(...) call inserts a product and returns its generated ID.
- Save inventory-api/src/main/java/com/example/inventory/ProductController.java.
- Return to InventoryApiApplication.java.
- Run InventoryApiApplication from its gutter icon.
- Switch back to http://localhost:8080/api/products in your browser.
- Refresh the API page.
You see an empty JSON array represented by []. The temporary starter product is gone because the endpoint now reads from PostgreSQL.
Backend not starting after the migration?
- Check that ProductRepository.java uses the same com.example.inventory package as Product.java.
- Confirm that the old ArrayList field is absent from ProductController.java.
- Confirm that Product.java includes both the empty constructor and the three-argument constructor.
Help me troubleshoot the repository-backed Spring Boot API.
Use the complete project reference if you want to compare every file before testing persistence.
✔️ Awesome, I've got everything!
Your backend now has a PostgreSQL service, a JPA entity, a repository, and repository-backed endpoints. Keep both development servers available for the restart test.
ⓧ I'd like to double check the full code
- Compare each project file with the references below.
services:
postgres:
image: postgres:18
environment:
POSTGRES_DB: inventory
POSTGRES_USER: inventory
POSTGRES_PASSWORD: inventory
ports:
- "5432"
volumes:
- inventory-data:/var/lib/postgresql
volumes:
inventory-data:
Database service file
This file defines the PostgreSQL service and its persistent named volume.
<?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>4.1.2</version><relativePath/></parent>
<groupId>com.example</groupId><artifactId>inventory-api</artifactId><version>0.0.1-SNAPSHOT</version><name>inventory-api</name><description>Inventory REST API</description>
<properties><java.version>25</java.version></properties>
<dependencies>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webmvc</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
<dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><scope>runtime</scope></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-docker-compose</artifactId><optional>true</optional></dependency>
</dependencies>
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
Backend dependency file
This file combines Spring MVC with JPA, PostgreSQL, and Docker Compose support.
package com.example.inventory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class InventoryApiApplication {
public static void main(String[] args) {
SpringApplication.run(InventoryApiApplication.class, args);
}
}
Backend entry point
This class starts the Spring Boot application and discovers the other backend components.
package com.example.inventory;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Product {
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String sku;
private String name;
private int quantity;
public Product() {}
public Product(String sku, String name, int quantity) { this.sku = sku; this.name = name; this.quantity = quantity; }
public Long getId() { return id; }
public String getSku() { return sku; }
public void setSku(String sku) { this.sku = sku; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
}
Persistent product model
This class maps each inventory product to a database record with a generated ID.
package com.example.inventory;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {}
Product data access
This interface supplies the database operations used by the controller.
package com.example.inventory;
import java.util.List;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/products")
@CrossOrigin(origins = "http://localhost:5173")
public class ProductController {
private final ProductRepository repository;
public ProductController(ProductRepository repository) { this.repository = repository; }
@GetMapping public List<Product> all() { return repository.findAll(); }
@PostMapping public Product create(@RequestBody Product product) {
return repository.save(new Product(product.getSku(), product.getName(), product.getQuantity()));
}
}
Repository-backed endpoints
This controller loads and saves products through the repository while preserving the existing API routes.
server.port=8080
spring.jpa.hibernate.ddl-auto=update
Backend application settings
These settings keep the existing server port and update the local database schema.
<!doctype html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Inventory Dashboard</title></head><body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>
Dashboard document
This document provides the root element used to render the dashboard.
{
"name": "inventory-ui",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {"dev": "vite", "build": "vite build", "preview": "vite preview"},
"dependencies": {"react": "19.3.1", "react-dom": "19.3.1"},
"devDependencies": {"@vitejs/plugin-react": "6.1.2", "vite": "8.3.5"}
}
Dashboard dependencies
This file defines the React dependencies and Vite scripts used by the dashboard.
import { useEffect, useState } from 'react';
const API_URL = 'http://localhost:8080/api/products';
export default function App() {
const [products, setProducts] = useState([]);
const [form, setForm] = useState({ sku: '', name: '', quantity: 0 });
const [message, setMessage] = useState('Loading inventory...');
useEffect(() => {
let ignore = false;
async function loadProducts() {
try {
const response = await fetch(API_URL);
if (!response.ok) throw new Error('Could not load inventory.');
const data = await response.json();
if (!ignore) { setProducts(data); setMessage(data.length === 0 ? 'No products yet.' : 'Inventory loaded.'); }
} catch (error) { if (!ignore) setMessage(error.message); }
}
loadProducts();
return () => { ignore = true; };
}, []);
function updateField(event) {
const { name, value } = event.target;
setForm(current => ({ ...current, [name]: name === 'quantity' ? Number(value) : value }));
}
async function addProduct(event) {
event.preventDefault(); setMessage('Saving product...');
try {
const response = await fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form) });
if (!response.ok) throw new Error('Could not save the product.');
const savedProduct = await response.json();
setProducts(current => [...current, savedProduct]);
setForm({ sku: '', name: '', quantity: 0 }); setMessage('Product saved.');
} catch (error) { setMessage(error.message); }
}
return <main className="app-shell"><section className="panel"><p className="eyebrow">Spring Boot Inventory</p><h1>Stock dashboard</h1><p className="status">{message}</p><form className="product-form" onSubmit={addProduct}><input name="sku" value={form.sku} onChange={updateField} placeholder="SKU" required /><input name="name" value={form.name} onChange={updateField} placeholder="Product name" required /><input name="quantity" type="number" min="0" value={form.quantity} onChange={updateField} required /><button type="submit">Add product</button></form><div className="table-wrap"><table><thead><tr><th>SKU</th><th>Product</th><th>Quantity</th></tr></thead><tbody>{products.map(product => <tr key={product.id}><td>{product.sku}</td><td>{product.name}</td><td>{product.quantity}</td></tr>)}</tbody></table></div></section></main>;
}
Dashboard behavior
This component loads products from the API and submits new products through the existing form.
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './styles.css';
createRoot(document.getElementById('root')).render(<App />);
Dashboard entry point
This file renders the inventory application into the document root.
:root { font-family: Inter, system-ui, sans-serif; color: #172033; background: #eef2f8; }
* { box-sizing: border-box; }
body { margin: 0; }
button, input { font: inherit; }
.app-shell { min-height: 100vh; display: grid; place-items: center; padding: 32px; }
.panel { width: min(900px, 100%); padding: 32px; border-radius: 20px; background: white; box-shadow: 0 18px 50px rgba(23, 32, 51, 0.12); }
.eyebrow { margin: 0; color: #3568d4; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; }
h1 { margin: 8px 0; font-size: clamp(2rem, 5vw, 3.4rem); }
.status { min-height: 24px; color: #5d6678; }
.product-form { display: grid; grid-template-columns: 1fr 2fr 1fr auto; gap: 12px; margin: 24px 0; }
input, button { border: 1px solid #cbd3e1; border-radius: 10px; padding: 12px 14px; }
button { border-color: #3568d4; color: white; background: #3568d4; cursor: pointer; }
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 14px; border-bottom: 1px solid #e3e7ef; text-align: left; }
@media (max-width: 720px) { .product-form { grid-template-columns: 1fr; } }
Dashboard presentation
This stylesheet controls the form, table, responsive layout, and dashboard presentation.
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({ plugins: [react()] });
Dashboard build configuration
This configuration enables the React plugin for the Vite development server.
Prove the product survives a restart
The browser can now test the complete React-to-Spring-to-PostgreSQL flow. The decisive check is whether a row still exists after only the Java process restarts.
- Switch back to the Vite dashboard at http://localhost:5173.
- Enter PERSIST-001 in the SKU field.
- Enter Persistent product in the Product name field.
- Enter 7 in the quantity field.
- Click Add product.
The table shows a PERSIST-001 row with a quantity of 7. The status reads Product saved.
- Stop InventoryApiApplication from the IntelliJ IDEA Run tool window.
Before you restart the backend, do you expect the PERSIST-001 row to return after the dashboard refreshes?
- Return to InventoryApiApplication.java in IntelliJ IDEA.
- Run InventoryApiApplication from its gutter icon.
- Wait until the Run tool window reports that the server is listening on port 8080.
- Switch back to the Vite dashboard.
- Refresh the page.
The PERSIST-001 row returns with the same quantity. PostgreSQL loaded the record from the inventory-data volume after the Java process restarted.
You closed the persistence gap. Your dashboard now keeps inventory records beyond the lifetime of one backend process.
Secret mission
Spotlight Low-Stock Products
A growing inventory table can bury products that need attention. Add a low-stock switch that filters the table and highlights every product with fewer than five units.
Clean Up Your Resources
Clean Up Your Resources
Your persistent inventory dashboard is ready for its final resource decision. Its local resources create no cloud charges whether you keep them running, pause them, or delete them.
What about Docker Desktop licensing?
Docker Desktop is free for personal use, education, non-commercial open source, and qualifying small businesses. Larger commercial organizations require a paid subscription.
Resources you used:
- The running Spring Boot backend called InventoryApiApplication in IntelliJ IDEA.
- The running React dashboard served by Vite from inventory-ui.
- The local PostgreSQL container created from postgres:18.
- The inventory-data named volume containing your saved products.
- The local inventory-api and inventory-ui project folders.
Keep everything running
No action is needed. Choose this option if you are still testing products or improving the dashboard.
- Continue using the dashboard at http://localhost:5173.
- Leave the inventory-data volume in place to preserve your products.
- Retain the inventory-api and inventory-ui folders for future changes.
Pause - I'll come back to this later
Shut down the running applications to free local memory. Your project files and saved inventory remain available for your next session.
- In the Vite terminal from earlier, press Ctrl+C to stop the dashboard.
- In IntelliJ IDEA's Run tool window, click the square stop control for InventoryApiApplication.
Spring Boot's Docker Compose support stops the PostgreSQL service with the application. The inventory-data volume keeps your products for the next run.
Delete - I don't want to use this again
Remove all project resources when you are finished with the dashboard. Deleting inventory-data permanently erases every saved product, so choose the Pause option if you may want that inventory later.
- In the Vite terminal from earlier, press Ctrl+C to stop the dashboard.
- In IntelliJ IDEA's Run tool window, click the square stop control for InventoryApiApplication.
- Open IntelliJ IDEA's terminal for the inventory-api project.
- Remove the PostgreSQL container and its saved volume by running this command:
docker compose down -v
What does this remove?
Compose removes the project's PostgreSQL container. The -v flag also deletes the named volume that stores your inventory data.
- Check the command output for container and volume removal messages.
Can't remove the database?
Confirm the terminal is inside inventory-api, where compose.yaml is located. Keep Docker Desktop running until the cleanup command finishes.
Help me troubleshoot Docker Compose cleanup.
Delete the project files:
- Close the Vite terminal.
- Close IntelliJ IDEA.
- In File Explorer, go to the folder containing inventory-api and inventory-ui.
- Select inventory-api and inventory-ui.
- Press Shift+Delete to permanently delete the selected folders.
- Confirm the permanent deletion when Windows asks.
- Verify that neither project folder remains in File Explorer.
Nice Work!
Nice Work!
You did it! Your React inventory dashboard now moves products through a Spring Boot REST API into PostgreSQL storage.
You've learned how to:
- Build a Spring Boot REST controller with explicit GET and POST endpoints that list or create inventory products.
- Replace temporary in-memory storage with a Spring Data JPA repository connected to PostgreSQL through Docker Compose. Prove persistence by finding the same products after a backend restart.
- Build a React dashboard with Vite that loads products into a table. Submit new products from the same page.
- Complete the optional Secret Mission by building a low-stock filter with highlighted rows for quantities below five.
Ready to quiz yourself?