Build a Product Catalog API with Spring Boot

Build a REST API with full CRUD, pagination, and validation using Spring Boot.

Introduction

30 Second Summary

Every online store you browse uses an API behind the scenes to serve product data to your screen. When you search, filter by price, or page through results, that's an API handling your request.

In this project, you will build a Product Catalog REST API using Spring Boot that supports full CRUD operations, input validation, and pagination for an ecommerce store.

What You'll Build

A REST API for an ecommerce product catalog that lets you create, read, update, and delete products stored in an H2 in-memory database.

By the end of this project, you'll have:

  • A running Spring Boot application with an embedded Tomcat server serving product data on localhost:8080.
  • A layered architecture (Controller, Service, Repository) following real-world enterprise Java patterns.
  • Pagination and input validation built into your API, demonstrating how production systems handle large datasets safely.
  • Secret Mission: Add product search and filtering by category and price range.

Are there any prerequisites?

You need basic Java knowledge (classes, methods, annotations). No Spring experience is required.

Set Up Your Development Environment

Before you can build a REST API, you need the tools that make Java development possible. Spring Boot applications run on the Java Virtual Machine (JVM), which means you need a Java Development Kit (JDK) installed on your machine.

You also need an IDE (Integrated Development Environment) to write and manage your code efficiently. In this step, you will install JDK 17, set up IntelliJ IDEA, and confirm that Maven is ready to manage your project dependencies.

In this step, get ready to:
  • Install JDK 17 and verify it works.
  • Download and open IntelliJ IDEA (or configure VS Code with the Spring Boot Extension Pack).
  • Confirm Maven is available for building your project.
Install JDK 17

Spring Boot 3.5.x requires Java SDK version 17 or higher. You will install BellSoft Liberica JDK, a free and open-source distribution recommended by the Spring team.

  • Open your browser and navigate to the Liberica JDK 17 download page.
  • Select your operating system (macOS, Windows, or Linux).
  • Choose the Standard JDK package type.
  • Download the installer (.dmg for macOS, .msi for Windows).
  • Run the installer and follow the on-screen prompts to complete installation.

What is a JDK?

A JDK (Java Development Kit) contains everything you need to compile and run Java programs. It includes the Java compiler, the Java Runtime Environment (JRE), and developer tools.

After installation, verify your JDK by opening a terminal.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or cmd (Windows) and press Enter to open it.
  • Check your Java version by running this command:
java -version

✔️ I see version 17 or higher

You should see output that includes openjdk version "17 (or a higher number like 21 or 25). Your JDK is ready to go.

ⓧ I see an older version

If you see a version number lower than 17 (for example, 11 or 8), you have an outdated Java installation.

  • Go back to the Liberica JDK 17 download page and download the JDK 17 installer for your OS.
  • Run the installer. It will update your system's default Java version.
  • Close and reopen your terminal, then run java -version again.

macOS may default to an older version

macOS sometimes ships with an older Java version. After installing Liberica JDK 17, close your terminal completely and open a new one before retrying the version check.

ⓧ Command not found

If you see an error like "command not found" or "java is not recognized", Java is not installed yet.

  • Go to the Liberica JDK 17 download page.
  • Select your operating system and download the Standard JDK installer (.dmg for macOS, .msi for Windows).
  • Run the installer and follow the prompts.
  • Close and reopen your terminal, then run java -version again to confirm the installation.
Download and install IntelliJ IDEA

IntelliJ IDEA is the most popular IDE for Java and Spring Boot development. Its core Java features are completely free to use.

  • Open your browser and go to the IntelliJ IDEA download page.
  • Click Download for your operating system (Windows, macOS, or Linux).
  • Run the downloaded installer and follow the setup wizard.
  • When prompted about a 30-day Ultimate trial, you can skip it or accept it. The free core features are all you need for this project.

What about VS Code?

If you prefer VS Code, you can use it instead. Install the Spring Boot Extension Pack from the Extensions marketplace to get Spring Boot support, Java language features, and Maven integration.

  • Once the installation finishes, open IntelliJ IDEA.
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type IntelliJ and press Enter to launch the IDE.

You should see the IntelliJ IDEA Welcome screen with options to create or open a project.

IntelliJ not opening?

On macOS, you may need to allow IntelliJ IDEA in System Settings > Privacy & Security if you see a security warning. On Windows, try running the installer as Administrator if it fails to launch.

help me troubleshoot why IntelliJ IDEA won't open on my machine

Verify Maven is available

Maven is the build tool that manages your project's dependencies and compiles your code. You do not need to install Maven separately.

