Spring Boot

Building a CRUD REST API with Spring Boot 3, Spring Data JPA, and PostgreSQL

By Utility Zone · 2025-11-01T14:24:34.383329

This comprehensive tutorial will guide you through creating a production-ready REST API using Spring Boot 3, Spring Data JPA, and PostgreSQL. We'll implement a complete CRUD (Create, Read, Update, Delete) application following industry best practices with a clean layered architecture.


Project Overview

Lets build a Product Management API that demonstrates all CRUD operations. The application follows a layered architecture pattern with four distinct layers:

  • Entity Layer: Data models mapped to database tables
  • Repository Layer: Data access abstraction using Spring Data JPA
  • Service Layer: Business logic and transaction management
  • Controller Layer: REST API endpoints and HTTP handling

Prerequisites

  • Java 17 or higher
  • PostgreSQL installed and running
  • Maven 3.6+
  • IDE (IntelliJ IDEA, Eclipse, or VS Code)

Step by Step Guide

Step 1: Create Spring Boot Project

Create a new Spring Boot 3 project with the following dependencies in your pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>product-api</artifactId>
    <version>1.0.0</version>
    <name>product-api</name>
    <description>CRUD REST API with Spring Boot and PostgreSQL</description>
    
    <properties>
        <java.version>17</java.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Web Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Spring Data JPA -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        
        <!-- PostgreSQL Driver -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- Validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        
        <!-- Lombok (Optional - reduces boilerplate) -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- Spring Boot DevTools (Optional) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Step 2: Configure PostgreSQL Database

First, create a PostgreSQL database and user:1

-- Create database
CREATE DATABASE productdb;

-- Create user
CREATE USER productuser WITH ENCRYPTED PASSWORD 'productpass';

-- Grant privileges
GRANT ALL PRIVILEGES ON DATABASE productdb TO productuser;

Configure the database connection in src/main/resources/application.properties:23

# PostgreSQL Database Configuration
spring.datasource.url=jdbc:postgresql://localhost:5432/productdb
spring.datasource.username=productuser
spring.datasource.password=productpass
spring.datasource.driver-class-name=org.postgresql.Driver

# JPA/Hibernate Configuration
spring.jpa.database=POSTGRESQL
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.format_sql=true

# Logging
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

# Server Configuration
server.port=8080

Important Configuration Notes:32

  • spring.jpa.hibernate.ddl-auto=update: Automatically creates/updates database tables based on entity classes. Use validate in production2
  • spring.jpa.show-sql=true: Displays SQL queries in console for debugging
  • spring.jpa.properties.hibernate.dialect: Specifies PostgreSQL-specific SQL dialect4

Step 3: Create Entity Layer

Create the Product entity class that maps to the database table:567

package com.example.productapi.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.time.LocalDateTime;

@Entity
@Table(name = "products")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, length = 100)
    @NotBlank(message = "Product name is required")
    @Size(min = 3, max = 100, message = "Name must be between 3 and 100 characters")
    private String name;
    
    @Column(length = 500)
    @Size(max = 500, message = "Description cannot exceed 500 characters")
    private String description;
    
    @Column(nullable = false, precision = 10, scale = 2)
    @NotNull(message = "Price is required")
    @DecimalMin(value = "0.01", message = "Price must be greater than 0")
    private BigDecimal price;
    
    @Column(nullable = false)
    @NotNull(message = "Quantity is required")
    @Min(value = 0, message = "Quantity cannot be negative")
    private Integer quantity;
    
    @Column(name = "is_available")
    private Boolean isAvailable = true;
    
    @Column(length = 50)
    private String category;
    
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;
    
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    
    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }
    
    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }
}

Key Annotations Explained:67

  • @Entity: Marks the class as a JPA entity6
  • @Table(name = "products"): Specifies the database table name7
  • @Id: Designates the primary key field7
  • @GeneratedValue(strategy = GenerationType.IDENTITY): Auto-generates ID values using database identity column7
  • @Column: Customizes column properties (nullable, length, precision)7
  • @NotBlank, @NotNull, @Size, etc.: Validation constraints89
  • @PrePersist and @PreUpdate: Lifecycle callbacks for automatic timestamp management