Why no Maven install?

Spring Initializr projects include a Maven Wrapper (mvnw on macOS/Linux, mvnw.cmd on Windows) bundled directly in the project. This means you can build and run your project without a system-wide Maven installation.

IntelliJ IDEA also bundles its own Maven distribution, so if you open the project there, everything works out of the box.

If you want to confirm Maven works on your system independently, you can check in your terminal:

  • Run this command in your terminal:
mvn -version

✔️ I see a version number

You have Maven installed system-wide. This is a nice bonus but not required for this project since you will use the Maven Wrapper included in the generated project.

ⓧ Command not found

This is completely fine. You do not need a system-wide Maven installation. The Maven Wrapper (mvnw/mvnw.cmd) included in your Spring Boot project will handle everything.

Your development environment is ready. Next up, you will use Spring Initializr to generate the skeleton of your Spring Boot project.

Generate Your Spring Boot Project

Your development environment is ready with JDK 17 and IntelliJ IDEA installed. Now it's time to create the actual Spring Boot project that will become your product catalog API.

Instead of setting up a project from scratch, you'll use Spring Initializr, a web tool that generates a fully configured Spring Boot project for you. It handles all the boilerplate configuration so you can jump straight into building your API.

In this step, get ready to:
  • Configure a new Spring Boot project on Spring Initializr.
  • Add the four dependencies your API needs.
  • Download and open the project in IntelliJ IDEA.
Configure your project on Spring Initializr

Spring Initializr is a web-based project generator provided by the Spring team. You fill out a form with your project settings, and it produces a ready-to-run project as a ZIP file.

  • Open your browser and navigate to start.spring.io.
  • Under **Project**, select Maven.
  • Under Language, select Java.
  • Under Spring Boot, select 3.5.14.
  • In the Group field, enter com.example.
  • In the Artifact field, enter productcatalog.
  • Leave Name, Description, Package name, and Packaging at their default values.
  • Under Java, select 17.

What is Spring Initializr?

Spring Initializr generates a complete project skeleton with the correct folder structure, build configuration, and starter dependencies already wired together.

The Group field (com.example) sets your Java package namespace. The Artifact field (productcatalog) becomes your project folder name and the name of your application.

Add your project dependencies

Dependencies are pre-built libraries that give your application specific capabilities. You need four dependencies for a REST API that stores data with validation.

  • Click the ADD DEPENDENCIES button (or press Ctrl+B).
  • Search for Spring Web and select it.
  • Search for Spring Data JPA and select it.
  • Search for H2 Database and select it.
  • Search for Validation and select it.

You should now see all four dependencies listed on the right side of the page.

What do these dependencies do?

  • Spring Web adds an embedded Tomcat server and Spring MVC for handling HTTP requests.
  • Spring Data JPA provides the tools to map Java objects to database tables using JPA and Hibernate.
  • H2 Database is a lightweight in-memory database that requires zero setup.
  • Validation adds Jakarta Bean Validation so you can enforce rules on incoming data.
Download and open the project in IntelliJ
  • Click the Generate button at the bottom of the page.
  • A file called productcatalog.zip downloads to your computer.
  • Locate the ZIP file in your Downloads folder and extract it.
  • Move the extracted productcatalog folder to your Desktop.

Now open the project in IntelliJ IDEA.

  • Switch back to IntelliJ IDEA.
  • Click File in the top menu bar.
  • Click Open.
  • Navigate to your Desktop and select the productcatalog folder.
  • Click OK to open the project.
  • If IntelliJ asks whether to trust the project, click Trust Project.

What should I see?

IntelliJ will take a moment to index and import the project. Once complete, you should see a project structure in the left sidebar with folders like src/main/java and src/main/resources.

The key file is ProductcatalogApplication.java inside src/main/java/com/example/productcatalog/. This is your application's entry point.

Project not importing correctly?

Make sure you opened the productcatalog folder itself (the one containing pom.xml), not a parent folder.

If IntelliJ does not recognise the project as a Maven project, right-click pom.xml in the sidebar, then select Add as Maven Project.

Help me open my Spring Boot project in IntelliJ

✔️ Awesome, I've got everything!

Great. Make sure you've saved everything and your project is fully indexed before moving on.

ⓧ I'd like to double check the full code

Here is what your generated files should look like. These are auto-generated by Spring Initializr.

package com.example.productcatalog;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ProductcatalogApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProductcatalogApplication.class, args);
    }
}
spring.application.name=productcatalog