Step 4: Create Repository Layer

Create the repository interface extending JpaRepository:10115

package com.example.productapi.repository;

import com.example.productapi.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    
    // Custom query methods - Spring Data JPA automatically implements these
    
    // Find products by availability status
    List<Product> findByIsAvailable(Boolean isAvailable);
    
    // Find products by category
    List<Product> findByCategory(String category);
    
    // Find products by name containing a keyword (case-insensitive)
    List<Product> findByNameContainingIgnoreCase(String keyword);
    
    // Find products within a price range
    List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
    
    // Find products by category and availability
    List<Product> findByCategoryAndIsAvailable(String category, Boolean isAvailable);
    
    // Custom JPQL query to find low stock products
    @Query("SELECT p FROM Product p WHERE p.quantity < :threshold AND p.isAvailable = true")
    List<Product> findLowStockProducts(@Param("threshold") Integer threshold);
    
    // Native SQL query example
    @Query(value = "SELECT * FROM products WHERE price > :minPrice ORDER BY price DESC", 
           nativeQuery = true)
    List<Product> findExpensiveProducts(@Param("minPrice") BigDecimal minPrice);
    
    // Check if product exists by name
    boolean existsByName(String name);
}

Repository Features:115

  • JpaRepository<Product, Long> provides built-in CRUD methods (save, findById, findAll, delete, etc.)5
  • Derived Query Methods: Spring Data JPA automatically generates implementation from method names11
  • Custom JPQL Queries: Use @Query annotation for complex queries11
  • Native SQL Queries: Set nativeQuery = true for database-specific queries11

Step 5: Create Service Layer

Create a service interface and implementation:121314

ProductService.java (Interface):

package com.example.productapi.service;

import com.example.productapi.entity.Product;

import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

public interface ProductService {
    
    Product createProduct(Product product);
    
    Optional<Product> getProductById(Long id);
    
    List<Product> getAllProducts();
    
    Product updateProduct(Long id, Product product);
    
    void deleteProduct(Long id);
    
    List<Product> getAvailableProducts();
    
    List<Product> getProductsByCategory(String category);
    
    List<Product> searchProductsByName(String keyword);
    
    List<Product> getProductsByPriceRange(BigDecimal minPrice, BigDecimal maxPrice);
    
    List<Product> getLowStockProducts(Integer threshold);
}

ProductServiceImpl.java (Implementation):1312

package com.example.productapi.service;