Your Spring Boot project is ready to go. Next up, you'll create the Product entity that defines what a product looks like in your database.

Create the Product Entity

Your Spring Boot project is generated and open in your IDE. Now it needs something to work with: product data.

Every ecommerce store has products, and your API needs a Java class that represents a product in both your code and your database. In this step, you'll create a Product entity that maps directly to a database table using JPA annotations.

In this step, get ready to:
  • Create a Product entity class with JPA annotations.
  • Define fields for product data like name, price, and category.
  • Add validation annotations to enforce data quality.
Create the Product entity class

In Java, a JPA entity is a class that maps to a database table. Each instance of the class becomes a row in the table, and each field becomes a column.

  • In the Project tool window on the left, expand src/main/java/com/example/productcatalog.
  • Right-click the productcatalog package and select New then Package.
  • Type model and press Enter.
  • Right-click the new model package and select New then Java Class.
  • Type Product and press Enter.

IntelliJ creates the file with a basic class skeleton. Replace the entire contents of Product.java with the following code:

package com.example.productcatalog.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import java.math.BigDecimal;

@Entity
@Table(name = "products")
public class Product {

    // Primary key, auto-generated by the database
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // Product name - required field
    @NotBlank(message = "Product name is required")
    private String name;

    // Optional description
    private String description;

    // Price - must be a positive number
    @NotNull(message = "Price is required")
    @Positive(message = "Price must be greater than zero")
    private BigDecimal price;

    // Category - required for organizing products
    @NotBlank(message = "Category is required")
    private String category;

    // Stock - cannot be negative
    @NotNull(message = "Stock quantity is required")
    @PositiveOrZero(message = "Stock quantity cannot be negative")
    private Integer stockQuantity;

    // No-arg constructor required by JPA
    public Product() {
    }

    // Constructor for creating new products (id is auto-generated)
    public Product(String name, String description, BigDecimal price, String category, Integer stockQuantity) {
        this.name = name;
        this.description = description;
        this.price = price;
        this.category = category;
        this.stockQuantity = stockQuantity;
    }

    // Getters and setters for all fields
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }
    public BigDecimal getPrice() { return price; }
    public void setPrice(BigDecimal price) { this.price = price; }
    public String getCategory() { return category; }
    public void setCategory(String category) { this.category = category; }
    public Integer getStockQuantity() { return stockQuantity; }
    public void setStockQuantity(Integer stockQuantity) { this.stockQuantity = stockQuantity; }
}

What does this code do?

This single class defines your entire product data model. There are three key sections to understand:

  • JPA annotations: @Entity tells Spring Data JPA this class maps to a database table. @Table(name = "products") names the table. @Id and @GeneratedValue mark the primary key and tell the database to auto-generate it.
  • Validation annotations: @NotBlank ensures a string is not empty. @NotNull prevents null values. @Positive and @PositiveOrZero enforce numeric constraints. Spring automatically checks these when data arrives at your API.
  • Constructors and accessors: JPA requires a no-argument constructor. The second constructor lets you create products in code without setting the id (since the database generates it). Getters and setters let Spring read and write each field.
  • Save Product.java by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Check the Project tool window on the left. You should see the model package under productcatalog with Product.java inside it.
  • Confirm there are no red underlines in the editor. If IntelliJ shows errors, check that your imports match the code above exactly.

Seeing red underlines on the imports?

If IntelliJ cannot resolve jakarta.persistence or jakarta.validation imports, the project dependencies may not have loaded yet. Right-click your pom.xml file and select Maven then Reload Project. This downloads the Spring Data JPA and Validation starter libraries.

Help me fix unresolved imports in my Product entity

Why BigDecimal instead of double for price?

Money calculations need exact precision. The double type introduces tiny rounding errors (like 0.1 + 0.2 = 0.30000000000000004). BigDecimal stores exact decimal values, which is why every production ecommerce system uses it for prices.

Your database column automatically uses a precise numeric type to match.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete Product.java file:

package com.example.productcatalog.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import java.math.BigDecimal;

@Entity
@Table(name = "products")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank(message = "Product name is required")
    private String name;

    private String description;

    @NotNull(message = "Price is required")
    @Positive(message = "Price must be greater than zero")
    private BigDecimal price;

    @NotBlank(message = "Category is required")
    private String category;

    @NotNull(message = "Stock quantity is required")
    @PositiveOrZero(message = "Stock quantity cannot be negative")
    private Integer stockQuantity;

    public Product() {
    }

    public Product(String name, String description, BigDecimal price, String category, Integer stockQuantity) {
        this.name = name;
        this.description = description;
        this.price = price;
        this.category = category;
        this.stockQuantity = stockQuantity;
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }
    public BigDecimal getPrice() { return price; }
    public void setPrice(BigDecimal price) { this.price = price; }
    public String getCategory() { return category; }
    public void setCategory(String category) { this.category = category; }
    public Integer getStockQuantity() { return stockQuantity; }
    public void setStockQuantity(Integer stockQuantity) { this.stockQuantity = stockQuantity; }
}

Your data model is ready. Next up, you'll build the Repository and Service layers that read and write products to the database using this entity.

Build the Repository and Service Layers

In the previous step, you created a Product entity that maps directly to your database table. Now you need a way to actually read and write product data.

Enterprise Java applications use a layered architecture to keep code organized. The repository layer handles database operations. The service layer contains your business logic. In this step, you will build both layers for your product catalog.

In this step, get ready to:
  • Create a repository interface that gives you database operations for free.
  • Create a service class with full CRUD business logic.
  • Wire the repository into the service using constructor injection.
Create the ProductRepository interface

Spring Data JPA eliminates the need to write SQL queries by hand. You define an interface, extend JpaRepository, and Spring automatically provides implementations for common operations like save, find, delete, and pagination.

  • In your IDE, right-click the com.example.productcatalog package in the src/main/java directory.
  • Create a new package called repository.
  • Inside the repository package, create a new Java file called ProductRepository.java.
  • Add the following code to define your repository interface:
package com.example.productcatalog.repository;

import com.example.productcatalog.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
}

What does this code do?

This interface has no method bodies, yet it gives you a full set of database operations.

  • @Repository marks this as a Spring-managed data access component.
  • JpaRepository<Product, Long> tells Spring the entity type is Product and the primary key type is Long.
  • Spring Data JPA automatically generates implementations for methods like save(), findById(), findAll(), and delete() at runtime.
  • Save ProductRepository.java.
  • Check that your IDE shows no red underlines or errors in the file.

Seeing red underlines on imports?

  • Make sure your pom.xml includes the spring-boot-starter-data-jpa dependency. Without it, the JpaRepository import cannot resolve.
  • Confirm the file is inside the repository package under com.example.productcatalog. Spring Boot scans sub-packages of your main application class automatically.

Why isn't my JpaRepository import resolving?

✔️ Awesome, I've got everything!

Great. Make sure you have saved ProductRepository.java before moving on.

ⓧ I'd like to double check the full code

package com.example.productcatalog.repository;

import com.example.productcatalog.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
}

Why is the interface empty?

JpaRepository already declares methods for all standard database operations. Your ProductRepository inherits those methods without writing a single line of implementation. Spring generates the actual SQL at runtime based on the method signatures.

Create the ProductService class with constructor injection

The service layer sits between your controller and repository. It encapsulates business logic so your controller only handles HTTP concerns and your repository only handles data access.

  • Inside the com.example.productcatalog package, create a new package called service.
  • Inside the service package, create a new Java file called ProductService.java.
  • Add the following code to create the service class with its repository dependency:
package com.example.productcatalog.service;

import com.example.productcatalog.model.Product;
import com.example.productcatalog.repository.ProductRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }
}

What does this code do?

  • @Service tells Spring this class contains business logic and should be managed as a Spring bean.
  • private final ProductRepository productRepository declares the dependency as a field. The final keyword ensures it cannot be reassigned after construction.
  • The constructor accepts a ProductRepository parameter. Spring automatically injects the repository instance when creating this service. This pattern is called constructor injection.
  • Save ProductService.java.
  • Confirm your IDE shows no red underlines on the imports or class declaration.

Import errors on ProductRepository?

  • Make sure ProductRepository.java is saved and error-free. The service imports it directly.
  • Verify the package declaration in your repository file is com.example.productcatalog.repository. A typo here prevents the import from resolving.

Why is my ProductRepository import failing in ProductService?

Implement CRUD business logic

CRUD stands for Create, Read, Update, and Delete. These are the four fundamental operations every data-driven API needs. You will now add methods to your service that delegate to the repository.

  • In ProductService.java, add the following three methods inside the class, below the constructor:
    public Product createProduct(Product product) {
        return productRepository.save(product);
    }

    public Page<Product> getAllProducts(Pageable pageable) {
        return productRepository.findAll(pageable);
    }

    public Optional<Product> getProductById(Long id) {
        return productRepository.findById(id);
    }