import com.example.productapi.entity.Product;
import com.example.productapi.exception.ResourceNotFoundException;
import com.example.productapi.exception.DuplicateResourceException;
import com.example.productapi.repository.ProductRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class ProductServiceImpl implements ProductService {
    
    private final ProductRepository productRepository;
    
    @Override
    public Product createProduct(Product product) {
        log.info("Creating new product: {}", product.getName());
        
        // Check if product with same name already exists
        if (productRepository.existsByName(product.getName())) {
            throw new DuplicateResourceException(
                "Product with name '" + product.getName() + "' already exists"
            );
        }
        
        Product savedProduct = productRepository.save(product);
        log.info("Product created successfully with ID: {}", savedProduct.getId());
        return savedProduct;
    }
    
    @Override
    @Transactional(readOnly = true)
    public Optional<Product> getProductById(Long id) {
        log.info("Fetching product with ID: {}", id);
        return productRepository.findById(id);
    }
    
    @Override
    @Transactional(readOnly = true)
    public List<Product> getAllProducts() {
        log.info("Fetching all products");
        return productRepository.findAll();
    }
    
    @Override
    public Product updateProduct(Long id, Product productDetails) {
        log.info("Updating product with ID: {}", id);
        
        Product existingProduct = productRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException(
                "Product not found with ID: " + id
            ));
        
        // Update fields
        existingProduct.setName(productDetails.getName());
        existingProduct.setDescription(productDetails.getDescription());
        existingProduct.setPrice(productDetails.getPrice());
        existingProduct.setQuantity(productDetails.getQuantity());
        existingProduct.setIsAvailable(productDetails.getIsAvailable());
        existingProduct.setCategory(productDetails.getCategory());
        
        Product updatedProduct = productRepository.save(existingProduct);
        log.info("Product updated successfully with ID: {}", id);
        return updatedProduct;
    }
    
    @Override
    public void deleteProduct(Long id) {
        log.info("Deleting product with ID: {}", id);
        
        Product product = productRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException(
                "Product not found with ID: " + id
            ));
        
        productRepository.delete(product);
        log.info("Product deleted successfully with ID: {}", id);
    }
    
    @Override
    @Transactional(readOnly = true)
    public List<Product> getAvailableProducts() {
        log.info("Fetching available products");
        return productRepository.findByIsAvailable(true);
    }
    
    @Override
    @Transactional(readOnly = true)
    public List<Product> getProductsByCategory(String category) {
        log.info("Fetching products by category: {}", category);
        return productRepository.findByCategory(category);
    }
    
    @Override
    @Transactional(readOnly = true)
    public List<Product> searchProductsByName(String keyword) {
        log.info("Searching products with keyword: {}", keyword);
        return productRepository.findByNameContainingIgnoreCase(keyword);
    }
    
    @Override
    @Transactional(readOnly = true)
    public List<Product> getProductsByPriceRange(BigDecimal minPrice, BigDecimal maxPrice) {
        log.info("Fetching products in price range: {} - {}", minPrice, maxPrice);
        return productRepository.findByPriceBetween(minPrice, maxPrice);
    }
    
    @Override
    @Transactional(readOnly = true)
    public List<Product> getLowStockProducts(Integer threshold) {
        log.info("Fetching low stock products with threshold: {}", threshold);
        return productRepository.findLowStockProducts(threshold);
    }
}

Service Layer Best Practices:141213

  • Business Logic: Service layer contains all business logic, not controllers12
  • Transaction Management: Use @Transactional for database operations13
  • Error Handling: Validate data and throw custom exceptions13
  • Logging: Add logging for debugging and monitoring13
  • Read-Only Transactions: Use @Transactional(readOnly = true) for query methods to optimize performance13

Step 6: Create Custom Exceptions

Create custom exception classes for better error handling:15

ResourceNotFoundException.java:

package com.example.productapi.exception;

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

DuplicateResourceException.java:

package com.example.productapi.exception;

public class DuplicateResourceException extends RuntimeException {
    public DuplicateResourceException(String message) {
        super(message);
    }
}

GlobalExceptionHandler.java:15

package com.example.productapi.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFoundException(
            ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            LocalDateTime.now()
        );
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }
    
    @ExceptionHandler(DuplicateResourceException.class)
    public ResponseEntity<ErrorResponse> handleDuplicateResourceException(
            DuplicateResourceException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.CONFLICT.value(),
            ex.getMessage(),
            LocalDateTime.now()
        );
        return new ResponseEntity<>(error, HttpStatus.CONFLICT);
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGlobalException(Exception ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "An unexpected error occurred: " + ex.getMessage(),
            LocalDateTime.now()
        );
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

ErrorResponse.java:

package com.example.productapi.exception;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ErrorResponse {
    private int status;
    private String message;
    private LocalDateTime timestamp;
}

Step 7: Create Controller Layer

Create the REST controller with all CRUD endpoints:16171819

package com.example.productapi.controller;

import com.example.productapi.entity.Product;
import com.example.productapi.service.ProductService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.math.BigDecimal;
import java.util.List;

@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
@Slf4j
@CrossOrigin(origins = "*")
public class ProductController {
    
    private final ProductService productService;
    
    /**
     * Create a new product
     * POST /api/products
     */
    @PostMapping
    public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
        log.info("REST request to create product: {}", product.getName());
        Product createdProduct = productService.createProduct(product);
        return new ResponseEntity<>(createdProduct, HttpStatus.CREATED);
    }
    
    /**
     * Get all products
     * GET /api/products
     */
    @GetMapping
    public ResponseEntity<List<Product>> getAllProducts() {
        log.info("REST request to get all products");
        List<Product> products = productService.getAllProducts();
        return ResponseEntity.ok(products);
    }
    
    /**
     * Get product by ID
     * GET /api/products/{id}
     */
    @GetMapping("/{id}")
    public ResponseEntity<Product> getProductById(@PathVariable Long id) {
        log.info("REST request to get product with ID: {}", id);
        return productService.getProductById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
    
    /**
     * Update product
     * PUT /api/products/{id}
     */
    @PutMapping("/{id}")
    public ResponseEntity<Product> updateProduct(
            @PathVariable Long id,
            @Valid @RequestBody Product productDetails) {
        log.info("REST request to update product with ID: {}", id);
        Product updatedProduct = productService.updateProduct(id, productDetails);
        return ResponseEntity.ok(updatedProduct);
    }
    
    /**
     * Delete product
     * DELETE /api/products/{id}
     */
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
        log.info("REST request to delete product with ID: {}", id);
        productService.deleteProduct(id);
        return ResponseEntity.noContent().build();
    }
    
    /**
     * Get available products
     * GET /api/products/available
     */
    @GetMapping("/available")
    public ResponseEntity<List<Product>> getAvailableProducts() {
        log.info("REST request to get available products");
        List<Product> products = productService.getAvailableProducts();
        return ResponseEntity.ok(products);
    }
    
    /**
     * Get products by category
     * GET /api/products/category/{category}
     */
    @GetMapping("/category/{category}")
    public ResponseEntity<List<Product>> getProductsByCategory(
            @PathVariable String category) {
        log.info("REST request to get products by category: {}", category);
        List<Product> products = productService.getProductsByCategory(category);
        return ResponseEntity.ok(products);
    }
    
    /**
     * Search products by name
     * GET /api/products/search?keyword={keyword}
     */
    @GetMapping("/search")
    public ResponseEntity<List<Product>> searchProducts(
            @RequestParam String keyword) {
        log.info("REST request to search products with keyword: {}", keyword);
        List<Product> products = productService.searchProductsByName(keyword);
        return ResponseEntity.ok(products);
    }
    
    /**
     * Get products by price range
     * GET /api/products/price-range?min={min}&max={max}
     */
    @GetMapping("/price-range")
    public ResponseEntity<List<Product>> getProductsByPriceRange(
            @RequestParam BigDecimal min,
            @RequestParam BigDecimal max) {
        log.info("REST request to get products in price range: {} - {}", min, max);
        List<Product> products = productService.getProductsByPriceRange(min, max);
        return ResponseEntity.ok(products);
    }
    
    /**
     * Get low stock products
     * GET /api/products/low-stock?threshold={threshold}
     */
    @GetMapping("/low-stock")
    public ResponseEntity<List<Product>> getLowStockProducts(
            @RequestParam(defaultValue = "10") Integer threshold) {
        log.info("REST request to get low stock products with threshold: {}", threshold);
        List<Product> products = productService.getLowStockProducts(threshold);
        return ResponseEntity.ok(products);
    }
}

Controller Best Practices:17192016

  • @RestController: Combines @Controller and @ResponseBody for REST APIs1617
  • @RequestMapping("/api/products"): Base URL for all endpoints18
  • @Valid: Triggers validation on request body98
  • ResponseEntity: Provides full control over HTTP response (status, headers, body)1920
  • @PathVariable: Extracts values from URI path (e.g., /products/{id})16
  • @RequestParam: Extracts query parameters (e.g., ?keyword=laptop)16
  • HTTP Status Codes: Use appropriate status codes (200 OK, 201 Created, 204 No Content, 404 Not Found)2019

Step 8: Create Main Application Class

package com.example.productapi;

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