What do these methods do?

  • createProduct() saves a new product to the database and returns the saved entity (now with a generated ID).
  • getAllProducts() accepts a Pageable parameter and returns a Page<Product>. This enables pagination so clients can request specific pages of results instead of loading every product at once.
  • getProductById() returns an Optional<Product>. The Optional wrapper safely handles the case where no product exists with that ID.
  • Save ProductService.java and confirm no errors appear.
  • Below the getProductById method, add the updateProduct method:
    public Product updateProduct(Long id, Product productDetails) {
        Product product = productRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Product not found with id: " + id));

        product.setName(productDetails.getName());
        product.setDescription(productDetails.getDescription());
        product.setPrice(productDetails.getPrice());
        product.setCategory(productDetails.getCategory());
        product.setStockQuantity(productDetails.getStockQuantity());

        return productRepository.save(product);
    }

What does updateProduct do?

This method first looks up the existing product by ID. If no product is found, it throws a RuntimeException immediately.

If the product exists, the method overwrites each field with the new values from productDetails. Finally, it saves the updated product back to the database and returns it.

  • Save ProductService.java and confirm no errors appear.
  • Below the updateProduct method, add the deleteProduct method:
    public void deleteProduct(Long id) {
        Product product = productRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Product not found with id: " + id));
        productRepository.delete(product);
    }

What does deleteProduct do?

This follows the same find-or-throw pattern as updateProduct. It looks up the product by ID, throws an exception if it does not exist, then removes it from the database.

  • Save ProductService.java.
  • Confirm your IDE shows no red underlines anywhere in the file. All five methods should compile cleanly.

Seeing errors on Page, Pageable, or Optional?

  • Make sure the imports at the top of your file include org.springframework.data.domain.Page, org.springframework.data.domain.Pageable, and java.util.Optional. These are required by the method signatures.
  • If your IDE offers auto-import suggestions, accept the org.springframework.data.domain versions (not any other package).

Why is my ProductService showing import errors on Page or Pageable?

✔️ Awesome, I've got everything!

Great. Make sure you have saved ProductService.java before moving on.

ⓧ I'd like to double check the full code

package com.example.productcatalog.service;

import com.example.productcatalog.model.Product;
import com.example.productcatalog.repository.ProductRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    public Product createProduct(Product product) {
        return productRepository.save(product);
    }

    public Page<Product> getAllProducts(Pageable pageable) {
        return productRepository.findAll(pageable);
    }

    public Optional<Product> getProductById(Long id) {
        return productRepository.findById(id);
    }

    public Product updateProduct(Long id, Product productDetails) {
        Product product = productRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Product not found with id: " + id));

        product.setName(productDetails.getName());
        product.setDescription(productDetails.getDescription());
        product.setPrice(productDetails.getPrice());
        product.setCategory(productDetails.getCategory());
        product.setStockQuantity(productDetails.getStockQuantity());

        return productRepository.save(product);
    }

    public void deleteProduct(Long id) {
        Product product = productRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Product not found with id: " + id));
        productRepository.delete(product);
    }
}

Your repository and service layers are complete. In the next step, you will build the REST controller that exposes these operations as HTTP endpoints.

Create the REST Controller with CRUD Endpoints

Your service layer is ready to handle business logic. Now you need a way for external clients to actually reach that logic over HTTP.

A REST controller is the front door to your application. It receives HTTP requests, delegates work to the service, and returns appropriate HTTP responses. In this step, you will create the controller that exposes your product CRUD operations as a proper REST API.

In this step, get ready to:
  • Create the ProductController class with REST annotations.
  • Implement POST, GET by ID, PUT, and DELETE endpoints.
  • Add a paginated GET all products endpoint using Spring Data's Pageable.
Create the controller class with REST annotations

The controller sits in the top layer of your architecture. It maps incoming HTTP requests to Java methods and returns ResponseEntity objects that control the HTTP status code sent back to the client.

  • In your IDE, create a new package called controller inside src/main/java/com/example/productcatalog/.
  • Create a new Java class called ProductController.java inside the controller package.
  • Add the following code to set up the class with all necessary imports and constructor injection:
package com.example.productcatalog.controller;

import com.example.productcatalog.model.Product;
import com.example.productcatalog.service.ProductService;
import jakarta.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
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")
public class ProductController {

    private final ProductService productService;

    // Constructor injection wires the service into this controller
    public ProductController(ProductService productService) {
        this.productService = productService;
    }
}

What does this code do?

@RestController combines @Controller and @ResponseBody. Every method in this class automatically serializes its return value to JSON.

@RequestMapping("/api/products") sets a base path for all endpoints in this controller. Every method-level mapping builds on top of this base path.

The constructor accepts ProductService as a parameter. Spring automatically injects the service bean here because there is only one constructor.

  • Save ProductController.java.
  • Confirm the file compiles without errors in your IDE. You should see no red underlines or error markers.

Seeing import errors?

Make sure you created the file inside src/main/java/com/example/productcatalog/controller/. The package declaration must match the folder path exactly.

If your IDE cannot resolve ProductService, confirm that ProductService.java exists in the service package and compiles successfully.

Help me fix import errors in my Spring Boot controller.

Implement the CRUD endpoints

Each endpoint maps an HTTP method to a Java method. You will add the POST endpoint first, then the remaining read, update, and delete operations.

  • Inside the ProductController class (below the constructor), add the POST endpoint that creates a new product:
    @PostMapping
    public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
        // Delegate creation to the service layer
        Product createdProduct = productService.createProduct(product);
        // Return 201 Created status with the saved product
        return new ResponseEntity<>(createdProduct, HttpStatus.CREATED);
    }

What does this endpoint do?

@PostMapping maps HTTP POST requests to /api/products. The @RequestBody annotation tells Spring to deserialize the incoming JSON body into a Product object.

@Valid triggers the validation annotations you placed on the Product entity. If a required field is missing or invalid, Spring returns a 400 Bad Request automatically.