@SpringBootApplication
public class ProductApiApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(ProductApiApplication.class, args);
    }
}

Step 9: Test the API

Running the Application:21

# Using Maven
mvn spring-boot:run

# Or run the JAR
mvn clean package
java -jar target/product-api-1.0.0.jar

The application will start on http://localhost:8080.

Testing with cURL or Postman:18

1. Create a Product (POST):

curl -X POST http://localhost:8080/api/products \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Laptop",
    "description": "High-performance laptop",
    "price": 1299.99,
    "quantity": 50,
    "isAvailable": true,
    "category": "Electronics"
  }'

Response (201 Created):

{
  "id": 1,
  "name": "Laptop",
  "description": "High-performance laptop",
  "price": 1299.99,
  "quantity": 50,
  "isAvailable": true,
  "category": "Electronics",
  "createdAt": "2025-10-22T22:28:00",
  "updatedAt": "2025-10-22T22:28:00"
}

2. Get All Products (GET):

curl http://localhost:8080/api/products

Response (200 OK):

[
  {
    "id": 1,
    "name": "Laptop",
    "description": "High-performance laptop",
    "price": 1299.99,
    "quantity": 50,
    "isAvailable": true,
    "category": "Electronics",
    "createdAt": "2025-10-22T22:28:00",
    "updatedAt": "2025-10-22T22:28:00"
  }
]

3. Get Product by ID (GET):

curl http://localhost:8080/api/products/1

4. Update Product (PUT):

curl -X PUT http://localhost:8080/api/products/1 \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Gaming Laptop",
    "description": "High-performance gaming laptop",
    "price": 1499.99,
    "quantity": 45,
    "isAvailable": true,
    "category": "Electronics"
  }'

Response (200 OK): Updated product with new values.

5. Delete Product (DELETE):

curl -X DELETE http://localhost:8080/api/products/1

Response (204 No Content): Empty response body.

6. Search Products (GET with Query Param):

curl "http://localhost:8080/api/products/search?keyword=laptop"

7. Get Products by Price Range:

curl "http://localhost:8080/api/products/price-range?min=100&max=2000"

8. Get Available Products:

curl http://localhost:8080/api/products/available

Complete API Endpoints Summary

HTTP MethodEndpointDescriptionStatus Code
POST/api/productsCreate new product201 Created
GET/api/productsGet all products200 OK
GET/api/products/{id}Get product by ID200 OK / 404 Not Found
PUT/api/products/{id}Update product200 OK / 404 Not Found
DELETE/api/products/{id}Delete product204 No Content / 404 Not Found
GET/api/products/availableGet available products200 OK
GET/api/products/category/{category}Get by category200 OK
GET/api/products/search?keyword=xSearch by name200 OK
GET/api/products/price-range?min=x&max=yGet by price range200 OK
GET/api/products/low-stock?threshold=xGet low stock products200 OK

Project Structure

product-api/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/productapi/
│   │   │       ├── ProductApiApplication.java
│   │   │       ├── controller/
│   │   │       │   └── ProductController.java
│   │   │       ├── service/
│   │   │       │   ├── ProductService.java
│   │   │       │   └── ProductServiceImpl.java
│   │   │       ├── repository/
│   │   │       │   └── ProductRepository.java
│   │   │       ├── entity/
│   │   │       │   └── Product.java
│   │   │       └── exception/
│   │   │           ├── ResourceNotFoundException.java
│   │   │           ├── DuplicateResourceException.java
│   │   │           ├── ErrorResponse.java
│   │   │           └── GlobalExceptionHandler.java
│   │   └── resources/
│   │       └── application.properties
│   └── test/
│       └── java/
└── pom.xml

Key Takeaways and Best Practices

Architecture:1213

  • Separation of Concerns: Each layer has a distinct responsibility12
  • Entity Layer: Data models and database mapping6
  • Repository Layer: Data access abstraction511
  • Service Layer: Business logic and transactions1213
  • Controller Layer: HTTP request handling1716