The method returns HttpStatus.CREATED (HTTP 201) instead of the default 200, which is the correct status code when a new resource is created.

  • Below the createProduct method, add the GET by ID, PUT, and DELETE endpoints:
    @GetMapping("/{id}")
    public ResponseEntity<Product> getProductById(@PathVariable Long id) {
        // Return 200 if found, 404 if not
        return productService.getProductById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @PutMapping("/{id}")
    public ResponseEntity<Product> updateProduct(@PathVariable Long id, @Valid @RequestBody Product product) {
        // Update the product and return the updated version
        Product updatedProduct = productService.updateProduct(id, product);
        return ResponseEntity.ok(updatedProduct);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
        // Delete the product and return 204 No Content
        productService.deleteProduct(id);
        return ResponseEntity.noContent().build();
    }

What do these endpoints do?

  • @GetMapping("/{id}") maps to GET /api/products/1. The @PathVariable annotation extracts the id from the URL path. If the product exists, it returns 200. If not, it returns 404.
  • @PutMapping("/{id}") handles update requests. It uses both @PathVariable for the ID and @Valid @RequestBody for the updated product data.
  • @DeleteMapping("/{id}") removes the product and returns ResponseEntity.noContent().build() which sends HTTP 204 with no body.
  • Save ProductController.java.
  • Confirm the file still compiles without errors in your IDE.

Red underline on ResponseEntity methods?

Check that your imports at the top of the file include org.springframework.http.ResponseEntity and org.springframework.http.HttpStatus. If you copied the full class setup from the first chunk, these should already be present.

Help me resolve compilation errors in my ProductController.

Add the paginated GET all products endpoint

A real ecommerce catalog can have thousands of products. Returning them all in one response would overwhelm clients and waste bandwidth. Pagination solves this by returning data in manageable chunks.

  • In ProductController.java, find the createProduct method. Add the following GET all endpoint between createProduct and getProductById:
    @GetMapping
    public ResponseEntity<Page<Product>> getAllProducts(Pageable pageable) {
        // Return a paginated list of all products
        Page<Product> products = productService.getAllProducts(pageable);
        return ResponseEntity.ok(products);
    }

What is Pageable?

Spring Data's Pageable interface represents a page request. Spring automatically resolves it from query parameters like ?page=0&size=10&sort=price,asc.

The return type Page<Product> includes the product data plus metadata about total pages, total elements, and whether there are more pages. Clients use this metadata to build pagination controls.

  • Save ProductController.java.
  • Confirm the file compiles without errors. You now have five endpoints defined in your controller.

Cannot resolve Pageable or Page?

Verify that your imports include org.springframework.data.domain.Page and org.springframework.data.domain.Pageable. These come from the Spring Data JPA dependency you added when generating the project.

Help me fix Pageable import issues in my Spring Boot controller.

✔️ Awesome, I've got everything!

Great. Double check you have saved ProductController.java and it compiles cleanly.

ⓧ I'd like to double check the full code

Here is the complete ProductController.java file. Compare yours against it:

package com.example.productcatalog.controller;

import com.example.productcatalog.model.Product;
import com.example.productcatalog.service.ProductService;
import jakarta.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
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")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @PostMapping
    public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
        Product createdProduct = productService.createProduct(product);
        return new ResponseEntity<>(createdProduct, HttpStatus.CREATED);
    }

    @GetMapping
    public ResponseEntity<Page<Product>> getAllProducts(Pageable pageable) {
        Page<Product> products = productService.getAllProducts(pageable);
        return ResponseEntity.ok(products);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> getProductById(@PathVariable Long id) {
        return productService.getProductById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @PutMapping("/{id}")
    public ResponseEntity<Product> updateProduct(@PathVariable Long id, @Valid @RequestBody Product product) {
        Product updatedProduct = productService.updateProduct(id, product);
        return ResponseEntity.ok(updatedProduct);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
        productService.deleteProduct(id);
        return ResponseEntity.noContent().build();
    }
}

Your REST controller is complete. All five CRUD endpoints are wired to your service layer and ready to handle HTTP requests. Next up, you will configure the database and run the application to test these endpoints live.

Configure the Application and Test Your API

Your controller, service, and repository layers are all wired together. The last piece is telling Spring Boot where to store your data and how to manage the database connection.

In this step, you will configure the H2 in-memory database, start your application, and test every CRUD endpoint to prove your API works end-to-end.

In this step, get ready to:
  • Configure your application.properties with H2 database and JPA settings.
  • Run the Spring Boot application and confirm it starts on port 8080.
  • Test all CRUD endpoints using curl commands.
Configure the H2 database connection

Spring Boot needs to know which database to connect to, how to generate tables from your entity, and whether to expose a database browser.

  • In your IDE, open application.properties (located in src/main/resources/).
  • Find the existing line spring.application.name=productcatalog and add the following configuration below it:
spring.application.name=productcatalog

# H2 in-memory database connection
spring.datasource.url=jdbc:h2:mem:productdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# JPA/Hibernate settings
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

# Enable H2 web console for inspecting data
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

What does this configuration do?

  • spring.datasource.url=jdbc:h2:mem:productdb creates an in-memory database called productdb. Data lives only while the app runs.
  • spring.jpa.hibernate.ddl-auto=create-drop tells Hibernate to auto-generate your products table from the Product entity on startup, and drop it on shutdown.
  • spring.jpa.show-sql=true prints every SQL query to your console so you can see exactly what Hibernate does behind the scenes.
  • spring.h2.console.enabled=true activates a browser-based database viewer at /h2-console.
  • Save application.properties.

File not saving?

Make sure you are editing the file in src/main/resources/ and not any other application.properties that might exist in a test folder.

help me find the right application.properties file in my Spring Boot project

✔️ Awesome, I've got everything!

Great. Make sure you have saved the file before moving on.

ⓧ I'd like to double check the full code

spring.application.name=productcatalog

spring.datasource.url=jdbc:h2:mem:productdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
Run your Spring Boot application

The Maven Wrapper bundled with your project lets you start the application without installing Maven separately.

  • Open a terminal in your project's root folder (the folder containing pom.xml).
  • Start the application by running the appropriate command for your operating system:

macOS

./mvnw spring-boot:run

Windows

mvnw.cmd spring-boot:run

What should I see?

Spring Boot prints a startup banner followed by log lines. Look for a line containing Started ProductcatalogApplication and Tomcat started on port 8080. This confirms your embedded web server is running and ready to accept requests.

You will also see SQL statements printed because you enabled show-sql. These show Hibernate creating the products table automatically.

Application won't start?

If you see a "port already in use" error, another process is using port 8080. Close any other running servers or applications that might be occupying that port.

If you see compilation errors, double-check that all five Java files saved correctly and that your package declarations match the folder structure.

help me debug why my Spring Boot application won't start

Test the CRUD endpoints with curl

Your API is live. Now you will send HTTP requests to each endpoint and confirm the correct responses come back.

  • Open a new terminal window (keep the first one running your server).
  • Create a product by sending a POST request:

macOS

curl -X POST http://localhost:8080/api/products -H "Content-Type: application/json" -d '{"name":"Wireless Mouse","description":"Ergonomic wireless mouse","price":29.99,"category":"Electronics","stockQuantity":150}'

Windows

curl -X POST http://localhost:8080/api/products -H "Content-Type: application/json" -d "{\"name\":\"Wireless Mouse\",\"description\":\"Ergonomic wireless mouse\",\"price\":29.99,\"category\":\"Electronics\",\"stockQuantity\":150}"

You should see a JSON response with a generated id field and HTTP status 201 (Created).

  • Retrieve all products with a GET request:
curl http://localhost:8080/api/products

You should see a paginated JSON response containing your Wireless Mouse product inside a content array.

  • Retrieve a single product by ID:
curl http://localhost:8080/api/products/1

You should see just the single product object with id of 1.

  • Update the product's price and name with a PUT request:

macOS

curl -X PUT http://localhost:8080/api/products/1 -H "Content-Type: application/json" -d '{"name":"Wireless Mouse Pro","description":"Updated ergonomic wireless mouse","price":34.99,"category":"Electronics","stockQuantity":120}'

Windows

curl -X PUT http://localhost:8080/api/products/1 -H "Content-Type: application/json" -d "{\"name\":\"Wireless Mouse Pro\",\"description\":\"Updated ergonomic wireless mouse\",\"price\":34.99,\"category\":\"Electronics\",\"stockQuantity\":120}"

You should see the updated product returned with the new name Wireless Mouse Pro and price 34.99.

  • Delete the product:
curl -X DELETE http://localhost:8080/api/products/1

You should see no response body. The server returns HTTP status 204 (No Content), confirming the product was deleted.

  • Confirm the deletion by requesting the same product again:
curl http://localhost:8080/api/products/1

You should see an empty response with HTTP status 404 (Not Found). Your API correctly reports that the product no longer exists.

Inspect your database directly

Open your browser and navigate to http://localhost:8080/h2-console. In the JDBC URL field, enter jdbc:h2:mem:productdb and click Connect.

You can run SQL queries here to see your PRODUCTS table and any data stored in it.

Seeing a validation error on POST?

Make sure your JSON includes all required fields: name, price, category, and stockQuantity. Missing any of these triggers your validation annotations.

On Windows, double-check that your escaped quotes match the format shown in the Windows tab exactly.

help me troubleshoot curl errors when testing my Spring Boot API

Your ecommerce product catalog API is fully functional. Every layer is working together: the controller handles HTTP, the service manages business logic, and the repository persists data to your H2 database. Next up, push your API further with a secret mission that adds search and filtering.

Secret mission

Add Search and Filtering

Your product catalog works, but real ecommerce APIs let users find what they need fast. Can you add search by category and price range using JPQL queries?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with an in-memory database, so there are no ongoing costs.

Resources you used:

  • The running Spring Boot application (localhost:8080).
  • The productcatalog project folder on your Desktop.
  • JDK 17 installation.
  • IntelliJ IDEA Community Edition installation.

Keep everything running

No action needed. Choose this if you're still actively building or plan to extend this API further.

  • Your Spring Boot application continues running on localhost:8080 until you stop it or close the terminal.
  • The H2 in-memory database resets every time you restart the application, so no data accumulates over time.
  • JDK 17 and IntelliJ IDEA remain installed for future Java projects.

Pause - I'll come back to this later

Shut down the running application to free up your terminal and port 8080. Your project files stay on disk so you can restart anytime.

  • Switch to the terminal where your Spring Boot application is running.
  • Press Ctrl+C to stop the application.
  • To restart later, open a terminal in the productcatalog folder and run mvnw.cmd spring-boot:run (Windows) or ./mvnw spring-boot:run (macOS/Linux).

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow these steps to clean up everything.

  • Switch to the terminal where your Spring Boot application is running and press Ctrl+C to stop it.
  • Delete the project folder by running the appropriate command for your operating system.

macOS

  • Delete the project folder by running:
rm -rf ~/Desktop/productcatalog

Windows

  • Delete the project folder by running:
rmdir /s /q %USERPROFILE%\Desktop\productcatalog
  • If you no longer need JDK 17, uninstall it through Apps & features (Windows) or by removing the JDK directory from /Library/Java/JavaVirtualMachines/ (macOS).
  • If you no longer need IntelliJ IDEA, uninstall it through Apps & features (Windows) or by dragging it from your Applications folder to the Trash (macOS).

Nice Work!

Nice Work!

You just built a production-style ecommerce REST API from scratch using Spring Boot. Your API handles full product lifecycle management with validation, pagination, and search.

Here's what you learned:

  • Built a Spring Boot REST API with full CRUD endpoints running on an embedded Tomcat server at localhost:8080.
  • Designed a layered architecture (Controller, Service, Repository) following enterprise Java patterns with constructor injection and separation of concerns.
  • Added pagination and input validation using Spring Data JPA and Jakarta Bean Validation annotations to handle large product catalogs at scale.
  • Secret Mission: Added product search and filtering by category and price range using custom JPQL queries.

Ready to quiz yourself?