Spring Data JPA Benefits:511

  • Reduces boilerplate code with built-in CRUD methods5
  • Automatic query generation from method names11
  • Support for custom queries with @Query annotation11
  • Transaction management with @Transactional13

Validation:89

  • Use Bean Validation annotations (@NotBlank, @Size, etc.)8
  • Enable validation with @Valid in controllers9
  • Handle validation errors with @RestControllerAdvice15

Exception Handling:15

  • Create custom exceptions for domain-specific errors15
  • Use @RestControllerAdvice for global exception handling15
  • Return consistent error responses with appropriate HTTP status codes15

ResponseEntity Usage:1920

  • Provides control over HTTP status codes, headers, and body19
  • Use builder methods (ok(), created(), noContent(), etc.)20
  • Return appropriate status codes for different scenarios20

This tutorial provides a complete, production-ready foundation for building REST APIs with Spring Boot 3, Spring Data JPA, and PostgreSQL. You can extend this by adding features like pagination, sorting, security with Spring Security, API documentation with Swagger/OpenAPI, and comprehensive unit and integration tests.221021185 <span style="display:none">2324252627282930313233343536373839404142434445464748495051525354555657585960</span>


<div align="center">⁂</div>

Footnotes

  1. https://developer.okta.com/blog/2018/12/13/build-basic-app-spring-boot-jpa ↩

  2. https://www.bezkoder.com/spring-boot-postgresql-example/ ↩ ↩2 ↩3

  3. https://dev.to/igventurelli/connecting-spring-boot-applications-to-a-database-with-spring-data-jpa-46ch ↩ ↩2

  4. https://stackoverflow.com/questions/76418413/add-postgresql-dependency-with-spring-boot ↩

  5. https://www.bezkoder.com/spring-boot-jpa-crud-rest-api/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8

  6. https://www.linkedin.com/pulse/entity-annotation-spring-boot-venura-pavan-zesde ↩ ↩2 ↩3 ↩4

  7. https://www.codingshuttle.com/blog/top-10-jpa-annotations-in-spring-boot ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  8. https://www.bezkoder.com/spring-boot-validate-request-body/ ↩ ↩2 ↩3 ↩4

  9. https://www.geeksforgeeks.org/springboot/request-body-and-parameter-validation-with-spring-boot/ ↩ ↩2 ↩3 ↩4

  10. https://www.bezkoder.com/spring-boot-3-rest-api/ ↩ ↩2

  11. https://dev.to/tienbku/spring-boot-postgresql-maven-crud-example-598m ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9

  12. https://stackoverflow.com/questions/57284058/what-is-the-best-practice-for-restcontroller ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  13. https://pwskills.com/blog/architecture-of-spring-boot-examples-pattern-layered-controller-layer/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10

  14. https://stackoverflow.com/questions/58234187/what-is-the-use-of-service-layer-in-spring-boot-applications ↩ ↩2

  15. https://www.javacodegeeks.com/2024/12/handling-api-responses-in-spring-boot-best-practices-and-real-life-examples.html ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  16. https://www.vervecopilot.com/interview-questions/can-spring-boot-rest-controller-be-the-secret-weapon-for-acing-your-next-interview ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  17. https://camunda.com/blog/2025/05/how-to-build-a-rest-api-with-spring-boot-a-step-by-step-guide/ ↩ ↩2 ↩3 ↩4

  18. https://dzone.com/articles/build-crud-restful-api-using-spring-boot-3 ↩ ↩2 ↩3 ↩4

  19. https://dev.to/devcorner/mastering-responseentity-and-controller-in-spring-boot-agc ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  20. https://www.geeksforgeeks.org/advance-java/how-to-use-spring-responseentity-to-manipulate-the-http-response/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  21. https://blog.jetbrains.com/idea/2024/10/how-to-build-a-crud-rest-api-using-spring-boot/ ↩ ↩2

  22. https://www.geeksforgeeks.org/advance-java/best-practices-while-making-rest-apis-in-spring-boot-application/ ↩

  23. https://github.com/bezkoder/spring-boot-3-rest-api-example ↩

  24. https://learn.microsoft.com/en-us/azure/developer/java/spring-framework/configure-spring-data-jpa-with-azure-postgresql ↩

  25. https://www.youtube.com/watch?v=uZA_MjUXuFg ↩

  26. https://stackoverflow.com/questions/69408767/can-i-use-spring-data-jpa-with-postgresql ↩

  27. https://amigoscode.com/blogs/top-10-spring-boot-rest-api-best-practices ↩

  28. https://www.youtube.com/watch?v=v1IFQWzuSrw ↩

  29. https://mkyong.com/spring-boot/spring-boot-spring-data-jpa-postgresql/ ↩

  30. https://www.youtube.com/watch?v=EgQJRB9Vs3Y ↩

  31. https://stackoverflow.com/questions/48307487/setting-up-spring-boot-jpa-postgresql ↩

  32. https://www.geeksforgeeks.org/springboot/spring-boot-integration-with-postgresql-as-a-maven-project/ ↩

  33. https://stackoverflow.com/questions/60800005/how-to-correctly-configure-spring-data-jpa-into-the-application-properties-confi ↩

  34. https://stackoverflow.com/questions/13242196/how-do-you-add-postgresql-driver-as-a-dependency-in-maven ↩

  35. https://stackoverflow.com/questions/30310988/how-can-i-load-jpa-properties-to-datasource-in-spring ↩

  36. https://thorben-janssen.com/configuring-spring-data-jpa-with-spring-boot/ ↩

  37. https://dev.to/wkreuch/create-an-entity-and-repository-using-spring-boot-3-2l6 ↩

  38. https://www.geeksforgeeks.org/springboot/spring-boot-application-properties/ ↩

  39. https://spring.io/guides/gs/accessing-data-jpa ↩

  40. https://mvnrepository.com/artifact/org.postgresql/postgresql ↩

  41. https://docs.spring.io/spring-boot/appendix/application-properties/index.html ↩

  42. https://www.baeldung.com/jpa-entities ↩

  43. https://www.enterprisedb.com/postgres-tutorials/how-add-postgresql-driver-dependency-maven ↩

  44. https://www.tutorialspoint.com/spring_boot_orm/spring_boot_orm_application.htm ↩

  45. https://stackoverflow.com/questions/43832705/spring-boot-does-entity-annotation-exist ↩

  46. https://www.youtube.com/watch?v=kUefIBtemGc ↩

  47. https://docs.spring.io/spring-boot/how-to/properties-and-configuration.html ↩

  48. https://java-design-patterns.com/patterns/service-layer/ ↩

  49. https://stackoverflow.com/questions/64517537/springboot-validate-requestbody ↩

  50. https://stackoverflow.com/questions/49732262/spring-responseentity-best-practice ↩

  51. https://www.geeksforgeeks.org/springboot/spring-boot-architecture/ ↩

  52. https://stackoverflow.com/questions/48385362/spring-boot-validation-of-requestbody-dto-annotated-in-rest-api ↩

  53. https://www.youtube.com/watch?v=ibHSzuZaKPM ↩

  54. https://www.baeldung.com/jsf-spring-boot-controller-service-dao ↩

  55. https://www.linkedin.com/pulse/setting-up-validations-request-body-spring-boot-kāshān-asim-vy8le ↩

  56. https://www.reddit.com/r/SpringBoot/comments/1buv6hn/what_are_the_best_practices_in_spring_boot/ ↩

  57. https://stackoverflow.com/questions/68264638/correct-pattern-for-handling-service-layer-results ↩

  58. https://blog.tericcabrel.com/validate-request-body-and-parameter-in-spring-boot/ ↩

  59. https://www.reddit.com/r/SpringBoot/comments/1e8smxf/best_practices_return_value/ ↩

  60. https://dev.to/writech/designing-a-multi-layered-architecture-for-building-restful-web-services-with-spring-boot-and-kotlin-51l5 ↩